fix(git): let the forge judge commit signatures, refuse only unsigned commits locally #487

Merged
hexajon merged 1 commit from issue-424-lean-on-git-signing into main 2026-09-14 20:47:56 +00:00
Owner

Summary

Summary

The signature check refused every commit that git did not report as a good signature (G or U). That includes E, "git cannot check it here". This is the normal state of an SSH-signed commit on a machine with no allowed-signers file, and of a GPG signature whose public key is not in the local keyring. The forge checks against the keys on the user's account, so it can accept a commit that the local check refused.

The check is now split by what each side can prove:

  • Before the push, locally: CheckBranchSignatures refuses only N, a commit with no signature. Git can prove that alone, and it fails a signing requirement on any forge. Every signed commit passes, whatever its status on this machine.
  • After the push, before the PR: finish reads the forge's own verification of every commit the branch adds (VerifyBranchSignaturesOnForge: the commits API with verification=true, sha=<branch>, not=<base>). If the forge does not verify a commit, finish stops before it opens the PR. The refusal names each commit with the forge's reason and gives both remedies: fix the key on the forge account, or re-sign and push with --force.

Both halves run only when a rule that covers the default branch sets require_signed_commits. Both fail open on read errors. madtea copies none of the forge's key rules and holds no key material, so the check works the same for every signature format git supports. A plain push keeps only the local half, because a push opens no PR.

ADR 0013 records this as a dated amendment.

Tests

  • Local check: N refuses. G, U, B, E, X, Y and R pass. The existing gating and fail-open tests carry over.
  • Forge verdict: refuses with the forge's reason, passes when every commit is verified or has no verification block, skips the commit list when no rule requires signing, fails open on read errors, and pages through long branches.
  • finish: the new step passes the target repo, the base and the branch, and returns the forge refusal unchanged.
  • Full gate passes.

Closes #424

## Summary ## Summary The signature check refused every commit that git did not report as a good signature (`G` or `U`). That includes `E`, "git cannot check it here". This is the normal state of an SSH-signed commit on a machine with no allowed-signers file, and of a GPG signature whose public key is not in the local keyring. The forge checks against the keys on the user's account, so it can accept a commit that the local check refused. The check is now split by what each side can prove: - **Before the push, locally:** `CheckBranchSignatures` refuses only `N`, a commit with no signature. Git can prove that alone, and it fails a signing requirement on any forge. Every signed commit passes, whatever its status on this machine. - **After the push, before the PR:** `finish` reads the forge's own verification of every commit the branch adds (`VerifyBranchSignaturesOnForge`: the commits API with `verification=true`, `sha=<branch>`, `not=<base>`). If the forge does not verify a commit, `finish` stops before it opens the PR. The refusal names each commit with the forge's reason and gives both remedies: fix the key on the forge account, or re-sign and push with `--force`. Both halves run only when a rule that covers the default branch sets `require_signed_commits`. Both fail open on read errors. madtea copies none of the forge's key rules and holds no key material, so the check works the same for every signature format git supports. A plain `push` keeps only the local half, because a push opens no PR. ADR 0013 records this as a dated amendment. ## Tests - Local check: `N` refuses. `G`, `U`, `B`, `E`, `X`, `Y` and `R` pass. The existing gating and fail-open tests carry over. - Forge verdict: refuses with the forge's reason, passes when every commit is verified or has no verification block, skips the commit list when no rule requires signing, fails open on read errors, and pages through long branches. - `finish`: the new step passes the target repo, the base and the branch, and returns the forge refusal unchanged. - Full gate passes. Closes #424
fix(git): let the forge judge commit signatures, refuse only unsigned commits locally
Some checks failed
drift / govulncheck (pull_request) Has been skipped
drift / surface-diff (pull_request) Successful in 1m6s
drift / forgejo-smoke (pull_request) Has been skipped
ci / mcp-conformance (pull_request) Successful in 1m59s
ci / mcp-conformance (push) Successful in 6s
drift / govulncheck (push) Has been skipped
drift / surface-diff (push) Successful in 0s
drift / forgejo-smoke (push) Has been skipped
ci / gate (push) Failing after 3m19s
ci / gate (pull_request) Failing after 7m38s
b924051f9f
Closes #424
Sign in to join this conversation.
No reviewers
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.

Dependencies

No dependencies set.

Reference
sixfold-space/madtea!487
No description provided.