GitLab CI skill for Claude Code
One pipeline per change, jobs that start early. Claude adds workflow rules so branch and merge request pipelines stop doubling up, converts stage chains into a needs DAG, and separates lockfile-keyed caches from artifacts that later jobs rely on. Shared config moves into extends, includes and pinned CI/CD components, and deploys get protected environments, a resource group, a manual gate and OIDC tokens.
- name
gitlab-ci - size244 lines · ≈1,950 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Writes and optimizes .gitlab-ci.yml pipelines — workflow rules that prevent duplicate pipelines, job rules with changes and manual gates, needs-based DAGs, cache vs. artifacts, test reports, includes and CI/CD components, environments with resource groups, and OIDC id_tokens for cloud deploys. Use when creating or debugging GitLab CI pipelines, when pipelines run twice or jobs never start, when CI is slow, or when setting up deploy stages and protected environments. Try asking
- Every push to my MR branch starts two pipelines. Fix our .gitlab-ci.yml
- Our GitLab pipeline takes 20 minutes; restructure it so jobs run in parallel
- Add a manual production deploy job that uses OIDC instead of AWS keys
Use it when
- You are writing or refactoring .gitlab-ci.yml
- Pipelines run twice, or jobs are skipped when they should run
- CI is slow because jobs wait on whole stages
- You are setting up environments and deploy gates in GitLab
Skip it when
- The repository uses GitHub workflows — use github-actions instead
- You need a canary or blue/green rollout strategy itself — use blue-green-deploys instead
How to check the result
glab ci lint passes, a branch with an open merge request produces exactly one pipeline, and the needs graph shows jobs starting as soon as their dependencies finish.
The common mistake it prevents
Leaving out workflow rules, so every push to a branch with an open merge request triggers both a branch pipeline and an MR pipeline.
A good pipeline runs once per change (not once for the branch and again for the MR), starts each job as soon as its real dependencies finish, caches dependencies but passes build outputs as artifacts, and deploys through protected environments that serialize with a resource group.
Ground rules
- Use
rules:.only/exceptare legacy and do not combine well withrules. - Define
workflow:rulesat the top so branch and MR pipelines do not both run. - Cache is a best-effort speedup; artifacts are the contract between jobs.
- Deploy jobs:
environment:,resource_group:, protected variables, and a manual gate for production. Triggering a production deploy requires user confirmation. - Validate the YAML before pushing (
glab ci lintor the pipeline editor).
Workflow
- [ ] 1. Read the current file, includes, and recent pipeline durations
- [ ] 2. Add workflow:rules and defaults
- [ ] 3. Define stages, then needs for a DAG
- [ ] 4. Cache dependencies; artifacts for outputs and reports
- [ ] 5. Factor shared config (extends, !reference, include, components)
- [ ] 6. Deploy jobs with environments, resource_group, OIDC
- [ ] 7. Lint, run on a branch, compare durations
1. Inspect
glab ci lint # validates .gitlab-ci.yml against the project
glab ci view # interactive view of the current pipeline
glab ci list # recent pipelines and status
Use the pipeline editor’s “Full configuration” tab (or the CI Lint API) to see the
merged result of all include: files before debugging rule logic.
2–4. Baseline pipeline
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
default:
image: node:22-bookworm-slim
interruptible: true
retry:
max: 2
when: [runner_system_failure, stuck_or_timeout_failure]
stages: [build, test, deploy]
.node_cache: &node_cache
key:
files: [package-lock.json]
paths: [.npm/]
policy: pull
install:
stage: build
script:
- npm ci --cache .npm --prefer-offline
- npm run build
cache:
<<: *node_cache
policy: pull-push
artifacts:
paths: [dist/, node_modules/]
expire_in: 1 day
unit:
stage: test
needs: [install]
script:
- npm test -- --reporter=junit --outputFile=junit.xml --coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
when: always
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
lint:
stage: test
needs: [install]
script: [npm run lint]
e2e:
stage: test
needs: [install]
parallel: 4
script:
- npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
paths: [src/**/*, e2e/**/*]
compare_to: refs/heads/main
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Passing large node_modules as an artifact is a tradeoff; for big trees, cache it per
lockfile and re-run npm ci instead.
Cache vs. artifacts
| Data | Mechanism | Why |
|---|---|---|
| Package manager download cache | cache: keyed on lockfile | Reusable across pipelines, safe to miss |
| Build output used by later jobs | artifacts: + needs: | Guaranteed to exist for that pipeline |
| Test results / coverage | artifacts:reports | Shows in MR widget |
| Files for humans (logs, screenshots) | artifacts: with when: on_failure and expire_in | Debugging |
Use policy: pull on jobs that only read the cache; only one job should push it.
Caches are per runner unless a distributed cache (S3/GCS) is configured.
Rules cheat sheet
| Goal | Rule |
|---|---|
| Only in MRs | if: $CI_PIPELINE_SOURCE == "merge_request_event" |
| Only on default branch | if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH |
| Only for tags | if: $CI_COMMIT_TAG |
| When files changed | changes: { paths: [...], compare_to: refs/heads/main } |
| Manual, non-blocking | when: manual + allow_failure: true |
| Manual, blocks later stages | when: manual (allow_failure defaults to false in rules) |
| Scheduled jobs only | if: $CI_PIPELINE_SOURCE == "schedule" |
| Skip everything else | final - when: never (implicit when no rule matches) |
Rules are evaluated in order; the first match wins.
5. Reuse
include:
- local: ci/templates.yml
- project: platform/ci-templates
ref: v3.2.0
file: /node.yml
- component: $CI_SERVER_FQDN/platform/components/[email protected]
inputs:
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
.deploy_base:
image: alpine:3.22
before_script: [apk add --no-cache aws-cli]
deploy-staging:
extends: .deploy_base
script:
- !reference [.deploy_base, before_script]
- ./deploy.sh staging
Pin include:project and components to a tag or SHA, never a moving branch.
6. Deploy with environments and OIDC
deploy-production:
stage: deploy
needs: [unit, lint]
image: alpine:3.22
environment:
name: production
url: https://shop.example.com
resource_group: production
id_tokens:
AWS_ID_TOKEN:
aud: https://gitlab.example.com
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
script:
- apk add --no-cache aws-cli
- >
export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
$(aws sts assume-role-with-web-identity
--role-arn "$AWS_ROLE_ARN"
--role-session-name "gitlab-$CI_PIPELINE_ID"
--web-identity-token "$AWS_ID_TOKEN"
--duration-seconds 3600
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
--output text))
- ./deploy.sh production
- Restrict the IAM trust policy on the token’s
subclaim (project_path:group/project:ref_type:branch:ref:main). - Mark the
productionenvironment as protected so only maintainers can run it. resource_groupensures two deploys never run concurrently.
Container builds
Prefer BuildKit rootless or Buildah on shared runners. Docker-in-Docker needs a
privileged runner (services: [docker:dind]) and DOCKER_TLS_CERTDIR: "/certs".
Kaniko’s upstream Google repository was archived in 2025; use a maintained fork if
the project depends on it.
7. Verify
glab ci lintpasses; the merged configuration shows the expected jobs.- Push a branch with an open MR: exactly one pipeline appears (the MR pipeline).
- Check the needs graph view: jobs start as soon as dependencies finish.
- Compare pipeline duration across three warm runs, not one.
Gotchas
- Without
workflow:rules, pushing to a branch with an open MR creates two pipelines. changes:withoutcompare_toon branch pipelines compares to the previous push; on a new branch it evaluates to true for every file.- Jobs without
needsdownload artifacts from all earlier stages; useneedsordependencies: []to avoid pulling GBs. - Protected variables are only exposed on protected branches and tags; a deploy that
works on
mainfails on a feature branch by design. - Masked variables must meet length and character rules or masking is refused.
interruptible: trueplus “auto-cancel redundant pipelines” cancels old runs; never mark deploy jobs interruptible.- YAML anchors do not work across
include:files; useextendsor!reference.
Output
**Pipeline changes:** workflow:rules added (no more duplicate MR/branch pipelines),
DAG via needs, lockfile-keyed npm cache, e2e sharded x4, prod deploy behind manual
gate + resource_group, OIDC replaces AWS keys
**Lint:** glab ci lint OK
**Duration (MR, warm):** 18m -> 7m30s About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.