The release script tests could only observe behaviour through a process exit code, because die() called process.exit from inside otherwise-pure functions. That forced every test to spawn a subprocess, which forced its input to come from cli/package.json, which is why editing that file kept breaking tests — most recently #39658, and next the difyctl version bump that cli-release.yml requires on every release. Move the decisions into lib/release-rules.mjs and lib/edge-manifest.mjs, which take every input as an argument and return values instead of exiting. release-naming.mjs and release-r2-edge.mjs keep argv parsing, manifest reading, stdout and exit codes, and nothing else. Tests now split by what they ask. Logic is unit-tested against literal inputs, the shells are tested for plumbing only, and release-config.test.ts is the single place that reads the real manifest — asserting it is internally consistent, never what it currently contains. Editing cli/package.json no longer breaks a logic test; a malformed config still fails. Verified by mutating each field in both directions. Three intentional behaviour changes: - compat-check compares the numeric A.B.C core only, ignoring prerelease and build suffixes, so Dify 1.16.0-rc1 now satisfies a 1.16.0 window. This matches the shipped runtime check in src/version/compat.ts, which already stripped suffixes; the release gate was the one disagreeing. Removes the hand-rolled prerelease ordering entirely. - validate now rejects a missing or inverted compat window. It previously passed such a config while github-env emitted minDify=undefined. - release-r2-edge reports a bad channel version under its own name rather than release-naming's, since it no longer borrows that script's die(). release-config.test.ts also pins the install scripts to the naming config. They hardcode artifact names by necessity — they run standalone on a user machine with no repo and no node — so changing tagPrefix or checksumsSuffix now fails until all four installers are updated to match, instead of silently publishing names no installer looks for.
184 lines
6.5 KiB
TypeScript
184 lines
6.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
assetNameFor,
|
|
channelVersionProblem,
|
|
checksumsName,
|
|
compatProblems,
|
|
edgeVersionFrom,
|
|
edgeVersionProblem,
|
|
isWithinCompatWindow,
|
|
releaseConfigProblems,
|
|
tagName,
|
|
} from './release-rules.mjs'
|
|
|
|
const RELEASE = {
|
|
tagPrefix: 'testctl-v',
|
|
binName: 'testctl',
|
|
checksumsSuffix: '.sums',
|
|
targets: [
|
|
{ id: 'linux-x64', bunTarget: 'bun-linux-x64', exe: false },
|
|
{ id: 'windows-x64', bunTarget: 'bun-windows-x64', exe: true },
|
|
],
|
|
}
|
|
|
|
const WINDOW = { minDify: '2.0.0', maxDify: '2.5.0' }
|
|
|
|
describe('isWithinCompatWindow', () => {
|
|
it.each([
|
|
['2.3.0', true, 'inside the window'],
|
|
['2.0.0', true, 'the inclusive lower bound'],
|
|
['2.5.0', true, 'the inclusive upper bound'],
|
|
['v2.3.0', true, 'a v-prefixed tag'],
|
|
['1.9.9', false, 'below the lower bound'],
|
|
['2.5.1', false, 'above the upper bound'],
|
|
])('%s -> %s (%s)', (version, expected) => {
|
|
expect(isWithinCompatWindow(version, WINDOW)).toBe(expected)
|
|
})
|
|
|
|
it.each([
|
|
['2.0.0-rc.1', true, 'a prerelease of the lower bound ranks equal to it'],
|
|
['2.5.0-rc.1', true, 'a prerelease of the upper bound ranks equal to it'],
|
|
['2.3.0-alpha.7+build9', true, 'both suffixes stripped before comparing'],
|
|
['2.5.0+build123', true, 'build metadata on the upper bound'],
|
|
['2.5.1-rc.1', false, 'a prerelease whose core is above the window'],
|
|
['1.9.9-rc.1', false, 'a prerelease whose core is below the window'],
|
|
['2.5.1+build123', false, 'build metadata out of range'],
|
|
])('ignores suffixes: %s -> %s (%s)', (version, expected) => {
|
|
expect(isWithinCompatWindow(version, WINDOW)).toBe(expected)
|
|
})
|
|
})
|
|
|
|
describe('channelVersionProblem', () => {
|
|
it.each([
|
|
['1.2.3', 'stable'],
|
|
['1.2.3+build', 'stable'],
|
|
['1.2.3-alpha', 'alpha'],
|
|
['1.2.3-alpha.4', 'alpha'],
|
|
['1.2.3-rc.4', 'rc'],
|
|
['1.2.3-edge.2fd7b82', 'edge'],
|
|
])('accepts %s on the %s channel', (version, channel) => {
|
|
expect(channelVersionProblem(version, channel)).toBeNull()
|
|
})
|
|
|
|
it.each([
|
|
['1.2.3-rc.1', 'edge'],
|
|
['1.2.3-alpha', 'stable'],
|
|
['1.2.3', 'rc'],
|
|
])('rejects %s on the %s channel', (version, channel) => {
|
|
expect(channelVersionProblem(version, channel)).toMatch(/does not match the .* channel form/)
|
|
})
|
|
|
|
it('rejects an unknown channel', () => {
|
|
expect(channelVersionProblem('1.2.3', 'nightly')).toMatch(/unknown channel/)
|
|
})
|
|
|
|
it('rejects an empty version', () => {
|
|
expect(channelVersionProblem('', 'stable')).toMatch(/non-empty string/)
|
|
})
|
|
})
|
|
|
|
describe('edge version derivation', () => {
|
|
it('derives <core>-edge.<sha>, dropping the prerelease', () => {
|
|
expect(edgeVersionProblem('3.4.5-alpha', '2fd7b82')).toBeNull()
|
|
expect(edgeVersionFrom('3.4.5-alpha', '2fd7b82')).toBe('3.4.5-edge.2fd7b82')
|
|
})
|
|
|
|
it('accepts a 40-char sha', () => {
|
|
const sha = '2fd7b829e1f0aaaabbbbccccddddeeeeffff0000'
|
|
expect(edgeVersionProblem('3.4.5-alpha', sha)).toBeNull()
|
|
expect(edgeVersionFrom('3.4.5-alpha', sha)).toBe(`3.4.5-edge.${sha}`)
|
|
})
|
|
|
|
it.each([['nothex!'], [''], ['abc'], [undefined]])('rejects sha %s', (sha) => {
|
|
expect(edgeVersionProblem('3.4.5-alpha', sha)).toMatch(/git short sha/)
|
|
})
|
|
|
|
it('rejects a version with no derivable core', () => {
|
|
expect(edgeVersionProblem('not-a-version', '2fd7b82')).toMatch(/cannot derive edge base/)
|
|
})
|
|
})
|
|
|
|
describe('artifact names', () => {
|
|
it('builds the tag and checksums names from the prefix', () => {
|
|
expect(tagName(RELEASE, '9.9.9')).toBe('testctl-v9.9.9')
|
|
expect(checksumsName(RELEASE, '9.9.9')).toBe('testctl-v9.9.9.sums')
|
|
})
|
|
|
|
it('appends .exe only for targets flagged exe', () => {
|
|
expect(assetNameFor(RELEASE, '9.9.9', 'linux-x64')).toBe('testctl-v9.9.9-linux-x64')
|
|
expect(assetNameFor(RELEASE, '9.9.9', 'windows-x64')).toBe('testctl-v9.9.9-windows-x64.exe')
|
|
})
|
|
|
|
it('returns null for an unknown target id', () => {
|
|
expect(assetNameFor(RELEASE, '9.9.9', 'solaris-sparc')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('compatProblems', () => {
|
|
it('accepts a well-formed window', () => {
|
|
expect(compatProblems(WINDOW)).toEqual([])
|
|
expect(compatProblems({ minDify: '2.0.0', maxDify: '2.0.0' })).toEqual([])
|
|
})
|
|
|
|
it.each([
|
|
[undefined, /minDify must be a non-empty string/],
|
|
[{ maxDify: '2.5.0' }, /minDify must be a non-empty string/],
|
|
[{ minDify: '2.0.0' }, /maxDify must be a non-empty string/],
|
|
[{ minDify: '', maxDify: '' }, /minDify must be a non-empty string/],
|
|
])('flags a missing bound', (compat, expected) => {
|
|
expect(compatProblems(compat).join('\n')).toMatch(expected)
|
|
})
|
|
|
|
it('compares bounds by core, so suffixes do not make a window inverted', () => {
|
|
expect(compatProblems({ minDify: '2.0.0-rc.1', maxDify: '2.0.0' })).toEqual([])
|
|
})
|
|
|
|
it('flags an inverted window', () => {
|
|
expect(compatProblems({ minDify: '2.5.0', maxDify: '2.0.0' }).join('\n')).toMatch(
|
|
/must not exceed/,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('releaseConfigProblems', () => {
|
|
it('accepts a well-formed config', () => {
|
|
expect(releaseConfigProblems(RELEASE)).toEqual([])
|
|
})
|
|
|
|
it.each([
|
|
[{ ...RELEASE, tagPrefix: '' }, /tagPrefix must be a non-empty string/],
|
|
[{ ...RELEASE, binName: '' }, /binName must be a non-empty string/],
|
|
[{ ...RELEASE, checksumsSuffix: '' }, /checksumsSuffix must be a non-empty string/],
|
|
[{ ...RELEASE, targets: [] }, /targets must be a non-empty array/],
|
|
[{ ...RELEASE, targets: undefined }, /targets must be a non-empty array/],
|
|
])('flags a malformed field', (release, expected) => {
|
|
expect(releaseConfigProblems(release).join('\n')).toMatch(expected)
|
|
})
|
|
|
|
it('flags a duplicate target id', () => {
|
|
const release = { ...RELEASE, targets: [RELEASE.targets[0], { ...RELEASE.targets[0] }] }
|
|
expect(releaseConfigProblems(release).join('\n')).toMatch(/duplicate target id: linux-x64/)
|
|
})
|
|
|
|
it('flags a bunTarget that is not a bun triple', () => {
|
|
const release = { ...RELEASE, targets: [{ id: 'a', bunTarget: 'linux-x64', exe: false }] }
|
|
expect(releaseConfigProblems(release).join('\n')).toMatch(/bunTarget must match/)
|
|
})
|
|
|
|
it('flags exe disagreeing with a windows bunTarget', () => {
|
|
const release = {
|
|
...RELEASE,
|
|
targets: [{ id: 'windows-x64', bunTarget: 'bun-windows-x64', exe: false }],
|
|
}
|
|
expect(releaseConfigProblems(release).join('\n')).toMatch(/exe must be true iff/)
|
|
})
|
|
|
|
it('flags a non-boolean exe', () => {
|
|
const release = {
|
|
...RELEASE,
|
|
targets: [{ id: 'linux-x64', bunTarget: 'bun-linux-x64', exe: 'no' }],
|
|
}
|
|
expect(releaseConfigProblems(release).join('\n')).toMatch(/exe must be a boolean/)
|
|
})
|
|
})
|