Files
dify/cli/scripts/release-config.test.ts
GareArc f1c32e1bc8 refactor(cli): split release scripts into pure rules and CLI shells
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.
2026-07-27 19:13:42 -07:00

73 lines
2.8 KiB
TypeScript

import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { main } from './release-naming.mjs'
const env = Object.fromEntries(
main(['github-env'])
.split('\n')
.filter(Boolean)
.map((line: string) => line.split(/=(.*)/s).slice(0, 2)),
)
// Install scripts run standalone on an end user's machine — no repo, no node —
// so they hardcode artifact names instead of asking release-naming.mjs. These
// checks are what keeps the two copies from drifting: change the naming config
// and they fail until the installers are updated to match.
const INSTALLERS = ['install-cli.sh', 'install-r2.sh', 'install.ps1', 'install-r2.ps1']
const installerText = (name: string) =>
readFileSync(fileURLToPath(new URL(`./${name}`, import.meta.url)), 'utf8')
const CHECKSUMS_SUFFIX = main(['checksums', '1.2.3']).trim().replace(`${env.tagPrefix}1.2.3`, '')
const EXE_TARGET_ID = main(['targets'])
.trim()
.split('\n')
.map((line: string) => line.split('\t'))
.find(([, , exe]: string[]) => exe === '1')?.[1]
describe('install scripts stay aligned with the release naming config', () => {
it.each(INSTALLERS)('%s uses the configured tag prefix', (name) => {
expect(installerText(name)).toContain(env.tagPrefix)
})
it.each(INSTALLERS)('%s uses the configured checksums suffix', (name) => {
expect(installerText(name)).toContain(CHECKSUMS_SUFFIX)
})
it.each(['install.ps1', 'install-r2.ps1'])('%s targets the declared windows build', (name) => {
expect(EXE_TARGET_ID).toBeTruthy()
expect(installerText(name)).toContain(EXE_TARGET_ID)
})
})
describe('the shipped difyctl release config', () => {
it('passes the release gate (`release-naming.mjs validate`)', () => {
expect(main(['validate'])).toMatch(/^difyctl release valid:/)
})
it('declares a usable, correctly ordered compat window', () => {
expect(main(['compat-check', env.minDify])).toContain('compatible')
expect(main(['compat-check', env.maxDify])).toContain('compatible')
})
it('gates a Dify version outside the declared window', () => {
const aboveMax = `${Number(env.maxDify.split('.')[0]) + 1}.0.0`
expect(() => main(['compat-check', aboveMax])).toThrow('outside difyctl compatibility window')
expect(() => main(['compat-check', '0.0.1'])).toThrow('outside difyctl compatibility window')
})
it('declares a version valid for the channel it ships on', () => {
expect(main(['validate-version', env.version, env.channel])).toContain('valid')
})
it('can name an artifact for every target it declares', () => {
const ids = main(['targets'])
.trim()
.split('\n')
.map((line: string) => line.split('\t')[1])
expect(ids.length).toBeGreaterThan(0)
for (const id of ids) expect(main(['asset', env.version, id])).toContain(id)
})
})