Refine release model
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
# Release model
|
# Release model
|
||||||
|
|
||||||
The cactus release workflows use one canonical release document per native
|
Each app release gets one document that keeps its version, build information,
|
||||||
version. This contract is intentionally independent of the existing production
|
and public changelog together in one place. We are building this new release
|
||||||
workflows while the new system is being developed and tested.
|
model alongside the existing production workflows so we can try it safely
|
||||||
|
before using it for a real release.
|
||||||
|
|
||||||
## Identity
|
## Identity
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
"start": "expo start --dev-client",
|
"start": "expo start --dev-client",
|
||||||
"start:prod": "expo start --dev-client --no-dev --minify",
|
"start:prod": "expo start --dev-client --no-dev --minify",
|
||||||
"test": "NODE_ENV=test jest --forceExit --testTimeout=20000 --bail",
|
"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-watch": "NODE_ENV=test jest --watchAll",
|
||||||
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
|
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
|
||||||
"test-coverage": "NODE_ENV=test jest --coverage",
|
"test-coverage": "NODE_ENV=test jest --coverage",
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ const FRONTMATTER_KEYS = [
|
|||||||
const PUBLIC_CHANGELOG_START = '<!-- public-changelog:start -->'
|
const PUBLIC_CHANGELOG_START = '<!-- public-changelog:start -->'
|
||||||
const PUBLIC_CHANGELOG_END = '<!-- public-changelog:end -->'
|
const PUBLIC_CHANGELOG_END = '<!-- public-changelog:end -->'
|
||||||
|
|
||||||
export class ReleaseModelError extends Error {
|
export class ReleaseError extends Error {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = 'ReleaseModelError'
|
this.name = 'ReleaseError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fail(message) {
|
function fail(message) {
|
||||||
throw new ReleaseModelError(message)
|
throw new ReleaseError(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertReleaseVersion(version) {
|
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