release: sign GoReleaser's tap/scoop bot commits before routing them through signed repos #434

Open
opened 2026-08-17 19:11:49 +00:00 by hexajon · 2 comments
Owner

Problem

Each release, GoReleaser bumps the Homebrew cask and Scoop manifest as a commit in sixfold-space/homebrew-tap and sixfold-space/scoop-bucket (scripts/release.sh:67-74, docs/contributing/releasing.md:102,114-115). Those bot commits are UNSIGNED. GoReleaser authenticates with GITHUB_TOKEN and targets the GitHub distribution edge (docs/contributing/release-credentials.md:71, ADR 0032).

The sixfold-space repos on git.cheshirecube.cloud now enforce require_signed_commits on main (2026-08-17), including homebrew-tap and scoop-bucket.

Current state - no break today

The cheshirecube homebrew-tap/scoop-bucket are migrated-from-Codeberg copies with mirror=false, and GoReleaser pushes the tap/scoop to the GitHub edge, not to these. So the new require_signed on the cheshirecube copies does NOT reject the current release flow. Confirmed before enabling.

The gap

  1. The distribution-edge tap/scoop commits are unsigned. That is a provenance gap for exactly what a user's machine fetches (brew/scoop install).
  2. ADR 0032 makes cheshirecube the canonical source home. If release commits are ever routed through the canonical (signed) tap/bucket, GoReleaser's unsigned commits are rejected.

Fix

Give the release bot its own signing key. Same posture as #231: the private half lives only in Bitwarden SM, never on any forge, and the bot is an author, not the platform. Configure GoReleaser to sign its tap/scoop/plugin commits with it. Then require_signed safely covers wherever those commits land.

  • #231 - platform-gitops bootstrap bot signing (same author-signing pattern).
  • #413 - sigstore/cosign for release artifacts (different layer: artifact signing, not git commit signing).
  • #227 - org-wide branch-protection + signing enforcement.
  • ADR 0032 - canonical home vs GitHub distribution edge.

Status 2026-09-14 (corrects the facts above)

  • Target: GoReleaser no longer writes to a GitHub edge. .goreleaser.yaml gitea_urls points at git.cheshirecube.cloud (DIST_HOST). It writes the cask and manifest into sixfold-space/homebrew-tap and sixfold-space/scoop-bucket through the forge contents API, with author hexajon <jon@sixfold.space>.
  • Enforcement: neither repo has branch protection today (branch_protections returns []). The claim that they enforce require_signed_commits is no longer true, which is why releases still succeed.
  • The gap is live: the forge reports every tap and bucket release commit as gpg.error.not_signed_commit, from v0.18.1 through v0.18.5 (for example c8628dbd and 73be9b3a for v0.18.5). These are the repos that brew and scoop read from.
  • What a fix needs: a commit that the forge creates through its contents API cannot carry a client signature. GoReleaser supports commit_author.signing (GPG, X.509 or SSH) and a git push mode (repository.git with url and private_key), so signing means switching these two repos to git push mode. That needs a signing key and a push key.
  • Open decision (key custody): sign with a key that is already on the release machine (the release runs locally and already GPG-signs checksums.txt), or with a new release-bot key held only in Bitwarden SM, or accept unsigned. Not decided yet.
## Problem Each release, GoReleaser bumps the Homebrew cask and Scoop manifest as a commit in `sixfold-space/homebrew-tap` and `sixfold-space/scoop-bucket` (`scripts/release.sh:67-74`, `docs/contributing/releasing.md:102,114-115`). Those bot commits are UNSIGNED. GoReleaser authenticates with `GITHUB_TOKEN` and targets the GitHub distribution edge (`docs/contributing/release-credentials.md:71`, ADR 0032). The sixfold-space repos on git.cheshirecube.cloud now enforce `require_signed_commits` on `main` (2026-08-17), including `homebrew-tap` and `scoop-bucket`. ## Current state - no break today The cheshirecube `homebrew-tap`/`scoop-bucket` are migrated-from-Codeberg copies with `mirror=false`, and GoReleaser pushes the tap/scoop to the GitHub edge, not to these. So the new `require_signed` on the cheshirecube copies does NOT reject the current release flow. Confirmed before enabling. ## The gap 1. The distribution-edge tap/scoop commits are unsigned. That is a provenance gap for exactly what a user's machine fetches (`brew`/`scoop` install). 2. ADR 0032 makes cheshirecube the canonical source home. If release commits are ever routed through the canonical (signed) tap/bucket, GoReleaser's unsigned commits are rejected. ## Fix Give the release bot its own signing key. Same posture as #231: the private half lives only in Bitwarden SM, never on any forge, and the bot is an author, not the platform. Configure GoReleaser to sign its tap/scoop/plugin commits with it. Then `require_signed` safely covers wherever those commits land. ## Related - #231 - platform-gitops bootstrap bot signing (same author-signing pattern). - #413 - sigstore/cosign for release artifacts (different layer: artifact signing, not git commit signing). - #227 - org-wide branch-protection + signing enforcement. - ADR 0032 - canonical home vs GitHub distribution edge. ## Status 2026-09-14 (corrects the facts above) - **Target:** GoReleaser no longer writes to a GitHub edge. `.goreleaser.yaml` `gitea_urls` points at `git.cheshirecube.cloud` (`DIST_HOST`). It writes the cask and manifest into `sixfold-space/homebrew-tap` and `sixfold-space/scoop-bucket` through the forge contents API, with author `hexajon <jon@sixfold.space>`. - **Enforcement:** neither repo has branch protection today (`branch_protections` returns `[]`). The claim that they enforce `require_signed_commits` is no longer true, which is why releases still succeed. - **The gap is live:** the forge reports every tap and bucket release commit as `gpg.error.not_signed_commit`, from v0.18.1 through v0.18.5 (for example `c8628dbd` and `73be9b3a` for v0.18.5). These are the repos that `brew` and `scoop` read from. - **What a fix needs:** a commit that the forge creates through its contents API cannot carry a client signature. GoReleaser supports `commit_author.signing` (GPG, X.509 or SSH) and a git push mode (`repository.git` with `url` and `private_key`), so signing means switching these two repos to git push mode. That needs a signing key and a push key. - **Open decision (key custody):** sign with a key that is already on the release machine (the release runs locally and already GPG-signs `checksums.txt`), or with a new release-bot key held only in Bitwarden SM, or accept unsigned. Not decided yet.
Author
Owner

Decided: the release key, madtea release signing (sixfold.space) <releases@sixfold.space> (fingerprint 4CF168E8EF9E40DC9173505F07D6FBD4B0D2A065), also signs the tap and bucket commits. It is the same key that signs checksums.txt and that madtea update trusts. I accept that a leak of this key then affects both jobs, in exchange for no second key to hold.

What the build needs, from the GoReleaser v2 docs for homebrew_casks and scoops:

  • Git push mode: repository.git.url (an SSH URL for each repo) and repository.git.private_key. The docs say the key "must not be password-protected". Commit signing works only in this mode.
  • commit_author.signing: enabled: true, key: "{{ .Env.GPG_FINGERPRINT }}" (the variable scripts/release.sh already sets), format: openpgp.

Before the config change:

  1. A push key: an SSH key with write access to homebrew-tap and scoop-bucket, with no passphrase.
  2. Forge verification: the commits are authored as hexajon <jon@sixfold.space>, and the key's identity is releases@sixfold.space. I still have to confirm how Forgejo matches a signing key to the committer, and whether the author email must change to releases@sixfold.space.

The change is tested with the first release after it lands: the forge must report both new commits as verified.

Decided: the release key, `madtea release signing (sixfold.space) <releases@sixfold.space>` (fingerprint `4CF168E8EF9E40DC9173505F07D6FBD4B0D2A065`), also signs the tap and bucket commits. It is the same key that signs `checksums.txt` and that `madtea update` trusts. I accept that a leak of this key then affects both jobs, in exchange for no second key to hold. What the build needs, from the GoReleaser v2 docs for `homebrew_casks` and `scoops`: - Git push mode: `repository.git.url` (an SSH URL for each repo) and `repository.git.private_key`. The docs say the key "must not be password-protected". Commit signing works only in this mode. - `commit_author.signing`: `enabled: true`, `key: "{{ .Env.GPG_FINGERPRINT }}"` (the variable `scripts/release.sh` already sets), `format: openpgp`. Before the config change: 1. A push key: an SSH key with write access to `homebrew-tap` and `scoop-bucket`, with no passphrase. 2. Forge verification: the commits are authored as `hexajon <jon@sixfold.space>`, and the key's identity is `releases@sixfold.space`. I still have to confirm how Forgejo matches a signing key to the committer, and whether the author email must change to `releases@sixfold.space`. The change is tested with the first release after it lands: the forge must report both new commits as verified.
Author
Owner

On point 2, forge verification: Forgejo's docs do not state how the forge matches a signing key to the commit author. The user guide has no page on commit signatures. The admin page on signing (/docs/latest/admin/advanced/signing/) covers only the commits the instance signs itself: initial commits, wiki edits, web-editor and API file changes, and merges.

So whether a commit authored as hexajon <jon@sixfold.space> and signed with the releases@sixfold.space key shows as verified is still open. The first release after the change answers it: the commits API reports each commit's verification and its reason, and finish already reads that (VerifyBranchSignaturesOnForge). If it reports an email mismatch, the fix is commit_author.email: releases@sixfold.space, with that address added and verified on the committing account and the key uploaded there.

On point 2, forge verification: Forgejo's docs do not state how the forge matches a signing key to the commit author. The user guide has no page on commit signatures. The admin page on signing (`/docs/latest/admin/advanced/signing/`) covers only the commits the instance signs itself: initial commits, wiki edits, web-editor and API file changes, and merges. So whether a commit authored as `hexajon <jon@sixfold.space>` and signed with the `releases@sixfold.space` key shows as verified is still open. The first release after the change answers it: the commits API reports each commit's verification and its reason, and `finish` already reads that (`VerifyBranchSignaturesOnForge`). If it reports an email mismatch, the fix is `commit_author.email: releases@sixfold.space`, with that address added and verified on the committing account and the key uploaded there.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
sixfold-space/madtea#434
No description provided.