The gating pipeline

What happens between a contributor clicking “Create pull request” and the pull request being closed, labelled, checked and scored. This page describes the GitHub App path. The Actions path runs the same decision function and is covered at the end.

Events that trigger a run

The webhook handler acts on six pull_request actions and ignores everything else.

ActionWhat it does
openedFull run.
reopenedFull run.
ready_for_reviewFull run.
synchronizeFull run, and the stored Check Run ids are cleared because the head SHA moved.
labeledOnly when the label is the project's labelEvaluate, default contribution:evaluate. Any other label, including the bot's own, exits before touching the database.
closedRecords whether the close was terminal. A merge or a human close ends the pull request; a close the bot performed stays reopenable.

Adding the evaluate label by hand is the manual re-run. The label is always removed afterwards, whether or not labels are enabled, so it does not retrigger on the next event.

Precedence

decideForRepo returns one of six statuses. The checks run in this order and the first match wins.

  1. Repository inactive. IGNORED. The installation was removed.
  2. Checker disabled. checkerEnabled is false, so the result is APPROVED with the reason checker_disabled. The pull request is not closed and no pending or denied label is applied. Whether a row is written at all depends on trackWhenDisabled.
  3. Manual denial. A maintainer denied this login directly. DENIED, and the pipeline stops here. A denied contributor is never asked to sign a CLA.
  4. Bypass list. The login matches a glob in bypassHandles, for example *[bot]. BYPASSED, and the pipeline stops here, which is what exempts bots from the CLA and DCO gates.
  5. Manual approval. APPROVED, but not returned yet. The CLA layer below still applies.
  6. Collaborator. bypassCollabs is on and GitHub confirms the author is a collaborator. BYPASSED, cached for five minutes. If the collaborator call fails, the failure is logged and the pipeline falls through to the application check rather than guessing.
  7. Application. The author's latest application for this project decides it. Approved means approved. Denied with no resubmission allowed, or denied inside a cooldown, means DENIED. Denied with the cooldown elapsed becomes PENDING. Submitted but undecided is PENDING. No application at all is PENDING.

If applicationRequired is off, a missing or undecided application no longer blocks. An existing denial still does.

What each outcome does

StatusPull requestCommentLabelDecision check
APPROVEDLeft openNoneapprovedsuccess
APPROVED (checker_disabled)Left openNoneapprovedsuccess
BYPASSEDLeft openNoneapprovedsuccess
PENDINGClosedApply link, or awaiting reviewpendingaction_required
CHECK_REQUIREDLeft openSign the CLA, or add the sign-offcla-pendingaction_required
DENIEDClosedLink to status page, no reasondeniedfailure
IGNOREDNothingNoneNoneNone

The status check

One Check Run named contribution-checker / decision, published against the pull request's head SHA. Require it in branch protection and an unapproved contributor cannot merge, even if someone reopens the pull request by hand.

Every state below is produced by the same function the webhook calls.

contribution-checker / decisionApproved

Approved contributor for Acme Router.

success
contribution-checker / decisionChecker disabled

Contribution checker is currently disabled for Acme Router; PR auto-approved.

success
contribution-checker / decisionBypassed (bot)

PR author matches the project's bot bypass list.

success
contribution-checker / decisionBypassed (collaborator)

PR author is a repo collaborator and is bypassed.

success
contribution-checker / decisionApplication required

Open an application for Acme Router to unblock this PR.

action_required
contribution-checker / decisionApplication under review

Your application for Acme Router is awaiting reviewer action.

action_required
contribution-checker / decisionCLA required

Sign the Acme Router CLA to unblock this PR. Your PR stays open and we'll re-check automatically once signed.

action_required
contribution-checker / decisionCLA required

A new version of the Acme Router CLA must be signed. Sign it to unblock this PR. Your PR stays open and we'll re-check automatically once signed.

action_required
contribution-checker / decisionDCO sign-off required

One or more commits are missing a Developer Certificate of Origin sign-off. Add a "Signed-off-by" trailer to each commit (e.g. `git commit -s`). Your PR stays open and we'll re-check automatically.

action_required
contribution-checker / decisionDenied until 2026-09-01.

Denied until 2026-09-01.

failure

Projects with a CLA get a second, independent check named contribution-checker / cla. It reports the author's signature coverage directly rather than the overall decision, so it can be required on its own.

Order of operations

For a single pull request event, convergePr runs this sequence.

  1. Decide.
  2. Load the project's labels, gates and quality settings. Exit if the project is gone.
  3. Apply the DCO layer, which may override the decision.
  4. Work out the standalone CLA state, separately from the decision, so the CLA check stays accurate on paths where the decision short-circuited earlier.
  5. Write or update the PrCheck row. An active CLA or DCO gate always writes one, even when tracking is otherwise off, because the re-check sweep finds affected pull requests by their gate reason.
  6. Create the labels if they are missing.
  7. Apply the outcome: reopen, or close with a comment, or comment and keep open.
  8. Publish the decision check, then the CLA check.
  9. Run quality scoring, if it is enabled and a row exists.

Every step is idempotent, so a redelivered webhook converges to the same state instead of stacking duplicate comments. The gate comment is posted once per gate reason. The quality warning comment is claimed atomically before it is sent, so concurrent deliveries cannot double-post.

Nothing after step 5 can fail the request. Each side effect is caught and logged. The handler returns 200 unless the signature is invalid, because a non-200 makes GitHub retry the delivery indefinitely.

Approval, denial, revocation

On approval

Every pull request the bot closed for that author, across every linked repository in the project, is reopened with a comment naming the project, relabelled to approved, and marked approved. Both pull requests closed as pending and those closed as denied are reopened. The flag that says “we closed this” is what makes it safe: pull requests the contributor closed themselves are never touched.

On denial

Previously closed pull requests are not reopened. They get the denial comment and the denied label, and stay closed.

On revocation

Optional. When the maintainer ticks the box, currently open pull requests from that contributor are closed with a comment linking to their status page. The reason is not published.

Running it from GitHub Actions

If you cannot install a GitHub App, two workflow files do the same job from inside the repository.

Requests carry the OIDC token GitHub mints for the workflow. The server checks its signature against GitHub's JWKS and then trusts two claims: aud, which must be your instance's URL for that exact project, and repository, which must be a repository registered under that project. There is no shared secret to configure or rotate, and a leaked token is worthless after about six minutes.

The gate workflow uses pull_request_target so the token exists for fork pull requests. It never checks out the pull request head. Only the base branch's workflow file runs, which closes the usual pull_request_target injection hole.

The exact YAML is generated per project. Copy it from the project's Repos tab.

When something breaks

The gate is built to fail open rather than trap a contributor.

FailureResult
Collaborator lookup errorsLogged, falls through to the application check.
Commit fetch for DCO errorsDCO passes.
Installation lacks checks:writeNo check published, everything else runs.
Comment, label, close or check call errorsLogged, the handler still returns 200.
Invalid webhook signature401, nothing runs.

Next