fix(cli): decouple release script tests from the live compat window (#39658)

This commit is contained in:
Xiyuan Chen
2026-07-27 23:50:05 +00:00
committed by GitHub
parent b25b28cc76
commit d8506efed6
4 changed files with 109 additions and 20 deletions
+8 -2
View File
@@ -114,9 +114,15 @@ function die(msg) {
process.exit(1)
}
// Tests point this at a fixture manifest so their assertions stay fixed while
// the real version and compat window move with every release. The name is
// mirrored in test/fixtures/pkg-manifest.ts rather than imported from here,
// because this file's shebang breaks the Windows test runner.
const PKG_PATH_ENV = 'DIFYCTL_PKG_PATH'
function loadPkg() {
const pkgUrl = new URL('../package.json', import.meta.url)
const pkg = JSON.parse(readFileSync(pkgUrl, 'utf8'))
const pkgPath = process.env[PKG_PATH_ENV] || new URL('../package.json', import.meta.url)
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
if (!pkg.difyctl?.release) die('cli/package.json missing difyctl.release')
return {
version: pkg.version,
+36 -16
View File
@@ -1,12 +1,19 @@
import { execFileSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { FIXTURE_COMPAT, pkgManifestEnv } from '../test/fixtures/pkg-manifest'
const SCRIPT = fileURLToPath(new URL('./release-naming.mjs', import.meta.url))
function run(args: string[]): { code: number; stdout: string; stderr: string } {
function run(
args: string[],
env: Record<string, string> = {},
): { code: number; stdout: string; stderr: string } {
try {
const stdout = execFileSync('node', [SCRIPT, ...args], { encoding: 'utf8' })
const stdout = execFileSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
env: { ...process.env, ...env },
})
return { code: 0, stdout, stderr: '' }
} catch (e) {
const err = e as { status?: number; stdout?: string; stderr?: string }
@@ -14,45 +21,50 @@ function run(args: string[]): { code: number; stdout: string; stderr: string } {
}
}
describe('release-naming compat-check (compat 1.16.0..1.16.0)', () => {
describe('release-naming compat-check', () => {
const { minDify, maxDify } = FIXTURE_COMPAT // 2.0.0 .. 2.5.0
const pkgEnv = pkgManifestEnv()
const compatCheck = (difyVersion?: string) =>
run(difyVersion === undefined ? ['compat-check'] : ['compat-check', difyVersion], pkgEnv).code
it('accepts a version inside the window', () => {
expect(run(['compat-check', '1.16.0']).code).toBe(0)
expect(compatCheck('2.3.0')).toBe(0)
})
it('accepts the inclusive lower bound', () => {
expect(run(['compat-check', '1.16.0']).code).toBe(0)
expect(compatCheck(minDify)).toBe(0)
})
it('accepts the inclusive upper bound', () => {
expect(run(['compat-check', '1.16.0']).code).toBe(0)
expect(compatCheck(maxDify)).toBe(0)
})
it('accepts a v-prefixed tag', () => {
expect(run(['compat-check', 'v1.16.0']).code).toBe(0)
expect(compatCheck('v2.3.0')).toBe(0)
})
it('rejects a version below the lower bound', () => {
expect(run(['compat-check', '1.15.9']).code).not.toBe(0)
expect(compatCheck('1.9.9')).not.toBe(0)
})
it('rejects a version above the upper bound', () => {
expect(run(['compat-check', '1.16.1']).code).not.toBe(0)
expect(compatCheck('2.5.1')).not.toBe(0)
})
it('treats a prerelease of the bound as below it (1.16.0-rc1 < 1.16.0)', () => {
expect(run(['compat-check', '1.16.0-rc1']).code).not.toBe(0)
it('treats a prerelease of the lower bound as below it', () => {
expect(compatCheck(`${minDify}-rc1`)).not.toBe(0)
})
it('ignores build metadata on the bound (1.16.0+build == 1.16.0)', () => {
expect(run(['compat-check', '1.16.0+build123']).code).toBe(0)
it('ignores build metadata on the bound', () => {
expect(compatCheck(`${maxDify}+build123`)).toBe(0)
})
it('ignores build metadata when out of range (1.16.1+build still rejected)', () => {
expect(run(['compat-check', '1.16.1+build123']).code).not.toBe(0)
it('ignores build metadata when out of range', () => {
expect(compatCheck('2.5.1+build123')).not.toBe(0)
})
it('requires a version argument', () => {
expect(run(['compat-check']).code).not.toBe(0)
expect(compatCheck()).not.toBe(0)
})
})
@@ -67,6 +79,14 @@ describe('release-naming github-env', () => {
for (const key of ['version', 'channel', 'prerelease', 'minDify', 'maxDify', 'tagPrefix'])
expect(stdout).toMatch(new RegExp(`^${key}=`, 'm'))
})
// The only assertion against the live manifest: the window must exist and be
// well-formed, whatever release it currently points at.
it('emits a well-formed compat window from the real cli/package.json', () => {
const { stdout } = run(['github-env'])
expect(stdout).toMatch(/^minDify=\d+\.\d+\.\d+$/m)
expect(stdout).toMatch(/^maxDify=\d+\.\d+\.\d+$/m)
})
})
describe('release-naming edge channel', () => {
+8 -2
View File
@@ -4,14 +4,20 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { FIXTURE_COMPAT, pkgManifestEnv } from '../test/fixtures/pkg-manifest'
const SCRIPT = fileURLToPath(new URL('./release-r2-edge.mjs', import.meta.url))
const PKG_ENV = pkgManifestEnv()
function run(args: string[]): { code: number; stdout: string; stderr: string } {
try {
return {
code: 0,
stdout: execFileSync('node', [SCRIPT, ...args], { encoding: 'utf8' }),
stdout: execFileSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
env: { ...process.env, ...PKG_ENV },
}),
stderr: '',
}
} catch (e) {
@@ -108,7 +114,7 @@ describe('release-r2-edge manifest', () => {
it('carries the compat window from package.json', () => {
const { json } = buildManifest()
expect(json.compat).toEqual({ minDify: '1.16.0', maxDify: '1.16.0' })
expect(json.compat).toEqual(FIXTURE_COMPAT)
})
it('lists all 5 targets with asset name + sha256 from the checksums file', () => {
+57
View File
@@ -0,0 +1,57 @@
import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
// Mirrors PKG_PATH_ENV in scripts/release-naming.mjs, which cannot be imported
// here: its shebang breaks the Windows test runner. Divergence is self-
// reporting, not silent — the script would fall back to the real
// cli/package.json and every fixture-window assertion would fail.
const PKG_PATH_ENV = 'DIFYCTL_PKG_PATH'
// release-naming.mjs and release-r2-edge.mjs read their data from
// cli/package.json. Tests spawn them against this fixture instead, so
// assertions can name exact versions without tracking the live release.
// Deliberately far from any real Dify version, and min != max so "inside the
// window" is a case distinct from either bound.
export const FIXTURE_COMPAT = { minDify: '2.0.0', maxDify: '2.5.0' }
export const FIXTURE_TARGET_IDS = [
'linux-x64',
'linux-arm64',
'darwin-x64',
'darwin-arm64',
'windows-x64',
] as const
const FIXTURE_RELEASE = {
tagPrefix: 'difyctl-v',
binName: 'difyctl',
checksumsSuffix: '-checksums.txt',
targets: FIXTURE_TARGET_IDS.map((id) => ({
id,
bunTarget: `bun-${id}`,
exe: id.startsWith('windows'),
})),
}
export type PkgManifestOverrides = {
version?: string
channel?: string
compat?: { minDify: string; maxDify: string }
}
// Returns the env additions that point a spawned script at the fixture.
export function pkgManifestEnv(overrides: PkgManifestOverrides = {}): Record<string, string> {
const manifest = {
version: overrides.version ?? '0.2.0-alpha',
difyctl: {
channel: overrides.channel ?? 'alpha',
compat: overrides.compat ?? FIXTURE_COMPAT,
release: FIXTURE_RELEASE,
},
}
const path = join(mkdtempSync(join(tmpdir(), 'difyctl-pkg-')), 'package.json')
writeFileSync(path, JSON.stringify(manifest))
return { [PKG_PATH_ENV]: path }
}