scripts/tests/release-notes-test.mjs
Rama master · réplica de lectura del repositorio original.
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<details>`,
)
contains(body, '<details>\n<summary>Full changelog</summary>\n\n## [1.2.3]')
contains(body, '- Fixed feature.\n\n</details>')
excludes(body, '<details open')
excludes(notes, '<details>')
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<!-- highlights -->\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, '<details>\n<summary>Full changelog</summary>\n\n## [1.2.3]')
contains(body, 'Fixed the released feature.\n\n</details>')
excludes(body, 'Old change.')
excludes(body, 'Later change.')
excludes(body, 'Later lede.')
excludes(body, 'Later warning.')
contains(body, 'Lede.')
excludes(body, '<details open')
const uploaded = text(captured)
contains(uploaded, '## [1.2.3]')
contains(uploaded, 'Fixed the released feature.')
contains(uploaded, 'Old change.')
excludes(uploaded, 'Later change.')
assert.equal(git('rev-parse', 'start-sdk/v1.2.3'), taggedCommit)
env.EXISTING_RELEASE = '1'
for (const draft of ['true', 'false']) {
env.RELEASE_DRAFT = draft
unlinkSync(calls)
script('create-gh-release')
recorded = ghCalls()
assert.deepEqual(
recorded.map(args => 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: '<summary>Full changelog</summary>' },
false,
],
[
{
isDraft: true,
assets: [{ name: 'CHANGELOG.md' }],
body: '<summary>Full changelog</summary>',
},
false,
],
[
{
isDraft: false,
assets: [{ name: 'CHANGELOG.md' }],
body: '<summary>Full changelog</summary>',
},
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,
'<summary>Full changelog</summary>\n\nNo changelog entries.\n\n</details>',
)
})
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<!-- highlights -->\n',
)
write(
`projects/start-os/release-notes/${version}.de_DE.md`,
'## Neuigkeiten\n\n<!-- highlights -->\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 <!-- highlights -->')
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<!-- highlights -->\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<!-- highlights -->\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<!-- highlights -->\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, '<!-- highlights -->')
}
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<!-- highlights -->\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 '<RELEASE_NOTES_OR_JSON>'
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<!-- highlights -->\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 <!-- highlights -->')
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<!-- highlights -->\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<!-- highlights -->\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, '<changelog link>')
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 <RELEASE_NOTES_OR_JSON>\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)
})