Code, changes and reviews

Version 3 · Updated

Rulesets

Branch and tag rulesets: targets, rules, bypass lists, Release tags, and how the push guard enforces them.

A ruleset says which branches or tags follow which rules, and who may bypass them. Branch rulesets protect branches such as main; tag rulesets protect tags such as v1.4.0. The push guard enforces them on every git push, every merge and every release that makes a tag, and refuses everything when it can't be reached.

Where they are#

In a repository's Settings, under Rules:

  • Rulesets: all of the repository's rulesets, branch and tag together, each marked Branch or Tag. New ruleset makes either kind.
  • Branches: the branch rulesets, with Add branch ruleset.
  • Tags: the tag rulesets, with Add tag ruleset.

Repository administrators see and change them. Agents can't change them, nor can OAuth apps: a person does. An archived repository's rulesets can't be changed. A repository can have up to 50.

What a ruleset has#

  • Ruleset name: unique in the repository.
  • Enforcement status: Active (enforced on every push and merge), Evaluate (never refuses, but records what it would have refused: see Evaluate mode) or Disabled (kept, but not enforced).
  • Bypass list: who is exempt from this ruleset, and only this one (below).
  • Target branches or Target tags: what it applies to (below).
  • Rules: what the targeted branches or tags allow (below).

A new ruleset starts with Restrict deletions, Block force pushes and Block agent updates turned on. A ruleset's kind, branch or tag, doesn't change once it's made.

Bypass list#

Add bypass lists:

KindWho is exempt
RolesRepository admin (its owner, the organization's owners, admin collaborators and site administrators), or Write (everyone who can push, admins too)
PeopleThose you name
Agent appsAn agent app, app:<slug>

A person's bypass never covers an agent acting for them. An agent is exempt only if its own app is on the list. So with alice on the list, she can push to main while her agent, acting for her, is still refused. An agent signed in with the CLI under a person's name can't be put on the list. Organization teams are coming soon.

Targets#

Add target gives:

  • Include default branch (branches only): the repository's default branch, as set under Settings → General (main for a new repository). Changing it there moves these rulesets to the new branch at once.
  • Include all branches or Include all tags.
  • Include by pattern: a name, or a pattern: main, release/**, v*.
  • Exclude by pattern: leaves out what matches, even if an inclusion matches it.

A branch or tag is targeted when it matches an inclusion and no exclusion. A ruleset with no targets applies to nothing.

Patterns#

In a pattern, * matches any characters except /, and ** matches any characters, / too. Everything else is literal. So release/* matches release/1.4 but not release/1.4/hotfix, release/** matches both, and v* matches v1.4. Patterns work the same way everywhere: rulesets, workflow triggers, environments' required tag and mirrors.

Branch rules#

Those on a ruleset's bypass list are exempt from all of its rules.

RuleWhat it does
Restrict creationsOnly those on the bypass list can create matching branches.
Restrict updatesOnly those on the bypass list can update matching branches, by push or by merge.
Restrict deletionsOnly those on the bypass list can delete matching branches.
Require linear historyNo merge commit (a commit with more than one parent) can land on matching branches, by push or by merge. Changes merge with Squash and merge or Rebase and merge; Create a merge commit isn't offered.
Require deployments to succeedThe change's head must have been deployed, successfully, to each environment named, before it merges (see Deployments).
Require signed commitsEvery commit a push or merge adds to matching branches must carry a valid signature by an SSH key registered to a Branchscope account (see Signing commits).
Require a change before mergingMatching branches change only by merging a change: no direct pushes, for people too.
Require status checks to passThe checks named must have passed on the change's head before it merges. Add branchscope/review, offered under the field, to require the change's graph review to pass.
Block force pushesNobody can rewrite matching branches' history.
Block agent updatesAgents can't create, update or delete matching branches, by push or by merge: their work lands through a change a person merges.

Requiring checks or deployments turns on Require a change before merging too: a direct push can't wait for them.

Coming soon, shown but not enforced yet: require code scanning results, require code quality results, restrict code coverage, and, under Restrictions, restrict commit metadata and restrict branch names.

Merging past required checks#

While a required check is failing, missing or still running, Merge is refused, for people and agents alike. That includes branchscope/review. A repository administrator can Merge anyway, with a reason; the change keeps a record of which checks were skipped, who merged and why. Agents can't. There's no way to merge past required deployments: deploy the change's head, or be on the ruleset's bypass list.

Protected branches#

A branch that an active branch ruleset targets counts as protected: the Branches tab marks it with a shield, and its graph is built on every push.

Every repository starts with a branch ruleset named Protected branches. It targets main and turns on Restrict deletions, Block force pushes and Block agent updates. A repository that set protected branches before rulesets existed has it with those settings: the same patterns, and Require a change before merging and Require status checks to pass if it required changes or checks. Change it, or delete it, like any other ruleset.

Tag rules#

As for branches, those on the bypass list are exempt.

RuleWhat it does
Restrict creationsOnly those on the bypass list can create matching tags, by push or by publishing a release.
Restrict updatesOnly those on the bypass list can move matching tags.
Restrict deletionsOnly those on the bypass list can delete matching tags.
Require deployments to succeedA matching tag can only be made on a commit deployed successfully to the environments named.
Require status checks to passA matching tag can only be made on a commit whose named checks have passed. They run on a branch first.
Require signed commitsThe commits a matching tag brings, and the tag itself if it's annotated, must carry a valid signature by an SSH key registered to a Branchscope account. A lightweight tag on commits already in the repository brings none.
Block force pushesNobody can move a matching tag with a force push.
Block agent updatesAgents can't create, move or delete matching tags: a person tags, or publishes a release.

With required checks or deployments, a matching tag is made by publishing a release, which checks them first. A plain git push of the tag is refused, unless the pusher is on the bypass list.

Linear history is a branch rule: a tag has no history of its own, so tag rulesets don't have it.

Release tags#

New repositories (including forks and imports, once their history has come over) start with a tag ruleset called Release tags:

  • targets v*;
  • restricts updates and deletions, and blocks force pushes: a release tag stays where it was made;
  • blocks agent updates;
  • has an empty bypass list.

A repository from before then is offered it instead, so nobody's tag pushes start failing unannounced: Settings → Tags shows Protect your release tags with Add Release tags, while the repository has no tag rulesets.

Linear history and signed commits#

For these two rules, the git service looks at the commits a push adds to each branch or tag: those between the old and the new tip, or, for a new branch or tag, those no existing branch or tag already has. It tells the push guard whether any of them is a merge commit, and whether all of them are signed. Deleting a branch or tag adds nothing.

  • A push with a merge commit onto a branch that requires linear history is refused: rebase onto it instead of merging (git pull --rebase), or squash, and push again.
  • Merging a change into such a branch offers Squash and merge and Rebase and merge only; Create a merge commit is shown as unavailable, with why. Those on the ruleset's bypass list can still create one.
  • An unsigned commit, or one signed by a key no Branchscope account has, is refused on a branch or tag that requires signed commits. The refusal names the oldest such commit.
  • Up to 1,000 new commits are verified in one push to a branch or tag; a push bringing more is refused where signed commits are required. Push them in parts.

Signing commits#

Branchscope verifies SSH signatures, made with an SSH key you've added under Settings → SSH keys: the same keys you push with. Any key of yours that isn't revoked or expired signs for you. GPG and S/MIME signatures aren't verified yet: a commit signed that way counts as unsigned.

To sign your commits with git (2.34 or later):

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true   # sign annotated tags too

user.signingkey is the public key's file (git signs with its private key, or with your SSH agent). Commits you made before won't be signed: sign them again with git rebase --exec 'git commit --amend --no-edit -S' <base>, or squash them, and push.

Commits Branchscope makes (merge commits, squashes and rebased commits when you merge a change) are signed with Branchscope's own SSH key, when the site has one (see Running Branchscope). A squash or a rebase is then signed by Branchscope, which vouches it merged a change you could merge; a merge commit's own signature is Branchscope's, and the change's commits must be signed themselves. Without a key, merging a change into a branch that requires signed commits is refused, with why: merge it with git and push signed commits instead.

Evaluate mode#

A ruleset in Evaluate never refuses anything. Every push and merge is decided as if it were active, by the same rules, for the same people (its bypass list counts too), and what it would have refused is recorded:

  • in the repository's Activity, on the push or merge it would have refused, as "Ruleset … would have refused this", with the refusal it would have made;
  • on the ruleset's own page, under Insights: its last 50, newest first.

Use it to try a ruleset before turning it on: see who and what it would stop, then make it Active. What it would refuse is only recorded for pushes and merges that went through; one an active ruleset refused isn't. Required checks and deployments are decided when merging and aren't evaluated, and a ruleset in evaluate mode doesn't make a branch protected nor take Create a merge commit away.

Several rulesets#

Every push, merge and tag must satisfy each active ruleset that targets it. Being on one ruleset's bypass list exempts you from that one only. The checks and environments required add up: a merge needs every check, and every deployment, that any ruleset you don't bypass asks for.

A push that changes both branches and tags is checked against both kinds. Branch rulesets never apply to tags, nor tag rulesets to branches.

How they're enforced#

The control plane gives each repository's active rulesets to the push guard, within seconds of a change, and again whenever the guard restarts.

  • Git pushes, over HTTP and SSH, ask the guard about every branch and tag they change.
  • Merges ask it as a push to the target branch by the person or agent merging. Required checks and deployments are decided when merging.
  • Publishing a release makes its tag through the guard, as a push by the person publishing. Required checks and deployments are decided first.
  • Imports and forks go through it too.
  • Commits: for linear history and signed commits, the git service reports what each push or merge adds. If it can't say (a git service from before these rules, or a signing key list it couldn't get), the push is refused where the rule is on: it fails closed.

A refusal says what's not allowed and ends with the ruleset's name, for example main is a protected branch: agents can't update it (claude-code, acting for alice). … (ruleset "Protected branches").

It fails closed: if the guard can't be reached, every push, merge and tag write is refused. A guard that has just started refuses everything until it has its policy. Nothing bypasses an archived repository or deny-all.

The API#

GET /repos/{owner}/{name}/rulesets?kind=branch|tag, GET …/rulesets/{id}a repository's rulesets; one ruleset
POST …/rulesets, PUT …/rulesets/{id}, DELETE …/rulesets/{id}make, change or delete one: {name, enforcement, target: {kind, include, exclude}, rules, bypass}; enforcement is active, evaluate or disabled, and rules has requireLinearHistory and requireSignedCommits
POST …/rulesets/release-tagsadd Release tags
GET …/rulesets/{id}/insightswhat a ruleset would have refused in evaluate mode, newest first

In target.include, ~DEFAULT_BRANCH is the repository's stored default branch and ~ALL every branch or tag. These are for repository administrators, with a session or a personal token; never agents or apps. See the API reference.

The CLI lists them, read-only: bscope ruleset list [--kind branch|tag].