import assert from 'node:assert/strict' import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, } from 'node:fs' import { join } from 'node:path' import { test } from 'node:test' import { REPO, fixture } from './fixtures.mjs' const VERSION = '1.2.3' const MAIN = 'Lede.\n\n## Highlights\n\n- Feature.\n\n## Important\n\nFollow up.\n' const FRAMING = 'Lede.\n\n## Important\n\nFollow up.\n' const PRE_UPDATE = '## ⚠️ Before You Update\n\n> Prepare first.' const METADATA = '---\nrelease-notes: true\n---\n\n' function releaseFixture(t) { const f = fixture(t) const { root, env, write, run, git } = f mkdirSync(join(root, 'scripts')) for (const script of [ 'manage-release.sh', 'changelog.mjs', 'changelog-version.mjs', ]) copyFileSync(join(REPO, 'scripts', script), join(root, 'scripts', script)) for (const config of ['package.json', '.prettierrc.json']) copyFileSync(join(REPO, config), join(root, config)) const labels = 'shared-libs/crates/start-core/locales/release-notes.json' write(labels, readFileSync(join(REPO, labels))) const main = write(`projects/start-sdk/release-notes/${VERSION}.md`, MAIN) const preUpdate = join( root, `projects/start-sdk/release-notes/${VERSION}.pre-update.md`, ) Object.assign(env, { VERSION, CHANGELOG_REF: 'fixture-ref' }) function script( command = 'notes', project = 'start-sdk', success = true, cwd = root, ) { const result = run( 'bash', [join(cwd, 'scripts/manage-release.sh'), command, project], { cwd, encoding: 'utf8' }, ) if (success) assert.equal(result.status, 0, result.stderr) else assert.notEqual(result.status, 0) return result } function commit(message = 'fixture') { git('add', '.') git( '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.com', '-c', 'commit.gpgsign=false', 'commit', '-qm', message, ) } function blockPython() { const marker = join(root, 'python-called') for (const name of ['python', 'python3']) { const path = write( `bin/${name}`, `#!/bin/sh\ntouch "${marker}"\nexit 97\n`, ) chmodSync(path, 0o755) } env.PATH = `${join(root, 'bin')}:${env.PATH}` return marker } return { ...f, main, preUpdate, script, commit, blockPython } } const text = path => readFileSync(path, 'utf8') const contains = (value, expected) => assert.ok( value.includes(expected), `Expected ${JSON.stringify(expected)} in ${JSON.stringify(value)}`, ) const excludes = (value, unexpected) => assert.ok( !value.includes(unexpected), `Unexpected ${JSON.stringify(unexpected)} in ${JSON.stringify(value)}`, ) const linkLast = notes => assert.match(notes.trimEnd().split('\n').at(-1), /^\*\*\[.+\]\(.+\)\*\*/) test('optional companion', t => { const { script, write } = releaseFixture(t) const without = script().stdout write(`projects/start-sdk/release-notes/${VERSION}.pre-update.md`, '') assert.equal(script().stdout, without) write(`projects/start-sdk/release-notes/${VERSION}.pre-update.md`, PRE_UPDATE) const combined = script().stdout assert.equal(combined, PRE_UPDATE + '\n\n' + without) linkLast(combined) assert.equal(combined.match(/Full changelog/g).length, 1) contains(combined, '/blob/fixture-ref/projects/start-sdk/CHANGELOG.md#123') }) test('reads canonical version without override', t => { const { env, write, script } = releaseFixture(t) delete env.VERSION write('projects/start-sdk/package.json', '{"version":"1.2.3"}') contains(script().stdout, 'v1.2.3') }) test('canonical versions and notes do not require Python', t => { const { env, write, script, blockPython } = releaseFixture(t) delete env.VERSION const marker = blockPython() for (const [project, manifest, contents, version, anchor] of [ [ 'start-sdk', 'projects/start-sdk/package.json', '{"version":"1.2.3"}', VERSION, '123', ], ['start-os', 'package.json', '{"version":"0.4.0.3"}', '0.4.0.3', '0403'], [ 'start-cli', 'projects/start-cli/Cargo.toml', '[package]\nversion = "1.2.3"\n', VERSION, '123', ], ]) { write(manifest, contents) write(`projects/${project}/release-notes/${version}.md`, MAIN) const output = script('notes', project).stdout contains(output, '- Feature.') contains(output, `/projects/${project}/CHANGELOG.md#${anchor}`) } assert.equal(existsSync(marker), false, 'release caller invoked Python') }) test('sparse deploy canonical versions and notes', t => { const { root, env, write, script, blockPython, git, commit, run } = releaseFixture(t) delete env.VERSION const marker = blockPython() write( 'package.json', JSON.stringify({ ...JSON.parse(text(join(REPO, 'package.json'))), version: '0.4.0.3', }), ) write( 'projects/start-wrt/backend/ctrl/Cargo.toml', '[package]\nname = "ctrl"\nversion = "1.2.3"\n', ) const cases = [ ['start-os', 'startos-iso.yaml', '0.4.0.3', '0403'], ['start-wrt', 'start-wrt.yaml', VERSION, '123'], ] for (const [project, , version] of cases) { write(`projects/${project}/release-notes/${version}.md`, FRAMING) write( `projects/${project}/changelog/patch-fixed-feature.md`, METADATA + '- Generated feature.\n', ) write( `projects/${project}/release-notes/${version}.pre-update.md`, PRE_UPDATE, ) write( `projects/${project}/changelog/patch-security-internal.md`, '---\nrelease-notes: false\n---\n\n- Security correction.\n', ) } git('init', '-q') commit() const ref = git('rev-parse', 'HEAD').trim() env.CHANGELOG_REF = ref for (const [project, workflow, version, anchor] of cases) { const deploy = text(join(REPO, '.github/workflows', workflow)).split( '\n deploy:\n', )[1] const sparse = deploy.match( / sparse-checkout: \|\n((?: .+\n)+)/, ) assert.ok(sparse) const patterns = sparse[1] .trim() .split('\n') .map(line => line.trim()) .join('\n') + '\n' const checkout = join(root, `checkout-${project}`) git('clone', '-q', '--no-checkout', root, checkout) const mode = deploy.includes('sparse-checkout-cone-mode: false') ? '--no-cone' : '--cone' git('-C', checkout, 'sparse-checkout', 'init', mode) const set = run( 'git', ['-C', checkout, 'sparse-checkout', 'set', '--stdin'], { input: patterns, encoding: 'utf8' }, ) assert.equal(set.status, 0, set.stderr) git('-C', checkout, 'checkout', '-q') assert.equal(existsSync(join(checkout, '.prettierrc.json')), true) assert.equal(existsSync(join(checkout, 'node_modules')), false) assert.equal(existsSync(join(checkout, 'package.json')), true) const determine = deploy.match( / - name: Determine version\n id: version\n run: \|\n((?: .+\n)+)/, ) assert.ok(determine) const outputFile = join(root, `outputs-${project}`) const result = run('bash', ['-e', '-c', determine[1]], { cwd: checkout, env: { ...env, GITHUB_OUTPUT: outputFile }, encoding: 'utf8', }) assert.equal(result.status, 0, result.stderr) assert.equal(text(outputFile), `version=${version}\n`) const derived = script('notes', project, true, checkout).stdout contains(derived, PRE_UPDATE) contains(derived, '- Generated feature.') contains(derived, 'Follow up.') contains(derived, `/tree/${ref}/projects/${project}/changelog`) const compose = deploy .split(' - name: Compile release notes\n')[1] ?.split(' run: |\n')[1] ?.split('\n - ')[0] assert.ok(compose) assert.ok( deploy.indexOf('Compile release notes') < deploy.indexOf('Drop the previous alpha build'), ) const composeShell = compose .split('\n') .map(line => line.slice(10)) .join('\n') .replaceAll('${{ github.sha }}', ref) const runnerTemp = join(root, `notes-${project}`) mkdirSync(runnerTemp) const compile = () => run('bash', ['-e', '-c', composeShell], { cwd: checkout, env: { ...env, VERSION: version, RUNNER_TEMP: runnerTemp }, encoding: 'utf8', }) const compiled = compile() assert.equal(compiled.status, 0, compiled.stderr) assert.deepEqual(JSON.parse(text(join(runnerTemp, 'release-notes.json'))), { releaseNotes: derived, }) if (project === 'start-os') { assert.equal(text(join(runnerTemp, 'release-security')), 'true\n') contains(deploy, '--git-hash "${{ github.sha }}" "${SECURITY_ARGS[@]}"') } const fragmentPath = `checkout-${project}/projects/${project}/changelog/patch-fixed-feature.md` write(fragmentPath, '---\nrelease-notes: invalid\n---\n\n- Invalid.\n') const invalid = compile() assert.notEqual(invalid.status, 0) contains(invalid.stderr, 'release-notes: true or false') write(fragmentPath, METADATA + '- Generated feature.\n') env.VERSION = version assert.equal(script('notes', project, true, checkout).stdout, derived) delete env.VERSION } assert.equal( existsSync(marker), false, 'deploy release caller invoked Python', ) }) test('release links GitHub release without depending on tag CI', t => { const { env, write, script } = releaseFixture(t) delete env.CHANGELOG_REF for (const [project, version] of [ ['start-sdk', '1.2.3'], ['start-os', '0.4.0.3'], ['start-os', '0.4.1'], ['start-sdk', '1.2.3-RC.1+Build.2'], ]) { env.VERSION = version write(`projects/${project}/release-notes/${version}.md`, MAIN) const output = script('notes', project).stdout contains(output, `/releases/tag/${project}/v${version}`) excludes(output, '/blob/') excludes(output, '/releases/download/') } }) test('source links survive fragment consumption', t => { const { env, write, script, git, commit } = releaseFixture(t) git('init', '-q') write(`projects/start-sdk/release-notes/${VERSION}.md`, FRAMING) write( 'projects/start-sdk/changelog/patch-fixed-example.md', METADATA + '- Fixed feature.\n', ) write( 'projects/start-sdk/CHANGELOG.md', '# Changelog\n\n## [1.2.2]\n\n- Previous release.\n', ) commit() env.CHANGELOG_REF = 'HEAD' contains(script().stdout, '/tree/HEAD/projects/start-sdk/changelog') git('rm', 'projects/start-sdk/changelog/patch-fixed-example.md') write( 'projects/start-sdk/CHANGELOG.md', '# Changelog\n\n## [1.2.3]\n\n- Fixed feature.\n', ) commit() const output = script().stdout contains(output, '/blob/HEAD/projects/start-sdk/CHANGELOG.md#123') excludes(output, '/tree/') }) test('GitHub body carries the notes and changelog without the link', t => { const { root, write, script, git, commit } = releaseFixture(t) git('init', '-q') write(`projects/start-sdk/release-notes/${VERSION}.md`, FRAMING) git('remote', 'add', 'origin', root) write(`projects/start-sdk/release-notes/${VERSION}.pre-update.md`, PRE_UPDATE) write( 'projects/start-sdk/changelog/patch-fixed-example.md', METADATA + '- Fixed feature.\n', ) commit() const notes = script().stdout const body = script('body').stdout contains(notes, '**[Full changelog') excludes(body, '**[Full changelog') contains( body, `\n${PRE_UPDATE}\n\n${MAIN.replace('Feature.', 'Fixed feature.')}\n
`, ) contains(body, '
\nFull changelog\n\n## [1.2.3]') contains(body, '- Fixed feature.\n\n
') excludes(body, '
') assert.equal(body.split(PRE_UPDATE).length - 1, 1) }) test('registry commands reject invalid highlights before writes', t => { const { root, write, run } = releaseFixture(t) write( 'projects/start-wrt/changelog/patch-fixed-invalid.md', '---\nrelease-notes: invalid\n---\n\n- Invalid.\n', ) const marker = join(root, 'registry-write') for (const command of [ 'refresh_registry_notes fixture-registry', 'cmd_register', ]) { const result = run( 'bash', [ '-c', ` source <(awk '/^SUBCOMMAND=/{exit} {print}' scripts/manage-release.sh) REPO_ROOT="$PWD"; PROJECT=start-wrt; KIND=wrt; VERSION=1.2.3 load_registry_index() { _INDEX_JSON='{"versions":{"1.2.3":{"sourceVersion":">=0.1.0"}}}'; } enter_release_dir() { :; } start-cli() { touch "${marker}"; } ${command} `, ], { encoding: 'utf8' }, ) assert.notEqual(result.status, 0) contains(result.stderr, 'release-notes: true or false') assert.equal(existsSync(marker), false) } }) test('main framing is optional', t => { const { root, main, write, script, git, commit } = releaseFixture(t) git('init', '-q') git('remote', 'add', 'origin', root) commit() write(`projects/start-sdk/release-notes/${VERSION}.pre-update.md`, PRE_UPDATE) unlinkSync(main) contains(script('notes').stdout, PRE_UPDATE) contains(script('body').stdout, '## Highlights') }) test('existing changelog link is replaced', t => { const { write, script } = releaseFixture(t) write( `projects/start-sdk/release-notes/${VERSION}.md`, MAIN.replace('- Feature.', '- Feature.\n\n**[Full changelog old](old)\n'), ) write(`projects/start-sdk/release-notes/${VERSION}.pre-update.md`, PRE_UPDATE) const notes = script().stdout excludes(notes, '](old)') assert.equal(notes.match(/Full changelog/g).length, 1) }) test('companion changes block adopted release', t => { const { root, preUpdate, write, run, git, commit } = releaseFixture(t) git('init', '-q') git('remote', 'add', 'origin', root) commit() let adopted = git('rev-parse', 'HEAD').trim() const command = `source <(awk '/^SUBCOMMAND=/{exit} {print}' "$1") REPO_ROOT="$2"; PROJECT=start-sdk; VERSION=1.2.3; COMMIT="$3" assert_metadata_matches_adopted` const check = () => run( 'bash', [ '-c', command, 'test', join(root, 'scripts/manage-release.sh'), root, adopted, ], { encoding: 'utf8' }, ) function blocked(name = '1.2.3.pre-update.md') { const result = check() assert.notEqual(result.status, 0) contains(result.stderr, name) contains(result.stderr, 'differs') } write(`projects/start-sdk/release-notes/${VERSION}.pre-update.md`, PRE_UPDATE) blocked() git('add', preUpdate) blocked() write( `projects/start-sdk/release-notes/${VERSION}.md`, MAIN + 'Uncommitted instruction.\n', ) blocked('1.2.3.md') write(`projects/start-sdk/release-notes/${VERSION}.md`, MAIN) commit() blocked() adopted = git('rev-parse', 'HEAD').trim() write( `projects/start-sdk/release-notes/${VERSION}.pre-update.md`, PRE_UPDATE + '\nChanged warning.\n', ) commit() blocked() adopted = git('rev-parse', 'HEAD').trim() unlinkSync(preUpdate) commit() blocked() adopted = git('rev-parse', 'HEAD').trim() write('projects/start-sdk/CHANGELOG.md', '') commit() const result = check() assert.equal(result.status, 0, result.stderr) write( 'projects/start-sdk/changelog/patch-fixed-example.md', '- Fixed a released feature.\n', ) commit() blocked('changelog') }) test('adopted release accepts compiled metadata after fragments are consumed', t => { const { root, write, run, git, commit } = releaseFixture(t) git('init', '-q') git('remote', 'add', 'origin', root) write(`projects/start-sdk/release-notes/${VERSION}.md`, 'Lede.\n') const path = write( 'projects/start-sdk/changelog/patch-fixed-public.md', METADATA + '- Released fix.\n', ) const translation = write( 'projects/start-sdk/changelog/patch-fixed-public.pl_PL.md', '- Poprawka.\n', ) const localizedPath = `projects/start-sdk/release-notes/${VERSION}.pl_PL.md` write(localizedPath, '## Nowości\n\n\n') commit() const adopted = git('rev-parse', 'HEAD').trim() git('-c', 'tag.gpgsign=false', 'tag', 'start-sdk/v1.2.3') const synced = run(process.execPath, ['scripts/changelog.mjs', 'sync']) assert.equal(synced.status, 0, synced.stderr.toString()) assert.equal(existsSync(path), false) assert.equal(existsSync(translation), false) commit() const command = `source <(awk '/^SUBCOMMAND=/{exit} {print}' scripts/manage-release.sh) REPO_ROOT="$PWD"; PROJECT=start-sdk; VERSION=1.2.3; COMMIT="${adopted}" assert_metadata_matches_adopted` const check = () => run('bash', ['-c', command], { encoding: 'utf8' }) const result = check() assert.equal(result.status, 0, result.stderr) const localized = text(join(root, localizedPath)) write(localizedPath, localized + '\nChanged translated notice.\n') const translatedChange = check() assert.notEqual(translatedChange.status, 0) contains(translatedChange.stderr, 'release-note bundle') write(localizedPath, localized) write( 'projects/start-sdk/changelog/patch-fixed-other.md', METADATA + '- New change.\n', ) assert.notEqual(check().status, 0) }) test('release uploads changelog from tag before publication', t => { const { root, env, write, script, git, commit, nodeExecutable, run } = releaseFixture(t) delete env.CHANGELOG_REF git('init', '-q') write(`projects/start-sdk/release-notes/${VERSION}.md`, FRAMING) write( 'projects/start-sdk/CHANGELOG.md', '# Changelog\n\n## [1.2.2]\n\n- Old change.\n', ) write( 'projects/start-sdk/changelog/patch-fixed-example.md', METADATA + '- Fixed the released feature.\n', ) commit() git('-c', 'tag.gpgsign=false', 'tag', 'start-sdk/v1.2.3') const remote = join(root, 'origin.git') git('init', '--bare', '-q', remote) git('remote', 'add', 'origin', remote) git('push', '-q', 'origin', 'HEAD:master', '--tags') write( 'projects/start-sdk/changelog/minor-added-later.md', METADATA + '- Later change.\n', ) git('add', 'projects') git( '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.com', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'later', ) write(`projects/start-sdk/release-notes/${VERSION}.md`, 'Later lede.\n') write( `projects/start-sdk/release-notes/${VERSION}.pre-update.md`, PRE_UPDATE + '\nLater warning.\n', ) const captured = join(root, 'uploaded.md') const bodyFile = join(root, 'release-body.md') const calls = join(root, 'gh-calls') nodeExecutable( 'bin/gh', `import { appendFileSync, copyFileSync } from 'node:fs' const args = process.argv.slice(2) appendFileSync(process.env.CALLS, JSON.stringify(args) + '\\n') if (args[1] === 'view') { if (!process.env.EXISTING_RELEASE) process.exit(1) console.log(process.env.RELEASE_DRAFT || 'false') } if (args.includes('--notes-file')) copyFileSync(args[args.indexOf('--notes-file') + 1], process.env.BODY) if (args[1] === 'upload') { if (process.env.FAIL_UPLOAD) process.exit(1) copyFileSync(args.find(arg => arg.endsWith('/CHANGELOG.md')), process.env.CAPTURE) } if (args.includes('--draft=false') && process.env.FAIL_PUBLISH) process.exit(1) `, ) Object.assign(env, { PATH: `${join(root, 'bin')}:${env.PATH}`, CAPTURE: captured, BODY: bodyFile, CALLS: calls, }) const ghCalls = () => text(calls) .trim() .split('\n') .map(line => JSON.parse(line)) const taggedCommit = git('rev-parse', 'start-sdk/v1.2.3') script('create-gh-release') let recorded = ghCalls() assert.deepEqual( recorded.map(args => args[1]), ['view', 'create', 'upload', 'edit'], ) assert.ok(recorded[1].includes('--draft')) assert.ok(recorded[1].includes('--verify-tag')) assert.ok(recorded[3].includes('--draft=false')) const body = text(bodyFile) excludes(body, '**[Full changelog') contains(body, '
\nFull changelog\n\n## [1.2.3]') contains(body, 'Fixed the released feature.\n\n
') excludes(body, 'Old change.') excludes(body, 'Later change.') excludes(body, 'Later lede.') excludes(body, 'Later warning.') contains(body, 'Lede.') excludes(body, '
args[1]), draft === 'true' ? ['view', 'edit', 'upload', 'edit'] : ['view', 'edit', 'upload'], ) assert.equal(text(bodyFile), body) assert.equal(text(captured), uploaded) assert.ok(!recorded[1].includes('--draft')) } env.FAIL_UPLOAD = '1' env.EXISTING_RELEASE = '' unlinkSync(calls) const published = join(root, 'published') const failed = run( 'bash', [ '-c', ` source <(awk '/^SUBCOMMAND=/{exit} {print}' scripts/manage-release.sh) REPO_ROOT="$PWD" PROJECT=start-sdk; KIND=npm; VERSION=1.2.3; TAG=start-sdk/v1.2.3 cmd_pre_check() { :; } cmd_tag() { :; } cmd_push() { touch "${published}"; } cmd_release `, ], { encoding: 'utf8' }, ) assert.notEqual(failed.status, 0) assert.equal(existsSync(published), false) assert.ok(!ghCalls().some(args => args.includes('--draft=false'))) delete env.FAIL_UPLOAD env.EXISTING_RELEASE = '1' env.RELEASE_DRAFT = 'true' env.FAIL_PUBLISH = '1' script('create-gh-release', 'start-sdk', false) delete env.FAIL_PUBLISH script('create-gh-release') write( 'projects/start-sdk/changelog/patch-fixed-example.md', 'invalid fragment\n', ) git('add', 'projects') git( '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.com', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'invalid', ) git('-c', 'tag.gpgsign=false', 'tag', '-f', 'start-sdk/v1.2.3') git('push', '-q', '--force', 'origin', 'refs/tags/start-sdk/v1.2.3') unlinkSync(captured) for (const existing of ['', '1']) { env.EXISTING_RELEASE = existing if (existsSync(calls)) unlinkSync(calls) script('create-gh-release', 'start-sdk', false) assert.equal(existsSync(captured), false) assert.equal(existsSync(calls), false) } }) test('resume requires a published release with inline changelog and attachment', t => { const { root, env, run, git, commit, nodeExecutable } = releaseFixture(t) git('init', '-q') git('remote', 'add', 'origin', root) commit() nodeExecutable( 'bin/gh', ` if (!process.env.RELEASE_METADATA) process.exit(1) console.log(process.env.RELEASE_METADATA) `, ) env.PATH = `${join(root, 'bin')}:${env.PATH}` const command = ` source <(awk '/^SUBCOMMAND=/{exit} {print}' scripts/manage-release.sh) REPO_ROOT="$PWD" PROJECT=start-wrt; KIND=wrt; VERSION=1.2.3; TAG=start-wrt/v1.2.3 missing_registry_assets() { echo missing; } detect_progress printf '%s' "\${DONE[create-gh-release]:-incomplete}" ` for (const [metadata, complete] of [ [null, false], [{ isDraft: false, assets: [], body: '' }, false], [{ isDraft: false, assets: [{ name: 'CHANGELOG.md' }], body: MAIN }, false], [ { isDraft: false, assets: [], body: 'Full changelog' }, false, ], [ { isDraft: true, assets: [{ name: 'CHANGELOG.md' }], body: 'Full changelog', }, false, ], [ { isDraft: false, assets: [{ name: 'CHANGELOG.md' }], body: 'Full changelog', }, true, ], ]) { env.RELEASE_METADATA = metadata ? JSON.stringify(metadata) : '' const result = run('bash', ['-c', command], { encoding: 'utf8' }) assert.equal(result.status, 0, result.stderr) assert.equal(result.stdout, complete ? '1' : 'incomplete') } }) test('release without fragments still has an expandable changelog', t => { const { root, script, git, commit } = releaseFixture(t) git('init', '-q') git('remote', 'add', 'origin', root) commit() const body = script('body').stdout contains( body, 'Full changelog\n\nNo changelog entries.\n\n
', ) }) test('StartOS registry and packaged welcome', t => { const { root, env, write, script, run, git, commit } = releaseFixture(t) const version = '0.4.0.2' write('projects/start-os/build/lib/firmware.json', '[]') write('projects/start-os/build/dpkg-deps/fixture', '') write('build/bash-completion', '') const source = join(REPO, 'projects/start-os/release-notes') for (const name of [`${version}.md`, `${version}.pre-update.md`]) write( `projects/start-os/release-notes/${name}`, readFileSync(join(source, name)), ) write( `projects/start-os/release-notes/${version}.pl_PL.md`, 'Polskie nowości\n', ) write(`projects/start-os/release-notes/${version}.de_DE.md`, 'Neuigkeiten\n') write('projects/start-os/release-notes/0.4.0.1.pl_PL.md', 'Starsze wydanie\n') env.VERSION = version const combined = script('notes', 'start-os').stdout contains(combined, '## ⚠️ Before You Update') contains(combined, 'only way to update from 0.3.5.1') contains(combined, text(join(source, `${version}.pre-update.md`)).trim()) const stage = join(root, 'image/usr/lib/startos') mkdirSync(stage, { recursive: true }) const versionFile = write('VERSION.txt', version) const renderTemps = () => readdirSync(root).filter(name => name.startsWith('.release-notes.')) const copies = join(root, 'copies') const harness = write( 'install.mk', `mkdir = $(if $(filter %/release-notes,$(1)),mkdir -p "$(1)",true) rm = true ln = true ls-files = $(wildcard $(1)/*) cp = $(if $(filter %/release-notes.md,$(2))$(findstring /release-notes/,$(2)),printf '%s\\n' "$(2)" >> "${copies}"; cp "$(1)" "$(2)",true) include ${REPO}/projects/start-os/build.mk notes-inputs: \t@printf '%s\\n' $(filter $(STARTOS_RELEASE_NOTES_SRC),$(STARTOS_TARGETS)) `, ) const inputs = run( 'make', ['-f', harness, 'notes-inputs', `VERSION_FILE=${versionFile}`], { cwd: root, encoding: 'utf8' }, ) assert.equal(inputs.status, 0, inputs.stderr) contains(inputs.stdout, 'scripts/changelog.mjs') contains( inputs.stdout, 'shared-libs/crates/start-core/locales/release-notes.json', ) contains(inputs.stdout, `${version}.md`) contains(inputs.stdout, `${version}.pl_PL.md`) contains(inputs.stdout, `${version}.de_DE.md`) excludes(inputs.stdout, 'pre-update') excludes(inputs.stdout, '0.4.0.1') const install = (makefile = harness) => run( 'make', [ '-f', makefile, 'start-os-install', 'STARTOS_TARGETS=', 'PLATFORM=x86_64', `VERSION_FILE=${versionFile}`, `DESTDIR=${root}/image`, 'ENVIRONMENT=', ], { cwd: root, encoding: 'utf8' }, ) const result = install() assert.equal(result.status, 0, result.stderr) const packaged = text(join(stage, 'release-notes.md')) assert.equal(packaged, text(join(source, `${version}.md`))) excludes(packaged, 'Before You Update') excludes(packaged, 'only way to update') contains(packaged, '## Highlights') assert.equal(text(join(stage, 'release-notes/pl_PL.md')), 'Polskie nowości\n') assert.equal(text(join(stage, 'release-notes/de_DE.md')), 'Neuigkeiten\n') assert.deepEqual(readdirSync(join(stage, 'release-notes')).sort(), [ 'de_DE.md', 'pl_PL.md', ]) assert.deepEqual(text(copies).trim().split('\n'), [ join(stage, 'release-notes.md'), join(stage, 'release-notes/de_DE.md'), join(stage, 'release-notes/pl_PL.md'), ]) assert.deepEqual(renderTemps(), []) git('init', '-q') write( `projects/start-os/release-notes/${version}.md`, 'Lede.\n\n## Important\n\nKeep this notice.\n', ) write( 'projects/start-os/changelog/patch-fixed-public.md', METADATA + '- Generated highlight.\n', ) write( 'projects/start-os/changelog/patch-changed-internal.md', '---\nrelease-notes: false\n---\n\n- Internal detail.\n', ) write( `projects/start-os/release-notes/${version}.pl_PL.md`, 'Polska wersja.\n\n## Nowości\n\n\n', ) write( `projects/start-os/release-notes/${version}.de_DE.md`, '## Neuigkeiten\n\n\n', ) write( 'projects/start-os/changelog/patch-fixed-public.pl_PL.md', '- Wygenerowana nowość.\n', ) write( 'projects/start-os/changelog/patch-fixed-public.fr_FR.md', '- Nouveauté générée.\n', ) const polish = 'Polska wersja.\n\n## Nowości\n\n- Wygenerowana nowość.\n' const german = '## Neuigkeiten\n\n- Generated highlight.\n' const french = '## Nouveautés\n\n- Nouveauté générée.\n' commit() const generated = install() assert.equal(generated.status, 0, generated.stderr) const welcome = text(join(stage, 'release-notes.md')) assert.equal( welcome, 'Lede.\n\n## Highlights\n\n- Generated highlight.\n\n## Important\n\nKeep this notice.\n', ) const registry = script('notes', 'start-os').stdout contains(registry, 'Generated highlight.') contains(registry, PRE_UPDATE.split('\n')[0]) excludes(welcome, 'Before You Update') excludes(registry, 'Internal detail.') assert.equal(text(join(stage, 'release-notes/pl_PL.md')), polish) assert.equal(text(join(stage, 'release-notes/de_DE.md')), german) assert.equal( text(join(stage, 'release-notes/fr_FR.md')), 'Lede.\n\n## Nouveautés\n\n- Nouveauté générée.\n\n## Important\n\nKeep this notice.\n', ) assert.deepEqual(renderTemps(), []) unlinkSync(join(root, `projects/start-os/release-notes/${version}.md`)) const withoutFraming = install() assert.equal(withoutFraming.status, 0, withoutFraming.stderr) const generatedOnly = text(join(stage, 'release-notes.md')) assert.equal(generatedOnly, '## Highlights\n\n- Generated highlight.\n') const remoteCopy = readFileSync(join(REPO, 'build/common.mk'), 'utf8') .match(/define cp\n[\s\S]+?\nendef/)[0] .replace('define cp', 'define remote-copy') const remoteHarness = write( 'remote-install.mk', `${text(harness)} TAR_BIN = tar ssh = sh -c $(subst sudo ,,$1) ${remoteCopy} cp = $(if $(filter %/release-notes.md,$(2))$(findstring /release-notes/,$(2)),$(call remote-copy,$(1),$(2)),true) `, ) for (const path of [ 'release-notes.md', 'release-notes/de_DE.md', 'release-notes/fr_FR.md', 'release-notes/pl_PL.md', ]) unlinkSync(join(stage, path)) const remote = install(remoteHarness) assert.equal(remote.status, 0, remote.stderr) assert.equal(text(join(stage, 'release-notes.md')), generatedOnly) assert.equal(text(join(stage, 'release-notes/de_DE.md')), german) assert.equal(text(join(stage, 'release-notes/pl_PL.md')), polish) assert.equal(text(join(stage, 'release-notes/fr_FR.md')), french) assert.deepEqual(renderTemps(), []) const copiedBeforeFailure = text(copies) write( 'projects/start-os/changelog/patch-fixed-public.md', '---\nrelease-notes: invalid\n---\n\n- Invalid.\n', ) const failed = install() assert.notEqual(failed.status, 0) contains(failed.stderr, 'release-notes: true or false') assert.equal(text(join(stage, 'release-notes.md')), generatedOnly) assert.equal(text(copies), copiedBeforeFailure) assert.deepEqual(renderTemps(), []) write( 'projects/start-os/changelog/patch-fixed-public.md', METADATA + '- Updated English highlight.\n', ) write( `projects/start-os/release-notes/${version}.pl_PL.md`, '## Nowości\n\n- Ręcznie napisane.\n', ) const localeFailure = install() assert.notEqual(localeFailure.status, 0) contains(localeFailure.stderr, 'localized framing needs ') assert.equal(text(join(stage, 'release-notes.md')), generatedOnly) assert.equal(text(copies), copiedBeforeFailure) assert.deepEqual(renderTemps(), []) }) test('registry payload composes localized notes, warnings, fallbacks and links', t => { const f = releaseFixture(t) const { root, env, write, script, run, git, commit } = f delete env.CHANGELOG_REF git('init', '-q') git('remote', 'add', 'origin', root) write(`projects/start-sdk/release-notes/${VERSION}.md`, FRAMING) write(`projects/start-sdk/release-notes/${VERSION}.pre-update.md`, PRE_UPDATE) write( `projects/start-sdk/release-notes/${VERSION}.pl_PL.md`, 'Wstęp.\n\n## Nowości\n\n\n\n## Ważne\n\nWażna uwaga.\n', ) write( `projects/start-sdk/release-notes/${VERSION}.pl_PL.pre-update.md`, '## Przed aktualizacją\n\nPolskie ostrzeżenie.\n', ) write( `projects/start-sdk/release-notes/${VERSION}.de_DE.md`, '## Neuigkeiten\n\n\n\n## Wichtig\n\nHinweis.\n', ) write(`projects/start-sdk/release-notes/${VERSION}.de_DE.pre-update.md`, '') write( `projects/start-sdk/release-notes/${VERSION}.es_ES.md`, '## Novedades\n\n\n', ) write( `projects/start-sdk/release-notes/${VERSION}.fr_FR.pre-update.md`, '## Avant la mise à jour\n\nAttention.\n', ) write( 'projects/start-sdk/changelog/patch-added-feature.md', METADATA + '- Feature.\n', ) write( 'projects/start-sdk/changelog/patch-added-feature.pl_PL.md', '- Funkcja.\n', ) write( 'projects/start-sdk/changelog/patch-fixed-other.md', METADATA + '- Another fix.\n', ) write( 'projects/start-sdk/changelog/patch-changed-internal.md', '---\nrelease-notes: false\n---\n\n- Internal.\n', ) commit() const snapshot = git('rev-parse', 'HEAD').trim() const payload = () => JSON.parse(script('registry-notes').stdout) const notes = payload() const translations = notes.releaseNotes const english = translations.en_US assert.deepEqual(Object.keys(translations).sort(), [ 'de_DE', 'en_US', 'es_ES', 'fr_FR', 'pl_PL', ]) assert.equal(script('notes').stdout, english) contains(english, PRE_UPDATE) contains(english, 'Lede.') contains(english, 'Follow up.') contains(english, '/releases/tag/start-sdk/v1.2.3') linkLast(english) contains(translations.pl_PL, 'Wstęp.') contains(translations.pl_PL, 'Ważna uwaga.') contains(translations.pl_PL, '- Funkcja.') contains(translations.pl_PL, '- Another fix.') contains(translations.pl_PL, 'Polskie ostrzeżenie.') contains(translations.pl_PL, 'Pełna lista zmian') excludes(translations.pl_PL, 'Prepare first.') linkLast(translations.pl_PL) contains(translations.de_DE, PRE_UPDATE) contains(translations.de_DE, '- Feature.') contains(translations.de_DE, '- Another fix.') contains(translations.de_DE, 'Vollständiges Änderungsprotokoll') linkLast(translations.de_DE) contains(translations.es_ES, PRE_UPDATE) contains(translations.es_ES, 'Registro completo de cambios') contains(translations.fr_FR, 'Attention.') contains(translations.fr_FR, '- Feature.') contains(translations.fr_FR, 'Journal complet des modifications') for (const body of [english, ...Object.values(translations)]) { excludes(body, 'Internal.') excludes(body, '') } const welcome = run( process.execPath, ['scripts/changelog.mjs', 'notes-bundle', 'projects/start-sdk', VERSION], { encoding: 'utf8' }, ) assert.equal(welcome.status, 0, welcome.stderr) excludes(welcome.stdout, 'Prepare first.') excludes(welcome.stdout, 'Polskie ostrzeżenie.') excludes(welcome.stdout, 'Attention.') excludes(welcome.stdout, 'Full changelog') const github = script('body').stdout contains(github, PRE_UPDATE) contains(github, 'Lede.') contains(github, 'Follow up.') excludes(github, 'Wstęp.') excludes(github, 'Full changelog for') env.CHANGELOG_REF = snapshot const pending = payload() contains(pending.releaseNotes.en_US, 'Full changelog fragments for v1.2.3') contains(pending.releaseNotes.en_US, 'changes awaiting release.') for (const [locale, label, description] of [ [ 'de_DE', 'Alle Änderungsfragmente für v1.2.3', 'noch nicht veröffentlichte Änderungen.', ], [ 'es_ES', 'Fragmentos del registro completo de cambios de v1.2.3', 'cambios pendientes de publicación.', ], [ 'fr_FR', 'Fragments du journal complet des modifications de la v1.2.3', 'modifications en attente de publication.', ], [ 'pl_PL', 'Fragmenty pełnej listy zmian w wersji 1.2.3', 'zmiany oczekujące na wydanie.', ], ]) { const body = pending.releaseNotes[locale] contains(body, label) contains(body, description) contains(body, `/tree/${snapshot}/projects/start-sdk/changelog`) } env.NOTES_REF = snapshot write( `projects/start-sdk/release-notes/${VERSION}.pl_PL.pre-update.md`, 'Later warning.\n', ) write( 'projects/start-sdk/changelog/patch-fixed-other.md', METADATA + '- Later fix.\n', ) assert.deepEqual( payload(), pending, 'publishing from a ref uses its notes and warnings', ) delete env.NOTES_REF delete env.CHANGELOG_REF }) test('registry and GitHub notes include localized Security without summary files', t => { const { root, main, write, script, git, commit } = releaseFixture(t) unlinkSync(main) git('init', '-q') git('remote', 'add', 'origin', root) write( 'projects/start-sdk/changelog/patch-added-feature.md', METADATA + '- Feature.\n', ) write( 'projects/start-sdk/changelog/patch-security-fix.md', METADATA + '- Security fix.\n', ) write( 'projects/start-sdk/changelog/patch-security-fix.pl_PL.md', '- Poprawka bezpieczeństwa.\n', ) write( 'projects/start-sdk/changelog/patch-security-internal.md', '---\nrelease-notes: false\n---\n\n- Internal security detail.\n', ) write(`projects/start-sdk/release-notes/${VERSION}.pre-update.md`, PRE_UPDATE) commit() const notes = JSON.parse(script('registry-notes').stdout).releaseNotes for (const [locale, highlights, security, body] of [ ['en_US', 'Highlights', 'Security', 'Security fix.'], ['pl_PL', 'Nowości', 'Bezpieczeństwo', 'Poprawka bezpieczeństwa.'], ]) { contains(notes[locale], `## ${highlights}`) contains(notes[locale], `## ${security}\n\n- ${body}`) contains(notes[locale], PRE_UPDATE) excludes(notes[locale], 'Internal security detail.') linkLast(notes[locale]) } assert.ok( !existsSync(join(root, `projects/start-sdk/release-notes/${VERSION}.md`)), ) assert.ok( !existsSync( join(root, `projects/start-sdk/release-notes/${VERSION}.pl_PL.md`), ), ) const github = script('body').stdout contains(github, '## Security\n\n- Security fix.') excludes(github, 'Poprawka bezpieczeństwa.') write( `projects/start-sdk/release-notes/${VERSION}.md`, '## Important\n\nKeep this notice.\n', ) const fallback = JSON.parse(script('registry-notes').stdout).releaseNotes .pl_PL contains(fallback, '## Important\n\nKeep this notice.') assert.ok( fallback.indexOf('## Bezpieczeństwo') < fallback.indexOf('## Important'), ) }) test('older source catalogs fall back to shared pending-fragment labels', t => { const { root, env, write, script, git, commit } = releaseFixture(t) git('init', '-q') git('remote', 'add', 'origin', root) const catalog = 'shared-libs/crates/start-core/locales/release-notes.json' const current = JSON.parse(text(join(root, catalog))) const older = structuredClone(current) delete older['release-notes.full-changelog-fragments'] write(catalog, JSON.stringify(older)) write(`projects/start-sdk/release-notes/${VERSION}.md`, FRAMING) write( `projects/start-sdk/release-notes/${VERSION}.pl_PL.md`, '## Nowości\n\n\n', ) write( 'projects/start-sdk/changelog/patch-fixed-feature.md', METADATA + '- Feature.\n', ) commit() const snapshot = git('rev-parse', 'HEAD').trim() write(catalog, JSON.stringify(current)) env.NOTES_REF = snapshot env.CHANGELOG_REF = snapshot const notes = JSON.parse(script('registry-notes').stdout) contains(notes.releaseNotes.en_US, 'Full changelog fragments for v1.2.3') contains( notes.releaseNotes.pl_PL, 'Fragmenty pełnej listy zmian w wersji 1.2.3', ) contains(notes.releaseNotes.pl_PL, 'zmiany oczekujące na wydanie.') contains( notes.releaseNotes.pl_PL, `/tree/${snapshot}/projects/start-sdk/changelog`, ) }) test('registry publishers pass the complete LocaleString as the release-notes argument', t => { const { root, env, write, run, script } = releaseFixture(t) write(`projects/start-wrt/release-notes/${VERSION}.md`, MAIN) write( `projects/start-wrt/release-notes/${VERSION}.pl_PL.md`, 'Polskie nowości.\n', ) const expected = JSON.parse(script('registry-notes', 'start-wrt').stdout) const capture = join(root, 'arguments') const harness = ` source <(awk '/^SUBCOMMAND=/{exit} {print}' scripts/manage-release.sh) REPO_ROOT="$PWD"; PROJECT=start-wrt; KIND=wrt; VERSION=1.2.3 load_registry_index() { _INDEX_JSON='{"versions":{"1.2.3":{"sourceVersion":">=0.1.0"}}}'; } enter_release_dir() { :; } release_files() { :; } start-cli() { if [ "$1" = registry ]; then echo '' else printf '%s\\0' "$@" > "${capture}" fi } ` for (const command of [ 'refresh_registry_notes fixture-registry', 'cmd_register', ]) { const result = run('bash', ['-c', harness + command], { encoding: 'utf8' }) assert.equal(result.status, 0, result.stderr) const args = text(capture).split('\0').slice(0, -1) assert.deepEqual(args.slice(1, 7), [ 'registry', 'os', 'version', 'add', VERSION, `v${VERSION}`, ]) assert.deepEqual(JSON.parse(args[7]), expected.releaseNotes) assert.equal(args.length, 9) } for (const [workflow, name] of [ ['startos-iso.yaml', 'Register OS version'], ['start-wrt.yaml', 'Register StartWRT version in the target registry'], ]) { const source = text(join(REPO, '.github/workflows', workflow)) const step = source .split(` - name: ${name}\n`)[1] .split(' run: |\n')[1] .split('\n - ')[0] const shell = step .split('\n') .map(line => line.slice(10)) .join('\n') .replaceAll('${{ env.REGISTRY }}', 'fixture-registry') .replaceAll('${{ github.sha }}', 'fixture-sha') const runnerTemp = join(root, 'runner') write('runner/release-notes.json', JSON.stringify(expected)) write('runner/release-security', 'true\n') const result = run( 'bash', [ '-e', '-c', `start-cli() { printf '%s\\0' "$@" > "${capture}"; }\n${shell}`, ], { encoding: 'utf8', env: { ...env, RUNNER_TEMP: runnerTemp } }, ) assert.equal(result.status, 0, result.stderr) const args = text(capture).split('\0').slice(0, -1) const notesIndex = workflow === 'startos-iso.yaml' ? 10 : 7 assert.deepEqual(JSON.parse(args[notesIndex]), expected.releaseNotes) assert.equal(args.length, notesIndex + 2) if (workflow === 'startos-iso.yaml') assert.deepEqual(args.slice(5, 8), [ '--git-hash', 'fixture-sha', '--security', ]) else { excludes(args.join(' '), '--git-hash') excludes(args.join(' '), '--security') } } }) test('localized compiler failures prevent registry writes and warning edits block alpha adoption', t => { const { root, write, run, git, commit } = releaseFixture(t) git('init', '-q') git('remote', 'add', 'origin', root) write(`projects/start-wrt/release-notes/${VERSION}.md`, FRAMING) write( `projects/start-wrt/release-notes/${VERSION}.pl_PL.md`, '## Nowości\n\n\n', ) write( 'projects/start-wrt/changelog/patch-fixed-feature.md', METADATA + '- Feature.\n', ) commit() const adopted = git('rev-parse', 'HEAD').trim() const harness = ` source <(awk '/^SUBCOMMAND=/{exit} {print}' scripts/manage-release.sh) REPO_ROOT="$PWD"; PROJECT=start-wrt; KIND=wrt; VERSION=1.2.3; COMMIT="${adopted}" ` const check = () => run('bash', ['-c', harness + 'assert_metadata_matches_adopted'], { encoding: 'utf8', }) assert.equal(check().status, 0) const warning = `projects/start-wrt/release-notes/${VERSION}.pl_PL.pre-update.md` write(warning, 'Translated preparation.\n') const changed = check() assert.notEqual(changed.status, 0) contains(changed.stderr, 'localized pre-update warnings') unlinkSync(join(root, warning)) const marker = join(root, 'registry-write') write( `projects/start-wrt/release-notes/${VERSION}.pl_PL.md`, '## Nowości\n\n- Invalid authored highlights.\n', ) for (const command of [ 'refresh_registry_notes fixture-registry', 'cmd_register', ]) { const failed = run( 'bash', [ '-c', harness + ` load_registry_index() { touch "${marker}"; } enter_release_dir() { touch "${marker}"; } start-cli() { touch "${marker}"; } ${command} `, ], { encoding: 'utf8' }, ) assert.notEqual(failed.status, 0) contains(failed.stderr, 'localized framing needs ') assert.equal(existsSync(marker), false) } }) test('registry source refs pin shared link translations and reject mismatched alpha metadata', t => { const { root, env, write, script, run, git, commit } = releaseFixture(t) delete env.CHANGELOG_REF git('init', '-q') git('remote', 'add', 'origin', root) write(`projects/start-sdk/release-notes/${VERSION}.md`, FRAMING) write( `projects/start-sdk/release-notes/${VERSION}.pl_PL.md`, '## Nowości\n\n\n', ) write( 'projects/start-sdk/changelog/patch-fixed-feature.md', METADATA + '- Feature.\n', ) commit() const adopted = git('rev-parse', 'HEAD').trim() const original = JSON.parse(script('registry-notes').stdout) const catalog = 'shared-libs/crates/start-core/locales/release-notes.json' const labels = JSON.parse(text(join(root, catalog))) labels['release-notes.full-changelog'].pl_PL = '**[Zmieniony odsyłacz %{version}](%{url})**' write(catalog, JSON.stringify(labels)) contains( JSON.parse(script('registry-notes').stdout).releaseNotes.pl_PL, 'Zmieniony odsyłacz', ) const pinned = run( process.execPath, [ 'scripts/changelog.mjs', 'registry-notes', 'projects/start-sdk', VERSION, '--ref', adopted, ], { encoding: 'utf8' }, ) assert.equal(pinned.status, 0, pinned.stderr) assert.deepEqual(JSON.parse(pinned.stdout), original) const mismatch = run( 'bash', [ '-c', ` source <(awk '/^SUBCOMMAND=/{exit} {print}' scripts/manage-release.sh) REPO_ROOT="$PWD"; PROJECT=start-sdk; KIND=npm; VERSION=1.2.3; COMMIT="${adopted}" assert_metadata_matches_adopted `, ], { encoding: 'utf8' }, ) assert.notEqual(mismatch.status, 0) contains(mismatch.stderr, 'release-note bundle') }) test('archived localized notes keep the changelog link last and its source URL', t => { const { root, env, write, script, run, git, commit } = releaseFixture(t) git('init', '-q') git('remote', 'add', 'origin', root) write(`projects/start-sdk/release-notes/${VERSION}.md`, FRAMING) write( `projects/start-sdk/release-notes/${VERSION}.pl_PL.md`, 'Wstęp.\n\n## Przedmowa\n\nUwaga.\n\n## Nowości\n\n\n\n## Ważne\n\nWażna uwaga.\n', ) write( 'projects/start-sdk/changelog/patch-fixed-feature.md', METADATA + '- Feature.\n', ) commit() git('-c', 'tag.gpgsign=false', 'tag', 'start-sdk/v1.2.3') env.CHANGELOG_REF = 'HEAD' const pending = JSON.parse(script('registry-notes').stdout) const synced = run(process.execPath, ['scripts/changelog.mjs', 'sync'], { encoding: 'utf8', }) assert.equal(synced.status, 0, synced.stderr) commit() const archived = JSON.parse(script('registry-notes').stdout) const polish = archived.releaseNotes.pl_PL contains(polish, '/blob/HEAD/projects/start-sdk/CHANGELOG.md#123') linkLast(polish) const pendingPolish = pending.releaseNotes.pl_PL contains(pendingPolish, 'Fragmenty pełnej listy zmian w wersji 1.2.3') contains(pendingPolish, '/tree/HEAD/projects/start-sdk/changelog') const withoutLink = body => body.replace(/^\*\*\[.+$/m, '') assert.equal(withoutLink(polish), withoutLink(pendingPolish)) }) test('CLI capability gates registry writes and CI uploads without gating pure notes', t => { const { root, env, write, script, run, nodeExecutable } = releaseFixture(t) write(`projects/start-wrt/release-notes/${VERSION}.md`, MAIN) const marker = join(root, 'registry-write') const helpCalls = join(root, 'help-calls') nodeExecutable( 'bin/start-cli', ` import { appendFileSync, writeFileSync } from 'node:fs' const args = process.argv.slice(2) if (args.at(-1) === '--help') { appendFileSync(process.env.HELP_CALLS, JSON.stringify(args) + '\\n') console.log(process.env.CLI_HELP || 'English-only CLI help') process.exit(Number(process.env.HELP_STATUS || 0)) } writeFileSync(process.env.WRITE_MARKER, JSON.stringify(args)) `, ) Object.assign(env, { PATH: `${join(root, 'bin')}:${env.PATH}`, HELP_CALLS: helpCalls, WRITE_MARKER: marker, }) script('notes', 'start-wrt') script('registry-notes', 'start-wrt') assert.equal(existsSync(marker), false) assert.equal( existsSync(helpCalls), false, 'note compilation must not require an installed CLI', ) const harness = ` source <(awk '/^SUBCOMMAND=/{exit} {print}' scripts/manage-release.sh) REPO_ROOT="$PWD"; PROJECT=start-wrt; KIND=wrt; VERSION=1.2.3 load_registry_index() { touch "${marker}"; } enter_release_dir() { touch "${marker}"; } ` for (const command of [ 'refresh_registry_notes fixture-registry', 'cmd_register', ]) { const rejected = run('bash', ['-c', harness + command], { encoding: 'utf8', }) assert.notEqual(rejected.status, 0) contains(rejected.stderr, 'update it before publishing registry notes') assert.equal(existsSync(marker), false) } for (const [workflow, product, upload] of [ ['startos-iso.yaml', 'start-os', 'Upload to S3'], ['start-wrt.yaml', 'start-wrt', 'Upload images to S3'], ]) { const source = text(join(REPO, '.github/workflows', workflow)).split( '\n deploy:\n', )[1] const check = source .split(' - name: Check start-cli registry notes support\n')[1] .split('\n - name: ')[0] const command = `./scripts/manage-release.sh check-registry-notes-cli ${product}` contains(check, command) assert.ok(source.indexOf(command) < source.indexOf(`- name: ${upload}`)) assert.ok( source.indexOf(command) < source.indexOf('- name: Drop the previous alpha build'), ) const rejected = run('bash', ['-e', '-c', command], { encoding: 'utf8' }) assert.notEqual(rejected.status, 0) contains(rejected.stderr, 'lacks LocaleString release notes') } env.CLI_HELP = 'Arguments:\n \n' script('check-registry-notes-cli', 'start-wrt') env.HELP_STATUS = '1' script('check-registry-notes-cli', 'start-wrt', false) assert.equal(existsSync(marker), false) for (const call of text(helpCalls).trim().split('\n')) assert.deepEqual(JSON.parse(call), [ 'registry', 'os', 'version', 'add', '--help', ]) }) for (const [project, kind] of [ ['start-os', 'os'], ['start-wrt', 'wrt'], ]) { for (const [name, indexCommand] of [ ['unavailable index', 'return 42'], ['missing version', `printf '%s\\n' '{"versions":{}}'`], ]) { test(`cosign-registry rejects ${name} for ${project}`, t => { const { root, env, run } = releaseFixture(t) env.HOME = join(root, 'home') const result = run( 'bash', [ '-c', ` source <(awk '/^SUBCOMMAND=/{exit} {print}' scripts/manage-release.sh) REPO_ROOT="$PWD"; PROJECT=${project}; KIND=${kind}; VERSION=${VERSION}; TAG=${project}/v${VERSION} mkdir -p "$(release_dir)" start-cli() { ${indexCommand}; } cmd_cosign_registry `, ], { encoding: 'utf8' }, ) assert.notEqual(result.status, 0) contains(result.stderr, `has no ${VERSION} entry`) excludes(result.stdout, 'Done. Developer signatures added') }) } } test('historical CRLF English notes keep the changelog link last', t => { const { write, script } = releaseFixture(t) write( `projects/start-sdk/release-notes/${VERSION}.md`, MAIN.replaceAll('\n', '\r\n'), ) const notes = script().stdout contains(notes, 'Lede.\r\n') linkLast(notes) })