Skip to content

release: make SHA256SUMS cover every payload asset - #918

Open
Retsumdk wants to merge 1 commit into
libredb:mainfrom
Retsumdk:checksums-cover-all-payload-assets
Open

Retsumdk wants to merge 1 commit into
libredb:mainfrom
Retsumdk:checksums-cover-all-payload-assets

Conversation

@Retsumdk

@Retsumdk Retsumdk commented Sep 16, 2026

Copy link
Copy Markdown

Closes #913.

What

SHA256SUMS only ever covered the standalone tarballs and the win32 zip, because the publish job generates it from the artifacts that exist at that point in the chain. The .deb/.rpm/AppImage assets ship their own .sha256 sidecars instead, and the two snaps and libredb-studio-<version>.cdx.json ended up with neither.

The file is now widened in publish-release, on the draft, where the complete asset set is attached — and re-uploaded before the release is flipped. That is the last point at which an asset can still be replaced (immutable releases), and it sits before the existing "full asset set" verification, so a release still cannot publish with an incomplete or inconsistent checksum file.

How

New scripts/release-sums.mjs turns the releases API's asset list into a sha256sum-format file:

  • Payload assets only. SHA256SUMS itself and the <artifact>.sha256 sidecars are excluded, so the sidecars keep their current job and the combined file does not hash checksums.
  • Hashes come from each asset's sha256 digest, with a fallback that downloads and hashes the asset when the API reports no digest (--allow-download, which the workflow passes). For a draft this is the digest of the bytes as stored, so it is the same value a local sha256sum of a download produces.
  • Entries the file already carried must survive unchanged (--existing). If widening would alter a hash that npx, Homebrew, winget or Chocolatey may already rely on, the script fails and the release does not publish with it.
  • The rebuild is deterministic: payload assets sorted by name, so the tarball and zip lines keep the order and format they have today.

docs/DISTRIBUTION.md is updated where it described the old split: the artifact table row, the paragraph about which mechanism covers which artifact, and the provenance note that said the .snap release asset ships no sidecar.

Behaviour change

SHA256SUMS gains 11 lines on the next release (2 snaps, 4 .deb, 2 .rpm, 2 AppImages, the SBOM). Consumers that look entries up by filename — the launcher, the Homebrew/winget/Chocolatey renderers — are unaffected; the five lines they read are byte-identical.

Testing

  • bun tests/run-tests.ts tests/unit/release-sums.test.ts — 26 tests covering asset selection, sha256sum formatting, ordering, the digest path, the download fallback, duplicate names, and both refusal cases; plus the workflow wiring (the widening runs on the draft after checkout and before the publish step, and uploads with --clobber).
  • bun tests/run-tests.ts tests/unit/release-sums.test.ts tests/unit/release-sbom.test.ts tests/unit/release-provenance.test.ts tests/unit/render-homebrew-formula.test.ts tests/unit/workflow-timeouts.test.ts — 94 tests, all passing.
  • bunx biome check scripts/release-sums.mjs tests/unit/release-sums.test.ts — clean.

Testing notes (not run)

  • Full bun tests/unit in my sandbox leaves four failing files — db/sqlite-driver.test.ts (no node:sqlite builtin), docker-entrypoint.test.ts, launcher-utils.test.ts and test-runner-cli.test.ts (POSIX permissions / SIGINT, running as root). All four fail identically at 81d382f with this change absent, so they are environment-limited here rather than related.
  • bun run build not run locally; nothing here touches the app build.

The publish job writes SHA256SUMS from the assets that exist when it runs -
the standalone tarballs and the win32 zip - so the two snaps and the CycloneDX
SBOM shipped with no checksum at all, and a third of the release could only be
verified by knowing which of the two mechanisms applied to it.

publish-release now rebuilds that file on the draft, where the complete asset
set is attached, and re-uploads it before the release is flipped - the last
point at which an asset can still be replaced. Assets are hashed from the
sha256 digest the releases API reports for them, falling back to hashing the
downloaded bytes when an asset has no digest, and every entry the file already
carried must survive the rebuild unchanged or the step refuses to continue.

The per-file .sha256 sidecars stay exactly as they are.

Closes libredb#913

@cevheri cevheri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Close, one blocker. The part that mattered most you got right: the asset set is derived from the release's own asset list rather than enumerated, so the snaps and the SBOM are covered by the rule and not by luck. I mutation-probed the suite and every mutant dies, including the empty-payload case.

Blocker: bun run typecheck fails with four errors, all in tests/unit/release-sums.test.ts: (68,13) TS7006 on the .map((a) => a.name) callback, then (124,7), (160,63) and (161,7) because fetchImpl = fetch infers typeof fetch and a two-line double does not satisfy it. Clean on main. That is the required Lint, Typecheck and Build check, and it is absent from your Testing list because it lives in our pre-commit set in CLAUDE.md.

Question rather than a claim: the widening reads the draft with gh api repos/.../releases/tags/$TAG, which GitHub documents as returning published releases, and every other draft read here uses gh release view (lines 177 and 1166). Have you seen it return a draft? Same doubt about --allow-download, which fetches browser_download_url unauthenticated.

The body also credits the tests with the workflow wiring and the file carries no YAML assertion. tests/unit/release-sbom.test.ts is the pattern; the step order is load-bearing.

Our own checks have not run yet, so all of the above is local. Fix the typecheck, answer the draft question, and I will merge.

Comment thread scripts/release-sums.mjs
}

export async function hashUrl(url, fetchImpl = fetch) {
return hashResponse(await fetchImpl(url));
@cevheri cevheri added enhancement New feature or request loop:needs-info Maintainer-loop task blocked on human-reviewed clarification labels Sep 16, 2026
@cevheri

cevheri commented Sep 17, 2026

Copy link
Copy Markdown
Member

frendly ping, any update

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request loop:needs-info Maintainer-loop task blocked on human-reviewed clarification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Both snap packages and the CycloneDX SBOM ship with no checksum of any kind

3 participants