feat(git): pre-push signature guard gated on the target repo's require_signed_commits protection (#425 item 2) #479

Merged
hexajon merged 1 commit from issue-425-fix-commit-authorcommitter-come-from-the-global-gi into main 2026-09-09 22:29:12 +00:00
Owner

Summary

Closes #425. The three items, with where each is satisfied:

Item 1 (identity from the target repo, plus the pre-push identity guard) landed in PR #448: svcgit.CheckBranchIdentity in internal/service/git/identity_guard.go, wired in Push and finishRun.pushStep.

Item 2 (pre-push signing verification) lands here, on the posture ADR 0013 recorded on 2026-09-08:

  • New svcgit.CheckBranchSignatures (internal/service/git/signing_guard.go). It lists the TARGET repo's branch protection rules from the forge (dir-scoped client, same construction as the push exit-128 diagnostic, ADR 0025), and only when a rule covering the default branch sets require_signed_commits (exact name or the forge's glob rule form) does it read each commit object the branch adds (git log <range> --format=%H %G?, the identity guard's own range) and refuse on the first commit that is not G or U, naming the sha and what git found (unsigned, bad, unverifiable here, expired, revoked) and the remedy. No key material in madtea; git verifies against the operator's own key.
  • Fail-open on reads, never on findings: an unreachable forge, unparseable rules, or an unreadable range passes and the forge's own enforcement stands. A repo with no signing rule never has its commits read, so the guard cannot block a commit the forge would accept (the #424 over-broad-guard bug is not repeated here).
  • Wired after the identity guard in Push (pushSigningGuard) and in both legs of finishRun.pushStep (finishPushSigningGuard); refusals return verbatim.
  • Tests: unsigned refuses; B/E/X/Y/R refuse with the right wording; G and U pass; no requirement means no log read; a glob rule covers its base; forge errors and log errors pass; rule matching; push and finish wiring tests. Existing push tests stub the new seam in resetPushSeams.
  • ADR 0013 gets a dated note pointing at the guard.

Item 3 (no unsigned server-side merge commit from finish): finish honors the repo's default_merge_style; on this repo that is fast-forward-only, so no merge commit is minted (every merge today landed with main at the work commit's own sha). The residual case, a signing-required base whose merge style mints a merge commit, is filed as #478.

Gate: ./scripts/gate.sh full run passes (all 18 checks).

Closes #425

## Summary Closes #425. The three items, with where each is satisfied: Item 1 (identity from the target repo, plus the pre-push identity guard) landed in PR #448: `svcgit.CheckBranchIdentity` in `internal/service/git/identity_guard.go`, wired in `Push` and `finishRun.pushStep`. Item 2 (pre-push signing verification) lands here, on the posture ADR 0013 recorded on 2026-09-08: - New `svcgit.CheckBranchSignatures` (`internal/service/git/signing_guard.go`). It lists the TARGET repo's branch protection rules from the forge (dir-scoped client, same construction as the push exit-128 diagnostic, ADR 0025), and only when a rule covering the default branch sets `require_signed_commits` (exact name or the forge's glob rule form) does it read each commit object the branch adds (`git log <range> --format=%H %G?`, the identity guard's own range) and refuse on the first commit that is not G or U, naming the sha and what git found (unsigned, bad, unverifiable here, expired, revoked) and the remedy. No key material in madtea; git verifies against the operator's own key. - Fail-open on reads, never on findings: an unreachable forge, unparseable rules, or an unreadable range passes and the forge's own enforcement stands. A repo with no signing rule never has its commits read, so the guard cannot block a commit the forge would accept (the #424 over-broad-guard bug is not repeated here). - Wired after the identity guard in `Push` (`pushSigningGuard`) and in both legs of `finishRun.pushStep` (`finishPushSigningGuard`); refusals return verbatim. - Tests: unsigned refuses; B/E/X/Y/R refuse with the right wording; G and U pass; no requirement means no log read; a glob rule covers its base; forge errors and log errors pass; rule matching; push and finish wiring tests. Existing push tests stub the new seam in `resetPushSeams`. - ADR 0013 gets a dated note pointing at the guard. Item 3 (no unsigned server-side merge commit from finish): finish honors the repo's `default_merge_style`; on this repo that is fast-forward-only, so no merge commit is minted (every merge today landed with main at the work commit's own sha). The residual case, a signing-required base whose merge style mints a merge commit, is filed as #478. Gate: `./scripts/gate.sh` full run passes (all 18 checks). Closes #425
feat(git): pre-push signature guard gated on the target repo's require_signed_commits protection (#425 item 2)
All checks were successful
ci / mcp-conformance (pull_request) Successful in 1m52s
drift / govulncheck (pull_request) Has been skipped
drift / forgejo-smoke (pull_request) Has been skipped
drift / surface-diff (pull_request) Successful in 1m7s
ci / mcp-conformance (push) Successful in 3s
drift / govulncheck (push) Has been skipped
drift / surface-diff (push) Successful in -3s
drift / forgejo-smoke (push) Has been skipped
ci / gate (push) Successful in 11m3s
ci / gate (pull_request) Successful in 19m10s
9bce364d6d
Closes #425
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!479
No description provided.