From f4189b2e71e6acb6120c208ef2011ed1613425b7 Mon Sep 17 00:00:00 2001 From: agent-linusswartz Date: Thu, 30 Jul 2026 15:30:19 -0300 Subject: [PATCH] fix(release): make checksum manifest portable --- docs/releasing.md | 2 +- scripts/contract-tests/release-utils.test.mjs | 18 ++++++++++++++++++ scripts/release-assets.mjs | 8 ++++++-- scripts/release-utils.mjs | 13 +++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index 6cd07f3..72c7964 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -19,7 +19,7 @@ The exact order and ownership of static, contract, artifact/runtime, browser, se `index.html` and `sbom.spdx.json` are tracked, reproducible release evidence. Versioned `dist/` artifacts, `SHA256SUMS`, and the temporary Pages `_site/` tree are generated outputs and must not be committed as release history. -Generate and verify the portable checksum file with `npm run generate:release-checksums` and `npm run validate:release-assets`. After publication, verify provenance with `gh attestation verify dist/hSQLite-Editor-v.html --repo /`. Verify the associated SPDX predicate by adding `--predicate-type https://spdx.dev/Document/v2.3`. +Generate and verify the portable checksum file with `npm run generate:release-checksums` and `npm run validate:release-assets`. Published checksum entries use release asset basenames, so users can download the HTML, SBOM, and `SHA256SUMS` into one directory and run `shasum -a 256 -c SHA256SUMS`. After publication, verify provenance with `gh attestation verify dist/hSQLite-Editor-v.html --repo /`. Verify the associated SPDX predicate by adding `--predicate-type https://spdx.dev/Document/v2.3`. Immutable release publication is atomic: create the exact tag and draft, build from that tag, validate, attest, upload the complete asset bundle, then publish. The workflow forbids `--clobber`. If upload is partial or an asset-name conflict occurs, delete and recreate the unpublished draft after diagnosing the cause; never repair a release by overwriting an asset. diff --git a/scripts/contract-tests/release-utils.test.mjs b/scripts/contract-tests/release-utils.test.mjs index ca2b671..adef2d2 100644 --- a/scripts/contract-tests/release-utils.test.mjs +++ b/scripts/contract-tests/release-utils.test.mjs @@ -6,6 +6,7 @@ import { escapeRegExp, extractInlineScripts, extractMarkupText, + formatReleaseChecksumLine, getReleaseArtifactPath, getReleaseTag } from "../release-utils.mjs"; @@ -75,3 +76,20 @@ test("release tuple helpers reject unsafe package names and versions", () => { assert.throws(() => assertReleasePackageName(packageName), /Invalid release package name/); } }); + +test("release checksum lines use flat public asset names", () => { + const digest = "a".repeat(64); + + assert.equal( + formatReleaseChecksumLine("/tmp/release/dist/hSQLite-Editor-v0.6.1.html", digest), + `${digest} hSQLite-Editor-v0.6.1.html` + ); + assert.equal( + formatReleaseChecksumLine("/tmp/release/sbom.spdx.json", digest), + `${digest} sbom.spdx.json` + ); + assert.throws( + () => formatReleaseChecksumLine("/tmp/release/SHA256SUMS", "not-a-digest"), + /Invalid SHA-256 digest/ + ); +}); diff --git a/scripts/release-assets.mjs b/scripts/release-assets.mjs index 3946073..26e37c7 100644 --- a/scripts/release-assets.mjs +++ b/scripts/release-assets.mjs @@ -1,7 +1,11 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; -import { getReleaseArtifactPath, getReleaseTag } from "./release-utils.mjs"; +import { + formatReleaseChecksumLine, + getReleaseArtifactPath, + getReleaseTag +} from "./release-utils.mjs"; const rootDir = path.resolve(new URL("..", import.meta.url).pathname); const checkOnly = process.argv.includes("--check"); @@ -38,7 +42,7 @@ if (sbom.spdxVersion !== "SPDX-2.3" || sbom.documentNamespace !== expectedNamesp const checksumText = subjects.map(subjectPath => { const digest = crypto.createHash("sha256").update(fs.readFileSync(subjectPath)).digest("hex"); - return `${digest} ${path.relative(rootDir, subjectPath)}`; + return formatReleaseChecksumLine(subjectPath, digest); }).join("\n") + "\n"; if (checkOnly) { diff --git a/scripts/release-utils.mjs b/scripts/release-utils.mjs index fe5b0d0..3ecf53c 100644 --- a/scripts/release-utils.mjs +++ b/scripts/release-utils.mjs @@ -205,6 +205,19 @@ export function getReleaseArtifactPath(rootDir, version) { return path.join(rootDir, "dist", `hSQLite-Editor-v${assertStableReleaseVersion(version)}.html`); } +export function formatReleaseChecksumLine(subjectPath, digest) { + const normalizedDigest = String(digest ?? ""); + if (!/^[a-f0-9]{64}$/.test(normalizedDigest)) { + throw new Error(`Invalid SHA-256 digest: ${JSON.stringify(normalizedDigest)}`); + } + + const assetName = path.basename(String(subjectPath ?? "")); + if (!assetName || assetName === "." || assetName === "..") { + throw new Error(`Invalid release asset path: ${JSON.stringify(subjectPath)}`); + } + return `${normalizedDigest} ${assetName}`; +} + export function getReleaseTag(packageName, version) { return `${assertReleasePackageName(packageName)}-v${assertStableReleaseVersion(version)}`; }