# Protected pushes

How branch protection and rulesets judge a git push, a ref write and a contents write, and the refusal text each one gets.

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:

```bash
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](https://www.rystic.ai/docs/simulators/github-pulls/features/branch-protection.md).

### 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](https://www.rystic.ai/docs/simulators/github-pulls/features/branch-protection.md#merging) |
| `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](https://www.rystic.ai/docs/simulators/github-pulls/features/merge-queue.md).

`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_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`.

---

Source: https://www.rystic.ai/docs/simulators/github-pulls/features/protected-pushes · Markdown: https://www.rystic.ai/docs/simulators/github-pulls/features/protected-pushes.md
