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

GitHub

Diagnose 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-failure instead. It leads with the opposite assumption: a scheduled/live-API failure is usually backend or data drift, not your change.

Agent Behaviour Contract

  1. Identify the failing job first — Lint, Lint Markdown, Build and Test (macOS), Build (<platform>), Build and Test (Linux), or Test (<timezone>). Don't guess the cause before you know the job.
  2. 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".
  3. Treat warnings as errors. Build steps use -warnings-as-errors / --Werror — a deprecation or unused-binding warning is a real failure.
  4. Reproduce locally before declaring a fix using the matching tool (/lint, /build-for-testing, /test, make lint-markdown, make build-linux).
  5. 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 origin remote): mcp__github__actions_list method list_workflow_runs (resource_id: ci.yml, workflow_runs_filter: { branch: <branch> }), then mcp__github__get_job_logs (run_id: <id>, failed_only: true, return_content: true). (mcp__github__pull_request_read method get_check_runs also shows which job is red.) Headless / no MCP: gh run list --workflow CI --branch "$(git branch --show-current)" --limit 1, then gh run view <id> --log-failed.
  • CI pipes build/test output through xcsift in github-actions format, so the failing lines are GitHub ::error:: annotations carrying file:line — read those first.

Quick decision tree

Once you know which job failed:

  • Lint (swiftlint --strict / swiftformat --lint / one of the six Scripts/*.py gate 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 --Werror warnings
  • Build and Test — the test step failed? └─ references/unit-tests.md — failing Suite/test, fixture/model mismatch
  • Build (iOS / tvOS / watchOS / visionOS) — a simulator matrix build failed? └─ references/build.md — platform-specific API availability; it is xcodebuild, not SwiftPM, so make build green 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 with TZ=<zone> make test

Triage-first playbook

Symptom → next move:

  • error: … is unavailable / cannot find … in scope, Linux job only → references/linux.md
  • warning: … treated as error → references/build.md
  • error: from swiftc on macOS build → references/build.md
  • A Suite/test recorded a failure / #expect failed → 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_command on unchanged code → references/lint.md (suspect version drift)
  • SwiftFormat would reformat a file (--lint non-zero) → references/lint.md
  • markdownlint MD0xx violation → 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