A red check is a claim, not a diagnosis. Before you change code or press
re-run, establish three things from evidence: what actually failed, which class
of failure it is, and whether this change caused it.
This skill is for a failing required check on a pull request or a branch. For
cutting CI cost in general, use the github-actions-efficiency skill. For a
domain-specific flaky suite, use the skill for that tool (for examplecypress-flake-fixer) after this triage.
gh CLI authenticated, or the raw logs pasted by the userIf logs are truncated, partial, or only a summary, stop and ask for the full
log. Do not diagnose from the last few lines of a summarized check.
gh pr checks <pr-number> # which check, which state
gh run list --branch <branch> --limit 10 # current and recent runs
gh run view <run-id> --log-failed # only the failing steps
gh run view <run-id> --job <job-id> --log # one job, full log
Record the commit SHA under test, the runner image, and the time. A failure on
an older SHA is a different problem from a failure on the current head.
Read the top of the failing step, not the bottom. The first error is usually
the cause; everything after it is fallout from that cause or from the runner
giving up.
Quote the exact lines, and name the command that produced them. If the log
shows only a summary line (Process completed with exit code 1), find the step
that emitted it and read that step's output.
| Class | Signal | Confirm by |
|---|---|---|
| Bug | Same failure every run, deterministic input | Re-running fails identically at the same assertion |
| Flaky | Passes sometimes, timing/ordering/network/shared state | At least one green run of the same job on the same SHA, or a local loop that fails intermittently |
| Environment | Tool version, missing secret, runner image, cache, disk or memory | Version in CI differs from local, or the step fails before any project code runs |
| Pipeline | The workflow itself is wrong: bad path filter, wrong working directory, missing dependency install, YAML error, or a check that asserts a target the pull request cannot produce (such as a production URL for a page that is not merged yet) | The command succeeds against the pull request's own build, and the workflow line or its target is demonstrably wrong |
Write the class and the evidence for it in one sentence each. A failure that
cannot be classified is not ready for a fix.
Compare with the base branch before claiming anything about ownership:
gh run list --branch main --limit 10
git merge-base HEAD origin/main
If the same job fails on the base branch, or on recent runs of the base branch,
it is pre-existing. Say so and link the run.
Some jobs only run on pull requests, so no base-branch run exists. In that case,
say that instead of implying you compared, and use the diff to establish what
the change added.
To test the base directly without disturbing the worktree, create a unique
throwaway worktree and remove it when done:
base_dir="$(mktemp -d)"
git worktree add "$base_dir" "$(git merge-base HEAD origin/main)"
( cd "$base_dir" && <failing command> )
git worktree remove --force "$base_dir"
mktemp -d returns a fresh path, so concurrent runs cannot collide. --force is
safe here because the directory was created for this check and the failing
command may have written build output into it. Never point this at a path that
holds work, and remove the worktree with git worktree remove instead of
deleting the directory by hand. If a directory was deleted by hand, rungit worktree prune.
This example is POSIX shell. On Windows, run it from Git Bash, or use
PowerShell:
$baseDir = Join-Path $env:TEMP ("ci-base-" + [guid]::NewGuid())
git worktree add $baseDir (git merge-base HEAD origin/main)
Push-Location $baseDir; <failing command>; Pop-Location
git worktree remove --force $baseDir
If it passes on the base and fails on the head, name the commit and the file and
line that introduced it. If the change is a dependency bump, check the lockfile
diff and the tool version, not only the source diff.
Run the failing command with the versions CI used, from the same working
directory, with the same environment variables and the same install step. Read
the versions from the log, not from your local machine.
If you cannot reproduce it, say so and name every difference you found: OS,
runtime version, dependency resolution, environment variable, timezone,
parallelism, cached state. "Cannot reproduce" is a finding, not a failure.
the green run of the same job on the same SHA.
deterministic bug.
faults, not for hiding a real failure.
For a flaky test, do the follow-up work instead of re-running forever:
for i in $(seq 1 20); do <command>; doneRe-run the check after the fix and show the result. For a flaky test, show the
repeated runs.
Report in this shape:
Check: <name> on <SHA>
First error: <quoted line> (<step>, <command>)
Class: bug | flaky | environment | pipeline
Ownership: introduced by <commit/file:line> | pre-existing (<run link>)
Reproduced: yes (<command>) | no (<differences>)
Action: retry | fix code | fix pipeline
Result: <run link, or the repeated-run tally>
continue-on-error, if: always(), or a path filter to hide a real failurereferences/validation.md records this skill applied
to real failed checks on a public repository: the job log, the classification,
the ownership evidence, the reproduction attempt, and the recommended action.
The first scenario is a transcript from GitHub Copilot CLI, prompted only with
the failing job log, in a checkout where the failing command passes. It returned
the report format below, classified the failure as pipeline, and attributed it to
the parent commit rather than the change under test. Both findings were confirmed
afterwards, and a depth-1 checkout reproduces the failure. The scenario exists to
show the report surviving a green local run instead of collapsing into "cannot
reproduce, probably flaky".