.gitlab-ci.yml at the repo root, defining stages with jobs that run in parallel within each stage and sequentially across stages..gitlab-ci.yml with stages:, script:, rules:, and image: keywords.only:/except: instead of rules: -- removed in GitLab 17.0..gitlab-ci.yml at repo root (or custom path in settings).rules: instead of only:/except: -- deprecated since GitLab 14.x, removed in 17.0..gitlab-ci.yml -- use CI/CD Variables with masking.needs: for DAG ordering.timeout: for long jobs.| Keyword | Purpose | Example |
|---|---|---|
stages | Define pipeline stages | stages: [build, test, deploy] |
image | Docker image for job | image: node:20-alpine |
script | Commands to execute | script: [npm ci, npm test] |
stage | Assign job to stage | stage: test |
rules | Conditional execution | rules: - if: $CI_COMMIT_BRANCH == "main" |
needs | DAG dependencies | needs: [build_job] |
artifacts | Files between jobs | artifacts: paths: [dist/] |
cache | Persist between runs | cache: paths: [node_modules/] |
variables | Env variables | variables: NODE_ENV: production |
default | Default settings | default: image: node:20 |
include | Import external YAML | include: - template: ... |
extends | Inherit from job | extends: .base_test |
environment | Deploy environment | environment: name: production |
when | Execution condition | when: manual |
parallel | Run N times | parallel: 5 |
services | Linked containers | services: [postgres:16] |
START
├── Simple app (single language)?
│ ├── YES → Three stages: build → test → deploy with shared image
│ └── NO ↓
├── Multiple services/microservices?
│ ├── YES → Use include: to split config, needs: for DAG
│ └── NO ↓
├── Need deployment approval?
│ ├── YES → when: manual + environment: protection
│ └── NO ↓
├── Need matrix testing?
│ ├── YES → parallel:matrix: with version variables
│ └── NO ↓
└── DEFAULT → stages [build, test, deploy] with rules:
Create the file at repo root. GitLab auto-detects it. [src2]
stages:
- build
- test
- deploy
default:
image: node:20-alpine
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
test:
stage: test
script:
- npm ci
- npm test
deploy:
stage: deploy
script:
- echo "Deploy to production"
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: production
Verify: Push → CI/CD → Pipelines → see 3-stage pipeline.
Cache dependencies for faster pipelines. [src1]
default:
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
Verify: Second run shows "Restoring cache" and faster install.
Control when jobs run. [src1]
test:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
Verify: Create MR → test runs. Push to non-MR branch → test skipped.
Test across versions. [src1]
test:
parallel:
matrix:
- NODE_VERSION: ['18', '20', '22']
image: node:${NODE_VERSION}-alpine
script:
- npm ci
- npm test
Verify: Pipeline shows 3 parallel test jobs.
Deploy with approval gate. [src5]
deploy_production:
stage: deploy
script:
- echo "Deploying..."
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
Verify: Merge to main → deploy shows "play" button.
# .gitlab-ci.yml
stages:
- lint
- test
- build
- deploy
default:
image: node:20-alpine
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
lint:
stage: lint
script:
- npm ci
- npm run lint
test:
stage: test
script:
- npm ci
- npm test -- --coverage
artifacts:
reports:
junit: report.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
deploy_production:
stage: deploy
script:
- echo "Deploy dist/"
environment:
name: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
needs: [build]
# .gitlab-ci.yml
stages:
- build
build_image:
stage: build
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == "main"
# ❌ BAD — only/except is deprecated, removed in GitLab 17.0
deploy:
script: echo "deploy"
only:
- main
# ✅ GOOD — rules is current standard
deploy:
script: echo "deploy"
rules:
- if: $CI_COMMIT_BRANCH == "main"
# ❌ BAD — cache never invalidates
cache:
paths:
- node_modules/
# ✅ GOOD — invalidates when deps change
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
# ❌ BAD — visible to anyone with repo access
variables:
API_KEY: "sk-12345-secret"
# ✅ GOOD — stored in Settings → CI/CD → Variables
deploy:
script:
- curl -H "Authorization: Bearer $API_KEY" https://api.example.com
timeout: 6h. [src1]expire_in or use needs:. [src1]key:files: or distributed cache. [src6]# Validate .gitlab-ci.yml locally
gitlab-ci-lint .gitlab-ci.yml
# Check pipeline status via API
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines?per_page=5"
# List CI variables available in pipeline
env | grep CI_
# Debug job locally
gitlab-runner exec docker test_job
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| GitLab 17.x | Current | Removed only/except | Use rules: keyword |
| GitLab 16.x | Previous | CI/CD Components GA | Adopt include: component |
| GitLab 15.x | EOL | Removed CI_BUILD_* vars | Use CI_JOB_* equivalents |
| Shared runners (SaaS) | Active | — | 400 min/month Free, 10K Premium |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Project on GitLab | Project on GitHub | GitHub Actions |
| Need built-in container registry | Complex multi-repo orchestration | Jenkins or Tekton |
| Want integrated DevSecOps | 100+ parallel jobs on free tier | Self-hosted runners |
| Need environment dashboards | Vendor-agnostic CI required | Jenkins or Dagger |
cache: is per-runner, not global. Use S3/GCS distributed cache for consistency.artifacts: count against storage quotas. Set expire_in: to manage.services: only work with Docker or Kubernetes executors, not shell executor.needs: creates DAG pipeline, running jobs out of stage order -- visualize in UI.