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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- gitlab-ci Installs to ~/.agents/skills/gitlab-ci/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install gitlab-ci The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project gitlab-ci Installs to .agents/skills/gitlab-ci/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/gitlab-ci.zipunzip -o gitlab-ci.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) gitlab-ci PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- The pipeline runs on GitHub: use github-actions-workflows
- The pipeline is correct but slow and needs test sharding or affected-only builds: use ci-speedup
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.
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
- Read
AGENTS.mdfor the build, test and lint commands; reuse them verbatim. - Read
.gitlab-ci.ymland everything it pulls in:rg -n "include:|extends:|!reference" . - Note the runner setup: shared SaaS runners or self-managed, tags, executor (docker, kubernetes, shell). Docker-in-Docker and service containers depend on it.
- 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.
- Validate before pushing:
glab ci lint(network, authenticated) or the project’s Pipeline editor. For local dry runs,gitlab-ci-localcan 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
| Goal | Rule |
|---|---|
| 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.
changeswithoutcompare_toon branch pipelines compares against the previous push, and on new branches it evaluates true. Addcompare_tofor predictable behaviour.- Prefer
rulesoveronly/except; do not mix them in one job. - A
needsreference to a job excluded by rules fails the pipeline at creation. Useneeds: [{ job: build, optional: true }]when the upstream may be absent.
Reuse
extends: .templatefor 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
productionas a protected environment with approvers; protected variables then only reach jobs for that environment. - Use
id_tokensfor 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 astop-reviewjob usingaction: stopand the same rules pluswhen: manual. For the full per-PR lifecycle use thepreview-environmentsskill. - 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 byrules: 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: []orneeds: [{ job: x, artifacts: false }]where a job does not need them.
Debugging
| Symptom | Likely cause |
|---|---|
| Two pipelines per push on an MR branch | Missing workflow:rules duplicate guard |
| Job missing from pipeline | Rules evaluated false; check $CI_PIPELINE_SOURCE for that event |
| Variable empty on MR, set on main | Variable is protected; MR source branch is unprotected |
| Cache never restores | Key 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 fail | Missing 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.yaml
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.