GitLab CI skill for Codex

One pipeline per push, gated deploys, warm caches. Codex writes GitLab pipelines that run once per event thanks to workflow rules, start jobs from needs instead of waiting on stages, share caches keyed on lockfiles with a single writer job, and publish JUnit reports to the merge request. Deploys use named environments, resource_group locks, id_tokens for cloud access and a manual gate for production.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- gitlab-ci

Installs to ~/.agents/skills/gitlab-ci/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $gitlab-ci, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and fixes .gitlab-ci.yml pipelines - workflow rules that avoid duplicate branch and merge request pipelines, job rules with if, changes and exists, needs-based DAGs, cache keys from lockfiles, artifacts and JUnit reports, environments with on_stop and auto_stop_in, resource_group deploy locks, parallel matrix, CI/CD components with spec inputs, child pipelines and OIDC id_tokens. Use when creating or restructuring GitLab CI, a job runs when it should not (or never runs), pipelines duplicate on MRs, caches miss, or deploys need manual gates.

Use it when

  • A project needs a new or restructured .gitlab-ci.yml
  • Jobs run in the wrong pipelines or never appear
  • Pipeline duration is dominated by stage waits or cache misses
  • Deploys need environments, approvals or review apps in GitLab

Use something else when

How to check the result

glab ci lint passes, an MR push creates exactly one pipeline, cached jobs show a restore on the second run, and the test report appears in the MR widget.

The mistake it prevents

Leaving out the workflow rules guard, so branch pushes with an open merge request run both a branch pipeline and an MR pipeline.

~/.agents/skills/gitlab-ci/SKILL.md

--- name: gitlab-ci description: Writes and fixes .gitlab-ci.yml pipelines - workflow rules that avoid duplicate branch and merge request pipelines, job rules with if, changes and… license: MIT ---

A good GitLab pipeline runs exactly once per event, starts every job as soon as its real inputs exist (needs, not stage order), restores caches keyed on lockfiles, reports tests into the merge request widget, and deploys through named environments with a lock and a manual gate for production. Every rule is readable without simulating the whole file.

Inspect first

  1. Read AGENTS.md for the build, test and lint commands; reuse them verbatim.
  2. Read .gitlab-ci.yml and everything it pulls in: rg -n "include:|extends:|!reference" .
  3. Note the runner setup: shared SaaS runners or self-managed, tags, executor (docker, kubernetes, shell). Docker-in-Docker and service containers depend on it.
  4. Check which variables are protected or masked in Settings > CI/CD; protected variables are only present on protected branches and tags, a common “works on main, empty on MR” cause.
  5. Validate before pushing: glab ci lint (network, authenticated) or the project’s Pipeline editor. For local dry runs, gitlab-ci-local can execute jobs in Docker.

Skeleton

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never                       # branch push with an open MR: MR pipeline covers it
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

default:
  image: node:22-bookworm-slim
  interruptible: true                   # newer pipeline on same ref cancels this one
  retry:
    max: 1
    when: [runner_system_failure, stuck_or_timeout_failure]

stages: [build, test, deploy]

variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm"

.node-cache: &node_cache
  key:
    files: [package-lock.json]
  paths: [.npm/]
  policy: pull

install:
  stage: build
  script: npm ci
  cache:
    <<: *node_cache
    policy: pull-push                   # only this job writes the cache

lint:
  stage: test
  needs: [install]
  cache: *node_cache
  script: [npm ci --prefer-offline, npm run lint]

test:
  stage: test
  needs: [install]
  cache: *node_cache
  parallel: 4
  script:
    - npm ci --prefer-offline
    - npx vitest run --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL --reporter=junit --outputFile=junit.xml
  artifacts:
    when: always
    reports:
      junit: junit.xml
    expire_in: 1 week

Turn on “Auto-cancel redundant pipelines” in project settings so interruptible takes effect. Never mark deploy jobs interruptible: true.

Rules decision table

GoalRule
Run only in MR pipelines- if: $CI_PIPELINE_SOURCE == "merge_request_event"
Run only on default branch- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Run only when paths change- changes: { paths: [api/**/*], compare_to: refs/heads/main }
Manual, non-blocking- when: manual + allow_failure: true
Manual, blocks the pipeline- when: manual (in rules, manual jobs default to allow_failure: false)
Skip on draft MRs`- if: $CI_MERGE_REQUEST_TITLE =~ /^([Draft]
Run only on release tags- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
  • Rules are evaluated top to bottom; the first match wins. End with no catch-all when the job should otherwise not run.
  • changes without compare_to on branch pipelines compares against the previous push, and on new branches it evaluates true. Add compare_to for predictable behaviour.
  • Prefer rules over only/except; do not mix them in one job.
  • A needs reference to a job excluded by rules fails the pipeline at creation. Use needs: [{ job: build, optional: true }] when the upstream may be absent.

Reuse

  • extends: .template for job inheritance (deep merge of hashes, arrays replaced).
  • !reference [.setup, script] to splice script lines from another job.
  • include: local files, other projects (project: + ref: + file:), or CI/CD components:
include:
  - component: $CI_SERVER_FQDN/platform/ci-components/[email protected]
    inputs:
      node_version: "22"

A component template declares its inputs in a header:

spec:
  inputs:
    node_version:
      default: "22"
---
node-test:
  image: node:$[[ inputs.node_version ]]-bookworm-slim
  script: [npm ci, npm test]

Pin components and remote includes to a version or SHA, never a moving branch.

Deploys

deploy-staging:
  stage: deploy
  needs: [test, build-image]
  script: ./deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com
  resource_group: staging               # one deploy at a time, queued in order
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

deploy-production:
  extends: deploy-staging
  script: ./deploy.sh production
  environment:
    name: production
    url: https://example.com
  resource_group: production
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual
  • Mark production as a protected environment with approvers; protected variables then only reach jobs for that environment.
  • Use id_tokens for cloud auth instead of stored keys:
  id_tokens:
    AWS_ID_TOKEN:
      aud: sts.amazonaws.com
  script:
    - >
      aws sts assume-role-with-web-identity --role-arn "$ROLE_ARN"
      --role-session-name "gl-$CI_JOB_ID" --web-identity-token "$AWS_ID_TOKEN"

Scope the cloud trust policy on the token’s sub claim (project_path:group/app:ref_type:branch:ref:main).

  • Review apps: environment: { name: review/$CI_COMMIT_REF_SLUG, on_stop: stop-review, auto_stop_in: 3 days } with a stop-review job using action: stop and the same rules plus when: manual. For the full per-PR lifecycle use the preview-environments skill.
  • Running a production deploy job, retrying one, or editing protected variables is an outward-facing action: get the user’s explicit go-ahead first.

Larger pipelines

  • Monorepo: one child pipeline per package with trigger: { include: packages/api/.gitlab-ci.yml, strategy: depend } gated by rules: changes.
  • Generated pipelines: a job writes YAML as an artifact, then trigger: include: - artifact: gen.yml job: generate.
  • parallel: matrix: for OS or version fan-out; each combination becomes its own job.
  • Large artifacts slow every downstream job; use dependencies: [] or needs: [{ job: x, artifacts: false }] where a job does not need them.

Debugging

SymptomLikely cause
Two pipelines per push on an MR branchMissing workflow:rules duplicate guard
Job missing from pipelineRules evaluated false; check $CI_PIPELINE_SOURCE for that event
Variable empty on MR, set on mainVariable is protected; MR source branch is unprotected
Cache never restoresKey differs between jobs, or runners do not share cache storage
“job needs X but X is not in any previous stage”needs points to a rule-excluded job
Docker commands failMissing services: [docker:dind] / TLS vars, or runner not privileged

Add CI_DEBUG_TRACE: "true" only briefly and never on jobs that handle secrets: it prints every variable.

Verification

  • glab ci lint (or the Pipeline editor) reports valid
  • Push to an MR branch: exactly one pipeline, of type merged_result or merge_request
  • Second run shows cache restored for jobs with policy: pull
  • Test report appears in the MR widget
  • Production job is manual and serialized by resource_group

Output

When delivering a pipeline change, report: jobs added or changed, the rules each runs under (MR, default branch, tag), cache keys, environments touched, and anything the user must configure in the UI (protected variables, protected environments, auto-cancel).

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "GitLab CI"
  short_description: "One pipeline per push, gated deploys, warm caches"
  default_prompt: "$gitlab-ci Every push to my MR branch creates two pipelines, fix the .gitlab-ci.yml"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc