Branch protection

Which branches the gate watches and how hard it bites; the paths that always need a person; what happens to a push from outside; the audited way past the gate; and the merge rules that route what the gate holds.

Watched branches

A rule names a branch pattern and a mode. A push to a matching branch is decided by the mode; a branch no rule matches behaves like ordinary git.

  • off: anything can merge.
  • warn: the push lands, and the fact that it carried no proof is recorded. The migration state.
  • strict: the push is held until it passes. The product.

Rules exist at two levels: the space's floor, and a repository's own. A repository can only tighten its space's floor. A repository rule that would resolve weaker than the floor is refused when it is saved, with governance_weaker_than_floor, rather than saved and ignored; and the floor is applied again on every push, so a floor raised later takes effect on the next push. Each rule carries a reason that goes on the record. A rule also says whether a push may introduce a credential the secret scanner recognises (push protection, tighten-only), and may set a ceiling on the severity of a newly added or upgraded vulnerable dependency.

The Branch protection page of a repository: main is strict with no override, a form to protect another branch, the outside-proposal switch, and the files that always need a person
Settings → Branch protection: main is held until it passes; nothing else is listed, so every other branch merges the moment it is pushed

Removing the last rule that covers a default branch is not refused, since the floor rule is the only way a space can ever loosen; it asks the caller to name the repositories about to be unprotected, so somebody has looked at the list.

Who can change these

Only a governance admin of the space can write rules, at either level. A space owner grants and revokes that role; holding it does not let anyone grant it further, so the people pushing code cannot make their own rules easier. The rules and the list of admins are readable by any member.

PUT    /orgs/<space>/governance                       the floor: {pattern, mode, allow_break_glass, reason}
PUT    /orgs/<space>/repos/<repo>/governance          a repository rule, same body
DELETE /orgs/<space>/governance?pattern=              with a reason, and the repositories acknowledged as unprotected
PUT    /orgs/<space>/governance/admins/<account>      a space owner grants the governance role
GET    /orgs/<space>/repos/<repo>/governance/explain  what a push to a ref would meet

Held paths

A repository's own landing rules live in sigbound.policy, committed at its root, versioned with the code they govern. ack-paths names the files and folders that always need a person, even for a trusted agent, even on a branch that normally lets work through. verify lists the checks a push must pass. ack-timeout and ack-timeout-action say how long a held attempt waits and whether it then keeps waiting or is rejected; without a deadline it waits forever. The Files that always need a person section of the page edits the same file, through the normal gate.

verify = go test ./...
ack-paths = payments/**, auth/**, deploy.yaml
ack-timeout = 72h
ack-timeout-action = reject

The version of the file being pushed governs the push, so a repository can repair a broken file. A push may tighten its own rules freely; a push that loosens them is refused, whether the loosening and the edit arrive together or in two pushes. A change that touches sigbound.policy itself is held for a person.

Proposals from outside

A push from an account that is not a member of the space is handled by one switch per repository: park it as an attempt that needs a person, or refuse it at the door. Parked outside proposals are never auto-merged. The switch needs the repository's policy permission.

PUT /orgs/<space>/repos/<repo>/external-proposals   {"allowed": true}   park
                                                     {"allowed": false}  refuse

Break glass

Break glass is the audited way to land what the gate refuses, and it is two-key by design. The rule must allow it (Let someone override this in an emergency, allow_break_glass; a repository may withhold a hatch its space allowed, never open one it withheld). Then a governance admin opens the hatch for one person, on one exact ref of one repository, with a reason and an expiry bounded by the database's clock. With both in place, a space owner lands a parked attempt whose verification is red or absent through the attempt's break-glass door, giving a reason and the head they saw. The landing carries a permanent mark, the space is told while it is happening, and the grant closes by itself.

PUT  /orgs/<space>/repos/<repo>/break-glass-grants   {ref, account, expires_at, reason}   a governance admin
POST /orgs/<space>/repos/<repo>/changes/<id>/break-glass   {reason, head}                 a space owner, inside an open grant
GET  /orgs/<space>/break-glass                        every open grant in the space

The git push itself reads no break-glass declaration: a refused push is refused, and the hatch is used through the attempt.

Merge rules

Branch protection decides whether a push is held. Merge rules decide what happens to an attempt the gate is holding: .forge/rules.yml on the repository's default branch is an ordered list of rules, each a set of conditions and one action, and the first rule whose conditions all hold decides.

  • require_approval: the attempt waits for a person. The default when no file exists.
  • auto_merge: the attempt lands without a click, through the same verified path an approval takes; the signed merge record names the rule and the file's digest as the authority.
  • block: approval is refused, naming the rule.

Conditions: checks_pass, paths_touched (a glob), pusher_is, base_is, diff_size, and sigbound_ai, the standing verdict at this head: pass, fail, needs_human, or none when nothing has read it yet. "Sigbound AI must pass" is two ordered rules; a head with no verdict falls to the block until the read finishes.

rules:
  - when: { sigbound_ai: pass, checks_pass: true }
    action: auto_merge
  - action: block

Rules are read from the target branch's tree, never from the attempt's head, so a proposal that edits the rules file cannot vote on its own landing. Only an attempt parked for review is eligible for auto-merge: a held path, an unattested human push, an outside proposal, a suppression and a break glass all stay with a person, whatever the rules file says. The Merge rules page offers three templates and lands the file as a normal change through the gate; policy has no side door.

The Merge rules page of a repository: three templates, you approve everything, small safe stuff lands itself, freeze, and a fan-out setting
Settings → Merge rules: a template lands as a normal change to .forge/rules.yml on the default branch