release: the Linux / Windows updater signatures are made on the VDS (desktop-release add-remote, vds-key; tools/vds-sign.ts); the Mac no longer downloads those installers, verifies each returned .sig over its BLAKE2b-512 digest before publishing; release.ts --sign-local keeps the old path
This commit is contained in:
parent
d94bf67340
commit
ef3c66e8f0
6 changed files with 444 additions and 51 deletions
|
|
@ -7,6 +7,7 @@
|
|||
// rehearsal/<n>: a prerelease on git.crt.fyi and the rehearsal
|
||||
// channel of storebound.dev (no app reads it), never npm and never
|
||||
// /Applications; everything it made is deleted at the end
|
||||
// bun tools/release.ts <version> [--sign-local] fallback: download the Linux / Windows installers and sign here
|
||||
// bun tools/release.ts <version> --resume --yes after a stop past the push: HEAD is origin/main, tagged v<version>,
|
||||
// with that version; skips the bump and continues from step 4
|
||||
// bun tools/release.ts npm-token once: the npm granular token from the clipboard to ~/.storebound-keys
|
||||
|
|
@ -22,8 +23,10 @@
|
|||
// 4. at once, in parallel: the Mac build (tools/desktop-release.ts mac: universal app, sign, notarize app and DMG,
|
||||
// every check), Forgejo's ci run of the bump commit (version-only: the installers only, the parent's CI and
|
||||
// matrix count) and the promotion of the tag (release.yml); this waits for the candidate (heartbeat, timeout)
|
||||
// and downloads its Linux / Windows installers while the Mac builds;
|
||||
// 5. add (updater signatures of the Linux / Windows files), the full acceptance matrix (9 runs), verify;
|
||||
// (with --sign-local also downloads its Linux / Windows installers while the Mac builds);
|
||||
// 5. add-remote: the VDS fetches the Linux / Windows files of the release, checks their SHA-256 and signs them with
|
||||
// its copy of the updater key; only the .sig texts come back and are verified here (--sign-local: the old path,
|
||||
// the files downloaded to this Mac in step 4 and signed here); the acceptance matrix (TESTS), verify;
|
||||
// 6. publish: the VDS fetches the Linux / Windows files from Forgejo itself, rsync sends only the Mac's files,
|
||||
// the VDS attaches them to the Forgejo release from its copy, then storebound.dev's latest.json;
|
||||
// 7. /Applications/storebound.app: the running app is quit (gracefully, then its exact PID), the new one copied
|
||||
|
|
@ -55,6 +58,18 @@ const TESTS = false;
|
|||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// Pure parts (tests/release.test.ts)
|
||||
|
||||
/** The Linux / Windows assets of the promoted release that the VDS signs, with the SHA-256 of the candidate's record
|
||||
* (green.json). An installer without a recorded hash is refused: the VDS checks every byte against it. */
|
||||
export function vdsSignAssets(assets: { name: string; size: number; browser_download_url: string }[], recorded: Record<string, string>): { name: string; url: string; sha256: string; size: number }[] {
|
||||
const mine = assets.filter(a => /\.(AppImage|deb|exe)$/.test(a.name));
|
||||
if (!mine.length) throw new Error('the release has no Linux / Windows installers');
|
||||
return mine.map(a => {
|
||||
const sha256 = recorded[a.name];
|
||||
if (!sha256 || !/^[0-9a-f]{64}$/.test(sha256)) throw new Error(`${a.name}: no SHA-256 in the candidate's record`);
|
||||
return { name: a.name, url: a.browser_download_url, sha256, size: a.size };
|
||||
}).sort((x, y) => x.name.localeCompare(y.name));
|
||||
}
|
||||
|
||||
/** The CHANGELOG with its Unreleased section turned into the version's section and a new empty Unreleased on top. */
|
||||
export function cutChangelog(text: string, version: string, date: string): string {
|
||||
const lines = text.split('\n');
|
||||
|
|
@ -234,6 +249,7 @@ async function main(): Promise<void> {
|
|||
if (argv[0] === 'mac-acceptance') { const key = cliKey(ROOT); console.log(await macAcceptance(key, join(RELEASES, 'logs', `acceptance-${key}`))); return; }
|
||||
const rehearsal = has('--rehearsal');
|
||||
const resume = has('--resume');
|
||||
const signLocal = has('--sign-local');
|
||||
if (resume && rehearsal) fail('--resume is for a real version');
|
||||
const positional = argv.filter(a => !a.startsWith('--'));
|
||||
const pkgVersion = COPIES[0]!.read(readFileSync(join(ROOT, 'package.json'), 'utf8'))!;
|
||||
|
|
@ -342,15 +358,28 @@ async function main(): Promise<void> {
|
|||
const s = await waitGreen(sha, 45);
|
||||
return s;
|
||||
});
|
||||
const fetchP = candP.then(s => phase('download Linux / Windows installers + acceptance runs', () => fetchCandidate(s, dl)));
|
||||
// the Linux / Windows installers stay off this Mac: the VDS fetches and signs them (owner, 21 Oct 2026); only
|
||||
// --sign-local (the fallback) downloads them here to sign with ~/.storebound-keys
|
||||
const fetchP = signLocal ? candP.then(s => phase('download Linux / Windows installers + acceptance runs', () => fetchCandidate(s, dl)))
|
||||
: TESTS ? candP.then(s => phase('download acceptance runs', () => fetchCandidate(s, dl, /^acceptance-.*\.json$/))) : Promise.resolve([]);
|
||||
const relP = candP.then(() => phase('Forgejo: release.yml promotes the tag', () => waitRelease(tag, 15)));
|
||||
await Promise.all([macP, fetchP, relP, accP]);
|
||||
const cand = await candP;
|
||||
|
||||
// ---------------------------------------------------------------- 5. add, acceptance matrix, verify
|
||||
await phase('add Linux / Windows + acceptance matrix + verify', async () => {
|
||||
const files = readdirSync(dl).filter(f => /\.(AppImage|deb|exe)$/.test(f)).map(f => join(dl, f));
|
||||
sh(['bun', 'tools/desktop-release.ts', 'add', '--out', out, '--commit', sha, '--run', cand.record.run, ...files]);
|
||||
if (signLocal) {
|
||||
const files = readdirSync(dl).filter(f => /\.(AppImage|deb|exe)$/.test(f)).map(f => join(dl, f));
|
||||
sh(['bun', 'tools/desktop-release.ts', 'add', '--out', out, '--commit', sha, '--run', cand.record.run, ...files]);
|
||||
} else {
|
||||
const rel = (await listReleases()).find(r => r.tag_name === tag) ?? fail(`no Forgejo release ${tag}`);
|
||||
const plan = vdsSignAssets(rel.assets, cand.record.files);
|
||||
const assetsFile = join(logDir, 'vds-sign-assets.json');
|
||||
mkdirSync(logDir, { recursive: true });
|
||||
writeFileSync(assetsFile, `${JSON.stringify(plan, null, 2)}\n`);
|
||||
const code = await bg('VDS sign', ['bun', 'tools/desktop-release.ts', 'add-remote', '--out', out, '--host', host, '--assets', assetsFile, '--commit', sha, '--run', cand.record.run], join(logDir, 'add-remote.log'), { timeoutMs: 35 * 60_000 });
|
||||
if (code !== 0) fail(`VDS signing failed: ${join(logDir, 'add-remote.log').replace(HOME, '~')} (fallback: --resume --sign-local)`);
|
||||
}
|
||||
if (TESTS) {
|
||||
const accDir = mkdtempSync(join(tmpdir(), 'sb-acc-'));
|
||||
for (const f of readdirSync(dl).filter(f => /^acceptance-.*\.json$/.test(f))) writeFileSync(join(accDir, f), readFileSync(join(dl, f)));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue