Bind a merge to a review decision and a CI verdict taken on the same exact head.
merge-guard is a pre-merge gate. It refuses a merge unless the review and the CI result both belong to the commit being merged, and it names the rule that refused. The first public release is a walking skeleton — the change classifier and the check profile it selects — and the rest of the gate follows.
Pre-release. v0.0.2 is current: it carries the approval rule — an approval
counts only if the review could have seen the head — and the release
notes state what
that still does not prove. v0.0.1 released the change classifier as one
standard-library file. There is no package and no container image.
The composite action is installable today and pinned by commit SHA — see Use it
in a repository. This repository runs that same gate on
its own pull requests; .github/merge-guard.toml is its policy.
- Evidence is bound to a revision. A green run for another commit is not evidence for this one.
- Refusals are named. Every refusal carries a stable code, so a caller can branch on it without parsing prose.
- Fail closed. Missing CI, an unreadable forge or an ambiguous verdict is a refusal, never a pass.
- One owner per fact. The classifier owns the rule that a path needs a stronger check; nothing else copies that list.
- Copy
examples/merge-guard.tomlto.github/merge-guard.tomland replace every placeholder:OWNER/REPO,REVIEWER[bot], its numericreviewer_id,CHECKS, the workflow file names and the ordinary-path allowlist. - Add the three workflows from
examples/. The policy defaults namemerge-guard-review.ymlandmerge-guard-merge.yml;review-relay.ymlis the no-op relay and has no policy entry. Pinmikomiel/merge-guardby 40-hex SHA, and replace the placeholder values each file names at the top: the base branch, your CI workflow's display name, and the policy placeholders. - Grant them the permissions each file declares and nothing more. The gate
status is written by
github-actions[bot]; thetokeninput cannot change that identity, and a status from any other creator is refused.
Read docs/trusted-relay.md before changing the triggers: the relay is what makes the writes trustworthy.
A release is consumed as a pinned, generated copy rather than a dependency upgrade. docs/pin-bump.md is the procedure: what a release guarantees, how to record the pin, how to bump it, and the states to avoid.
Required branches-up-to-date stays on, so a base advance normally invalidates
an exact-head approval. When a person updates the branch, the engine carries
the configured reviewer's latest approval from the previous head only if the
pull request's own patch is identical: same files, same statuses, same patch
text, renames included. A changed hunk, an extra file, a conflict-resolution
merge that changes the patch, or a file whose patch GitHub does not return
(binary or too large) means no carry-over. The published status says
approved at <old head> (carried, unchanged diff) so the reviewer is visible.
Carry-over is a policy choice, not a fixed rule. carry_unchanged_diff = false
in [review] turns it off, and then an approval binds to the commit it was given
for: an identical patch on a new head inherits nothing and needs its own
approval. It defaults to true so a consumer that already relies on it keeps the
behaviour it pinned.
The gate is Python, standard library where it can be, so a forge integration adds a client rather than a runtime. CI runs the public-content scan now, and lint and tests once there is code:
python3 scripts/check_estate_identifiers.py
See CONTRIBUTING.md. When a pull request cannot merge and the reason is not obvious, docs/troubleshooting.md covers the missed-trigger case and what the gate's status descriptions mean.
Apache-2.0. See LICENSE.