Comprehensive toolkit for generating best practice GitLab CI/CD pipelines and configurations following current standards and conventions. Use this skill when creating new GitLab CI/CD resources, implementing CI/CD pipelines, or building GitLab pipelines from scratch.
Overall
score
93%
Does it follow best practices?
Validation for skill structure
This document outlines comprehensive best practices for creating production-ready, secure, and efficient GitLab CI/CD pipelines.
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 testBest practices:
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_TOKENBest practices:
$CI_JOB_TOKEN for GitLab API operationsBe 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 hourBest practices:
exclude to prevent sensitive filesartifacts:reports for test/coverage reportsAvoid 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.shBest practices:
eval and similar dynamic executionConfigure 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: productionBest practices:
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 pushCache best practices:
policy: pull for jobs that only read cachepolicy: pull-push for jobs that update cacheCACHE_VERSION variable for cache invalidationneedsUse 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: deployBenefits:
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}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:
artifacts:reports for test/coverage reportsextends for ReusabilityUse 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: manualinclude for Modular ConfigurationSplit 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 ciUse 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 productiondefault for Common SettingsSet 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 testConfigure 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: alwaysRetry scenarios:
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 minutesUse 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]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 canceledUse 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/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 testUse short, standard stage names.
stages:
- build # ✅ Standard, clear
- test # ✅ Standard, clear
- deploy # ✅ Standard, clear
- .pre # ✅ GitLab special stage
- .post # ✅ GitLab special stageUse UPPER_SNAKE_CASE for variables.
variables:
NODE_VERSION: "20"
DOCKER_DRIVER: overlay2
CACHE_VERSION: "v1"
DEPLOY_ENVIRONMENT: stagingUse lowercase for environment names.
deploy-staging:
environment:
name: staging # ✅ lowercase
url: https://staging.example.com
deploy-production:
environment:
name: production # ✅ lowercase
url: https://example.comSimple, 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: manualUse when:
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: deployUse when:
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: dependUse when:
Cross-project orchestration triggering other projects.
trigger-downstream:
stage: deploy
trigger:
project: group/downstream-project
branch: main
strategy: dependUse when:
:latest Tag# ❌ ANTI-PATTERN
test:
image: node:latest
script: npm test
# ✅ CORRECT
test:
image: node:20.11-alpine3.19
script: npm test# ❌ ANTI-PATTERN
deploy:
script:
- deploy --api-key abc123xyz
# ✅ CORRECT
deploy:
script:
- deploy --api-key $API_KEYonly/except# ❌ ANTI-PATTERN
deploy:
only:
- main
except:
- tags
# ✅ CORRECT
deploy:
rules:
- if: $CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG == null# ❌ 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# ❌ ANTI-PATTERN (artifacts stored forever)
build:
artifacts:
paths:
- dist/
# ✅ CORRECT (artifacts expire)
build:
artifacts:
paths:
- dist/
expire_in: 1 hour# ❌ ANTI-PATTERN (concurrent deployments possible)
deploy-production:
script: deploy production
# ✅ CORRECT (prevents concurrent deployments)
deploy-production:
script: deploy production
resource_group: production# ❌ ANTI-PATTERN
build:
artifacts:
paths:
- ./** # Includes everything
# ✅ CORRECT
build:
artifacts:
paths:
- dist/
- build/
exclude:
- "**/*.env"# ❌ 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 frontendWhen creating GitLab CI/CD pipelines, ensure:
needs keyword used for DAG optimizationrules used instead of deprecated only/exceptresource_group used for deployment jobsinterruptible: true for test jobsextends or include used to reduce duplicationafter_scriptReference this document when generating or reviewing GitLab CI/CD pipelines to ensure best practices are followed.
Install with Tessl CLI
npx tessl i pantheon-ai/gitlab-ci-generator@0.1.0