GitLab CI/CD Pipeline Generator
Creates .gitlab-ci.yml files, configures pipeline stages, defines CI jobs and runners, sets up deployment workflows, and generates reusable GitLab CI/CD templates following current best practices and security standards. Use when users ask to create or build a GitLab CI/CD pipeline, CI config, build pipeline, deploy pipeline, GitLab YAML, CI jobs, or any .gitlab-ci.yml configuration from scratch or for a new project.
npx skills add github:pantheon-org/tekhne --skill ci-cd--gitlab-ci--generatorOverview
Generate production-ready GitLab CI/CD pipeline configurations following current best practices, security standards, and naming conventions. All generated resources are validated using the devops-skills:gitlab-ci-validator skill before delivery.
Philosophy
- Read the references before writing YAML, so the pipeline starts from a vetted pattern rather than memory.
- Start from the closest template and customise it, rather than composing a pipeline from scratch.
- Secure by default: pinned images, masked variables, explicit timeouts and artifact expiry on every job.
- Never present a complete pipeline that has not been validated.
When to Use
- Creating a new
.gitlab-ci.ymlfor a project. - Adding Docker build, Kubernetes deployment or multi-project stages to a pipeline.
- Extracting repeated job setup into reusable
extendstemplates orincludefiles. - Adding GitLab security templates such as SAST or dependency scanning.
When Not to Use
- Reviewing or debugging an existing
.gitlab-ci.yml: use the GitLab CI validator skill instead. - Writing Azure Pipelines or GitHub Actions workflows: use the matching generator skill.
- Running or executing pipelines on a runner: this skill only produces configuration.
- Answering a one-line syntax question that needs no generated file.
MANDATORY PRE-GENERATION STEPS
CRITICAL: Before generating ANY pipeline, complete these steps in order:
Step 1: Load Reference Files (ALL REQUIRED)
Use the Read tool to load all four reference files plus the relevant template:
1. references/best-practices.md: Security, performance, and naming patterns
2. references/common-patterns.md: Standard pipeline patterns as foundation
3. references/gitlab-ci-reference.md: Syntax reference and keyword details
4. references/security-guidelines.md: Security-sensitive configurations
Template selection:
- Docker pipelines →
assets/templates/docker-build.yml - Kubernetes deployments →
assets/templates/kubernetes-deploy.yml - Multi-project pipelines →
assets/templates/multi-project.yml - Basic pipelines →
assets/templates/basic-pipeline.yml
Step 2: Output Confirmation Before Generating
After reading references, output this confirmation before proceeding:
## Reference Analysis Complete
**Pipeline Pattern Identified:** [Pattern name] from common-patterns.md
- [Why this pattern fits]
**Best Practices to Apply:**
- [3–5 key best practices relevant to this pipeline]
**Security Guidelines:**
- [Security measures to implement]
**Template Foundation:** [Template file name]
- [What will be customized]
Core Capabilities
1. Basic CI/CD Pipelines
Generate complete .gitlab-ci.yml files with proper structure, security best practices, and efficient CI/CD patterns. Use assets/templates/basic-pipeline.yml as structural foundation, referencing references/best-practices.md and references/common-patterns.md.
Apply these principles:
- Semantic stage and job names (kebab-case)
- Pin Docker images to specific versions (never
:latest) - Masked variables for secrets; never hardcode credentials
- Caching for dependencies (npm, pip, maven, etc.)
- Artifact expiration (
expire_inalways set) needskeyword for DAG optimizationrulesinstead of deprecatedonly/except- Explicit
timeouton ALL jobs (10–30 minutes typically) retryfor flaky operations (network, external APIs)resource_groupfor deployment jobs
Validate per the Validation Workflow section.
Minimal example:
stages: [build, test, deploy]
variables:
NODE_VERSION: "20"
default:
image: node:20-alpine
timeout: 20 minutes
cache:
key: ${CI_COMMIT_REF_SLUG}
paths: [node_modules/]
interruptible: true
build-application:
stage: build
timeout: 15 minutes
script: [npm ci, npm run build]
artifacts:
paths: [dist/]
expire_in: 1 hour
test-unit:
stage: test
needs: [build-application]
script: [npm run test:unit]
artifacts:
reports:
junit: junit.xml
deploy-production:
stage: deploy
needs: [build-application, test-unit]
script: [npm run deploy:production]
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
resource_group: production
timeout: 15 minutes
2. Docker Build Pipelines
Create pipelines for building, scanning, and pushing Docker images to container registries. Use assets/templates/docker-build.yml as foundation with Docker-in-Docker or Kaniko, registry authentication via predefined variables, image tagging strategy ($CI_COMMIT_SHORT_SHA), and container security scanning (Trivy or GitLab template). Validate per the Validation Workflow section.
Minimal example:
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
docker-build:
stage: build
image: docker:24-dind
timeout: 20 minutes
services: [docker:24-dind]
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build --cache-from $IMAGE_NAME:latest --tag $IMAGE_NAME:$IMAGE_TAG .
- docker push $IMAGE_NAME:$IMAGE_TAG
retry:
max: 2
when: [runner_system_failure]
3. Kubernetes Deployment Pipelines
Create pipelines deploying to Kubernetes clusters via kubectl, Helm, or Kustomize. Use assets/templates/kubernetes-deploy.yml as foundation with cluster authentication via $KUBE_CONTEXT, environment management, and rollback capabilities. Validate per the Validation Workflow section.
Minimal example:
deploy-k8s:
stage: deploy
image: bitnami/kubectl:1.29
timeout: 10 minutes
before_script: [kubectl config use-context $KUBE_CONTEXT]
script:
- kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -n $KUBE_NAMESPACE
- kubectl rollout status deployment/myapp -n $KUBE_NAMESPACE --timeout=5m
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
resource_group: k8s-production
4. Multi-Project Pipelines
Create pipelines that trigger other projects or use parent-child patterns for monorepos and microservices. Use assets/templates/multi-project.yml or parent-child templates with artifact passing between pipelines and parallel execution where appropriate. Validate per the Validation Workflow section.
5. Reusable Template Configurations
Create modular, DRY configurations using extends, YAML anchors, and include. Extract common patterns into hidden jobs (.template-name), use extends for inheritance (preferred over YAML anchors), organize into separate files with include, and include explicit timeout in all templates. Validate per the Validation Workflow section.
Minimal example:
.node-template:
image: node:20-alpine
timeout: 15 minutes
cache:
key: ${CI_COMMIT_REF_SLUG}
paths: [node_modules/]
before_script: [npm ci]
interruptible: true
build:
extends: .node-template
stage: build
script: [npm run build]
6. GitLab Feature Documentation Lookup
When the user requests specific GitLab features (Auto DevOps, SAST, dependency scanning, etc.):
- Search for current docs:
"GitLab CI/CD [feature] documentation 2025" - If Context7 MCP is available: use
mcp__context7__resolve-library-idthenmcp__context7__get-library-docs - If specific pages needed: use WebFetch on
docs.gitlab.com - Generate pipeline using discovered syntax, required variables, and template include paths
When using GitLab include templates:
include:
- template: Jobs/SAST.gitlab-ci.yml
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
variables:
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"
Note: Customize included template jobs via global
variablesrather than partial job overrides. GitLab merges at runtime, but local validators only see your file-partial overrides will fail validation.
Validation Workflow
Every complete pipeline MUST be validated before presenting to the user.
Process
- After generating, invoke
devops-skills:gitlab-ci-validator - Act on results by severity (CRITICAL/HIGH: must fix; MEDIUM: fix or explain; LOW: acknowledge; SUGGESTIONS: review and apply if beneficial)
- Fix CRITICAL/HIGH issues and re-validate until clear
- Skip validation only for partial snippets, documentation examples, or when user explicitly requests it
Presentation Requirements
See references/validation-presentation.md for detailed requirements on presenting validation results, including status tables, issue explanations, and usage instructions.
Best Practices
See references/best-practices.md for the full set of security, performance, reliability, naming, and organization guidelines that all generated pipelines must follow.
Anti-Patterns
NEVER use only: [master] or only: [main]
WHY: only/except are deprecated in GitLab 15+; rules is the current approach and offers more expressive conditions.
BAD: only: - main
GOOD: rules: - if: '$CI_COMMIT_BRANCH == "main"'
NEVER deploy to production without an environment and approval gate
WHY: Deploying directly without a GitLab environment loses deployment tracking, approval workflows, and rollback history.
BAD: A deployment job with no environment: key.
GOOD: environment: name: production url: https://example.com combined with when: manual for production jobs.
NEVER hardcode runner tags for every job
WHY: Hardcoding forces all jobs onto specific runners even when generic runners would work, reducing parallelism.
BAD: tags: [kubernetes, production] on lint and unit test jobs.
GOOD: Add runner tags only to jobs that genuinely require specific capabilities (GPU, privileged mode, specific OS).
NEVER define duplicate before_script blocks in every job
WHY: Repetitive before_script creates maintenance debt; use YAML anchors or extends to share setup.
BAD: Identical before_script: [npm ci] in every job.
GOOD: Define a .node_setup hidden job with before_script: [npm ci] and use extends: .node_setup in dependent jobs.
NEVER omit expire_in on artifacts
WHY: Artifacts without an expiration date are retained indefinitely, consuming storage quota and slowing artifact listing in the GitLab UI.
BAD: artifacts: paths: [dist/] with no expire_in.
GOOD: artifacts: paths: [dist/] expire_in: 7 days
NEVER use :latest or unpinned image tags
WHY: Floating tags make the same commit build differently on different days, and the validator flags them.
BAD: image: node:latest
GOOD: image: node:20-alpine
NEVER hardcode credentials in the pipeline file
WHY: Secrets committed to .gitlab-ci.yml stay in repository history and are visible to everyone with read access.
BAD: variables: { DB_PASSWORD: "hunter2" }
GOOD: Define a masked CI/CD variable in project settings and reference $DB_PASSWORD.
NEVER omit timeout on jobs
WHY: A job without an explicit timeout can occupy a runner until the instance default expires, blocking other pipelines.
BAD: A build job with no timeout.
GOOD: Set timeout: 15 minutes on the job, or once under default:.
NEVER skip the validation step before presenting a complete pipeline
WHY: Every complete pipeline must be validated with the GitLab CI validator skill, and CRITICAL and HIGH findings fixed, before delivery.
BAD: Generate a full .gitlab-ci.yml and hand it over unchecked.
GOOD: Run the validator, fix CRITICAL and HIGH findings, re-validate, then present the results.
NEVER start generating before loading the reference files
WHY: The pre-generation steps require reading all four reference files and confirming the chosen pattern, so security and naming rules are applied rather than guessed.
BAD: Write the pipeline straight from the request.
GOOD: Read references/best-practices.md, references/common-patterns.md, references/gitlab-ci-reference.md and references/security-guidelines.md, then output the Reference Analysis confirmation.
References
Reference Files
references/best-practices.md: Security, performance, pipeline design, anti-patternsreferences/common-patterns.md: Standard patterns (basic CI, Docker, K8s, multi-project)references/gitlab-ci-reference.md: Full keyword and syntax referencereferences/security-guidelines.md: Secrets, image, script, and artifact securityreferences/validation-presentation.md: Validation results presentation format
Template Files
assets/templates/basic-pipeline.yml: Basic pipeline templateassets/templates/docker-build.yml: Docker build pipeline templateassets/templates/kubernetes-deploy.yml: Kubernetes deployment templateassets/templates/multi-project.yml: Multi-project orchestration template
Template usage: Copy structure → replace [PLACEHOLDERS] → customize logic → remove unused sections → validate.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
No eval scenarios available.
GitLab CI/CD Best Practices
This document outlines comprehensive best practices for creating production-ready, secure, and efficient GitLab CI/CD pipelines.
Table of Contents
- Security Best Practices
- Performance Optimization
- Configuration Organization
- Reliability and Error Handling
- Naming Conventions
- Pipeline Architecture
- Common Anti-Patterns
Security Best Practices
1. Docker Image Pinning
Always pin Docker images to specific versions to ensure reproducibility and security.
# ❌ BAD: Using :latest tag
test-job:
image: node:latest
script: npm test
# ✅ GOOD: Pinned to specific version
test-job:
image: node:20.11-alpine3.19
script: npm test
Best practices:
- Pin to major.minor.patch versions
- Use official images from trusted registries
- Regularly update pinned versions
- Document why specific versions are chosen
2. Secrets and Variables Management
Never hardcode secrets in your .gitlab-ci.yml file. Use GitLab CI/CD variables instead.
# ❌ BAD: Hardcoded credentials
deploy:
script:
- deploy --token abc123xyz
# ✅ GOOD: Using masked variables
deploy:
script:
- deploy --token $DEPLOY_TOKEN
Best practices:
- Mark sensitive variables as Masked and Protected
- Use project/group CI/CD variables for secrets
- Rotate secrets regularly
- Use
$CI_JOB_TOKENfor GitLab API operations - Limit variable scope to specific environments
3. Artifact Security
Be careful with artifact paths to avoid exposing sensitive files.
# ❌ BAD: Overly broad artifact paths
build:
artifacts:
paths:
- ./** # Exposes everything including .env files
# ✅ GOOD: Specific artifact paths
build:
artifacts:
paths:
- dist/
- build/
exclude:
- "**/*.env"
- "**/*.pem"
- "**/credentials.*"
expire_in: 1 hour
Best practices:
- Be explicit about artifact paths
- Use
excludeto prevent sensitive files - Set appropriate expiration times
- Use
artifacts:reportsfor test/coverage reports - Don’t include node_modules or vendor directories
4. Script Security
Avoid dangerous script patterns that can introduce security vulnerabilities.
# ❌ BAD: Dangerous patterns
install:
script:
- curl https://install.sh | bash # Pipe to bash
- eval "$COMMAND" # Code injection risk
- chmod 777 /app # Overly permissive
# ✅ GOOD: Secure patterns
install:
script:
- curl -o install.sh https://install.sh
- sha256sum -c install.sh.sha256 # Verify integrity
- bash install.sh
Best practices:
- Never pipe curl directly to bash
- Validate downloaded scripts
- Use minimal file permissions
- Sanitize user inputs
- Avoid
evaland similar dynamic execution
5. Protected Branches and Environments
Configure protected branches and environments for critical deployments.
deploy-production:
stage: deploy
script:
- deploy production
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG == null
when: manual
resource_group: production
Best practices:
- Require manual approval for production deployments
- Use protected environments
- Restrict who can deploy to production
- Use resource_group to prevent concurrent deployments
- Implement approval rules in GitLab
Performance Optimization
1. Caching Strategies
Use cache to speed up repeated operations like dependency installation.
# ✅ GOOD: Comprehensive caching
variables:
CACHE_VERSION: "v1" # Bump to invalidate cache
default:
cache:
key: ${CACHE_VERSION}-${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
policy: pull
build:
stage: build
script:
- npm ci
- npm run build
cache:
key: ${CACHE_VERSION}-${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
policy: pull-push # Push after installing
test:
stage: test
script:
- npm test
cache:
key: ${CACHE_VERSION}-${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
policy: pull # Only pull, don't push
Cache best practices:
- Use appropriate cache keys (branch, commit, files)
- Set
policy: pullfor jobs that only read cache - Set
policy: pull-pushfor jobs that update cache - Cache language-specific directories (node_modules/, vendor/, .gradle/)
- Use
CACHE_VERSIONvariable for cache invalidation - Don’t cache build artifacts (use artifacts instead)
2. DAG Optimization with needs
Use the needs keyword to create Directed Acyclic Graphs for faster pipelines.
stages:
- build
- test
- deploy
# Without needs: runs sequentially (slow)
build-frontend:
stage: build
script: build frontend
build-backend:
stage: build
script: build backend
test-frontend:
stage: test
script: test frontend
test-backend:
stage: test
script: test backend
# ✅ With needs: runs in parallel (fast)
build-frontend:
stage: build
script: build frontend
build-backend:
stage: build
script: build backend
test-frontend:
stage: test
needs: [build-frontend] # Can start as soon as build-frontend finishes
script: test frontend
test-backend:
stage: test
needs: [build-backend] # Can start as soon as build-backend finishes
script: test backend
deploy:
stage: deploy
needs: [test-frontend, test-backend] # Only depends on tests
script: deploy
Benefits:
- Pipelines run faster by parallelizing independent jobs
- Reduces waiting time between stages
- Clear dependency visualization
3. Parallel Execution
Use parallel jobs for matrix testing or splitting workloads.
# Parallel with matrix
test:
parallel:
matrix:
- NODE_VERSION: ['18', '20', '22']
OS: ['ubuntu', 'alpine']
image: node:${NODE_VERSION}-${OS}
script:
- npm test
# Parallel with index
test-split:
parallel: 4
script:
- npm test -- --shard=${CI_NODE_INDEX}/${CI_NODE_TOTAL}
4. Artifact Optimization
Minimize artifact size and set appropriate expiration.
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
exclude:
- dist/**/*.map # Exclude source maps if not needed
expire_in: 1 hour # Short expiration for intermediate artifacts
deploy:
stage: deploy
needs: [build]
script:
- deploy dist/
Best practices:
- Set short expiration for intermediate artifacts (1 hour - 1 day)
- Set longer expiration for release artifacts (1 week - 1 month)
- Use
artifacts:reportsfor test/coverage reports - Exclude unnecessary files
- Compress large artifacts
Configuration Organization
1. Using extends for Reusability
Use extends to reduce duplication and create maintainable configurations.
# Hidden template jobs (prefixed with .)
.node-base:
image: node:20-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
before_script:
- npm ci
.deploy-base:
before_script:
- echo "Deploying to ${ENVIRONMENT}"
retry:
max: 2
when:
- runner_system_failure
resource_group: ${ENVIRONMENT}
# Actual jobs extending templates
build:
extends: .node-base
stage: build
script:
- npm run build
test:
extends: .node-base
stage: test
script:
- npm test
deploy-staging:
extends: .deploy-base
stage: deploy
variables:
ENVIRONMENT: staging
script:
- ./deploy.sh staging
deploy-production:
extends: .deploy-base
stage: deploy
variables:
ENVIRONMENT: production
script:
- ./deploy.sh production
when: manual
2. Using include for Modular Configuration
Split large configurations into multiple files using include.
# .gitlab-ci.yml (main file)
include:
- local: '.gitlab/ci/templates.yml'
- local: '.gitlab/ci/build-jobs.yml'
- local: '.gitlab/ci/test-jobs.yml'
- local: '.gitlab/ci/deploy-jobs.yml'
stages:
- build
- test
- deploy
variables:
NODE_VERSION: "20"
# .gitlab/ci/templates.yml
.node-base:
image: node:${NODE_VERSION}-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
before_script:
- npm ci
3. Using YAML Anchors
Use YAML anchors for complex repeated structures within a file.
# Define anchor
.retry-config: &retry-config
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
# Use anchor
deploy-staging:
stage: deploy
<<: *retry-config
script:
- deploy staging
deploy-production:
stage: deploy
<<: *retry-config
script:
- deploy production
4. Using default for Common Settings
Set default values for all jobs using the default keyword.
default:
image: node:20-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
before_script:
- echo "Starting job ${CI_JOB_NAME}"
retry:
max: 1
when:
- runner_system_failure
tags:
- docker
interruptible: true
# Jobs inherit default settings
build:
stage: build
script: npm run build
test:
stage: test
script: npm test
Reliability and Error Handling
1. Retry Configuration
Configure retry for flaky operations to improve reliability.
# Retry on specific failures
test-integration:
script:
- npm run test:integration
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
- api_failure
# Conditional retry
deploy:
script:
- deploy.sh
retry:
max: 2
when: always
Retry scenarios:
- Network-dependent operations
- External API calls
- Integration tests
- Deployment operations
- Runner system failures
2. Timeout Settings
Set appropriate timeouts to prevent jobs from hanging.
# Global default timeout (project settings)
# Job-specific timeout
test-quick:
script: npm run test:unit
timeout: 10 minutes
test-e2e:
script: npm run test:e2e
timeout: 30 minutes
deploy:
script: deploy.sh
timeout: 15 minutes
3. Allow Failure
Use allow_failure strategically for non-critical jobs.
# Job can fail without blocking pipeline
lint:
script: npm run lint
allow_failure: true
# Conditional allow_failure
test-experimental:
script: npm run test:experimental
allow_failure:
exit_codes: [1, 137]
4. Interruptible Jobs
Mark test jobs as interruptible to save resources.
test:
script: npm test
interruptible: true # Can be canceled if new pipeline starts
deploy:
script: deploy.sh
interruptible: false # Should not be canceled
5. After Script for Cleanup
Use after_script for cleanup operations that always run.
test:
script:
- npm test
after_script:
- echo "Cleaning up..."
- docker stop test-container || true
- rm -rf temp/
Naming Conventions
Job Names
Use descriptive, action-oriented names in kebab-case.
# ✅ GOOD: Clear, descriptive names
build-frontend:
script: npm run build:frontend
test-unit:
script: npm run test:unit
test-integration:
script: npm run test:integration
deploy-staging:
script: deploy staging
# ❌ BAD: Vague names
job1:
script: npm build
job2:
script: npm test
Stage Names
Use short, standard stage names.
stages:
- build # ✅ Standard, clear
- test # ✅ Standard, clear
- deploy # ✅ Standard, clear
- .pre # ✅ GitLab special stage
- .post # ✅ GitLab special stage
Variable Names
Use UPPER_SNAKE_CASE for variables.
variables:
NODE_VERSION: "20"
DOCKER_DRIVER: overlay2
CACHE_VERSION: "v1"
DEPLOY_ENVIRONMENT: staging
Environment Names
Use lowercase for environment names.
deploy-staging:
environment:
name: staging # ✅ lowercase
url: https://staging.example.com
deploy-production:
environment:
name: production # ✅ lowercase
url: https://example.com
Pipeline Architecture
1. Basic Three-Stage Pipeline
Simple, linear pipeline for straightforward projects.
stages:
- build
- test
- deploy
build:
stage: build
script: make build
test:
stage: test
script: make test
deploy:
stage: deploy
script: make deploy
when: manual
Use when:
- Simple projects with linear workflows
- Few dependencies between jobs
- Quick prototyping
2. DAG Pipeline with Needs
Optimized pipeline for complex projects with independent components.
stages:
- build
- test
- security
- deploy
build-frontend:
stage: build
script: build frontend
build-backend:
stage: build
script: build backend
test-frontend:
stage: test
needs: [build-frontend]
script: test frontend
test-backend:
stage: test
needs: [build-backend]
script: test backend
security-scan:
stage: security
needs: [] # Runs immediately
script: security scan
deploy:
stage: deploy
needs: [test-frontend, test-backend, security-scan]
script: deploy
Use when:
- Large projects with multiple components
- Need faster pipeline execution
- Clear dependencies between jobs
3. Parent-Child Pipelines
Hierarchical pipelines for monorepos or complex orchestration.
# Parent pipeline
stages:
- trigger
trigger-frontend:
stage: trigger
trigger:
include: frontend/.gitlab-ci.yml
strategy: depend
trigger-backend:
stage: trigger
trigger:
include: backend/.gitlab-ci.yml
strategy: depend
Use when:
- Monorepo with multiple projects
- Need isolated pipeline configurations
- Complex orchestration scenarios
4. Multi-Project Pipelines
Cross-project orchestration triggering other projects.
trigger-downstream:
stage: deploy
trigger:
project: group/downstream-project
branch: main
strategy: depend
Use when:
- Microservices deployment
- Library updates triggering dependent projects
- Complex multi-project workflows
Common Anti-Patterns
1. Using :latest Tag
# ❌ ANTI-PATTERN
test:
image: node:latest
script: npm test
# ✅ CORRECT
test:
image: node:20.11-alpine3.19
script: npm test
2. Hardcoding Secrets
# ❌ ANTI-PATTERN
deploy:
script:
- deploy --api-key abc123xyz
# ✅ CORRECT
deploy:
script:
- deploy --api-key $API_KEY
3. Using Deprecated only/except
# ❌ ANTI-PATTERN
deploy:
only:
- main
except:
- tags
# ✅ CORRECT
deploy:
rules:
- if: $CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG == null
4. Not Using Cache
# ❌ ANTI-PATTERN (installs dependencies every time)
test:
script:
- npm install
- npm test
# ✅ CORRECT (caches node_modules)
test:
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
script:
- npm ci
- npm test
5. No Artifact Expiration
# ❌ ANTI-PATTERN (artifacts stored forever)
build:
artifacts:
paths:
- dist/
# ✅ CORRECT (artifacts expire)
build:
artifacts:
paths:
- dist/
expire_in: 1 hour
6. Missing Resource Groups for Deployments
# ❌ ANTI-PATTERN (concurrent deployments possible)
deploy-production:
script: deploy production
# ✅ CORRECT (prevents concurrent deployments)
deploy-production:
script: deploy production
resource_group: production
7. Overly Broad Artifact Paths
# ❌ ANTI-PATTERN
build:
artifacts:
paths:
- ./** # Includes everything
# ✅ CORRECT
build:
artifacts:
paths:
- dist/
- build/
exclude:
- "**/*.env"
8. Not Using Needs for DAG Optimization
# ❌ ANTI-PATTERN (waits for all stage jobs)
stages:
- build
- test
build-frontend:
stage: build
script: build frontend
build-backend:
stage: build
script: build backend
test-frontend:
stage: test
script: test frontend # Waits for build-backend too
# ✅ CORRECT (starts as soon as build-frontend completes)
test-frontend:
stage: test
needs: [build-frontend]
script: test frontend
Summary Checklist
When creating GitLab CI/CD pipelines, ensure:
- Docker images pinned to specific versions
- Secrets stored in masked CI/CD variables
- Cache configured for dependencies
- Artifacts have appropriate expiration times
-
needskeyword used for DAG optimization -
rulesused instead of deprecatedonly/except -
resource_groupused for deployment jobs -
interruptible: truefor test jobs - Retry configured for flaky operations
- Timeout set for long-running jobs
-
extendsorincludeused to reduce duplication - Descriptive job and stage names
- Cleanup operations in
after_script - Manual approval for production deployments
- Security scanning included in pipeline
Reference this document when generating or reviewing GitLab CI/CD pipelines to ensure best practices are followed.
GitLab CI/CD Common Patterns
This document provides ready-to-use patterns for common GitLab CI/CD scenarios. Use these patterns as starting points and customize them for your specific needs.
Table of Contents
- Basic CI Pipeline Patterns
- Docker Build and Push Patterns
- Kubernetes Deployment Patterns
- Testing Patterns
- Deployment Patterns
- Multi-Project Pipeline Patterns
- Parent-Child Pipeline Patterns
- Monorepo Patterns
- Template and Reusability Patterns
Basic CI Pipeline Patterns
Pattern 1: Simple Three-Stage Pipeline
Use case: Basic projects with build, test, and deploy stages.
stages:
- build
- test
- deploy
variables:
NODE_VERSION: "20"
build-job:
stage: build
image: node:${NODE_VERSION}-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
test-job:
stage: test
image: node:${NODE_VERSION}-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
policy: pull
script:
- npm ci
- npm test
deploy-job:
stage: deploy
image: alpine:3.19
script:
- apk add --no-cache rsync openssh
- rsync -avz dist/ $DEPLOY_SERVER:/var/www/html/
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
Pattern 2: Multi-Language Build Pipeline
Use case: Projects with multiple language components.
stages:
- build
- test
- deploy
build-frontend:
stage: build
image: node:20-alpine
script:
- cd frontend
- npm ci
- npm run build
artifacts:
paths:
- frontend/dist/
expire_in: 1 hour
build-backend:
stage: build
image: golang:1.22-alpine
script:
- cd backend
- go build -o app
artifacts:
paths:
- backend/app
expire_in: 1 hour
test-frontend:
stage: test
image: node:20-alpine
needs: [build-frontend]
script:
- cd frontend
- npm ci
- npm test
test-backend:
stage: test
image: golang:1.22-alpine
needs: [build-backend]
script:
- cd backend
- go test ./...
Docker Build and Push Patterns
Pattern 1: Docker Build with Multi-Stage
Use case: Building and pushing Docker images to GitLab Container Registry.
stages:
- build
- push
variables:
DOCKER_DRIVER: overlay2
IMAGE_NAME: $CI_REGISTRY_IMAGE
DOCKER_TLS_CERTDIR: "/certs"
build-docker-image:
stage: build
image: docker:24-dind
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build
--cache-from $IMAGE_NAME:latest
--tag $IMAGE_NAME:$CI_COMMIT_SHORT_SHA
--tag $IMAGE_NAME:latest
--build-arg VERSION=$CI_COMMIT_SHORT_SHA
.
- docker push $IMAGE_NAME:$CI_COMMIT_SHORT_SHA
- docker push $IMAGE_NAME:latest
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# Alternative: Build and push with tags
push-release-image:
stage: push
image: docker:24-dind
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build --tag $IMAGE_NAME:$CI_COMMIT_TAG .
- docker push $IMAGE_NAME:$CI_COMMIT_TAG
rules:
- if: $CI_COMMIT_TAG
Pattern 2: Docker Build with Kaniko (Rootless)
Use case: Building Docker images without Docker-in-Docker (more secure).
stages:
- build
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE
docker-build-kaniko:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.21.0-debug
entrypoint: [""]
script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}"
--destination "${IMAGE_NAME}:latest"
--cache=true
--cache-ttl=24h
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Pattern 3: Multi-Platform Docker Build
Use case: Building Docker images for multiple architectures.
stages:
- build
variables:
DOCKER_DRIVER: overlay2
build-multiarch:
stage: build
image: docker:24-dind
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker buildx create --use
script:
- docker buildx build
--platform linux/amd64,linux/arm64
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
--tag $CI_REGISTRY_IMAGE:latest
--push
.
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Kubernetes Deployment Patterns
Pattern 1: kubectl Deployment
Use case: Deploying to Kubernetes using kubectl.
stages:
- build
- deploy
variables:
KUBE_NAMESPACE: production
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
deploy-k8s:
stage: deploy
image: bitnami/kubectl:1.29
before_script:
- kubectl config use-context $KUBE_CONTEXT
- kubectl config set-context --current --namespace=$KUBE_NAMESPACE
script:
- kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$IMAGE_TAG
- kubectl rollout status deployment/myapp --timeout=5m
environment:
name: production
url: https://example.com
kubernetes:
namespace: $KUBE_NAMESPACE
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
resource_group: k8s-production
Pattern 2: Helm Deployment
Use case: Deploying to Kubernetes using Helm charts.
stages:
- build
- deploy
variables:
HELM_CHART_PATH: ./helm/mychart
RELEASE_NAME: myapp
deploy-helm-staging:
stage: deploy
image: alpine/helm:3.14.0
before_script:
- kubectl config use-context $KUBE_CONTEXT
script:
- helm upgrade --install $RELEASE_NAME $HELM_CHART_PATH
--namespace staging
--create-namespace
--set image.tag=$CI_COMMIT_SHORT_SHA
--set environment=staging
--wait
--timeout 5m
environment:
name: staging
url: https://staging.example.com
rules:
- if: $CI_COMMIT_BRANCH == "develop"
deploy-helm-production:
stage: deploy
image: alpine/helm:3.14.0
before_script:
- kubectl config use-context $KUBE_CONTEXT
script:
- helm upgrade --install $RELEASE_NAME $HELM_CHART_PATH
--namespace production
--create-namespace
--set image.tag=$CI_COMMIT_TAG
--set environment=production
--wait
--timeout 10m
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_TAG
when: manual
resource_group: k8s-production
Pattern 3: Kustomize Deployment
Use case: Deploying to Kubernetes using Kustomize.
stages:
- deploy
deploy-kustomize:
stage: deploy
image:
name: registry.k8s.io/kubectl:v1.29.1
entrypoint: [""]
before_script:
- kubectl config use-context $KUBE_CONTEXT
script:
- cd k8s/overlays/$ENVIRONMENT
- kustomize edit set image myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- kustomize build . | kubectl apply -f -
- kubectl rollout status deployment/myapp -n $ENVIRONMENT
environment:
name: $ENVIRONMENT
kubernetes:
namespace: $ENVIRONMENT
rules:
- if: $CI_COMMIT_BRANCH == "main"
variables:
ENVIRONMENT: production
when: manual
Testing Patterns
Pattern 1: Comprehensive Testing Pipeline
Use case: Multiple types of tests (unit, integration, e2e).
stages:
- test
- integration
variables:
NODE_VERSION: "20"
default:
image: node:${NODE_VERSION}-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
policy: pull
test-unit:
stage: test
needs: []
script:
- npm ci
- npm run test:unit
coverage: '/Coverage: \d+\.\d+%/'
artifacts:
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
test-lint:
stage: test
needs: []
script:
- npm ci
- npm run lint
allow_failure: true
test-types:
stage: test
needs: []
image: node:${NODE_VERSION}-alpine
script:
- npm ci
- npm run type-check
test-integration:
stage: integration
needs: [test-unit]
services:
- postgres:15-alpine
- redis:7-alpine
variables:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
DATABASE_URL: postgres://testuser:testpass@postgres:5432/testdb
REDIS_URL: redis://redis:6379
script:
- npm ci
- npm run test:integration
retry:
max: 2
when:
- runner_system_failure
- api_failure
test-e2e:
stage: integration
needs: [test-unit]
image: cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-123.0-edge-121.0.2277.83-1
script:
- npm ci
- npm run start:test &
- npx wait-on http://localhost:3000
- npm run test:e2e
artifacts:
when: always
paths:
- cypress/videos/
- cypress/screenshots/
expire_in: 1 week
Pattern 2: Matrix Testing (Multiple Versions)
Use case: Testing across multiple language/platform versions.
stages:
- test
test-matrix:
stage: test
parallel:
matrix:
- NODE_VERSION: ['18', '20', '22']
OS: ['alpine', 'bookworm-slim']
image: node:${NODE_VERSION}-${OS}
script:
- node --version
- npm --version
- npm ci
- npm test
Pattern 3: Security Scanning
Use case: SAST, dependency scanning, container scanning.
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/Container-Scanning.gitlab-ci.yml
stages:
- test
- security
# Customize SAST
semgrep-sast:
variables:
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"
# Customize dependency scanning
gemnasium-dependency_scanning:
variables:
DS_EXCLUDED_PATHS: "node_modules, vendor"
# Container scanning after build
container_scanning:
variables:
CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
needs: [build-docker-image]
Deployment Patterns
Pattern 1: Multi-Environment Deployment
Use case: Deploy to multiple environments (dev, staging, production).
stages:
- build
- deploy
variables:
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
.deploy-template:
image: alpine:3.19
before_script:
- apk add --no-cache curl
script:
- curl -X POST $DEPLOY_WEBHOOK_URL
-H "Authorization: Bearer $DEPLOY_TOKEN"
-d "{\"environment\":\"${ENVIRONMENT}\",\"version\":\"${IMAGE_TAG}\"}"
resource_group: ${ENVIRONMENT}
deploy-dev:
extends: .deploy-template
stage: deploy
variables:
ENVIRONMENT: development
environment:
name: development
url: https://dev.example.com
rules:
- if: $CI_COMMIT_BRANCH == "develop"
deploy-staging:
extends: .deploy-template
stage: deploy
variables:
ENVIRONMENT: staging
environment:
name: staging
url: https://staging.example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
deploy-production:
extends: .deploy-template
stage: deploy
variables:
ENVIRONMENT: production
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
when: manual
needs: [deploy-staging]
Pattern 2: Blue-Green Deployment
Use case: Zero-downtime deployments with blue-green strategy.
stages:
- deploy
- verify
- switch
deploy-green:
stage: deploy
script:
- kubectl apply -f k8s/deployment-green.yaml
- kubectl rollout status deployment/myapp-green -n production
environment:
name: production-green
url: https://green.example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
verify-green:
stage: verify
needs: [deploy-green]
script:
- curl -f https://green.example.com/health || exit 1
- npm run test:smoke -- --baseUrl=https://green.example.com
retry:
max: 3
when: always
switch-traffic:
stage: switch
needs: [verify-green]
script:
- kubectl patch service myapp -n production -p '{"spec":{"selector":{"version":"green"}}}'
- echo "Traffic switched to green deployment"
environment:
name: production
url: https://example.com
when: manual
Pattern 3: Canary Deployment
Use case: Gradual rollout with traffic splitting.
stages:
- deploy
- canary
deploy-canary:
stage: deploy
script:
- kubectl apply -f k8s/deployment-canary.yaml
- kubectl set image deployment/myapp-canary myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
environment:
name: production-canary
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
# Increase canary traffic gradually
canary-10-percent:
stage: canary
script:
- kubectl patch virtualservice myapp -n production --type merge -p '{"spec":{"http":[{"route":[{"destination":{"host":"myapp-stable"},"weight":90},{"destination":{"host":"myapp-canary"},"weight":10}]}]}}'
needs: [deploy-canary]
when: manual
canary-50-percent:
stage: canary
script:
- kubectl patch virtualservice myapp -n production --type merge -p '{"spec":{"http":[{"route":[{"destination":{"host":"myapp-stable"},"weight":50},{"destination":{"host":"myapp-canary"},"weight":50}]}]}}'
needs: [canary-10-percent]
when: manual
canary-promote:
stage: canary
script:
- kubectl patch virtualservice myapp -n production --type merge -p '{"spec":{"http":[{"route":[{"destination":{"host":"myapp-canary"},"weight":100}]}]}}'
- kubectl set image deployment/myapp-stable myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
needs: [canary-50-percent]
when: manual
Multi-Project Pipeline Patterns
Pattern 1: Trigger Downstream Projects
Use case: Orchestrate multiple project pipelines.
stages:
- build
- trigger
build-library:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
trigger-dependent-projects:
stage: trigger
parallel:
matrix:
- PROJECT: ['group/app1', 'group/app2', 'group/app3']
trigger:
project: $PROJECT
branch: main
strategy: depend
needs: [build-library]
rules:
- if: $CI_COMMIT_BRANCH == "main"
Pattern 2: Multi-Project Pipeline with Variables
Use case: Pass variables to downstream pipelines.
trigger-downstream:
stage: deploy
trigger:
project: group/deployment-project
branch: main
strategy: depend
variables:
SERVICE_NAME: my-service
SERVICE_VERSION: $CI_COMMIT_SHORT_SHA
ENVIRONMENT: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
Parent-Child Pipeline Patterns
Pattern 1: Dynamic Child Pipeline
Use case: Generate child pipeline configuration dynamically.
stages:
- generate
- deploy
generate-child-pipeline:
stage: generate
script:
- python scripts/generate-pipeline.py > generated-pipeline.yml
artifacts:
paths:
- generated-pipeline.yml
trigger-child-pipeline:
stage: deploy
trigger:
include:
- artifact: generated-pipeline.yml
job: generate-child-pipeline
strategy: depend
needs: [generate-child-pipeline]
Pattern 2: Monorepo with Multiple Child Pipelines
Use case: Each component has its own pipeline configuration.
# Parent .gitlab-ci.yml
stages:
- trigger
trigger-frontend:
stage: trigger
trigger:
include: frontend/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- frontend/**/*
trigger-backend:
stage: trigger
trigger:
include: backend/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- backend/**/*
trigger-infrastructure:
stage: trigger
trigger:
include: infrastructure/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- infrastructure/**/*
Monorepo Patterns
Pattern 1: Conditional Jobs Based on Changes
Use case: Only run jobs for changed components.
stages:
- build
- test
- deploy
build-frontend:
stage: build
script:
- cd frontend
- npm ci
- npm run build
rules:
- changes:
- frontend/**/*
build-backend:
stage: build
script:
- cd backend
- go build
rules:
- changes:
- backend/**/*
test-frontend:
stage: test
needs: [build-frontend]
script:
- cd frontend
- npm test
rules:
- changes:
- frontend/**/*
test-backend:
stage: test
needs: [build-backend]
script:
- cd backend
- go test ./...
rules:
- changes:
- backend/**/*
Pattern 2: Monorepo with Parallel Child Pipelines
Use case: Run multiple child pipelines in parallel for different components.
stages:
- trigger
.trigger-template:
stage: trigger
trigger:
strategy: depend
trigger-service-a:
extends: .trigger-template
trigger:
include: services/service-a/.gitlab-ci.yml
rules:
- changes:
- services/service-a/**/*
trigger-service-b:
extends: .trigger-template
trigger:
include: services/service-b/.gitlab-ci.yml
rules:
- changes:
- services/service-b/**/*
trigger-service-c:
extends: .trigger-template
trigger:
include: services/service-c/.gitlab-ci.yml
rules:
- changes:
- services/service-c/**/*
Template and Reusability Patterns
Pattern 1: Global Template Library
Use case: Reusable templates across multiple projects.
# templates/build-templates.yml
.node-build-template:
image: node:20-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
before_script:
- npm ci
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
.python-build-template:
image: python:3.12-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .venv/
before_script:
- python -m venv .venv
- source .venv/bin/activate
- pip install -r requirements.txt
script:
- python setup.py build
# Project .gitlab-ci.yml
include:
- project: 'group/ci-templates'
file: 'templates/build-templates.yml'
stages:
- build
build-app:
extends: .node-build-template
stage: build
Pattern 2: Local Template with Extends
Use case: DRY configuration within a single project.
# Hidden template jobs
.deployment-base:
image: alpine:3.19
before_script:
- apk add --no-cache curl
script:
- ./scripts/deploy.sh $ENVIRONMENT
resource_group: ${ENVIRONMENT}
retry:
max: 2
when:
- runner_system_failure
# Actual deployment jobs
deploy-staging:
extends: .deployment-base
stage: deploy
variables:
ENVIRONMENT: staging
environment:
name: staging
url: https://staging.example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-production:
extends: .deployment-base
stage: deploy
variables:
ENVIRONMENT: production
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_TAG
when: manual
Summary
These patterns provide a solid foundation for common GitLab CI/CD scenarios. When using these patterns:
- Customize them for your specific needs
- Validate using gitlab-ci-validator skill
- Follow best practices from references/best-practices.md
- Test locally when possible
- Document any modifications
Remember: These are starting points. Always adapt them to your project’s specific requirements, security policies, and infrastructure.
GitLab CI/CD YAML Syntax Reference
Comprehensive reference for GitLab CI/CD .gitlab-ci.yml configuration syntax.
Table of Contents
- Global Keywords
- Job Keywords
- Script Execution
- Artifacts and Cache
- Rules and Conditions
- Dependencies and Needs
- Docker Configuration
- Environment and Deployment
- Advanced Features
Global Keywords
Global keywords control pipeline-wide behavior and configuration.
stages
Defines the order of pipeline stages. Jobs in the same stage run in parallel.
stages:
- build
- test
- deploy
Default stages:
stages:
- .pre # Special stage, runs before everything
- build
- test
- deploy
- .post # Special stage, runs after everything
default
Sets default values for all jobs. Job-level configurations override defaults completely (no merging).
default:
image: node:20-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
before_script:
- echo "Starting job"
tags:
- docker
interruptible: true
retry:
max: 1
when:
- runner_system_failure
include
Imports external YAML configuration files.
# Local file from same repository
include:
- local: '.gitlab/ci/build-jobs.yml'
# File from another project
include:
- project: 'group/ci-templates'
ref: main
file: 'templates/build.yml'
# Remote file via HTTP
include:
- remote: 'https://example.com/ci-template.yml'
# GitLab CI/CD template
include:
- template: 'Security/SAST.gitlab-ci.yml'
# CI/CD component
include:
- component: gitlab.com/my-org/components/build@1.0.0
variables
Defines CI/CD variables available to all jobs.
variables:
NODE_VERSION: "20"
DOCKER_DRIVER: overlay2
CACHE_VERSION: "v1"
Variable types:
$VARIABLE- Simple substitution${VARIABLE}- Explicit variable reference$$VARIABLE- Escaped variable (passed to script)
workflow
Controls when pipelines run and their auto-cancellation behavior.
workflow:
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_MERGE_REQUEST_ID
- if: $CI_COMMIT_TAG
auto_cancel:
on_new_commit: interruptible
Job Keywords
Jobs are the basic building blocks that define what to execute.
Job Structure
job-name:
stage: build
image: node:20-alpine
script:
- npm ci
- npm run build
Reserved job names:
image,services,stages,before_script,after_script,variables,cache,include
stage
Assigns the job to a pipeline stage.
build-job:
stage: build # Default: test
script: make build
script (required)
Shell commands to execute. At least one of script, trigger, or extends is required.
build:
script:
- echo "Building..."
- make build
- make package
# Multi-line script
test:
script:
- |
echo "Running tests"
npm test
echo "Tests complete"
before_script
Commands executed before script. Runs in the same shell context as script.
test:
before_script:
- echo "Setting up..."
- npm ci
script:
- npm test
after_script
Commands executed after script, runs in a separate shell. Cannot affect job exit code.
deploy:
script:
- deploy.sh
after_script:
- echo "Cleaning up..."
- rm -rf temp/
Note: after_script has a separate 5-minute timeout by default.
image
Docker image for job execution.
# Simple image
build:
image: node:20-alpine
# Image with entrypoint override
test:
image:
name: my-image:latest
entrypoint: [""]
services
Docker service containers (databases, caches, etc.).
test:
image: node:20-alpine
services:
- postgres:15-alpine
- redis:7-alpine
variables:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
tags
Selects runners with matching tags.
deploy:
tags:
- docker
- production
script: deploy.sh
when
Controls when jobs run.
Values:
on_success(default) - Run when all previous stage jobs succeedon_failure- Run when at least one previous stage job failsalways- Always run regardless of statusmanual- Requires manual triggerdelayed- Run after delaynever- Don’t run
cleanup:
when: always
script: cleanup.sh
deploy:
when: manual
script: deploy.sh
delayed-job:
when: delayed
start_in: 30 minutes
script: echo "Running after delay"
allow_failure
Allows job to fail without blocking pipeline.
lint:
script: npm run lint
allow_failure: true
# Conditional allow_failure
test-experimental:
script: npm test
allow_failure:
exit_codes: [1, 137]
retry
Configures automatic retry on failure.
# Simple retry
deploy:
retry: 2
# Advanced retry configuration
integration-test:
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
- api_failure
Retry conditions:
always- Retry on any failurerunner_system_failure- Runner system failedstuck_or_timeout_failure- Job stuck or timed outscript_failure- Script failedapi_failure- API failureunknown_failure- Unknown failure
timeout
Job-specific timeout override.
test-quick:
timeout: 10 minutes
script: npm run test:unit
test-e2e:
timeout: 1 hour
script: npm run test:e2e
interruptible
Marks job as cancellable when superseded by newer pipelines.
test:
interruptible: true # Can be canceled
script: npm test
deploy:
interruptible: false # Cannot be canceled
script: deploy.sh
resource_group
Limits job concurrency for resource-sensitive operations.
deploy-production:
resource_group: production
script: deploy.sh
deploy-staging:
resource_group: staging
script: deploy.sh
parallel
Runs multiple job instances in parallel.
# Parallel with count
test:
parallel: 5
script: npm test -- --shard=${CI_NODE_INDEX}/${CI_NODE_TOTAL}
# Parallel with matrix
test-matrix:
parallel:
matrix:
- NODE_VERSION: ['18', '20', '22']
OS: ['alpine', 'bookworm-slim']
image: node:${NODE_VERSION}-${OS}
script: npm test
Script Execution
Multiline Scripts
# Using |
test:
script:
- |
echo "Line 1"
echo "Line 2"
echo "Line 3"
# Using >
deploy:
script:
- >
kubectl apply -f deployment.yaml
--namespace production
--timeout 5m
Error Handling in Scripts
# Stop on first error (default)
build:
script:
- command1
- command2 # Won't run if command1 fails
# Continue on error
test:
script:
- command1 || true
- command2 # Runs even if command1 fails
Artifacts and Cache
artifacts
Files/directories to preserve after job completion.
build:
script: make build
artifacts:
paths:
- dist/
- build/
exclude:
- "**/*.map"
- dist/temp/
expire_in: 1 hour
when: on_success # on_success, on_failure, always
name: "build-${CI_COMMIT_SHORT_SHA}"
Artifact types:
test:
script: npm test
artifacts:
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
dotenv: build.env
Expiration values:
30 minutes,1 hour,2 hours1 day,2 days,1 week,1 monthnever- Keep forever
cache
Preserves files between pipeline runs.
build:
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
policy: pull-push
when: on_success
Cache policies:
pull-push(default) - Download and upload cachepull- Only download cachepush- Only upload cache
Cache keys:
# Branch-based key
cache:
key: ${CI_COMMIT_REF_SLUG}
# File-based key
cache:
key:
files:
- package-lock.json
prefix: npm
# Multiple caches
cache:
- key: npm-cache
paths:
- node_modules/
- key: build-cache
paths:
- dist/
Rules and Conditions
rules
Determines when to create jobs and which attributes to apply.
deploy:
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: always
- if: $CI_MERGE_REQUEST_ID
when: manual
- when: never # Default: don't run
Rule clauses:
if- Variable expressionschanges- File modificationsexists- File existencewhen- Execution timingvariables- Dynamic variablesallow_failure- Failure behavior
Examples
# Run on specific branch
deploy:
rules:
- if: $CI_COMMIT_BRANCH == "main"
# Run on file changes
test-frontend:
rules:
- changes:
- frontend/**/*
# Run if file exists
docs-build:
rules:
- exists:
- docs/mkdocs.yml
# Complex rules
deploy:
rules:
- if: $CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG == null
when: manual
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
when: on_success
- when: never
only / except (Deprecated)
Note: Use rules instead. only and except are deprecated.
# ❌ Deprecated
deploy:
only:
- main
except:
- tags
# ✅ Use rules instead
deploy:
rules:
- if: $CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG == null
Dependencies and Needs
dependencies
Restricts artifact downloads from specific jobs.
build:
stage: build
script: make build
artifacts:
paths:
- dist/
test:
stage: test
dependencies: [build] # Only download artifacts from build
script: test dist/
deploy:
stage: deploy
dependencies: [] # Don't download any artifacts
script: deploy
needs
Creates Directed Acyclic Graph (DAG) for faster pipelines.
build-frontend:
stage: build
script: build frontend
build-backend:
stage: build
script: build backend
test-frontend:
stage: test
needs: [build-frontend] # Starts as soon as build-frontend completes
script: test frontend
test-backend:
stage: test
needs: [build-backend]
script: test backend
deploy:
stage: deploy
needs: [test-frontend, test-backend]
script: deploy
Needs with artifacts:
deploy:
needs:
- job: build
artifacts: true # Default
- job: test
artifacts: false # Don't download artifacts
Docker Configuration
Docker-in-Docker (dind)
build-docker:
image: docker:24-dind
services:
- docker:24-dind
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker build -t myimage .
Kaniko (Rootless)
build-kaniko:
image:
name: gcr.io/kaniko-project/executor:v1.21.0-debug
entrypoint: [""]
script:
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}:latest"
Environment and Deployment
environment
Marks jobs that deploy to environments.
deploy-staging:
environment:
name: staging
url: https://staging.example.com
on_stop: stop-staging
auto_stop_in: 1 day
action: start # start, prepare, stop
script: deploy staging
stop-staging:
environment:
name: staging
action: stop
when: manual
script: stop staging
Kubernetes integration:
deploy-k8s:
environment:
name: production
url: https://example.com
kubernetes:
namespace: production
script: kubectl apply -f deployment.yaml
Dynamic environments:
review:
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_COMMIT_REF_SLUG.review.example.com
on_stop: stop-review
auto_stop_in: 1 week
script: deploy review
rules:
- if: $CI_MERGE_REQUEST_ID
Advanced Features
extends
Inherits configuration from other jobs.
.deploy-template:
image: alpine:3.19
before_script:
- apk add --no-cache curl
retry:
max: 2
deploy-staging:
extends: .deploy-template
script: deploy staging
deploy-production:
extends: .deploy-template
script: deploy production
Multiple inheritance:
.base:
image: alpine:3.19
.retry:
retry: 2
deploy:
extends:
- .base
- .retry
script: deploy
trigger
Triggers downstream pipelines.
# Trigger another project
trigger-downstream:
trigger:
project: group/downstream-project
branch: main
strategy: depend # Wait for downstream pipeline
# Trigger child pipeline
trigger-child:
trigger:
include: child-pipeline.yml
strategy: depend
# Trigger with variables
trigger-deploy:
trigger:
project: group/deploy-project
variables:
VERSION: $CI_COMMIT_SHORT_SHA
ENVIRONMENT: production
coverage
Extracts code coverage percentage from job output.
test:
script: npm test
coverage: '/Coverage: \d+\.\d+%/'
release
Creates GitLab releases.
release:
stage: deploy
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules:
- if: $CI_COMMIT_TAG
script:
- echo "Creating release"
release:
tag_name: $CI_COMMIT_TAG
name: 'Release $CI_COMMIT_TAG'
description: 'Release notes for $CI_COMMIT_TAG'
secrets
Retrieves secrets from external sources.
deploy:
secrets:
DATABASE_PASSWORD:
vault: production/db/password@secret
file: false
inherit
Controls inheritance of global defaults.
job:
inherit:
default: false # Don't inherit default settings
variables: [VAR1, VAR2] # Only inherit specific variables
Predefined CI/CD Variables
Common GitLab CI/CD variables:
Pipeline Variables
$CI_PIPELINE_ID- Pipeline ID$CI_PIPELINE_IID- Pipeline IID (internal ID)$CI_PIPELINE_SOURCE- Pipeline source (push, merge_request_event, etc.)$CI_PIPELINE_URL- Pipeline URL
Commit Variables
$CI_COMMIT_SHA- Full commit SHA$CI_COMMIT_SHORT_SHA- Short commit SHA (8 chars)$CI_COMMIT_BRANCH- Branch name$CI_COMMIT_TAG- Tag name (if pipeline for tag)$CI_COMMIT_REF_NAME- Branch or tag name$CI_COMMIT_REF_SLUG- Slugified branch/tag name$CI_COMMIT_MESSAGE- Commit message$CI_COMMIT_AUTHOR- Commit author
Job Variables
$CI_JOB_ID- Job ID$CI_JOB_NAME- Job name$CI_JOB_STAGE- Job stage$CI_JOB_URL- Job URL$CI_JOB_TOKEN- Job token for API access$CI_NODE_INDEX- Job index in parallel jobs (1-based)$CI_NODE_TOTAL- Total number of parallel jobs
Project Variables
$CI_PROJECT_ID- Project ID$CI_PROJECT_NAME- Project name$CI_PROJECT_PATH- Project path (group/project)$CI_PROJECT_DIR- Working directory$CI_PROJECT_URL- Project URL
Registry Variables
$CI_REGISTRY- GitLab Container Registry URL$CI_REGISTRY_IMAGE- Full image path$CI_REGISTRY_USER- Registry username$CI_REGISTRY_PASSWORD- Registry password
Merge Request Variables
$CI_MERGE_REQUEST_ID- MR ID$CI_MERGE_REQUEST_IID- MR IID$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME- Source branch$CI_MERGE_REQUEST_TARGET_BRANCH_NAME- Target branch
YAML Anchors and Aliases
YAML anchors for reusing configuration within a file.
# Define anchor
.retry-config: &retry-config
retry:
max: 2
when:
- runner_system_failure
# Use anchor
job1:
<<: *retry-config
script: command1
job2:
<<: *retry-config
script: command2
Merge multiple anchors:
.cache: &cache
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
.image: &image
image: node:20-alpine
build:
<<: [*cache, *image]
script: npm run build
Complete Example
# Global configuration
stages:
- build
- test
- security
- deploy
variables:
NODE_VERSION: "20"
DOCKER_DRIVER: overlay2
default:
image: node:${NODE_VERSION}-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
tags:
- docker
interruptible: true
# Hidden template
.deploy-template:
before_script:
- echo "Deploying to ${ENVIRONMENT}"
retry:
max: 2
when:
- runner_system_failure
resource_group: ${ENVIRONMENT}
# Build job
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
# Test jobs
test-unit:
stage: test
needs: []
script:
- npm ci
- npm test
coverage: '/Coverage: \d+\.\d+%/'
artifacts:
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
test-lint:
stage: test
needs: []
script:
- npm ci
- npm run lint
allow_failure: true
# Security scanning
include:
- template: Security/SAST.gitlab-ci.yml
# Deployment jobs
deploy-staging:
extends: .deploy-template
stage: deploy
variables:
ENVIRONMENT: staging
needs: [build, test-unit]
environment:
name: staging
url: https://staging.example.com
script:
- ./deploy.sh staging
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-production:
extends: .deploy-template
stage: deploy
variables:
ENVIRONMENT: production
needs: [build, test-unit]
environment:
name: production
url: https://example.com
script:
- ./deploy.sh production
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
when: manual
Additional Resources
- Official GitLab CI/CD YAML reference: https://docs.gitlab.com/ci/yaml/
- GitLab CI/CD examples: https://docs.gitlab.com/ci/examples/
- GitLab CI/CD templates: https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates
Use this reference when generating or troubleshooting GitLab CI/CD configurations.
GitLab CI/CD Security Guidelines
Comprehensive security guidelines for creating secure GitLab CI/CD pipelines. Follow these practices to protect your code, credentials, and infrastructure.
Table of Contents
- Secrets Management
- Image Security
- Script Security
- Artifact Security
- Network Security
- Access Control
- Supply Chain Security
- Compliance and Auditing
Secrets Management
1. Never Hardcode Secrets
❌ BAD:
deploy:
script:
- deploy --api-key sk_live_abc123xyz
- mysql -u admin -ppassword123 -h db.example.com
✅ GOOD:
deploy:
script:
- deploy --api-key $API_KEY
- mysql -u $DB_USER -p$DB_PASSWORD -h $DB_HOST
2. Use Masked and Protected Variables
Settings for sensitive variables:
- ✅ Masked - Hides value in job logs
- ✅ Protected - Only available in protected branches/tags
- ✅ Expanded - Controls variable expansion
Example configuration in GitLab UI:
Settings → CI/CD → Variables
Key: API_KEY
Value: sk_live_abc123xyz
Flags: [x] Mask variable
[x] Protect variable
[ ] Expand variable reference (disable for JSON/special chars)
3. Scope Variables Appropriately
# Environment-specific variables
deploy-staging:
environment:
name: staging
variables:
API_ENDPOINT: https://api-staging.example.com
script:
- deploy --endpoint $API_ENDPOINT
deploy-production:
environment:
name: production
variables:
API_ENDPOINT: https://api.example.com
script:
- deploy --endpoint $API_ENDPOINT
4. Use GitLab Secrets Management
deploy:
secrets:
DATABASE_PASSWORD:
vault: production/db/password@secret
token: $VAULT_TOKEN
script:
- deploy --db-password $DATABASE_PASSWORD
5. Rotate Secrets Regularly
- Set expiration for tokens and credentials
- Implement automated rotation where possible
- Remove unused credentials immediately
- Audit secret usage regularly
6. Avoid Secrets in Artifacts
build:
script:
- make build
artifacts:
paths:
- dist/
exclude:
- "**/*.env"
- "**/*.pem"
- "**/*.key"
- "**/credentials.*"
- "**/.env.*"
- "**/config.production.*"
Image Security
1. Pin Images to Specific Versions
❌ BAD:
test:
image: node:latest # Unpredictable, security risk
✅ GOOD:
test:
image: node:20.11-alpine3.19 # Pinned version
Best practice:
- Use specific major.minor.patch versions
- Consider using SHA256 digests for maximum security
- Document why specific versions are chosen
2. Use Official and Trusted Images
# ✅ GOOD: Official images
build:
image: node:20-alpine # Official Node.js image
test:
image: postgres:15-alpine # Official PostgreSQL image
# ⚠️ CAUTION: Third-party images (verify trust)
scan:
image: aquasec/trivy:0.49.0 # Verify publisher reputation
3. Scan Images for Vulnerabilities
stages:
- build
- scan
build-image:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
scan-image:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
needs: [build-image]
allow_failure: false # Fail pipeline on vulnerabilities
4. Use Minimal Base Images
# ✅ GOOD: Alpine-based images (smaller attack surface)
FROM node:20-alpine
# ⚠️ LARGER: Full images have more packages
FROM node:20-bookworm
5. Don’t Run Containers as Root
# Dockerfile best practice
FROM node:20-alpine
# Create non-root user
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
# Change ownership
COPY --chown=nodejs:nodejs . .
# Switch to non-root user
USER nodejs
CMD ["node", "server.js"]
Script Security
1. Avoid Dangerous Script Patterns
❌ DANGEROUS PATTERNS:
# Piping to bash
install:
script:
- curl https://install.sh | bash # ❌ Dangerous
# Using eval
deploy:
script:
- eval "$COMMAND" # ❌ Code injection risk
# Overly permissive permissions
setup:
script:
- chmod 777 /app # ❌ Security risk
✅ SECURE PATTERNS:
# Download and verify before execution
install:
script:
- curl -o install.sh https://install.sh
- sha256sum -c install.sh.sha256 # Verify integrity
- bash install.sh
# Avoid eval, use explicit commands
deploy:
script:
- ./deploy.sh $ENVIRONMENT
# Minimal permissions
setup:
script:
- chmod 755 /app
- chmod 600 /app/config.yml
2. Validate and Sanitize Inputs
deploy:
script:
- |
# Validate branch name
if [[ ! "$CI_COMMIT_BRANCH" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "Invalid branch name"
exit 1
fi
# Validate version format
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version format"
exit 1
fi
./deploy.sh "$CI_COMMIT_BRANCH" "$VERSION"
3. Use set -e for Error Handling
deploy:
script:
- |
set -e # Exit on first error
set -o pipefail # Exit on pipe failures
echo "Starting deployment"
./build.sh
./test.sh
./deploy.sh
4. Avoid Exposing Secrets in Logs
# ❌ BAD: Secret might appear in logs
deploy:
script:
- echo "Deploying with token $API_TOKEN"
# ✅ GOOD: Don't echo secrets
deploy:
script:
- echo "Starting deployment"
- deploy --token $API_TOKEN # Token won't appear if masked
# ✅ GOOD: Use GitLab's masking
deploy:
before_script:
- echo "::add-mask::$API_TOKEN" # Mask custom secrets
script:
- deploy --token $API_TOKEN
5. Disable Debug Mode in Production
# ❌ BAD: Debug mode exposes information
deploy-production:
variables:
CI_DEBUG_TRACE: "true" # ❌ Don't use in production
script:
- deploy production
# ✅ GOOD: Only debug in non-production
test:
variables:
CI_DEBUG_TRACE: "true" # ✅ OK for testing
script:
- npm test
Artifact Security
1. Use Specific Artifact Paths
❌ BAD:
build:
artifacts:
paths:
- ./** # Includes everything, might expose secrets
✅ GOOD:
build:
artifacts:
paths:
- dist/
- build/
exclude:
- "**/*.env"
- "**/*.pem"
- "**/*.key"
- "**/node_modules/"
2. Set Appropriate Expiration
# Short-lived artifacts for builds
build:
artifacts:
paths:
- dist/
expire_in: 1 hour # ✅ Minimize exposure window
# Longer retention for releases
release:
artifacts:
paths:
- release/
expire_in: 1 month # ✅ Appropriate for releases
3. Don’t Include Sensitive Dependencies
build:
artifacts:
paths:
- dist/
exclude:
- node_modules/ # ✅ Don't include dependencies
- vendor/ # ✅ Don't include vendor code
- .git/ # ✅ Don't include git history
4. Use Access Controls for Artifacts
Configure in Settings → CI/CD → General pipelines:
- Limit artifact downloads to project members
- Require authentication for artifact access
- Set appropriate visibility levels
Network Security
1. Use TLS/SSL for All Connections
deploy:
script:
# ✅ HTTPS
- curl -X POST https://api.example.com/deploy
# ❌ HTTP (only for local development)
# - curl -X POST http://api.example.com/deploy
2. Don’t Disable SSL Verification
❌ BAD:
test:
script:
- curl -k https://api.example.com # ❌ Disables verification
- git config --global http.sslVerify false # ❌ Dangerous
✅ GOOD:
test:
script:
- curl --cacert /etc/ssl/certs/ca-bundle.crt https://api.example.com
- git config --global http.sslCAInfo /etc/ssl/certs/ca-bundle.crt
3. Use Private Registries for Internal Images
variables:
# ✅ Use private registry for internal images
INTERNAL_REGISTRY: registry.internal.example.com
build:
image: $INTERNAL_REGISTRY/build-tools:latest
script:
- make build
4. Restrict Outbound Connections
# Configure runner to limit outbound connections
# Only allow specific domains/IPs in firewall rules
deploy:
script:
# Whitelist specific endpoints
- curl https://api.allowed-service.com/deploy
Access Control
1. Use Protected Branches
Settings → Repository → Protected branches:
- Protect
mainandproductionbranches - Require merge request approvals
- Restrict who can push
- Restrict who can merge
deploy-production:
script:
- deploy production
rules:
- if: $CI_COMMIT_BRANCH == "main" # Only on protected branch
when: manual
2. Use Protected Environments
Settings → CI/CD → Environments:
- Mark production environments as protected
- Define deployment approvals
- Restrict access to authorized users
deploy-production:
environment:
name: production # Protected environment
url: https://example.com
script:
- deploy production
when: manual # Requires manual approval
3. Use Protected Tags for Releases
release:
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ # Only on version tags
script:
- make release
when: manual
4. Limit Runner Access
Settings → CI/CD → Runners:
- Use specific runners for sensitive operations
- Tag runners appropriately
- Disable shared runners for security-sensitive projects
deploy-production:
tags:
- production-runner # Specific runner for production
- secured
script:
- deploy production
5. Use Resource Groups
deploy-production:
resource_group: production # ✅ Prevents concurrent deployments
script:
- deploy production
deploy-staging:
resource_group: staging # ✅ Independent resource group
script:
- deploy staging
Supply Chain Security
1. Pin All Dependencies
# ✅ GOOD: Lock file ensures reproducibility
build:
script:
- npm ci # Uses package-lock.json
# - npm install # ❌ Don't use in CI
2. Verify Dependency Integrity
build:
script:
- npm ci --audit # Check for vulnerabilities
- npm audit --audit-level=high # Fail on high severity issues
3. Use Dependency Scanning
include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
gemnasium-dependency_scanning:
variables:
DS_EXCLUDED_PATHS: "node_modules,vendor"
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json
4. Scan for Secrets in Code
include:
- template: Security/Secret-Detection.gitlab-ci.yml
secret_detection:
variables:
SECRET_DETECTION_EXCLUDED_PATHS: "tests/"
5. Use SBOM (Software Bill of Materials)
sbom-generation:
image: anchore/syft:latest
script:
- syft packages dir:. -o cyclonedx-json > sbom.json
artifacts:
paths:
- sbom.json
expire_in: 1 year
Compliance and Auditing
1. Enable Audit Logging
- Enable audit events in GitLab
- Monitor pipeline execution logs
- Track variable changes
- Review access patterns
2. Implement Compliance Pipelines
include:
- template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
- template: Security/Container-Scanning.gitlab-ci.yml
compliance-check:
stage: .pre
script:
- echo "Running compliance checks"
- ./scripts/compliance-audit.sh
allow_failure: false
3. Implement Separation of Duties
# Different teams/roles for different stages
deploy-staging:
rules:
- if: $CI_COMMIT_BRANCH == "develop"
# Can be triggered by developers
deploy-production:
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+/
when: manual # Requires ops team approval
environment:
name: production
4. Maintain Audit Trails
deploy:
before_script:
- echo "Deploy initiated by $GITLAB_USER_LOGIN at $(date)"
- echo "Commit: $CI_COMMIT_SHORT_SHA"
- echo "Branch: $CI_COMMIT_BRANCH"
script:
- deploy production
after_script:
- echo "Deploy completed at $(date)"
- ./scripts/log-audit-event.sh
5. Regular Security Reviews
- Review CI/CD configurations quarterly
- Audit access controls monthly
- Scan for exposed secrets weekly
- Update dependencies regularly
- Review runner security monthly
Security Checklist
When creating or reviewing GitLab CI/CD pipelines, ensure:
Secrets & Credentials
- No hardcoded secrets in
.gitlab-ci.yml - Sensitive variables are masked
- Production variables are protected
- Secrets are scoped to appropriate environments
- Regular secret rotation schedule exists
Images & Containers
- All images pinned to specific versions
- Images from trusted sources only
- Container vulnerability scanning enabled
- Containers run as non-root users
- Minimal base images used where possible
Scripts & Commands
- No
curl | bashpatterns - No use of
evalwith external input - Input validation implemented
- Proper error handling (
set -e) - No secrets echoed to logs
Artifacts & Cache
- Specific artifact paths (not
./**) - Sensitive files excluded from artifacts
- Appropriate expiration times set
- No credentials in cached files
Network & Communication
- HTTPS/TLS used for all external calls
- SSL verification enabled
- Private registries for internal images
- Outbound connections restricted
Access Control
- Protected branches configured
- Protected environments for production
- Protected tags for releases
- Specific runners for sensitive operations
- Resource groups for deployments
Supply Chain
- Dependencies pinned/locked
- Dependency scanning enabled
- SAST scanning enabled
- Secret detection enabled
- Regular dependency updates scheduled
Compliance
- Audit logging enabled
- Compliance pipelines implemented
- Separation of duties enforced
- Audit trails maintained
- Regular security reviews scheduled
Security Incident Response
If Secrets Are Exposed
-
Immediate Actions:
- Rotate the compromised credentials immediately
- Revoke exposed tokens/keys
- Review access logs for unauthorized usage
- Notify security team
-
Investigation:
- Identify scope of exposure
- Review pipeline logs
- Check for unauthorized access
- Document timeline
-
Remediation:
- Remove secrets from code/logs
- Update
.gitlab-ci.ymlwith proper secret management - Implement additional controls
- Update security documentation
-
Prevention:
- Enable secret scanning
- Implement pre-commit hooks
- Conduct security training
- Review secret management practices
Additional Resources
- GitLab Security Best Practices: https://docs.gitlab.com/ee/security/
- OWASP CI/CD Security: https://owasp.org/www-project-ci-cd-security/
- CIS Docker Benchmark: https://www.cisecurity.org/benchmark/docker
- NIST Supply Chain Security: https://www.nist.gov/itl/executive-order-improving-nations-cybersecurity/software-security-supply-chains
Always prioritize security in your GitLab CI/CD pipelines. When in doubt, choose the more secure option.
Validation Presentation Requirements
When presenting the final pipeline to the user, include the following sections:
1. Validation Status
Provide pass/fail status with issue counts by severity:
✓ Validation passed
- 0 CRITICAL issues
- 0 HIGH issues
- 2 MEDIUM issues
- 3 LOW issues
- 5 SUGGESTIONS
2. MEDIUM Issues Table (if any)
| Issue | Status | Explanation |
|---|---|---|
image-variable-no-digest |
Acceptable | Using python:${PYTHON_VERSION}-alpine allows flexible version management; PYTHON_VERSION is internally pinned to “3.12”. |
git-strategy-none |
Acceptable | stop-staging only runs kubectl commands requiring no source code. |
3. Suggestions Review Table (if any)
| Suggestion | Apply/Skip | Reason |
|---|---|---|
| Add caching for dependencies | Applied | Reduces build time by 40% |
Use needs for parallel execution |
Applied | Speeds up pipeline by running tests in parallel |
| Add retry logic for network calls | Skipped | Not applicable for this pipeline |
4. Usage Instructions
Provide:
- Required CI/CD variables with descriptions
- Setup steps for first-time configuration
- Pipeline behavior per branch/tag (when jobs trigger, manual vs automatic)
Example Usage Instructions
Required Variables (Settings → CI/CD → Variables):
| Variable | Type | Description |
|---|---|---|
DOCKER_REGISTRY_USER |
Variable (masked) | Registry username |
DOCKER_REGISTRY_PASSWORD |
Variable (masked) | Registry password |
KUBE_CONTEXT |
Variable | Kubernetes context name |
Setup Steps:
- Add required variables in project settings
- Ensure GitLab Runner has Docker executor configured
- Configure Kubernetes agent for deployment jobs
Pipeline Behavior:
- main branch: Builds, tests, and deploys to production (manual approval)
- develop branch: Builds, tests, and deploys to staging (automatic)
- feature branches: Builds and tests only
- tags: Creates release artifacts and deploys to production
Severity-Based Actions
| Severity | Action |
|---|---|
| CRITICAL | Must fix before presenting |
| HIGH | Must fix before presenting |
| MEDIUM | Fix or explain why acceptable |
| LOW | Acknowledge in output |
| SUGGESTIONS | Review and apply if beneficial |