diff --git a/cli/scripts/release-naming.mjs b/cli/scripts/release-naming.mjs index 2af172f1333..d9fcc75c441 100644 --- a/cli/scripts/release-naming.mjs +++ b/cli/scripts/release-naming.mjs @@ -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, diff --git a/cli/scripts/release-naming.test.ts b/cli/scripts/release-naming.test.ts index 97552ed0823..e6f36391801 100644 --- a/cli/scripts/release-naming.test.ts +++ b/cli/scripts/release-naming.test.ts @@ -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 = {}, +): { 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', () => { diff --git a/cli/scripts/release-r2-edge.test.ts b/cli/scripts/release-r2-edge.test.ts index 7c93133fddf..3054a8793b6 100644 --- a/cli/scripts/release-r2-edge.test.ts +++ b/cli/scripts/release-r2-edge.test.ts @@ -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', () => { diff --git a/cli/test/fixtures/pkg-manifest.ts b/cli/test/fixtures/pkg-manifest.ts new file mode 100644 index 00000000000..92f2299ad35 --- /dev/null +++ b/cli/test/fixtures/pkg-manifest.ts @@ -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 { + 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 } +}