# `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. And once a request is sent, it stands — PullApprove never withdraws a review request, so a pass that lands after one went out simply waives the scope and the request can be ignored. ## 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, and they're free to review anyway or ignore it.