GitLab CI/CD Reference
GitLab CI/CD Reference
.gitlab-ci.yml patterns for builds, tests, Docker images, deployments, and the keywords that trip everyone up: rules vs only/except, needs vs dependencies, and caching.
Pipeline structure — stages, jobs, scripts
# .gitlab-ci.yml — top-level structure
stages: # define order of execution
- build
- test
- security
- deploy
variables: # global variables (available to all jobs)
NODE_VERSION: "22"
DOCKER_DRIVER: overlay2
default: # defaults applied to all jobs
image: node:22-alpine
before_script:
- npm ci --cache .npm --prefer-offline
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .npm/
# A job — minimum viable
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
# Multiple scripts — each line is a separate command
# If one fails, the job fails (like set -e)
test:
stage: test
script:
- npm test
- npm run lint
- npm run type-check
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/' # parse coverage from output
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
junit: test-results.xml
rules — when jobs run (modern approach)
# rules: replaces the deprecated only:/except: keywords
# Rules are evaluated top-to-bottom; first match wins
deploy_production:
stage: deploy
script: ./deploy.sh
rules:
- if: $CI_COMMIT_BRANCH == "main" # only on main
when: manual # requires click to run
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/ # or on semver tags
when: on_success # run automatically
- when: never # otherwise don't run
# Common rule patterns
rules:
# Run on MR pipelines only (not branch pipelines)
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# Skip if commit message contains [skip ci]
- if: $CI_COMMIT_MESSAGE =~ /\[skip ci\]/
when: never
# Run on main + tags + manual trigger (covers most CI/CD cases)
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
- if: $CI_PIPELINE_SOURCE == "web"
# Run only when specific files changed
- changes:
- src/**/*
- package.json
# workflow: rules — control whether to create a pipeline at all
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
# This pattern: run on MRs and main/tags but NOT duplicate branch pipelines
Avoid only: and except: in new pipelines. rules: is more powerful, explicit, and handles edge cases better.
needs and dependencies — DAG pipelines
# By default: all jobs in a stage run in parallel
# stages: run sequentially (all jobs in stage N finish before stage N+1)
# needs: — run jobs out of stage order (DAG)
# Job runs as soon as its needs are met, not waiting for the whole stage
deploy_backend:
stage: deploy
needs:
- build_backend # run as soon as build_backend finishes
- job: test_backend
artifacts: false # don't download artifacts from this job
script: ./deploy-backend.sh
# needs with artifacts (default: true — downloads artifacts from needed jobs)
build_image:
stage: build
script: docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
artifacts:
paths: [dist/]
deploy:
stage: deploy
needs:
- job: build_image # also downloads dist/ artifacts
script: ./deploy.sh
# dependencies: — which artifacts to download (without affecting run order)
# If not set: downloads artifacts from all previous-stage jobs
# Set to [] to download nothing
deploy:
stage: deploy
dependencies:
- build # only download from build job
script: ./deploy.sh
Docker builds and registry
# Build and push to GitLab Container Registry
build_image:
stage: build
image: docker:26
services:
- docker:26-dind # Docker-in-Docker
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build
--cache-from $CI_REGISTRY_IMAGE:latest
--build-arg BUILDKIT_INLINE_CACHE=1
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
-t $CI_REGISTRY_IMAGE:latest
.
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# Using kaniko instead of DinD (no privileged mode needed)
build_image_kaniko:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.23.0-debug
entrypoint: [""]
script:
- /kaniko/executor
--context "$CI_PROJECT_DIR"
--dockerfile "$CI_PROJECT_DIR/Dockerfile"
--destination "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
--destination "$CI_REGISTRY_IMAGE:latest"
--cache=true
--cache-repo "$CI_REGISTRY_IMAGE/cache"
# Built-in CI variables for the registry:
# $CI_REGISTRY = registry.gitlab.com
# $CI_REGISTRY_IMAGE = registry.gitlab.com/group/project
# $CI_REGISTRY_USER = gitlab-ci-token
# $CI_REGISTRY_PASSWORD = $CI_JOB_TOKEN (auto, no setup)
Caching and artifacts
# cache: — persist files between pipelines on the SAME runner (best effort)
# artifacts: — share files between jobs in the SAME pipeline (guaranteed)
# Cache: npm dependencies
cache:
key:
files:
- package-lock.json # invalidate when lockfile changes
paths:
- node_modules/
- .npm/
# Cache by branch (separate cache per branch)
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .gradle/caches/
# Pull cache but don't push (read-only — good for release branches)
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .venv/
policy: pull # pull-push (default), pull, push
# Artifacts: always downloaded by subsequent-stage jobs (unless dependencies:[])
artifacts:
paths:
- dist/ # directories + files
- "*.log" # glob
exclude:
- dist/**/*.map # exclude source maps
expire_in: 1 week # auto-cleanup
when: always # upload even on failure (good for test reports)
when: on_failure # only upload if job failed (for crash dumps)
reports:
junit: test-results.xml
dotenv: deploy.env # exposes variables to downstream jobs
sast: gl-sast-report.json
# Pass variables between jobs via dotenv artifact
build:
script:
- echo "IMAGE_TAG=$CI_COMMIT_SHA" >> deploy.env
artifacts:
reports:
dotenv: deploy.env
deploy:
needs: [build]
script:
- echo $IMAGE_TAG # available from dotenv artifact
Environments and deployments
# environment: — tracks deployments in GitLab UI
deploy_staging:
stage: deploy
script: ./deploy.sh staging
environment:
name: staging
url: https://staging.example.com
deploy_production:
stage: deploy
script: ./deploy.sh production
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual # require manual approval
# Dynamic environments (per MR)
review:
stage: deploy
script: ./deploy-review.sh $CI_COMMIT_REF_SLUG
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_COMMIT_REF_SLUG.example.com
on_stop: stop_review # auto-cleanup
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
stop_review:
stage: deploy
script: ./teardown-review.sh $CI_COMMIT_REF_SLUG
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
when: manual
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
Useful CI variables and reusable templates
# Common predefined CI variables
$CI_COMMIT_SHA # full commit SHA
$CI_COMMIT_SHORT_SHA # first 8 chars
$CI_COMMIT_BRANCH # branch name
$CI_COMMIT_TAG # tag name (only in tag pipelines)
$CI_COMMIT_REF_SLUG # branch/tag as slug (no special chars, lowercase)
$CI_DEFAULT_BRANCH # default branch (usually main)
$CI_PROJECT_NAME # project name
$CI_PROJECT_PATH # group/project
$CI_PROJECT_DIR # /builds/group/project (repo root)
$CI_REGISTRY_IMAGE # registry.gitlab.com/group/project
$CI_PIPELINE_SOURCE # push, merge_request_event, schedule, web, trigger
$CI_MERGE_REQUEST_IID # MR number (only in MR pipelines)
$CI_ENVIRONMENT_NAME # current environment name
# YAML anchors — reuse job definitions
.test_template: &test_template
stage: test
image: node:22
before_script:
- npm ci
cache:
paths: [node_modules/]
unit_tests:
<<: *test_template # merge the anchor
script: npm run test:unit
integration_tests:
<<: *test_template
script: npm run test:integration
services:
- postgres:16
# !reference — GitLab's cross-job reference (better than anchors for scripts)
.setup:
before_script: &setup_script
- apt-get update && apt-get install -y curl
- curl -fsSL https://get.docker.com | sh
build:
before_script: !reference [.setup, before_script]
script: docker build .
🔍 Free tool: GitHub Actions Security Checker — if you also run GitHub Actions alongside GitLab CI, check your workflow files for 8 security issues.
Founded
2023 in London, UK
Contact
hello@releaserun.com