Skip to Content
SimulatorsGitHubBranch protection and rulesets

Simulators

Branch protection and rulesets

Branch protection and rulesets decide whether a pull request can merge, and they set the mergeable_state your client reads. Protect the base branch, watch a pull request go from clean to blocked and satisfy each requirement in turn:

H='Authorization: Bearer ghp_anything'
R="$RYSTIC_URL/repos/tb-owner/tb-repo"
curl -s "$R/pulls/41" -H "$H" | jq -c '{mergeable, mergeable_state}'
# -> {"mergeable":true,"mergeable_state":"clean"}
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":{"required_approving_review_count":1},"restrictions":null}' | jq -c '{enforce_admins: .enforce_admins.enabled, contexts: .required_status_checks.contexts}'
# -> {"enforce_admins":true,"contexts":["unit"]}
curl -s "$R/pulls/41" -H "$H" | jq -c '{mergeable, mergeable_state}'
# -> {"mergeable":true,"mergeable_state":"blocked"}
curl -s -X PUT "$R/pulls/41/merge" -H "$H" | jq -c '{message}'
# -> {"message":"Required status check \"unit\" is expected."}
curl -s -X POST "$R/statuses/517a4bbdfb525691b11db3e28f466eb3d7598d19" -H "$H" -d '{"state":"success","context":"unit"}' | jq -c '{state, context}'
# -> {"state":"success","context":"unit"}
curl -s -X PUT "$R/pulls/41/merge" -H "$H" | jq -c '{message}'
# -> {"message":"At least 1 approving review is required by reviewers with write access."}
curl -s -X POST "$R/pulls/41/reviews" -H "$H" -H 'X-Rystic-Actor: tb-reviewer-login' -d '{"event":"APPROVE"}' | jq -c '{state}'
# -> {"state":"APPROVED"}
curl -s -X PUT "$R/pulls/41/merge" -H "$H" | jq -c '{merged, message}'
# -> {"merged":true,"message":"Pull Request successfully merged"}
curl -s -X POST "$R/rulesets" -H "$H" -d '{"name":"main rules","target":"branch","enforcement":"active","conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":[]}},"rules":[{"type":"deletion"}]}' | jq -c '{id, name, enforcement}'
# -> {"id":1,"name":"main rules","enforcement":"active"}
curl -s "$R/rules/branches/tb-base-branch" -H "$H" | jq -c '[.[] | {type, ruleset_id}]'
# -> [{"type":"deletion","ruleset_id":1}]

Pushes, ref writes and contents writes to a protected branch are on Protected pushes.

mergeable_state

An open pull request’s mergeable_state follows its base’s protection, the rulesets that apply to its base and its head’s results. mergeable stays true in each of these states.

The pull requestmergeable_state
Its base is gated by protection or a ruleset, and a required context has no passing result on the head, approvals are short of required_approving_review_count or a review thread stands under required_review_thread_resolutionblocked
A check run on the head isn’t completed or didn’t conclude success, neutral or skipped, or a context’s latest commit status isn’t successunstable
Otherwiseclean

A required context’s result is the latest commit status of that context, else the latest check run of that name.

What re-settles it

Each status, check run, protection and ruleset write re-settles the pull requests it touches. So do submitting or dismissing a review, and creating a review comment through POST .../pulls/{n}/comments (a reply there included) or deleting one. These writes re-settle it too, for the head and base they leave it on:

Writemergeable_state then follows
A push, or a contents or git ref write, that moves the headthe new head’s results, in the pull request’s own repository even when the head is a fork’s branch
Update-branchits new merge commit, which has no results yet: clean, or blocked while its base’s protection or a ruleset is unmet, as the table above says (except from behind, below)
A reopenthe head pushed while it was closed, and any result or protection added meanwhile
A change of basethe new base’s protection, which can block or release it

On a base that requires no check the new head lacks, an unstable pull request whose new head has no results yet reads clean until an unfinished or failing result lands on it. The synchronize that update-branch sends carries the state it settles to, except from behind. An edit that sends the current base re-settles it as well.

Differs from GitHub: update-branch on a pull request reading behind leaves it clean, and its synchronize carries behind, even where the base requires a check the new merge commit lacks. GitHub reads blocked.

Differs from GitHub: a pull request reopened after its base’s protection or ruleset was lifted keeps the blocked it closed with. GitHub reads what its head’s results give.

A scenario seeds mergeable and mergeable_state. A seeded blocked on a base nothing gates stands until a protection or ruleset write changes what gates that base, or the base changes.

A seeded dirty or unknown stands until the head or the base moves (a push, a merge, a ref write or a contents write), an edit sends base or the pull request reopens. Update-branch on an unknown pull request also counts. That redoes the test merge: a conflict reads false and dirty, and a clean merge reads true and clean. A head move, a base edit or a reopen then settles it as the table says.

A merge or update-branch refuses a pull request reading dirty. It also asks git at request time, so a conflicting test merge refuses one whatever the pull request reads.

Differs from GitHub: update-branch moves only its own pull request. Another one based on the moved branch keeps its mergeable and mergeable_state. Another one headed by it keeps its old head.sha, commits and files. It gets no synchronize.

Differs from GitHub: a dirty or unknown pull request whose base moves and whose test merge is clean reads clean until a write re-settles it, whatever its protection, rulesets and head results say. GitHub reads blocked or unstable as they give.

Differs from GitHub: an open pull request’s mergeable and mergeable_state are always settled values. GitHub can return null on its first read while it computes them.

Merging

A merge through PUT .../pulls/{n}/merge checks the merge method first, once the pull request is found open and mergeable. A method the repository disables returns GitHub’s 405.

Into a base that protection alone gates, a merge returns 405 while a requirement is unmet. The message names the first unmet requirement:

UnmetMessage
One required checkRequired status check "<context>" is expected., is pending. or is failing.
Several checks, in one state<n> of <m> required status checks are <state>.
Several checks, in mixed states<n> of <m> required status checks have not succeeded: 1 expected and 1 failing., with each state’s count
ApprovalsAt least <n> approving review(s) is/are required by reviewers with write access.

A repository admin merges past protection unless its enforce_admins is enabled.

Rulesets

A ruleset applies to a branch while all of these hold:

  • its enforcement is active and its target is branch;
  • it holds at least one rule;
  • one of its conditions.ref_name.include patterns names the branch;
  • no exclude pattern names it.

GET .../rules/branches/{branch} returns [] for a branch no ruleset applies to, whether or not the branch exists.

Creating or updating a ruleset refuses what GitHub refuses on a personal repository’s plan, such as enforcement evaluate, with GitHub’s 422 error.

What a ruleset’s rules ask of a merge

A ruleset’s required_status_checks and pull_request rules block the pull requests into the branches it applies to. Merging one returns GitHub’s 405 with Repository rule violations found, then each violation once, an unmet context on a line of its own.

Once a ruleset applies to the base, protection’s unmet requirements join that list: all of them, not only the first.

A repository admin is refused like anyone else, unless the ruleset’s bypass_actors name a repository role the merger holds. Such an actor has actor_type RepositoryRole, any bypass_mode and actor_id 2 (maintain), 4 (write) or 5 (admin). mergeable_state reads blocked whoever asks.

A pull_request rule’s allowed_merge_methods refuses any other method with the 405 a repository’s disabled method returns. It is judged before every other rule, over GraphQL’s mergePullRequest too. Every ruleset that applies must allow the method. A bypass actor merges past it, and mergeable_state doesn’t move for it.

With required_review_thread_resolution, a review thread on the pull request blocks it. A thread is a top-level comment of a submitted review. While one stands, the merge returns A conversation must be resolved before this pull request can be merged. in place of the rule’s approval count.

Differs from GitHub: the simulator serves no way to resolve a review thread. Deleting a thread’s first comment ends it.

Differs from GitHub: protection’s required_conversation_resolution is stored, but a review thread blocks neither the merge nor mergeable_state under it.

Differs from GitHub: the stale-review and last-push settings are stored but don’t gate a merge: protection’s dismiss_stale_reviews and require_last_push_approval, and a pull_request rule’s dismiss_stale_reviews_on_push and require_last_push_approval.

Last updated on