What this state means
A required status check stuck on "Expected — Waiting for status to be reported" means GitHub is waiting for a check it was told is required, but that check has never reported a result on the pull request's head commit. The merge button stays disabled until it does. Nine times out of ten this is not a slow CI run — it is a check that will never report, because the configured required-check name does not match anything the repository actually produces, or the workflow that would produce it was skipped or never triggered. GitHub matches required checks by name only, so a check that never arrives under that exact name blocks the merge forever (Troubleshooting required status checks).
The fix is to find out why the expected name never arrives, not to re-run the job.
Why it happens
Required status checks are matched by the name of the reported check or commit status — not by which workflow, matrix leg, or event produced it. The rulesets troubleshooting page is explicit: "Required status checks do not take workflow, matrix, or event trigger types into account" (Troubleshooting rules). So if you mark build as required but your workflow produces a check named build (ubuntu-latest), the literal name build never arrives and the PR waits indefinitely.
A second class of cause is a skipped workflow. If a workflow is skipped by path, branch, or commit-message filtering, "checks associated with that workflow will remain in a 'Pending' state," and a PR that requires those checks "will be blocked from merging" (Troubleshooting required status checks). A skipped-via-filter workflow does not report a Skipped status to the required check — it reports nothing.
It helps to know that checks and commit statuses are different objects. "GitHub Actions generates checks, not commit statuses, when workflows are run," and checks "are only available for use with GitHub Apps" (About status checks). If you have a check and a status with the same name and mark that name required, both become required (Troubleshooting required status checks).
Diagnose it
Work from the head commit outward. First, get the combined commit status and the check runs for the PR's head SHA — this tells you exactly which names did report, so you can compare against the name that is required.
# Combined commit status for the head ref (statuses only).
# Requires pull access to the repo.
gh api repos/{owner}/{repo}/commits/{ref}/status \
--jq '{state, statuses: [.statuses[].context]}'
# Check runs for the same ref (GitHub Actions / App checks).
gh api repos/{owner}/{repo}/commits/{ref}/check-runs \
--jq '.check_runs[] | {name, status, conclusion, app: .app.slug}'
Both endpoint paths are official: the combined status is GET /repos/{owner}/{repo}/commits/{ref}/status (Commit statuses REST API) and check runs are GET /repos/{owner}/{repo}/commits/{ref}/check-runs (Check runs REST API). The ref may be a SHA, heads/BRANCH, or tags/TAG.
Now compare the names that reported against the name the rule requires:
- A name reported, but not the required one → name mismatch. The required rule wants
build; the run producedbuild (ubuntu-latest)orCI / build. For a reusable workflow the check name is<caller job> / <reusable job>; for a normal job it is<job name>(Troubleshooting rules). - Nothing reported at all → the workflow did not run. Check the Actions tab for this commit. If there is no run, the trigger did not fire (wrong event, branch filter, or a path filter that excluded the changed files).
- An app slug you did not expect → a different GitHub App posted the check. If the rule pins the check to a specific app, this reads as "not reported."
For the skipped-workflow case, open the workflow run list filtered to this branch — a skipped run shows as such in the Actions tab, but the PR still shows the check as Expected/Pending, not Skipped. That asymmetry is the tell.
Resolution options
Fix a name mismatch (most common). Make the required-check name equal the produced name. Either rename the job/check, or change the required rule to the name that actually reports. To see the exact produced names, read them from the check-runs call above and copy one verbatim into the branch-protection / ruleset "require status checks" list.
Stop skipping a required workflow. GitHub's own guidance: do not use path or branch filtering to skip a workflow whose check is required to pass before merging. The supported pattern is to keep the workflow triggering for every relevant event and make the job a no-op that still reports success when there is nothing to do, so the required name always arrives. The required-status-checks page documents the skipped-filter → permanent-pending failure mode directly (Troubleshooting required status checks).
Handle merge queues. If the repo uses a merge queue, the workflow must trigger on the merge_group event. Without it, "status checks will not be triggered when you add a pull request to a merge queue and the merge will fail as the required status check will not be reported" (Troubleshooting required status checks). Add merge_group to the workflow's on: triggers.
Fix a pinned-app mismatch. If the rule requires the check from a specific app and a different app is reporting it, either re-point the rule at the app that actually posts the check, or stop the second app from posting a same-named check.
Recommended approach (with tradeoffs)
Match names exactly and keep required workflows triggering on every relevant event — including merge_group if you queue. The cost is one always-running (often near-instant) job per required check, even on PRs where it has nothing to do. That is a small, predictable amount of CI time. The alternative — path-filtering the workflow to "save minutes" — is the single most common cause of this stuck state, and the blast radius is worse than the savings: every PR that touches the filtered paths is unmergeable until someone notices and force-merges with admin rights, which erodes the protection the rule was meant to provide.
If you cannot avoid filtering, do it inside the job (check the diff, exit 0 early) rather than at the workflow-trigger level, so the check still reports a result under its required name.
Validation steps
After changing names or triggers, push a trivial commit to the PR branch and re-run the diagnostics:
# Confirm the required name now appears with a terminal conclusion.
gh api repos/{owner}/{repo}/commits/{ref}/check-runs \
--jq '.check_runs[] | select(.name == "build") | {status, conclusion}'
You want status: completed and a conclusion of success, skipped, or neutral — those are the conclusions branch protection accepts as satisfying a required check (About protected branches). The PR's merge box should flip from "Expected" to a green check.
Prevention strategies
- Treat required-check names as a contract. When you rename a job, matrix dimension, or reusable workflow, update the required-check rule in the same change. A rename is the most common way a previously-green repo starts blocking every PR.
- Prefer rulesets for visibility. Anyone with read access can view active rulesets, which makes "why is this blocked" answerable without admin access (About rulesets).
- Never path-filter a required workflow at the trigger level. Filter inside the job instead, so the name always reports.
Operational lessons
This failure is quiet by design: nothing errors, nothing is red, and the run history looks healthy — the PR just never becomes mergeable. The fastest path to the answer is always the check-runs API on the head commit, because it shows the exact strings that reported. Once you can see that the produced name and the required name differ by a matrix suffix or a CI / prefix, the fix is obvious; without it you can lose an hour re-running green jobs.
For teams running this across many repos, the deeper signal is that a blocked merge is rarely just a CI problem — the change sitting behind the block (and the workflow run, the branch state, the reviewers) is the real context. Pulling the check-run state, the head commit, and the branch-protection rule into one view is exactly the kind of cross-system correlation that turns "it's stuck" into "here's the name that never reported and the commit that needs it."