Refine release model
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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/,
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user