GitLab CI/CD Essentials
Quick reference guide for GitLab CI/CD automation.
What is GitLab CI/CD?
GitLab CI/CD is a built-in continuous integration and deployment platform that:
Core Concepts
Pipeline
├── Stages (sequential)
│ ├── Build
│ ├── Test
│ └── Deploy
└── Jobs (within stages)
├── Script (commands)
├── Artifacts (outputs)
└── Cache (dependencies)
Key Components
Basic Configuration
Simple Pipeline
# .gitlab-ci.yml
stages:
- build
- test
build-job:
stage: build
script:
- echo "Building the application..."
- npm install
- npm run build
test-job:
stage: test
script:
- echo "Running tests..."
- npm test
Complete CI/CD Pipeline
# .gitlab-ci.yml
image: node:20
stages:
- build
- test
- deploy
variables:
NODE_ENV: production
cache:
paths:
- node_modules/
before_script:
- npm ci
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
test:lint:
stage: test
script:
- npm run lint
test:unit:
stage: test
script:
- npm test
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
deploy:production:
stage: deploy
script:
- npm run deploy
environment:
name: production
url: https://example.com
only:
- main
Stages
Default Stages
# Default stages (if not specified)
stages:
- .pre
- build
- test
- deploy
- .post
Custom Stages
stages:
- install
- lint
- test
- build
- package
- deploy
- cleanup
install:
stage: install
script:
- npm ci
lint:
stage: lint
script:
- npm run lint
test:
stage: test
script:
- npm test
build:
stage: build
script:
- npm run build
deploy:
stage: deploy
script:
- ./deploy.sh
Jobs
Basic Job Structure
job-name:
stage: test
image: node:20
variables:
VAR_NAME: value
before_script:
- echo "Before script"
script:
- echo "Main script"
- npm test
after_script:
- echo "After script"
artifacts:
paths:
- coverage/
only:
- main
Parallel Jobs
test:
stage: test
parallel: 3
script:
- npm test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
Matrix Jobs
test:
stage: test
parallel:
matrix:
- NODE_VERSION: ['16', '18', '20']
OS: ['linux', 'macos']
image: node:${NODE_VERSION}
script:
- npm test
Scripts
Multi-line Scripts
build:
script:
- |
echo "Building application..."
npm ci
npm run build
echo "Build complete!"
Script with Error Handling
deploy:
script:
- set -e # Exit on error
- |
if [ -z "$DEPLOY_TOKEN" ]; then
echo "Error: DEPLOY_TOKEN not set"
exit 1
fi
- ./deploy.sh
Variables
Global Variables
variables:
GLOBAL_VAR: "value"
DATABASE_URL: "postgres://localhost:5432/db"
job:
script:
- echo $GLOBAL_VAR
Job-specific Variables
variables:
GLOBAL_ENV: production
dev-deploy:
variables:
DEPLOY_ENV: development
script:
- echo "Deploying to $DEPLOY_ENV"
prod-deploy:
variables:
DEPLOY_ENV: production
script:
- echo "Deploying to $DEPLOY_ENV"
Built-in Variables
job:
script:
- echo "Pipeline ID: $CI_PIPELINE_ID"
- echo "Commit SHA: $CI_COMMIT_SHA"
- echo "Branch: $CI_COMMIT_BRANCH"
- echo "Project: $CI_PROJECT_NAME"
- echo "Runner: $CI_RUNNER_DESCRIPTION"
Protected Variables
# Set in GitLab UI: Settings > CI/CD > Variables
deploy:
script:
- echo "Using secret: $SECRET_KEY"
- deploy --token $DEPLOY_TOKEN
only:
- main
Rules and Conditions
Using rules
job:
script:
- echo "Running job"
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: always
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: manual
- when: never
# Run only on main branch
deploy:
script:
- ./deploy.sh
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
# Run on tags only
release:
script:
- ./release.sh
rules:
- if: '$CI_COMMIT_TAG'
Using only and except (Legacy)
# Run only on main
deploy:
script:
- ./deploy.sh
only:
- main
# Run except on main
test:
script:
- npm test
except:
- main
# Run on specific branches
deploy:
script:
- ./deploy.sh
only:
- main
- develop
- /^release-.*$/
Changes Detection
# Run if specific files changed
test-api:
script:
- npm test -- api
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- api/**/*
- package.json
# Run on any code changes
build:
script:
- npm run build
rules:
- changes:
- src/**/*
- package.json
Artifacts
Basic Artifacts
build:
script:
- npm run build
artifacts:
paths:
- dist/
- build/
expire_in: 1 week
Artifacts with Reports
test:
script:
- npm test
artifacts:
reports:
junit: test-results.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
paths:
- coverage/
when: always
expire_in: 30 days
Download Artifacts
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
deploy:
stage: deploy
dependencies:
- build
script:
- ls dist/
- ./deploy.sh
Cache
NPM Cache
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
before_script:
- npm ci --cache .npm --prefer-offline
Multiple Caches
cache:
- key:
files:
- package-lock.json
paths:
- node_modules/
- key:
files:
- Gemfile.lock
paths:
- vendor/ruby
Cache Policy
# Create cache
build:
cache:
key: build-cache
paths:
- node_modules/
policy: push
script:
- npm ci
# Use cache
test:
cache:
key: build-cache
paths:
- node_modules/
policy: pull
script:
- npm test
Docker Integration
Using Docker Image
image: node:20-alpine
job:
script:
- node --version
- npm --version
Different Images per Job
build:
image: node:20
script:
- npm run build
test:
image: node:20-alpine
script:
- npm test
deploy:
image: alpine:latest
script:
- ./deploy.sh
Docker-in-Docker (DinD)
build-docker:
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:latest .
- docker push $CI_REGISTRY_IMAGE:latest
Kaniko (Build without DinD)
build:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
script:
- |
echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- |
/kaniko/executor \
--context $CI_PROJECT_DIR \
--dockerfile $CI_PROJECT_DIR/Dockerfile \
--destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
Services
PostgreSQL Service
test:
image: node:20
services:
- postgres:15
variables:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
DATABASE_URL: "postgres://test_user:test_password@postgres:5432/test_db"
script:
- npm test
Multiple Services
integration-test:
services:
- postgres:15
- redis:7-alpine
- selenium/standalone-chrome:latest
variables:
POSTGRES_DB: test_db
POSTGRES_PASSWORD: secret
REDIS_URL: redis://redis:6379
script:
- npm run test:integration
Environments
Basic Environment
deploy:production:
stage: deploy
script:
- ./deploy.sh
environment:
name: production
url: https://example.com
only:
- main
deploy:staging:
stage: deploy
script:
- ./deploy.sh
environment:
name: staging
url: https://staging.example.com
only:
- develop
Dynamic Environments
deploy:review:
stage: deploy
script:
- ./deploy-review.sh
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_COMMIT_REF_SLUG.example.com
on_stop: stop:review
only:
- merge_requests
stop:review:
stage: deploy
script:
- ./stop-review.sh
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
when: manual
only:
- merge_requests
Templates and Includes
Local Templates
# .gitlab-ci.yml
include:
- local: '/templates/build.yml'
- local: '/templates/deploy.yml'
variables:
APP_NAME: myapp
# templates/build.yml
.build-template:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
build:prod:
extends: .build-template
variables:
NODE_ENV: production
Remote Templates
include:
- remote: 'https://gitlab.com/awesome-project/raw/main/.gitlab-ci-template.yml'
Extends (Inheritance)
.deploy-template:
stage: deploy
before_script:
- echo "Preparing deployment..."
script:
- ./deploy.sh
only:
- main
deploy:aws:
extends: .deploy-template
variables:
PROVIDER: aws
script:
- ./deploy-aws.sh
deploy:gcp:
extends: .deploy-template
variables:
PROVIDER: gcp
script:
- ./deploy-gcp.sh
Runners
Shared Runners
# Uses GitLab.com shared runners
job:
tags:
- docker
script:
- npm test
Specific Runner
# Uses runner with specific tag
deploy:
tags:
- deploy
- production
script:
- ./deploy.sh
Shell Executor
build:
tags:
- shell
script:
- npm ci
- npm run build
Pipeline Types
Merge Request Pipelines
test:
script:
- npm test
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
Scheduled Pipelines
# Create schedule in GitLab UI: CI/CD > Schedules
cleanup:
script:
- ./cleanup-old-data.sh
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
Manual Pipeline
deploy:
script:
- ./deploy.sh
when: manual
only:
- main
Deployment Strategies
Deploy to AWS
deploy:aws:
stage: deploy
image: python:3.11
before_script:
- pip install awscli
script:
- aws s3 sync ./dist s3://$S3_BUCKET --delete
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_ID --paths "/*"
environment:
name: production
url: https://example.com
only:
- main
Deploy to Kubernetes
deploy:k8s:
stage: deploy
image: bitnami/kubectl:latest
before_script:
- kubectl config set-cluster k8s --server="$KUBE_URL" --insecure-skip-tls-verify=true
- kubectl config set-credentials admin --token="$KUBE_TOKEN"
- kubectl config set-context default --cluster=k8s --user=admin
- kubectl config use-context default
script:
- kubectl apply -f k8s/
- kubectl rollout status deployment/myapp
environment:
name: production
only:
- main
Deploy to Vercel
deploy:vercel:
stage: deploy
image: node:20
before_script:
- npm install -g vercel
script:
- vercel --token $VERCEL_TOKEN --prod
environment:
name: production
only:
- main
Testing
Unit Tests
test:unit:
stage: test
script:
- npm run test:unit
coverage: '/Lines\s+:\s+(\d+\.\d+)%/'
artifacts:
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
Integration Tests
test:integration:
stage: test
services:
- postgres:15
- redis:7
variables:
DATABASE_URL: postgres://postgres:password@postgres/test_db
REDIS_URL: redis://redis:6379
script:
- npm run test:integration
E2E Tests
test:e2e:
stage: test
services:
- selenium/standalone-chrome:latest
variables:
SELENIUM_HOST: selenium__standalone-chrome
SELENIUM_PORT: 4444
script:
- npm run test:e2e
artifacts:
when: on_failure
paths:
- cypress/screenshots/
- cypress/videos/
expire_in: 1 week
Security Scanning
SAST (Static Application Security Testing)
include:
- template: Security/SAST.gitlab-ci.yml
variables:
SAST_EXCLUDED_PATHS: spec, test, tests, tmp
Dependency Scanning
include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
Container Scanning
include:
- template: Security/Container-Scanning.gitlab-ci.yml
container_scanning:
variables:
CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
Performance Optimization
Parallel Execution
test:
stage: test
parallel: 5
script:
- npm test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
Interruptible Jobs
test:
script:
- npm test
interruptible: true
DAG (Directed Acyclic Graph)
stages:
- build
- test
- deploy
build:
stage: build
script:
- npm run build
test:unit:
stage: test
needs: [build]
script:
- npm run test:unit
test:integration:
stage: test
needs: [build]
script:
- npm run test:integration
deploy:
stage: deploy
needs: [test:unit, test:integration]
script:
- npm run deploy
Debugging
Debug Variables
debug:
script:
- echo "Pipeline ID: $CI_PIPELINE_ID"
- echo "Commit SHA: $CI_COMMIT_SHA"
- echo "Branch: $CI_COMMIT_BRANCH"
- echo "Tag: $CI_COMMIT_TAG"
- env | sort
CI Lint
# Validate .gitlab-ci.yml locally
gitlab-ci-lint .gitlab-ci.yml
# Or use GitLab API
curl --header "Content-Type: application/json" \
--data @.gitlab-ci.yml \
https://gitlab.com/api/v4/ci/lint
Best Practices
Job Templates
.test-template:
image: node:20
before_script:
- npm ci
cache:
paths:
- node_modules/
test:unit:
extends: .test-template
script:
- npm run test:unit
test:integration:
extends: .test-template
script:
- npm run test:integration
Security
# Use protected variables for secrets
deploy:
script:
- deploy --token $DEPLOY_TOKEN
only:
- main
# Variables masked in logs
# Use environments for protection
deploy:production:
environment:
name: production
only:
- main
Resource Management
# Limit concurrent jobs
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
# Cancel redundant pipelines
workflow:
auto_cancel:
on_new_commit: interruptible
Common Patterns
Monorepo
.build-microservice:
script:
- cd $SERVICE_PATH
- npm ci
- npm run build
build:service-a:
extends: .build-microservice
variables:
SERVICE_PATH: services/service-a
rules:
- changes:
- services/service-a/**/*
build:service-b:
extends: .build-microservice
variables:
SERVICE_PATH: services/service-b
rules:
- changes:
- services/service-b/**/*
Release Pipeline
release:
stage: deploy
image: node:20
script:
- npm ci
- npx semantic-release
only:
- main
variables:
GIT_DEPTH: 0
Tips
needs for faster pipelines (DAG)Quick Reference
# Basic structure
image: node:20
stages: [build, test, deploy]
variables:
VAR: value
cache:
paths: [node_modules/]
before_script:
- npm ci
job-name:
stage: test
script:
- npm test
artifacts:
paths: [dist/]
rules:
- if: '$CI_COMMIT_BRANCH == "main"'