Repository navigation
Conversation
release-sign is a workspace crate, separate from the CLI, that a member of the signing group runs after a release. Its release command finds the push run of release.yml that built the tag and checks the SHA256SUMS in that run's release-assets artifact against the tarballs the run built. It then checks that both release pages, on actual-cli and the actual-releases mirror, carry that same SHA256SUMS, tarballs that match it and nothing else. Only then does it sign. The key is an Ed25519 key in Google Cloud KMS, which signs the exact bytes it's given, so a minisign signature takes two asymmetricSign calls: one over the BLAKE2b-512 digest of SHA256SUMS, and one over that signature followed by the trusted comment, timestamp:<seconds>, file:SHA256SUMS and tag:<tag>, separated by tabs. The key id isn't signed, so the tool attaches the id of whichever key in release-keys/ the signature verifies against, checked with the minisign-verify crate, and signers need only the KMS signer role. It then uploads SHA256SUMS.minisig to both release pages. For the one-time setup, public-key turns a KMS public key PEM into a minisign public key and sign-file signs a local file. release-keys/ holds placeholders until the setup exports the two keys. The tests sign with a local Ed25519 key in place of KMS, against a fake KMS endpoint and fake release pages, and an ignored test checks the output with stock minisign -V. No test calls KMS. City: factory-main · Agent: local-core.builder-2
release.yml and npm-publish.yml each built the four targets on every tag, so npm shipped binaries from one build and the GitHub releases and the Homebrew formula from another. release.yml now builds once, and a Package job writes SHA256SUMS for the four tarballs. The GitHub Releases job attaches SHA256SUMS to both release pages, the Homebrew job reads the formula's sha256 values from it, and the npm work moves over from npm-publish.yml, which is deleted. The npm work is two jobs. npm Packages fills the platform packages from the release tarballs, packs all five and checks that each platform package carries its tarball's binary, byte for byte. Publish to npm unpacks those checked packages and publishes them through OIDC trusted publishing, as before. It is the only job that holds id-token: write, because npm exchanges the OIDC token even for npm publish --dry-run. The scaffolding checkout uses the workflow's own token, since actual-releases is public, and keeps no credentials. The workflow's default permission drops to contents: read, with contents: write on the GitHub Releases job alone. A pull request that changes release.yml, crates/release-sign or release-keys runs the workflow as a dry run: the builds, SHA256SUMS, the npm packages and their checks, npm publish --dry-run on a 0.0.0-dry-run version the registry can't already hold, and the signing tool's tests on macOS with stock minisign. The GitHub release, Homebrew and npm publish jobs are skipped. The pre-release tag skip and the gated macOS signing steps are unchanged. The Linux arm64 cross-compiler install now times out after ten minutes, because a hung build would now hold back npm too. City: factory-main · Agent: local-core.builder-2
docs/RELEASE_SIGNING.md covers checking a download with minisign -V, the trusted comment's format, and signing a release with release-sign. It also gives the one-time Google Cloud setup: both keys, the signer role on the primary key for the signing group alone, Data Access audit logs for Cloud KMS, the public key export and a test signature. Finding who signed what and rotating to the spare key close it out. The basic Owner and Editor roles can sign with any key in their project, so the setup has the key ring live in a project where only signers hold them. CLI_DISTRIBUTION.md now describes the single release workflow, SHA256SUMS, the signing step and the npm trusted publisher that release.yml needs. The README gets a short section on checking a downloaded release. City: factory-main · Agent: local-core.builder-2
CodeQL's cleartext-logging query reads a variable named `trusted` as a secret, so it flagged the four messages that say which key signed. Those messages print the key's name and key id, and release-keys/ publishes both. The loop in encode_for_trusted_key now names each key `candidate`, and nothing else changes. City: factory-main · Agent: local-core.builder-1
The same CodeQL heuristic matches any name containing `trusted`, and the test helper that builds a release key for a test signer was called `trusted`. The tests pass its keys through sign_file and sign_release to the "Signed with the ... key" message, so two of the four alerts outlived the loop variable's rename. The helper is now release_key, and only test code changes. City: factory-main · Agent: local-core.builder-1
The stable-tag check's pattern starts with a dash, so GNU grep took it for options, printed "invalid option -- '('" and exited 2 on every run. The if fell through to its else branch and every ref counted as stable, so a pre-release tag would have published to every channel. Passing -- before the pattern makes -alpha, -beta, -rc and -test tags skip the release, as CLI_DISTRIBUTION.md already says, while stable tags and pull request dry runs still build.
City: factory-main · Agent: local-core.builder-1
minisign zero-pads a key id to 16 hex digits wherever it shows one, in the public key comment that minisign -G writes and in the key id minisign -V reports on a mismatch. key_id_hex used {:X}, so an id whose top hex digit is 0, about one in 16, came out a digit short in the .pub comment and in the tool's "Signed with" messages. RELEASE_SIGNING.md has users go by the key id minisign reports, and --key-id already takes 16 digits. The round-trip test, whose key id has a leading zero, now expects the padded form.
City: factory-main · Agent: local-core.builder-1
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Before the first release after this merges: someone with @ActualAI npm access adds
release.ymlas a trusted publisher on all five packages (@actualai/actualand the four platform packages) and ticks directnpm publish, since a new entry allows only staged publishing by default. npm checks the workflow's file name, so without the entry the npm publish job fails withENEEDAUTH. Thenpm-publish.ymlentry can go once that release is out.Before this leaves draft: someone with admin rights in the Google Cloud project runs the one-time setup in docs/RELEASE_SIGNING.md, test signature included, and the two public keys it writes to
release-keys/land here. Until then those files are placeholders.Summary
Each tag now gets one build, and every channel ships it.
release.ymlbuilds the four targets once, writesSHA256SUMSfor the tarballs and attaches it to both release pages, reads the Homebrew formula's sha256 values from it, and publishes the five npm packages from the same tarballs. Before publishing, it checks that each platform package carries its tarball's binary, byte for byte.npm-publish.yml, which ran a build of its own, is deleted.After a release, a member of the signing group runs
cargo run -p release-sign -- release <tag>. It checks both release pages against the release run's own artifact, signsSHA256SUMSwith an Ed25519 key in Cloud KMS that nobody can export, verifies the result againstrelease-keys/and uploadsSHA256SUMS.minisigto both pages. Nothing in GitHub can make a signature.The risk sits in the first release after merge, which is the first live run of the new npm and release jobs. This PR's own CI runs the workflow as a dry run that builds and checks everything and publishes nothing.
This PR also fixes the pre-release tag skip, which has never worked. Until now an
-rcor-testtag would have published to every channel, and from here it stops at the tag check. That means a pre-release tag can't be used to try the new jobs out.Audit matrix
minisign -V; self-update relies on it once its own check shipsOne addition from the build: the basic Owner and Editor roles can sign with any key in their project, whatever the key's own policy says. So the setup doc has the key ring live in a project where only signers hold those roles, and it gives a command that lists everyone who can sign.
Test plan, design notes and review rounds
Test plan
cargo test -p release-sign -- --include-ignoredon macOS: 55 passed, including the check with stockminisign -V0.12. The tests sign with a local Ed25519 key in place of KMS; a fake KMS endpoint signs whateverdatait's sent the way the real one does, and fake release pages cover a tampered tarball, an unlisted or missing asset, a page whoseSHA256SUMSdiffers from the run's, a keyrelease-keys/doesn't hold and a failed upload. No test calls KMS or GitHub.Edalgorithm marker, or a global signature that leaves out the file signature, turns both the stockminisign -Vtest and the minisign-verify tests red.release-sign public-keyconverted its SPKI PEM, OpenSSL made the two raw signatures, and stockminisign -Vaccepted the file.cargo clippy -p release-sign --all-targets -- -D warnings,cargo clippy -- -D warnings,cargo fmt --all --checkandcargo test --workspace --features integration --no-runall pass.actionlint1.7.12 with shellcheck reports nothing on the newrelease.yml.v0.9.0,v0.10.0and898/mergestay stable, and-alpha.3,-beta,-beta2,-rc,-rc.1,-testand-test.4tags skip. This PR's dry run on the fix logsStable release: 898/mergewith no grep error.run:scripts, extracted fromrelease.yml, ran in a Debian container with npm 11.21.1 and the actual-releases scaffolding atda65519. Both modes came out right.SHA256SUMSwas written and checked, all five packages packed, the four binaries compared equal,npm publish --dry-runaccepted all five, and the Homebrew step read its four values fromSHA256SUMS. The publish job's unpacked packages were identical to the packed directories. Negative controls: a tarball that no longer matchesSHA256SUMS, a package carrying a different binary, a binary without its executable bit and aSHA256SUMSmissing a line each stop their step with an error naming the problem.Design notes
npm publish --dry-run(the exchange runs before the dry-run check inlib/commands/publish.js). So the job that packs and checks the packages, which also runs on pull requests, has noid-tokenpermission. A separate job,Publish to npm, holdsid-token: write, runs only on a tag push, unpacks the checked packages and publishes them with the samenpm publish --access publicas before.npm publish --dry-runstill refuses a version the registry already holds, so a dry run packages0.0.0-dry-run.<run id>and passes--tag dry-run.release.yml,crates/release-signorrelease-keys, since it builds on two macOS runners.contents: read, withcontents: writeon the GitHub Releases job alone.--kms-key. The key id isn't covered by either minisign signature, so the tool attaches the id of whichever committed key the signature verifies against, and signers need nothing beyondroles/cloudkms.signer.--before it. GNU grep read the pattern as options, failed withinvalid option -- '(', and the check counted every tag as stable. The v0.9.0 release run and this PR's earlier dry run both log the error. With--added, an-alpha,-beta,-rcor-testtag skips the builds. Every publishing job sits downstream of them, so nothing publishes, which is what CLI_DISTRIBUTION.md already said. None of the repository's 13 tags is a pre-release, so no past release relied on the old behaviour.release-signisn't in it. Its tests run in this workflow's Signing Tool job, stockminisign -Vincluded, and in Build and Test'scargo test --workspace.Review rounds
trustedas a secret, and renaming a loop variable and a test helper cleared those alerts. The other two, the coverage gate's scope and the macOS signing steps in the dry run, are covered in the design notes.{:X}, so an id whose top hex digit is 0 lost a digit, while stockminisignalways shows 16. The id is now zero-padded, and the round-trip test, which uses a leading-zero id, expects 16 digits. Stockminisign0.12 prints a leading-zero id as02A5C0EA5B93F986in the commentminisign -Gwrites and in the mismatchminisign -Vreports. The test count is still 55.