Skip to Content
SimulatorsGitHubProtected pushes

Simulators

Protected pushes

A protected branch, or one a ruleset applies to, refuses a push that breaks its rules, in GitHub’s own words. Require a check on the base branch, push to it and write to it through the API, then add a ruleset and delete a branch:

H='Authorization: Bearer ghp_anything'
R="$RYSTIC_URL/repos/tb-owner/tb-repo"
curl -s -X PUT "$R/branches/tb-base-branch/protection" -H "$H" -d '{"required_status_checks":{"strict":false,"contexts":["unit"]},"enforce_admins":true,"required_pull_request_reviews":null,"restrictions":null}' | jq -c '{contexts: .required_status_checks.contexts}'
# -> {"contexts":["unit"]}
git clone -q "$RYSTIC_URL/tb-owner/tb-repo.git" repo
echo hi > repo/hello.txt
git -C repo add hello.txt
git -C repo -c user.name=Dev -c user.email=dev@example.com commit -q -m "add hello"
git -C repo push origin tb-base-branch 2>&1 | grep -e GH006 -e '^remote: - '
# -> remote: error: GH006: Protected branch update failed for refs/heads/tb-base-branch.
# -> remote: - Required status check "unit" is expected.
curl -s -X PUT "$R/contents/hello.txt" -H "$H" -d '{"message":"add hello","content":"aGkK","branch":"tb-base-branch"}' | jq -c '{message}'
# -> {"message":"Could not create file: Required status check \"unit\" is expected."}
curl -s -X POST "$R/rulesets" -H "$H" -d '{"name":"no deletes","target":"branch","enforcement":"active","conditions":{"ref_name":{"include":["~ALL"],"exclude":[]}},"rules":[{"type":"deletion"}]}' | jq -c '{id, name}'
# -> {"id":1,"name":"no deletes"}
git -C repo push origin --delete tb-add-retries 2>&1 | grep -e GH013 -e '^remote: - '
# -> remote: error: GH013: Repository rule violations found for refs/heads/tb-add-retries.
# -> remote: - Cannot delete this branch

What a pull request needs before it can merge is on Branch protection and rulesets.

Judging a push

A branch protected through PUT .../protection refuses a push that breaks its rules. The push prints GitHub’s remote: error: GH006 line, then one - <reason> per broken rule.

Protection settingWhat it refuses
required_linear_historya merge commit
required_pull_request_reviewsany direct push, unless the rule’s bypass_pull_request_allowances list the pusher
required_status_checksa commit with no passing result for a required check
allow_force_pushes offa force push
allow_deletions offdeleting the branch
lock_branchany change
restrictionsa push by anyone they don’t list

Unless enforce_admins is on, a repository admin passes every rule except the force push and deletion rules. The admin’s push then gets GitHub’s Bypassed rule violations notice. Push restrictions never judge an admin. Creating a deleted protected branch again is judged by no rule.

Differs from GitHub: restrictions and bypass_pull_request_allowances let through only the users they list. A member of a listed team is refused, and a listed GitHub App never passes.

Differs from GitHub: on a repository a user owns, PUT .../protection stores the users in bypass_pull_request_allowances, and they can push past the pull request rule. GitHub refuses them with 422.

What a ruleset’s rules ask of a push

These ruleset rules also judge a push to a branch the ruleset applies to, as GitHub judges it:

RuleWhat it judgesIts reason
deletiondeleting the branchCannot delete this branch
creationcreating the branchCannot create ref due to creations being restricted.
updateupdating the branchCannot update this protected ref.
pull_requestupdating the branchChanges must be made through a pull request.
non_fast_forwarda forced update onlyCannot force-push to this branch
required_status_checksupdating the branch, or creating it unless do_not_enforce_on_create is setprotection’s merge message, from the table on Branch protection and rulesets
required_linear_historywhen updating, the merge commit nearest the new tip that the old tip doesn’t reach; when creating, one anywhere in its historyThis branch must not contain merge commits., then Found 1 violation: and that commit’s sha
required_signatureseach commit GitHub didn’t sign (below)Commits must have verified signatures., then Found <n> violations: and one sha per line

Differs from GitHub: a merge_queue rule judges no push. See Merge queues.

required_signatures judges different commits for each kind of push. When updating a branch, it judges the commits the old tip doesn’t reach. When creating one, it judges the commits no branch, tag or pull request ref held before the push.

Differs from GitHub: the simulator holds no signing key. It reads a commit committed as GitHub <noreply@github.com> as signed by GitHub, signature or not, as it reads its own merge and squash commits.

What a refused push prints

A push a ruleset refuses prints:

  1. error: GH013: Repository rule violations found for refs/heads/<branch>.
  2. Review all repository rules at https://github.com/<owner>/<repo>/rules?ref=...
  3. One - <reason> per broken rule: each ruleset’s rules in their order, rulesets by id, signatures last, then the branch protection’s unmet rules.

The command then reports push declined due to repository rule violations. A push that branch protection alone refuses keeps its GH006 form.

A repository admin is judged like anyone else. A bypass actor whose role the pusher holds passes the ruleset as its bypass_mode says:

bypass_modeThe pusher’s push
alwayspasses, with GitHub’s Bypassed rule violations notice
exemptpasses, with no notice
pull_requestdoesn’t pass

Ref and contents writes

A ref write or a contents write through the REST API is judged by the branch’s protection as a push is. When refused, it returns GitHub’s message: 422 for a ref write and 409 for a contents write.

Differs from GitHub: a ruleset’s rules judge no REST write. A forced ref update, a ref deletion, a ref creation and a contents write go through where GitHub returns Repository rule violations found.

A seeded protected flag

A branch reads protected while it holds protection, while a ruleset applies to it or when its scenario seeds the flag. A scenario can seed the flag with no protection record behind it. With no ruleset refusing, that branch refuses every push.

Differs from GitHub: an active ruleset that holds no rules applies to no branch, so a branch it names reads protected: false. GitHub reads true.

Last updated on