Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
3.6 KiB
name, description
| name | description |
|---|---|
| ci-artifacts | Get the evidence out of a CI run: failed job logs (also mid-run) clustered by error line, and the artifacts that hold the cause. Trigger whenever a CI failure must be explained, and from triage. |
A job log tells you what broke. An artifact tells you why. Investigating a CI failure without downloading its artifacts is guessing with extra steps.
Getting the log
For a GitHub Actions run, one call does it:
${CLAUDE_SKILL_DIR}/scripts/ci_failures.py <RUN_ID> [--repo <owner>/<repo>] [--artifacts]
It prints how far HEAD moved since the run's commit, saves every finished
failed job's log to /tmp/ci-logs-<run>/ (colour stripped, even while the run
is still in progress), and clusters the jobs by the normalised error line that
ended them. Jobs still running are listed, not fetched: their API log can stop hours
early. The clustering is a heuristic; check each boundary against the logs.
Grep the saved files repeatedly. Re-fetching a log to find the line you just lost costs minutes; grepping the saved copy costs milliseconds.
Getting the artifacts
gh api /repos/<owner>/<repo>/actions/runs/<RUN_ID>/artifacts --paginate \
--jq '.artifacts[] | "\(.name)\t\(.size_in_bytes)"'
gh run download <RUN_ID> --repo <owner>/<repo> -n <NAME> -D <DIR>
Choose by content, not by name length:
- Rescue / diagnostics bundles carry the reverse-proxy access and error log, the per-service and per-container logs, the journal, and the event stream. This is where causes live. Prefer them.
- Test-report bundles (Playwright and friends) are mostly video and traces and can run to hundreds of megabytes for one assertion string. Pull one only when you need the assertion text or the error-context file.
- Inventory / runtime dumps settle "what was actually deployed" questions.
Inside a rescue bundle, read in this order: the reverse-proxy log (it shows status code, upstream, and time-to-response for the exact failing request), the application container log, then the journal and event stream for restarts and OOM kills.
An access-log line is a timing witness. A 499 means the client gave up; the
server was still working. A 504 or an upstream timed out ... while reading upstream means your backend never answered. ReqTime versus UpstreamTime
tells you whether the proxy or the upstream burned the seconds.
Grep hygiene
Two failure modes turn a search into a false conclusion:
- Clustered flags silently change the pattern. Write them separately —
grep -r -a -i -l -F "needle" dir/, not-ariel. A cluster where one letter takes an argument consumes your pattern and searches for something else, and the run still exits successfully. - Substring matches lie. A protocol token like
h2cmatches insideveth2cb3f8c; a short id matches inside a hash. Before concluding anything from a hit, print its context (grep -o ".\{30\}needle.\{30\}") and look at it. Before concluding anything from a miss, prove the command was well-formed by searching for a string you know is present.
Binary-ish logs need -a, or grep reports "binary file matches" and you see
nothing.
Rules
- Download before theorising. Do it even when the job log looks conclusive: a log that explains the symptom rarely explains the cause.
- Quote literally, with the file path and line number. Never paraphrase a log line you did not read, and never reconstruct one from memory.
- A negative result is a finding only when the search was verified sound.
- "The artifact does not settle this" is a legitimate answer. Say it instead of filling the gap with a plausible mechanism.