From 6e06f474fa82d55c1e8e676427a158d82da6f0c5 Mon Sep 17 00:00:00 2001 From: vineyardbovines Date: Thu, 3 Sep 2026 12:57:22 -0400 Subject: [PATCH] Add release model CLI --- docs/release-model.md | 4 ++ scripts/release/cli.mjs | 114 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 scripts/release/cli.mjs diff --git a/docs/release-model.md b/docs/release-model.md index b9fbe7787f..1ebb01034e 100644 --- a/docs/release-model.md +++ b/docs/release-model.md @@ -51,3 +51,7 @@ androidVersionCode: 1110 `sourceTag` must equal `releaseVersion`, `sourceSha` must be a full Git object ID, and both build numbers must be positive integers. Each successful OTA adds exactly one contiguous section (`OTA 1`, `OTA 2`, and so on) inside the public changelog delimiters. GitHub Release text is extracted only from those delimiters; operational frontmatter is never published. + +## Command line usage + +The release model can be exercised locally with `node scripts/release/cli.mjs`. Run it without arguments to see the available commands for creating, validating, finalizing, and updating a release document. diff --git a/scripts/release/cli.mjs b/scripts/release/cli.mjs new file mode 100644 index 0000000000..46cdbae8a4 --- /dev/null +++ b/scripts/release/cli.mjs @@ -0,0 +1,114 @@ +#!/usr/bin/env node + +import {readFileSync, writeFileSync} from 'node:fs' + +import { + ReleaseError, + appendOtaChangelog, + createReleaseDocument, + deriveReleaseIdentity, + extractPublicChangelog, + finalizeReleaseDocument, + parseReleaseDocument, +} from './model.mjs' + +function usage() { + return `Usage: + node scripts/release/cli.mjs identity + node scripts/release/cli.mjs create + node scripts/release/cli.mjs validate [prepared|final] + node scripts/release/cli.mjs extract-public + node scripts/release/cli.mjs append-ota + node scripts/release/cli.mjs finalize ` +} + +function requireArgs(args, count) { + if (args.length !== count) throw new ReleaseError(usage()) +} + +function read(path) { + return readFileSync(path, 'utf8') +} + +function write(path, content) { + writeFileSync(path, content, 'utf8') +} + +function run([command, ...args]) { + switch (command) { + case 'identity': { + requireArgs(args, 1) + console.log(JSON.stringify(deriveReleaseIdentity(args[0]), null, 2)) + return + } + case 'create': { + requireArgs(args, 3) + const [version, changelogFile, outputFile] = args + write(outputFile, createReleaseDocument(version, read(changelogFile))) + console.log(`Created ${outputFile}`) + return + } + case 'validate': { + if (args.length < 1 || args.length > 2) throw new ReleaseError(usage()) + const [releaseFile, stage = 'prepared'] = args + const parsed = parseReleaseDocument(read(releaseFile), { + filename: releaseFile.split('/').at(-1), + stage, + }) + console.log( + `Validated ${releaseFile} as a ${stage} release for ${parsed.metadata.releaseVersion}`, + ) + return + } + case 'extract-public': { + requireArgs(args, 1) + process.stdout.write(`${extractPublicChangelog(read(args[0]))}\n`) + return + } + case 'append-ota': { + requireArgs(args, 3) + const [releaseFile, rawSequence, changelogFile] = args + const sequence = Number(rawSequence) + write( + releaseFile, + appendOtaChangelog(read(releaseFile), sequence, read(changelogFile)), + ) + console.log(`Added OTA ${rawSequence} to ${releaseFile}`) + return + } + case 'finalize': { + requireArgs(args, 5) + const [ + releaseFile, + sourceTag, + sourceSha, + iosBuildNumber, + androidVersionCode, + ] = args + write( + releaseFile, + finalizeReleaseDocument(read(releaseFile), { + sourceTag, + sourceSha, + iosBuildNumber, + androidVersionCode, + }), + ) + console.log(`Finalized ${releaseFile}`) + return + } + default: + throw new ReleaseError(usage()) + } +} + +try { + run(process.argv.slice(2)) +} catch (error) { + if (error instanceof ReleaseError) { + console.error(`Error: ${error.message}`) + process.exitCode = 1 + } else { + throw error + } +}