# `unless` The `unless` setting names the checks that can waive a scope's review requirement. When every listed check passes on the pull request's current head commit, the scope is **waived**: nothing is required, nobody is requested, no checklist is owed — and the pull request shows which check waived it. ```toml [[scopes]] name = "payments" paths = ["payments/**"] reviewers = ["$payments_team"] require = 1 unless = ["acme-review/no-functional-change"] ``` Each reference is `producer/check-name` — the slug of the GitHub App that posts the check, then the check run's name (split on the first `/`, since check names can contain slashes). For a check posted by a GitHub Actions workflow, the producer is `github-actions`. A bare check name is a configuration error: any workflow with `checks: write` can post a check under any name, so the producing App is what proves the check came from who you meant. The producer slug is case-insensitive; the check name must match **exactly, including case**. A wrong-case name isn't an error — it's a reference to a check that never appears, so the scope simply never waives. Copy the name verbatim from the check as it appears on a pull request. ## Only a passing check waives There is deliberately no syntax for reacting to a failed or missing check: - **Passed** on the head commit → the scope is waived. - **Failed, skipped, cancelled, neutral, still running, or missing** → the requirement stands, unchanged. A push resets everything: checks are per-commit, so a new head commit means the requirement stands until the check passes again — the same way CI works. Listing several checks means **all** of them must pass. There is no OR, threshold, or 2-of-3 syntax — a producer that needs logic like that should decide for itself and post one combined check. ## The waived scope is the receipt A waived scope isn't hidden. It still shows up in the workspace and the PR comment, along with which check waived it and a link to the run. So the config always shows that a scope's review can be waived (`unless` sits right next to the `require` it can remove), and the pull request always shows when that happened and why. ## What PullApprove reads (and doesn't) - **PullApprove never creates these checks; it only reads them.** Its own status on the pull request can't be referenced from `unless`. - **Reviews never waive.** A bot's approval is a general-purpose verdict, not an answer to what a specific scope is asking — only a check run can waive. - **Bots are never reviewers.** Bots that open pull requests are routed with [`authors`](authors.md); bots whose checks can waive a scope go in `unless`. Listing a `[bot]` account in `reviewers`, `alternates`, or `cc` is a configuration error. - **GitHub only.** Check runs are a GitHub concept, so `unless` is a configuration error on GitLab rather than a setting that quietly does nothing. - **The GitHub App needs Checks: Read.** Apps created before this feature existed don't have it, and every pull request whose config references a check fails with "PullApprove needs Checks (read) access to this repository" until it's granted. In your GitHub organization: Settings → Developer settings → GitHub Apps → your PullApprove app → **Permissions & events** → Repository permissions → set **Checks** to **Read-only** → Save, then accept the permission request on the installation. Configs without `unless` are unaffected. ## Request timing The pull request's status never waits on anything. Sending a **review request** is a little more patient: > If a referenced check is visibly running on the head commit, requests wait until it concludes. If it hasn't appeared at all — a fresh push often reaches PullApprove seconds before the check exists — the first send waits about half a minute for it to show up, then goes out. The hold is per-scope: scopes without `unless` request immediately, so one slow check never holds a whole pull request. While a scope is held, the status comment lists no next step and @-mentions nobody — the reviewers haven't been asked, and the author is waiting on a check, not a review. PullApprove never withdraws a review request it has sent, so a pass that lands after one went out waives the scope and the request can be ignored. A person can still remove the request on the host; see [`request`](request.md). ## Guidance for check producers - **Use a dedicated GitHub App for a gating producer.** Every Actions-posted check shares the `github-actions` producer, so any workflow in the repo with `checks: write` can post under that producer and name. Fine for low-stakes automation in a repo you control — but a check that removes human review deserves its own App identity. - **Create your check run when you start, not when you finish.** A visible in-progress run is what holds review requests while you work (Actions does this automatically; an App should POST an `in_progress` run up front). A producer that's invisible until the end may find requests already sent — and sent requests stay sent. - **Be as strict as the requirement you waive.** If your check is nondeterministic, re-running it until it passes is a lever your policy has handed out. ## FAQ **I added `unless` in a pull request and nothing was waived — why?** Configuration governs from the pull request's *base* branch, like every other setting. The pull request that introduces `unless` is still reviewed under the configuration already merged; the waiver takes effect once the change merges, and open pull requests on that base pick it up automatically. **Does `unless` work on pull requests targeting a branch other than the default?** Yes — configuration is read from whatever base branch the pull request targets. One caveat: the instant re-check on a check's completion is indexed from the default branch's configs, so on another base a check that finishes late may not waive the scope until the next event on the pull request (a push, comment, or review). **What happens when the check is re-run?** The re-run replaces the previous result immediately. From the moment it's queued, the scope is un-waived and the requirement stands — the old pass doesn't count while a new answer is on the way. If the re-run passes, the scope waives again. **What if the same check name shows up twice on one commit?** A workflow with multiple triggers (`push` + `pull_request`) produces two check suites, each with its own same-named run. Every matching run must pass — if they disagree, the requirement stands. Prefer a single trigger for the producing workflow. **Someone requested changes — does a passing check still waive the scope?** No. A waiver removes a *requirement*, never a person's objection. A scope with an open changes-requested review fails regardless of its checks. **Can a fork pull request be waived?** Only by real evidence. Checks are read from the base repository, so a fork can't serve its own answer — and if the producing workflow doesn't run for fork PRs, the check is simply missing and the requirement stands. Nothing waits on it; reviewers are requested as usual. **A check passed after reviewers were already requested — now what?** The scope is waived; the request stands. PullApprove never withdraws a review request, so the reviewer keeps a pending request on a pull request that no longer needs them. The waived scope in the PR comment explains why. They can review anyway, ignore it, or remove the request. Nobody is asked in their place, because a waived scope requests no one.