A status check is a signal from an external system about the state of a commit or PR. Sources of checks:
- GitHub Actions: the built-in CI/CD. Runs automatically become checks.
- External CI via the Commit Status API: CircleCI, Jenkins, Travis, and others. They push status via the API.
- GitHub Apps: marketplace plugins for code coverage, security scanning, license compliance, and similar.
Each check has:
name: displayed on the PR.status: queued / in_progress / completed.conclusion(when completed): success / failure / cancelled / skipped / timed_out / action_required / neutral.
On the PR page, checks appear in the "Checks" section at the bottom. A green checkmark means conclusion success. A red X means failure. A yellow circle means in_progress.
Required checks
In branch-protection you select specific check names that must pass before merge is allowed. Those are the "required checks."
GitHub learns available names from recent runs in the repo. To make a check required, it must have run at least once on some PR. Push the workflow, wait for the first run, then go to Settings -> Branches -> Edit -> Status checks and select it from the list.
Pitfalls
- Renamed a workflow or job: the old name stays required.
GitHub stores the required list by string name. If you changed
name: buildtoname: build-and-testin.github/workflows/ci.yml, the old "build" entry becomes "failing" (nothing runs under that name anymore). Go to branch protection, remove the old name, and add the new one. - External CI stops pushing status: merge is blocked indefinitely. If the integration breaks, either fix it or temporarily remove the check from required.
- Required does not mean "passed recently." GitHub considers a check satisfied if the last commit in the PR has a successful run with that name. After a force-push without a new commit, the old run may still be considered current.
- Status API vs. Checks API. These are two different GitHub APIs; both produce what appears as a "check" in the UI. Older external CI systems often use the Status API; newer ones use the Checks API. There is no visible difference in the UI.
What to do about flaky checks
If a check sometimes fails for no real reason, do not mark it required. A required check must be deterministic. Options:
- Retry within the workflow using
if: failure()and a repeat step. - Manual retry:
gh run rerun --failed. - Remove from required and keep it as informational.
A required check is a contract: "you cannot merge without me." The contract works only if the check is honest.
What to do when a required check should not run on this PR
Sometimes a workflow does not trigger for a specific PR (for example, the PR only touches docs/, but the required check is a test suite that has nothing to do with docs). Solutions:
- Path filters in the workflow.
on.pull_request.paths: ['src/**']prevents the workflow from running on docs-only PRs. - "Required" combined with a path-filtered workflow is a problem: the required check never ran, so merge is blocked.
The standard solution: add a "skip job." The workflow always
triggers, but the first if condition skips all real work on
docs-only changes. The job completes with success. The required
check is satisfied; no actual work was done.