diagnose-ci-failure
Diagnose a failing CI workflow run (lint, markdown lint, build, or unit tests) — identify which job failed, the cause, and a concrete fix
SKILL.md.claude/skills/diagnose-ci-failure/SKILL.md
GitHubDiagnose a CI workflow failure
Overview
The CI workflow (.github/workflows/ci.yml) gates every PR and the merge to
main. Unlike the live-API integration suite, a CI failure is almost always
caused by the change under review — a lint violation, a compile warning/error,
a broken unit test, or a Linux-portability gap. Start from the diff, not from
"maybe it's flaky".
CI fans out into six real jobs (plus a changes paths-filter job and a ci
gate job that only aggregates results): Lint, Lint Markdown,
Build and Test (macOS), Build (<platform>) (an iOS/tvOS/watchOS/visionOS
simulator-build matrix), Build and Test (Linux), and Test (<timezone>) (a
time-zone matrix re-running the unit suites). The diagnosis differs per job, so
this skill is a router: identify the failing job, then follow the matching
reference file for that job's causes, fixes, and local-reproduction command.
Wrong suite? If the Integration workflow (the live-API suite from
integration.yml) failed — not a CI job — use/diagnose-integration-failureinstead. It leads with the opposite assumption: a scheduled/live-API failure is usually backend or data drift, not your change.
Agent Behaviour Contract
- Identify the failing job first —
Lint,Lint Markdown,Build and Test(macOS),Build (<platform>),Build and Test (Linux), orTest (<timezone>). Don't guess the cause before you know the job. - Assume the change caused it. CI gates the PR; read the diff and tie the failure to a changed file. Don't open with "transient" or "flaky".
- Treat warnings as errors. Build steps use
-warnings-as-errors/--Werror— a deprecation or unused-binding warning is a real failure. - Reproduce locally before declaring a fix using the matching tool (
/lint,/build-for-testing,/test,make lint-markdown,make build-linux). - Output the three sections (Summary / Cause / Fix) defined below — concise,
tied to
file:line.
Locate the failing run
Use the first that applies:
- A path or run id the caller handed you.
- The current branch's run via the GitHub MCP (owner/repo from the
originremote):mcp__github__actions_listmethodlist_workflow_runs(resource_id: ci.yml,workflow_runs_filter: { branch: <branch> }), thenmcp__github__get_job_logs(run_id: <id>,failed_only: true,return_content: true). (mcp__github__pull_request_readmethodget_check_runsalso shows which job is red.) Headless / no MCP:gh run list --workflow CI --branch "$(git branch --show-current)" --limit 1, thengh run view <id> --log-failed. - CI pipes build/test output through xcsift in
github-actionsformat, so the failing lines are GitHub::error::annotations carryingfile:line— read those first.
Quick decision tree
Once you know which job failed:
- Lint (
swiftlint --strict/swiftformat --lint/ one of the sixScripts/*.pygate steps)? └─references/lint.md— style/format violations, the Python gates, and the version-drift gotcha - Lint Markdown (
markdownlint)? └─references/markdown.md— README / DocC /.claude//knowledge/rules - Build and Test — the build step failed?
└─
references/build.md— compile errors and--Werrorwarnings - Build and Test — the test step failed?
└─
references/unit-tests.md— failingSuite/test, fixture/model mismatch - Build (iOS / tvOS / watchOS / visionOS) — a simulator matrix build failed?
└─
references/build.md— platform-specific API availability; it isxcodebuild, not SwiftPM, somake buildgreen does not clear it - Build and Test (Linux) — fails on Linux but passes on macOS?
└─
references/linux.md— Apple-only API gating, Foundation differences - Test (America/Los_Angeles or Pacific/Auckland) — the TZ matrix failed?
└─
references/unit-tests.md— a date/calendar assertion depending on the runner's zone; reproduce withTZ=<zone> make test
Triage-first playbook
Symptom → next move:
error: … is unavailable/cannot find … in scope, Linux job only →references/linux.mdwarning: … treated as error→references/build.mderror:fromswiftcon macOS build →references/build.md- A
Suite/testrecorded a failure /#expectfailed →references/unit-tests.md - Test fails to decode a fixture (
keyNotFound,valueNotFound) →references/unit-tests.md - SwiftLint rule violation (
error: … (rule_id)) →references/lint.md superfluous_disable_commandon unchanged code →references/lint.md(suspect version drift)- SwiftFormat would reformat a file (
--lintnon-zero) →references/lint.md - markdownlint
MD0xxviolation →references/markdown.md
Output format
Produce exactly these three sections (keep it under ~150 words; if the caller asked for a file, write the markdown there and nothing else, otherwise reply directly):
Summary: which job and step failed, and the specific error (rule /
file:line / failing Suite/test).
Cause: the root cause, tied to a changed file where possible.
Fix: the concrete next step from the relevant reference file.
Reference files
| File | Failing job | Covers |
|---|---|---|
references/_index.md |
— | Navigation index by symptom |
references/lint.md |
Lint | SwiftLint --strict, SwiftFormat --lint, the six Scripts/*.py gates, pinned versions, drift |
references/markdown.md |
Lint Markdown | markdownlint on README, CLAUDE.md, DocC, .claude/, knowledge/, .github/*.md |
references/build.md |
Build and Test (build step); Build (<platform>) | compile errors, --Werror warnings, release build, simulator-matrix availability |
references/unit-tests.md |
Build and Test (test step); Test (<timezone>) | Swift Testing failures, JSON fixture/model mismatch, TZ-matrix date dependencies |
references/linux.md |
Build and Test (Linux) | Apple-only API gating, Foundation portability |