Refine release model

This commit is contained in:
vineyardbovines
2026-09-03 10:37:24 -04:00
parent be13e2e785
commit b0a79ab2d0
4 changed files with 7 additions and 126 deletions
+4 -3
View File
@@ -1,8 +1,9 @@
# Release model
The cactus release workflows use one canonical release document per native
version. This contract is intentionally independent of the existing production
workflows while the new system is being developed and tested.
Each app release gets one document that keeps its version, build information,
and public changelog together in one place. We are building this new release
model alongside the existing production workflows so we can try it safely
before using it for a real release.
## Identity
-1
View File
@@ -56,7 +56,6 @@
"start": "expo start --dev-client",
"start:prod": "expo start --dev-client --no-dev --minify",
"test": "NODE_ENV=test jest --forceExit --testTimeout=20000 --bail",
"test:release-model": "node --test scripts/release/model.test.mjs",
"test-watch": "NODE_ENV=test jest --watchAll",
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "NODE_ENV=test jest --coverage",
+3 -3
View File
@@ -19,15 +19,15 @@ const FRONTMATTER_KEYS = [
const PUBLIC_CHANGELOG_START = '<!-- public-changelog:start -->'
const PUBLIC_CHANGELOG_END = '<!-- public-changelog:end -->'
export class ReleaseModelError extends Error {
export class ReleaseError extends Error {
constructor(message) {
super(message)
this.name = 'ReleaseModelError'
this.name = 'ReleaseError'
}
}
function fail(message) {
throw new ReleaseModelError(message)
throw new ReleaseError(message)
}
export function assertReleaseVersion(version) {
-119
View File
@@ -1,119 +0,0 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
ReleaseModelError,
appendOtaChangelog,
createReleaseDocument,
deriveReleaseIdentity,
extractPublicChangelog,
finalizeReleaseDocument,
parseReleaseDocument,
} from './model.mjs'
const SHA = '0123456789abcdef0123456789abcdef01234567'
test('derives every release identifier from one version', () => {
assert.deepEqual(deriveReleaseIdentity('1.131.1'), {
version: '1.131.1',
branch: 'release-1.131.1',
tag: '1.131.1',
filename: 'RELEASE-1.131.1.md',
githubReleaseName: 'Release 1.131.1',
})
})
test('rejects non-strict release versions', () => {
for (const version of [
'v1.131.1',
'1.131',
'1.131.1-beta',
' 1.131.1',
'1.0131.1',
]) {
assert.throws(() => deriveReleaseIdentity(version), ReleaseModelError)
}
})
test('creates and parses a prepared release document', () => {
const document = createReleaseDocument('1.131.1', '- Added something')
const parsed = parseReleaseDocument(document, {
filename: 'RELEASE-1.131.1.md',
})
assert.deepEqual(parsed.metadata, {releaseVersion: '1.131.1'})
assert.equal(parsed.sections.length, 1)
assert.equal(parsed.sections[0].type, 'initial')
assert.equal(
extractPublicChangelog(document),
'## Initial release\n\n- Added something',
)
})
test('finalizes a release using artifact-derived metadata', () => {
const prepared = createReleaseDocument('1.131.1', '- Added something')
const finalized = finalizeReleaseDocument(prepared, {
sourceTag: '1.131.1',
sourceSha: SHA,
iosBuildNumber: 1662,
androidVersionCode: 1110,
})
const parsed = parseReleaseDocument(finalized, {stage: 'final'})
assert.equal(parsed.metadata.sourceSha, SHA)
assert.equal(parsed.metadata.iosBuildNumber, '1662')
assert.equal(parsed.metadata.androidVersionCode, '1110')
})
test('requires all operational metadata in the final state', () => {
const prepared = createReleaseDocument('1.131.1', '- Added something')
assert.throws(
() => parseReleaseDocument(prepared, {stage: 'final'}),
/missing 'sourceTag'/,
)
})
test('appends contiguous OTA changelog sections', () => {
let document = createReleaseDocument('1.131.1', '- Initial change')
document = appendOtaChangelog(document, 1, '- First fix')
document = appendOtaChangelog(document, 2, '- Second fix')
const parsed = parseReleaseDocument(document)
assert.deepEqual(
parsed.sections.map(section => section.sequence),
[null, 1, 2],
)
assert.match(parsed.publicChangelog, /## OTA 2\n\n- Second fix$/)
})
test('rejects skipped OTA sequence numbers', () => {
const document = createReleaseDocument('1.131.1', '- Initial change')
assert.throws(() => appendOtaChangelog(document, 2, '- A fix'), /must be 1/)
})
test('rejects filename, metadata, and changelog inconsistencies', () => {
const document = createReleaseDocument('1.131.1', '- Initial change')
assert.throws(
() => parseReleaseDocument(document, {filename: 'RELEASE-1.132.0.md'}),
/Filename must be/,
)
assert.throws(
() =>
parseReleaseDocument(document.replace('releaseVersion:', 'surprise:')),
/Unknown release frontmatter field/,
)
assert.throws(
() => parseReleaseDocument(document.replace('Initial release', 'OTA 1')),
/must begin with an '## Initial release'/,
)
assert.throws(
() =>
parseReleaseDocument(
document.replace(
'- Initial change',
'- Initial change\n\n## Notes\n\nNope',
),
),
/Unsupported public changelog section/,
)
})