scripts/tests/prerelease-test.mjs
Rama master · réplica de lectura del repositorio original.
import assert from 'node:assert/strict'
import { copyFileSync, existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { test } from 'node:test'
import { fixture, REPO } from './fixtures.mjs'
const VERSION = '0.4.0.1'
const PREFIX = `start-os/v${VERSION}`
const ALPHA = 'https://alpha-registry-x.start9.com'
const BETA = 'https://beta-registry.start9.com'
function prereleaseFixture(t) {
const f = fixture(t)
const { root, env, write, git, run, nodeExecutable } = f
git('init', '-q', '-b', 'master')
git('config', 'user.name', 'Fixture')
git('config', 'user.email', 'fixture@example.com')
git('config', 'commit.gpgsign', 'false')
git('config', 'tag.gpgsign', 'false')
git('remote', 'add', 'origin', root)
for (const name of [
'manage-release.sh',
'changelog.mjs',
'changelog-version.mjs',
]) {
write(`scripts/${name}`, '')
copyFileSync(join(REPO, 'scripts', name), join(root, 'scripts', name))
}
const labels = 'shared-libs/crates/start-core/locales/release-notes.json'
write(labels, readFileSync(join(REPO, labels)))
write(
'projects/start-os/changelog/patch-fixed-localized.md',
'---\nrelease-notes: true\n---\n\n- Visible correction.\n',
)
write(
'projects/start-os/changelog/patch-fixed-localized.pl_PL.md',
'- Widoczna poprawka.\n',
)
write(
'package.json',
JSON.stringify({
version: VERSION,
devDependencies: JSON.parse(readFileSync(join(REPO, 'package.json')))
.devDependencies,
}),
)
copyFileSync(join(REPO, '.prettierrc.json'), write('.prettierrc.json', ''))
write('projects/start-os/Cargo.toml', '[package]\nversion = "0.4.0-rev.1"\n')
write(
'projects/start-os/docs/src/installing.md',
`https://github.com/Start9Labs/start-technologies/releases/tag/${PREFIX}\n`,
)
write(
'projects/start-os/changelog/patch-security-example.md',
'---\nrelease-notes: false\n---\n\n- Security correction.\n',
)
function commit() {
git(
'add',
'scripts',
'package.json',
'.prettierrc.json',
'projects',
'shared-libs',
)
git('commit', '--allow-empty', '-qm', 'fixture')
return git('rev-parse', 'HEAD').trim()
}
const imageCommit = commit()
Object.assign(env, {
HOME: join(root, 'home'),
FIXTURE_ROOT: root,
PATH: `${join(root, 'bin')}:${env.PATH}`,
})
write('home/.startos/id.key.pem', 'fixture')
const index = hash => ({
versions: {
[VERSION]: {
gitHash: hash,
sourceVersion: `>=0.3.5 <=${VERSION}`,
iso: { x86_64: { urls: [`https://images/${hash}.iso`] } },
squashfs: { x86_64: { urls: [`https://images/${hash}.squashfs`] } },
},
},
})
function alpha(hash) {
write('alpha.json', JSON.stringify(index(hash)))
}
alpha(imageCommit)
write('beta.json', '{"versions":{}}')
nodeExecutable(
'bin/start-cli',
`
import { readFileSync, writeFileSync, appendFileSync } from 'node:fs'
import { join } from 'node:path'
const root = process.env.FIXTURE_ROOT
const args = process.argv.slice(2)
const read = name => JSON.parse(readFileSync(join(root, name + '.json')))
const write = (name, data) => writeFileSync(join(root, name + '.json'), JSON.stringify(data))
const flag = name => args[args.indexOf(name) + 1]
const registry = args.find(arg => arg.startsWith('--registry='))?.slice(11)
const channel = registry === ${JSON.stringify(ALPHA)} ? 'alpha' : 'beta'
if (args.at(-1) === '--help') console.log('<RELEASE_NOTES_OR_JSON>')
else if (args.includes('index')) console.log(JSON.stringify(read(channel)))
else if (args.includes('promote')) {
appendFileSync(join(root, 'promotions'), 'promote\\n')
if (process.env.FAIL_PROMOTE === '1') process.exit(41)
if (flag('--expected-git-hash') !== read('alpha').versions[${JSON.stringify(VERSION)}].gitHash) throw new Error('promoted hash mismatch')
write('beta', read('alpha'))
} else if (args.includes('remove')) {
appendFileSync(join(root, 'removals'), 'remove\\n')
write(channel, {versions: {}})
} else if (args.includes('add')) {
const value = read(channel)
const item = value.versions[${JSON.stringify(VERSION)}]
if (flag('--expected-git-hash') !== item.gitHash) throw new Error('metadata hash mismatch')
item.gitHash = flag('--git-hash')
item.releaseNotes = JSON.parse(args.at(-2))
item.security = args.includes('--security')
write(channel, value)
} else throw new Error('unexpected start-cli args: ' + args.join(' '))
`,
)
nodeExecutable(
'bin/gh',
`
import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'node:fs'
import { join } from 'node:path'
const root = process.env.FIXTURE_ROOT
const args = process.argv.slice(2)
if (args[0] === 'auth') process.exit(0)
const tag = args.find(arg => arg.startsWith('start-os/v'))
const path = join(root, tag.replace('/', '_') + '.json')
const flag = name => args[args.indexOf(name) + 1]
let data = existsSync(path) ? JSON.parse(readFileSync(path)) : null
if (args[1] === 'view') {
if (!data) process.exit(1)
console.log(args.includes('--jq') ? data.isDraft : JSON.stringify(data))
process.exit(0)
}
appendFileSync(join(root, 'gh-events'), args.join(' ') + '\\n')
if (args[1] === 'create') data = {isDraft: true, assets: []}
if (!data) throw new Error('release missing')
if (args.includes('--notes-file')) data.body = readFileSync(flag('--notes-file'), 'utf8')
if (args.includes('--title')) data.title = flag('--title')
if (args.includes('--prerelease')) data.isPrerelease = true
if (args.includes('--draft=false')) data.isDraft = false
if (args[1] === 'upload') {
if (process.env.FAIL_UPLOAD === '1') process.exit(42)
const name = 'CHANGELOG.md'
if (!data.assets.some(asset => asset.name === name)) data.assets.push({name})
}
writeFileSync(path, JSON.stringify(data))
`,
)
function release(success = true, extra = '') {
const result = run(
'bash',
[
'-c',
`
source <(awk '/^SUBCOMMAND=/{exit} {print}' scripts/manage-release.sh)
REPO_ROOT="$PWD"; PROJECT=start-os; KIND=os; VERSION=${VERSION}; TAG=${PREFIX}
OS_PLATFORMS=x86_64
cmd_pull() { mkdir -p "$(release_dir)"; }
cmd_push_gz() { :; }
cmd_sign() { :; }
generated_sections() { :; }
gpg() { :; }
b3sum() { :; }
s3cmd() { :; }
${extra}
cmd_pre_release
`,
],
{ encoding: 'utf8' },
)
if (success) assert.equal(result.status, 0, result.stdout + result.stderr)
else assert.notEqual(result.status, 0)
return result
}
const data = name => JSON.parse(readFileSync(join(root, name), 'utf8'))
return { ...f, imageCommit, commit, alpha, release, data }
}
test('prerelease adopts the indexed image commit and allocates another RC after promotion failure', t => {
const { env, write, git, root, imageCommit, commit, release, data } =
prereleaseFixture(t)
git('tag', 'start-os/v0.4.0-rc.99')
write('projects/unrelated/file', 'later master change')
const head = commit()
assert.notEqual(head, imageCommit)
env.FAIL_PROMOTE = '1'
release(false)
assert.equal(git('rev-parse', `${PREFIX}-rc.1^{commit}`).trim(), imageCommit)
const published = data(`start-os_v${VERSION}-rc.1.json`)
assert.equal(published.isPrerelease, true)
assert.equal(published.isDraft, false)
assert.equal(published.title, `StartOS v${VERSION}-rc.1`)
assert.ok(published.body.includes('Security correction.'))
assert.equal(data('alpha.json').versions[VERSION].gitHash, imageCommit)
assert.equal(data('alpha.json').versions[VERSION].security, true)
delete env.FAIL_PROMOTE
release()
assert.equal(
git('tag', '--list', `${PREFIX}-rc.*`).trim(),
`${PREFIX}-rc.1\n${PREFIX}-rc.2`,
)
assert.equal(data('beta.json').versions[VERSION].gitHash, imageCommit)
assert.equal(data('beta.json').versions[VERSION].security, true)
const notes = data('beta.json').versions[VERSION].releaseNotes
assert.ok(notes.en_US.includes('Visible correction.'))
assert.ok(notes.pl_PL.includes('Widoczna poprawka.'))
assert.ok(notes.pl_PL.includes('Pełna lista zmian'))
assert.equal(existsSync(join(root, 'removals')), false)
assert.equal(
JSON.parse(readFileSync(join(root, 'package.json'))).version,
VERSION,
)
})
test('another invocation creates a new RC even when the previous draft is incomplete', t => {
const { env, release, git, data, root } = prereleaseFixture(t)
env.FAIL_UPLOAD = '1'
release(false)
assert.equal(data(`start-os_v${VERSION}-rc.1.json`).isDraft, true)
assert.equal(existsSync(join(root, 'promotions')), false)
delete env.FAIL_UPLOAD
release()
assert.equal(data(`start-os_v${VERSION}-rc.1.json`).isDraft, true)
assert.equal(data(`start-os_v${VERSION}-rc.2.json`).isDraft, false)
assert.equal(
git('tag', '--list', `${PREFIX}-rc.*`).trim(),
`${PREFIX}-rc.1\n${PREFIX}-rc.2`,
)
assert.ok(
readFileSync(join(root, 'gh-events'), 'utf8').includes('--latest=false'),
)
})
test('the same image commit gets a new GitHub prerelease and replaces beta', t => {
const { release, git, data, root, imageCommit } = prereleaseFixture(t)
release()
release()
assert.equal(git('rev-parse', `${PREFIX}-rc.2^{commit}`).trim(), imageCommit)
assert.equal(data(`start-os_v${VERSION}-rc.1.json`).isPrerelease, true)
assert.equal(data(`start-os_v${VERSION}-rc.2.json`).isPrerelease, true)
assert.deepEqual(Object.keys(data('beta.json').versions), [VERSION])
assert.equal(data('beta.json').versions[VERSION].gitHash, imageCommit)
assert.equal(readFileSync(join(root, 'removals'), 'utf8'), 'remove\n')
assert.equal(
readFileSync(join(root, 'promotions'), 'utf8'),
'promote\npromote\n',
)
})
test('new image commit increments the RC and replaces the old beta entry', t => {
const { write, commit, alpha, release, git, data, root } =
prereleaseFixture(t)
release()
write('projects/start-os/build-input', 'changed image')
const next = commit()
alpha(next)
release()
assert.equal(git('rev-parse', `${PREFIX}-rc.2^{commit}`).trim(), next)
assert.equal(data('beta.json').versions[VERSION].gitHash, next)
assert.equal(readFileSync(join(root, 'removals'), 'utf8'), 'remove\n')
assert.ok(
data(`start-os_v${VERSION}-rc.2.json`).body.includes(
'Security correction.',
),
)
for (const number of [1, 2]) {
assert.ok(
existsSync(
join(root, 'home/Downloads', `start-os_v${VERSION}-rc.${number}`),
),
)
}
})
test('missing image hash and explicit commit mismatch fail before tagging or publishing', t => {
const { write, release, git, imageCommit, commit, env, root } =
prereleaseFixture(t)
write('projects/unrelated/file', 'later change')
const other = commit()
env.COMMIT = other
const mismatch = release(false)
assert.ok(
mismatch.stderr.includes(
'The tag has to point at the commit that produced the artifacts',
),
)
delete env.COMMIT
write(
'alpha.json',
JSON.stringify({ versions: { [VERSION]: { sourceVersion: '>=0.3.5' } } }),
)
assert.ok(release(false).stderr.includes('has no gitHash'))
assert.equal(git('tag', '--list', `${PREFIX}-rc.*`).trim(), '')
assert.equal(existsSync(join(root, 'gh-events')), false)
assert.notEqual(imageCommit, other)
})
test('docs sync excludes StartOS RC tags at the trigger', () => {
const workflow = readFileSync(
join(REPO, '.github/workflows/docs-sync-on-tag.yml'),
'utf8',
)
assert.ok(workflow.includes("tags: ['*/v*', '!start-os/v*-rc.*']"))
})