Closes #33. #58 merged while I was writing this, so this is rebased straight onto main and carries one commit.
What was broken
The DMG was compiled from whatever sat in the working directory at that moment, while the tag naming it was cut by gh from the remote's default branch. Those are not always the same commit.
No git command ran anywhere in the file, so a dirty tree, a HEAD diverged from the remote, and an existing tag of the same name all passed unnoticed. Nothing re-checked the file that was uploaded either, while three places tell people it is signed and notarized.
The preflight
Before anything is built, because a release found unverifiable after gh release create has to be deleted by hand, and the download links resolve against latest from the moment it exists.
| condition | what happens |
|---|---|
| working tree not clean | refuses, prints the paths |
| HEAD is not the tracked remote branch | refuses, prints both SHAs |
| branch has no upstream | refuses |
v<version> exists locally or on the remote | refuses |
I exercised each of these against a scratch repo with a real remote rather than reasoning about the git plumbing: dirty tree, committed-but-unpushed, tag local only, tag pushed, and a branch with no upstream all produce the values the checks key on.
Verification of what actually ships
codesign --verify --deep --strict, spctl against the primary signature, and xcrun stapler validate, all against the stable copy rather than the build output. That copy is what releases/latest/download serves and what a user opens. Any non-zero exit aborts before gh runs.
--target pins the tag to a full SHA, the commit carrying this release's own metadata, which is what was built. A branch name pins nothing.
The digest
Hashed after verification and before anything is written, so it belongs to the bytes that were verified. Printed, put in the release notes with the command to check it, and recorded in release.json.
Bun.CryptoHasher and shasum -a 256 agree, checked on a 3 MB file:
Bun.CryptoHasher: 0dafd392988efec38f12bce92ca3f6f954a09274eb746f94b18e7e58799811ac
shasum -a 256: 0dafd392988efec38f12bce92ca3f6f954a09274eb746f94b18e7e58799811acOne deviation from the issue. It asked for the digest in README.md beside the download link. I put instructions there instead and the value in release.json, because a digest committed to the README is correct for exactly one release and wrong from the next one onwards. release.json is written by the release command, so it cannot go stale.
What I could not verify
The three verification commands themselves, and the end-to-end run. Both need a Developer ID certificate and a notarytool profile, which are on your Mac and not mine. The git preflight, the hashing and the argv assembly are all checked above; the codesign/spctl/stapler calls are wired but have not been run against a real signed DMG.
Worth doing the next release with --draft once, to watch the whole sequence without moving latest.
Verification
- every preflight condition, against a scratch repo
- the digest, against
shasum - 163 unit tests pass, lint, typecheck and
release:checkclean
🤖 Generated with Claude Code