GitLab CI Doctor

VerifiedCaution

Diagnose and fix GitLab CI pipeline failures, then harden .gitlab-ci.yml against the repo's recurring failure classes.

Sby Skills Guide Bot
DevOpsAdvanced
11/8/2026
Claude Code
#gitlab-ci#pipeline-debugging#ci-cd#release-engineering#yaml

Recommended for

Automated summary

Diagnoses GitLab CI pipeline failures and hardens .gitlab-ci.yml by matching symptoms against a repo-specific taxonomy of recurring failure classes.

Strengths

  • Starts from live pipeline facts (pipeline ID, failed jobs, job trace) through the GitLab API instead of guessing from the commit diff.
  • Provides a concrete symptom-to-root-cause taxonomy (first-contact tag jobs, silent green, artifact-path drift, missing release-gate wiring).
  • Captures repo-specific CI rules such as YAML anchors not crossing include boundaries, so templates must use extends or !reference.
  • Includes practical auth and branch quirks for glab and raw API usage.

Limitations

  • Hard-coded to the futo-notes/futo-notes GitLab project and its ci/<group>.yml layout.
  • Requires network access and a working GITLAB_TOKEN; the provided text also appears cut off mid-entry.
  • The taxonomy is historical and may miss genuinely new failure modes.
When to use it

Use when a GitLab pipeline is failing, a release/tag job did not publish, or before editing .gitlab-ci.yml or tagging a release.

When not to use it

Do not use for CI systems other than GitLab, or for a general YAML syntax review unrelated to this repository's pipeline behavior.

Security analysis

Caution
Quality score87/100

No destructive, obfuscated, or exfiltrating actions: the only secret use is an env-var token sent to the legitimate GitLab API endpoint for the repo, and nothing is piped to curl|sh or decoded. It does rely on Bash, network calls, and Edit of CI config, and contemplates pushing a throwaway branch, so it belongs in 'caution' rather than 'safe', but it is a legitimate diagnostics workflow with explicit guardrails (no silent green, ask before tagging).

Findings
  • •Uses Bash/curl with a private GitLab token (`$GITLAB_TOKEN` via `PRIVATE-TOKEN` header) against a remote API; the token is read from the environment rather than hardcoded, which is correct, but the skill does exercise authenticated network access.
  • •Step 4 suggests pushing a temporary branch with a `rules:` override to dry-run tag-only jobs, i.e. it expects the agent to commit/push to the real remote; it does caution to revert before merge and to ask the user before any retag.
  • •Grants Edit on `.gitlab-ci.yml`/`ci/*.yml` and guides CI/publish pipeline changes — high-impact configuration surface, though changes are reviewable and the skill repeatedly insists on verification and ask-first for publishing.
  • •Skill routinely makes authenticated network calls to the GitLab API (gitlab.futo.org) using a personal access token from $GITLAB_TOKEN, and prints job traces — traces could contain secrets if run verbatim on hostile logs, though nothing here exfiltrates data off-site.
  • •Guidance to edit .gitlab-ci.yml and, in Step 4, to temporarily push branch-scoped rule overrides that make tag-only jobs run on a throwaway branch; legitimate but modifies release/publish configuration, so changes must be reverted as instructed.
  • •Suggests ad-hoc shell loops (watch_pipeline, curl | jq, tail on traces) that are read-only, but the skill assumes the token has broad API scope with no least-privilege note.
How this analysis is made

Examples

Red pipeline triage
The pipeline on main is red. Get the latest pipeline, list the failed jobs, and show me the tail of the trace for the first failure before proposing a fix.
Tag publish failure
publish:android failed on the v1.2.0 tag build — check the job trace, classify the failure against our known CI failure classes, and patch .gitlab-ci.yml if needed.
Pre-tag CI check
I'm about to tag a release. Run the pre-tag check against the current .gitlab-ci.yml and flag any release-gate or artifact-path issues.

name: ci-doctor description: Diagnose and fix GitLab CI/pipeline failures, and harden .gitlab-ci.yml against this repo's recurring failure classes. Use when the user says "pipeline failed", "CI is red", "the tag build failed", "publish:android/publish:ios failed", "check the pipeline", "why didn't the release go out", "harden CI", or before tagging a release ("pre-tag check"). Also load this BEFORE editing .gitlab-ci.yml for any reason — it encodes the hard rules that past pipeline breakage taught. allowed-tools: Bash, Read, Edit, Grep, Glob

CI Doctor

.gitlab-ci.yml is the most-churned file in this repo — CI firefighting is the largest recurring time cost. This skill turns the historical failure classes into a triage procedure so a red pipeline costs minutes, not an afternoon of push-and-watch.

The root .gitlab-ci.yml holds default, variables, workflow, stages, and an include: list; the jobs live in ci/<group>.yml. A YAML anchor (&x/*x) does not resolve across included files, so a template used from another file goes through extends: or !reference. Text checks read the whole pipeline through scripts/lib/gitlab-ci.mjs.

Project: futo-notes/futo-notes on gitlab.futo.org. The curl calls below use $GITLAB_TOKEN, which always works. glab is usually also present, but whether it can authenticate is per-machine and per-shell — see the auth gotcha in docs/agents/issue-tracker.md before blaming a read.

Step 1 — Get the facts (never guess from the commit diff alone)

API="https://gitlab.futo.org/api/v4/projects/futo-notes%2Ffuto-notes"
H='--header PRIVATE-TOKEN:'"$GITLAB_TOKEN"

# Latest pipelines for a ref (branch or tag)
curl -s $H "$API/pipelines?ref=main&per_page=5" | jq '.[] | {id, status, ref, sha: .sha[0:8], web_url}'

# Jobs in a pipeline — find the failures
curl -s $H "$API/pipelines/<PIPELINE_ID>/jobs?per_page=100" \
  | jq '.[] | select(.status=="failed" or .status=="canceled") | {id, name, stage, status, allow_failure}'

# The job log (the tail is where the truth is)
curl -s $H "$API/jobs/<JOB_ID>/trace" | tail -80

glab ci status / glab ci view work for quick looks; use the raw API when you need job traces or to script a watch loop.

For pipelines on the current branch, glab ci list -b <branch> may parse the branch as a date and fail. Use the encoded project path through glab api:

BRANCH=$(git branch --show-current)
glab api "projects/futo-notes%2Ffuto-notes/pipelines?ref=$BRANCH"

Read the log tail FIRST, then the failing job's script: in its ci/*.yml file, then the diff of the commit that broke it (git log --oneline -5 -- .gitlab-ci.yml ci/).

Step 2 — Classify against the known failure taxonomy

Match the symptom before inventing a novel diagnosis. These classes account for most historical red pipelines here:

| Class | Symptom | Root cause pattern | |---|---|---| | First-contact tag job | A publish/release job fails on its first real run at tag time | Job only executes on tags, so it was never exercised: secrets not propagated (especially into nested VMs like Cirrus), cold caches hitting timeouts, wrong artifact lookup paths | | CWD/path assumption | A step "succeeds" but its effect didn't happen; later step fails mysteriously | GitLab preserves cd across script lines; relative path resolved elsewhere; -f/|| true masked it | | Silent green | "Job succeeded" but nothing was published/uploaded | An error branch special-cased to exit 0 | | Missing release-gate wiring | A test failed but the release published anyway | New test job absent from release:gate.needs (deliberate exceptions: test:audit, test:localization-audit, test:quality-report — see rule 3) | | Artifact-path drift | No files to upload / downstream job can't find inputs | Workspace layout changed; artifacts:paths/lookup globs didn't | | Cache death spiral | Retries keep timing out at the same wall | Caches upload only on_success, so every retry starts cold | | Flake | Same job passes on retry with no change | Fixed timeout or exact-string assertion in a cross-platform test | | Environment skew | Works locally, fails in CI image | Tool version differs (strip/binutils, glibc, NDK, Xcode) — check the job's image:/runner tag |

Step 3 — Fix by the hard rules

Apply the rule for the class; do not just patch the symptom:

  1. Absolute paths only in CI scripts — anchor on $CI_PROJECT_DIR. Never rely on a cd from an earlier line. After any destructive/cleanup line, verify the effect (ls, test -f) rather than trusting -f.
  2. No silent green. Every error path exits non-zero. If a failure is genuinely acceptable, model it as allow_failure: true on the JOB (visible in the UI), never as exit 0 in the script. Assert outcomes: artifact exists, upload count > 0, store accepted the bundle.
  3. Every new test job goes into release:gate.needs in the same commit. Publish jobs start as soon as their own needs succeed, so a red test job that is missing from that list does not hold anything back — that is how v1.4.0 twice came within one job of publishing past a failed test (the release:gate.needs comment in ci/publish.yml names both). Deliberate exceptions: test:audit, test:localization-audit, and test:quality-report are allow_failure: true and left out on purpose (docs/architecture-gates.md) — adding one would turn a reporter into a release blocker.
  4. Secrets into nested environments are passed explicitly (Cirrus VM, docker-in-docker). Never assume a CI variable propagates one level down.
  5. Caches upload when: always so a timed-out job doesn't doom every retry to a cold start.
  6. Flakes get root-caused — wait on conditions/events, not durations; don't assert exact user-facing strings. One commented timeout bump is the ceiling; a second bump on the same job means find the real cause.
  7. Keep MR-blocking vs allow_failure/manual intent explicit in rules: — don't accidentally promote a manual job to blocking or vice versa.

Step 4 — Verify without burning a tag

  • MR-scoped jobs: push a branch/MR and watch that pipeline.
  • Tag-only jobs: never verify by tagging. Options, in order of preference:
    1. Run the job's script locally with the same inputs (most publish scripts are scripts/*.py|mjs|sh — they run outside CI).
    2. Push a temp branch with a one-commit rule override making the job run on that branch (rules: - if: $CI_COMMIT_BRANCH == "ci-dryrun-x"); revert before merge.
    3. For the updater path specifically: node scripts/release-build.mjs e2e rehearses build→sign→serve→verify end-to-end with localdev keys.
  • After a failed tag: fix on main first. Then ask the user before any retag/new tag (publishing is an ask-first action). Repo convention favors bumping the patch version over deleting tags; release creation is retag-safe but partially-published artifacts from the failed run must be accounted for.

Watch loop after pushing a fix (jobs run 10–60 min; poll accordingly):

watch_pipeline() {
  curl -s $H "$API/pipelines/$1" | jq -r '"\(.status)  \(.web_url)"'
  curl -s $H "$API/pipelines/$1/jobs?per_page=100" \
    | jq -r '.[] | "\(.status | .[0:7])\t\(.stage)\t\(.name)"' | sort
}

Pre-tag checklist (run when asked for a "pre-tag check" or before /release tags)

  • [ ] just check green locally
  • [ ] Last main pipeline fully green (not "green with failed-but-allowed jobs you care about")
  • [ ] .gitlab-ci.yml diff since the last tag reviewed against the rules above (git diff $(git describe --tags --abbrev=0) -- .gitlab-ci.yml ci/)
  • [ ] Any NEW tag-only job since the last tag has been exercised (Step 4)
  • [ ] release:gate.needs still lists every test job except the three allow_failure reporters (test:audit, test:localization-audit, test:quality-report), non-blocking by design (grep -A30 'release:gate' ci/publish.yml)
  • [ ] test:audit was yellow because it FOUND something, not because it never ran — its log has no AUDIT-DID-NOT-RUN line (allow_failure renders both the same)
  • [ ] Updater artifacts: signing is the LAST touch on bytes (mesa patch / notarize / Authenticode all happen before .sig) — see docs/release/updater.md

Report format

State: pipeline + failing job(s) with links · the class from the taxonomy · root cause (quote the log line) · the fix and which hard rule it enforces · how it was verified (pipeline link or local run output) · any follow-up risk (e.g. "publish:ios remains unexercised until next tag").

From the file to a first result

The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.

View the source and supporting files

This download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.

1. Download the file

Download SKILL.md

From the root of a test project, save the file as .skill-downloads/gitlab-ci-doctor-mvj1oc4k/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.

Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.

mkdir -p '.skill-downloads/gitlab-ci-doctor-mvj1oc4k' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/gitlab-ci-doctor-mvj1oc4k/download' --output '.skill-downloads/gitlab-ci-doctor-mvj1oc4k/SKILL.md.part' && mv '.skill-downloads/gitlab-ci-doctor-mvj1oc4k/SKILL.md.part' '.skill-downloads/gitlab-ci-doctor-mvj1oc4k/SKILL.md'

2. Check prerequisites

Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.

Tools declared by the author (check availability in your agent): Bash, Read, Edit, Grep, Glob

No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.

The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.

3. Load the skill

After inspection, copy the complete folder into your project using one of the commands below, then open a new tool session. The command stops if a folder with the same name already exists: compare the files before replacing it.

Claude Code
test -f '.skill-downloads/gitlab-ci-doctor-mvj1oc4k/SKILL.md' && test -s '.skill-downloads/gitlab-ci-doctor-mvj1oc4k/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/ci-doctor' && cp -R '.skill-downloads/gitlab-ci-doctor-mvj1oc4k' '.claude/skills/ci-doctor'

OpenSkills

OpenSkills must already be installed. The local install/read procedure is checked with OpenSkills 1.5.0, which requires Node.js ≥ 20.6.0. Catalog slugs are not resolved automatically.

Official OpenSkills installation
openskills install './.skill-downloads/gitlab-ci-doctor-mvj1oc4k'

openskills read 'gitlab-ci-doctor-mvj1oc4k'

Reading should display the installed file content and its base directory. It does not run the skill or install its dependencies.

For an agent that reads AGENTS.md, then sync the index. This command changes AGENTS.md: review the diff before using it.

openskills sync

Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.

4. Try a first task

Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:

Read .skill-downloads/gitlab-ci-doctor-mvj1oc4k/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.

Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.

Related skills