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 setting | What it refuses |
|---|---|
required_linear_history | a merge commit |
required_pull_request_reviews | any direct push, unless the rule’s bypass_pull_request_allowances list the pusher |
required_status_checks | a commit with no passing result for a required check |
allow_force_pushes off | a force push |
allow_deletions off | deleting the branch |
lock_branch | any change |
restrictions | a 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:
| Rule | What it judges | Its reason |
|---|---|---|
deletion | deleting the branch | Cannot delete this branch |
creation | creating the branch | Cannot create ref due to creations being restricted. |
update | updating the branch | Cannot update this protected ref. |
pull_request | updating the branch | Changes must be made through a pull request. |
non_fast_forward | a forced update only | Cannot force-push to this branch |
required_status_checks | updating the branch, or creating it unless do_not_enforce_on_create is set | protection’s merge message, from the table on Branch protection and rulesets |
required_linear_history | when updating, the merge commit nearest the new tip that the old tip doesn’t reach; when creating, one anywhere in its history | This branch must not contain merge commits., then Found 1 violation: and that commit’s sha |
required_signatures | each 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:
error: GH013: Repository rule violations found for refs/heads/<branch>.Review all repository rules at https://github.com/<owner>/<repo>/rules?ref=...- 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_mode | The pusher’s push |
|---|---|
always | passes, with GitHub’s Bypassed rule violations notice |
exempt | passes, with no notice |
pull_request | doesn’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.