patterns
GitLab CI/CD Patterns
Common pipeline patterns for efficient, maintainable CI/CD across multiple projects.
Table of Contents
- Pipeline Types
- Workflow Patterns
- Job Patterns
- Deployment Patterns
- Testing Patterns
- Security Patterns
- Anti-Patterns
Pipeline Types
Branch Pipelines
Basic branch pipeline:
workflow: rules: - if: $CI_COMMIT_BRANCH
Runs on: Every push to any branch
Use case: Development branches, feature branches
Merge Request Pipelines
MR-only pipeline:
workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event"
Runs on: Merge request events only
Benefits:
- Avoid duplicate branch + MR pipelines
- Test exactly what will be merged
- Show pipeline results in MR UI
Source: GitLab Merge Request Pipelines
Hybrid Workflow
Run on MR or specific branches:
workflow: rules: # Run on MR events - if: $CI_PIPELINE_SOURCE == "merge_request_event" # Run on main/development branches (not if MR exists) - if: $CI_COMMIT_BRANCH == "main" && $CI_OPEN_MERGE_REQUESTS == null - if: $CI_COMMIT_BRANCH == "development" && $CI_OPEN_MERGE_REQUESTS == null # Skip all other branches - when: never
Prevents: Duplicate pipelines when both branch and MR exist
Scheduled Pipelines
Nightly builds:
nightly-tests: rules: - if: $CI_PIPELINE_SOURCE == "schedule" script: - npm run test:full # Comprehensive tests
Create schedule: Project CI/CD Schedules New schedule
Use cases:
- Comprehensive test suites (too slow for every commit)
- Dependency updates
- Security scans
- Performance benchmarks
Tag Pipelines
Release pipeline:
workflow: rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ # Semantic version tags release: stage: deploy script: - ./build-release.sh - ./publish-release.sh only: - tags
Trigger: git tag v1.0.0 && git push origin v1.0.0
Workflow Patterns
Pattern 1: Environment-Based Workflow
Different pipelines for different environments:
workflow: rules: # Production pipeline (main branch) - if: $CI_COMMIT_BRANCH == "main" variables: ENVIRONMENT: production DEPLOY_ENABLED: "true" # Staging pipeline (development branch) - if: $CI_COMMIT_BRANCH == "development" variables: ENVIRONMENT: staging DEPLOY_ENABLED: "true" # Feature pipeline (MR) - if: $CI_PIPELINE_SOURCE == "merge_request_event" variables: ENVIRONMENT: review DEPLOY_ENABLED: "false" deploy: script: - ./deploy.sh $ENVIRONMENT rules: - if: $DEPLOY_ENABLED == "true"
Pattern 2: Fast Feedback Workflow
Layered testing (fast slow):
stages: - quick-checks - build - test-fast - test-slow - deploy lint: stage: quick-checks script: npm run lint # 30 seconds unit-tests: stage: test-fast needs: [build] script: npm run test:unit # 2 minutes integration-tests: stage: test-slow needs: [unit-tests] # Only if fast tests pass script: npm run test:integration # 10 minutes e2e-tests: stage: test-slow needs: [unit-tests] script: npm run test:e2e # 15 minutes
Benefit: Fail fast on cheap checks, run expensive tests only if cheap ones pass
Pattern 3: Manual Gates
Require approval for production:
deploy-staging: stage: deploy script: ./deploy-staging.sh environment: name: staging rules: - if: $CI_COMMIT_BRANCH == "development" deploy-production: stage: deploy script: ./deploy-production.sh environment: name: production when: manual # Requires manual trigger needs: [deploy-staging] rules: - if: $CI_COMMIT_BRANCH == "main"
Pattern 4: Conditional Auto-Cancel
Cancel redundant pipelines:
workflow: auto_cancel: on_new_commit: interruptible default: interruptible: true # Don't interrupt production deploys deploy-production: interruptible: false script: ./deploy.sh
Benefit: Save CI minutes by canceling outdated pipelines
Source: GitLab Interruptible Jobs
Job Patterns
Pattern 1: DRY with YAML Anchors
Define reusable templates:
.node-job: &node-job image: node:18 cache: key: files: - package-lock.json paths: - node_modules/ before_script: - npm ci lint: <<: *node-job script: npm run lint test: <<: *node-job script: npm test build: <<: *node-job script: npm run build
Benefit: Single source of truth for common configuration
Pattern 2: Matrix Builds
Test across multiple versions/platforms:
test-matrix: parallel: matrix: - NODE_VERSION: ["16", "18", "20"] OS: ["linux", "macos"] image: node:${NODE_VERSION} script: - npm test
Generates 6 jobs:
- node:16 on linux
- node:16 on macos
- node:18 on linux
- node:18 on macos
- node:20 on linux
- node:20 on macos
Pattern 3: Job Inheritance
Base job with extensions:
.test-base: image: node:18 before_script: - npm ci cache: paths: - node_modules/ test-unit: extends: .test-base script: npm run test:unit test-integration: extends: .test-base script: npm run test:integration services: - postgres:14
Pattern 4: Dynamic Job Generation
Create jobs based on file changes:
generate-jobs: stage: .pre script: - | # Generate child pipeline for changed services cat > generated-pipeline.yml <<EOF stages: - test EOF for service in $(git diff --name-only $CI_MERGE_REQUEST_DIFF_BASE_SHA | cut -d/ -f1 | sort -u); do cat >> generated-pipeline.yml <<EOF test-$service: stage: test script: - cd $service && npm test EOF done artifacts: paths: - generated-pipeline.yml trigger-generated: stage: test trigger: include: - artifact: generated-pipeline.yml job: generate-jobs strategy: depend
Use case: Monorepos with many services/packages
Deployment Patterns
Pattern 1: Blue-Green Deployment
Zero-downtime deployments:
deploy-blue: stage: deploy script: - ./deploy.sh blue - ./health-check.sh blue environment: name: production-blue url: https://blue.example.com switch-traffic: stage: deploy needs: [deploy-blue] when: manual script: - ./switch-traffic.sh blue environment: name: production url: https://example.com cleanup-green: stage: deploy needs: [switch-traffic] script: - ./cleanup.sh green
Pattern 2: Canary Deployment
Gradual rollout:
deploy-canary: stage: deploy script: - ./deploy.sh --replicas=1 --weight=10% # 10% traffic environment: name: production-canary monitor-canary: stage: verify needs: [deploy-canary] script: - ./monitor-metrics.sh - ./check-error-rate.sh deploy-full: stage: deploy needs: [monitor-canary] when: manual script: - ./deploy.sh --replicas=10 --weight=100% environment: name: production
Pattern 3: Environment-Specific Configuration
Use environment variables:
.deploy-base: script: - ./deploy.sh only: - main - development deploy-staging: extends: .deploy-base variables: ENVIRONMENT: staging REPLICAS: 2 LOG_LEVEL: debug environment: name: staging url: https://staging.example.com only: - development deploy-production: extends: .deploy-base variables: ENVIRONMENT: production REPLICAS: 5 LOG_LEVEL: info environment: name: production url: https://example.com when: manual only: - main
Pattern 4: Review Apps
Ephemeral environments per MR:
review-app: stage: deploy script: - ./deploy-review-app.sh environment: name: review/$CI_COMMIT_REF_SLUG url: https://$CI_COMMIT_REF_SLUG.review.example.com on_stop: cleanup-review-app auto_stop_in: 7 days rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" cleanup-review-app: stage: deploy script: - ./cleanup-review-app.sh environment: name: review/$CI_COMMIT_REF_SLUG action: stop when: manual rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event"
Benefit: Test each MR in isolated environment
Testing Patterns
Pattern 1: Parallel Test Execution
Split test suite:
test: parallel: 5 script: - npm run test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL artifacts: reports: junit: junit-*.xml
Time savings: 20 min test 4 min with 5 parallel jobs
Pattern 2: Test Artifact Collection
Aggregate test reports:
test: parallel: 5 script: - npm test artifacts: reports: junit: junit-*.xml coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml test-summary: stage: report needs: [test] script: - ./generate-test-summary.sh artifacts: reports: junit: test-summary.xml
GitLab displays:
- Test results in MR
- Code coverage trends
- Test failure notifications
Pattern 3: Conditional Testing
Run expensive tests only on MRs:
unit-tests: stage: test script: npm run test:unit # Always run integration-tests: stage: test script: npm run test:integration rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == "main" e2e-tests: stage: test script: npm run test:e2e rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TITLE !~ /^Draft:/ - if: $CI_COMMIT_BRANCH == "main"
Pattern 4: Contract Testing
Test API contracts:
contract-tests-provider: stage: test script: - npm run test:contract:provider artifacts: paths: - pacts/ expire_in: 1 day contract-tests-consumer: stage: test needs: [contract-tests-provider] script: - npm run test:contract:consumer dependencies: - contract-tests-provider
Use case: Microservices ensuring API compatibility
Security Patterns
Pattern 1: Security Scanning
SAST, DAST, dependency scanning:
include: - template: Security/SAST.gitlab-ci.yml - template: Security/Dependency-Scanning.gitlab-ci.yml - template: Security/Secret-Detection.gitlab-ci.yml # Run security scans on MRs and main sast: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == "main" dependency_scanning: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == "main"
GitLab Ultimate: Displays vulnerabilities in MR UI
Pattern 2: Security Policy Enforcement
Block MRs with high-severity vulnerabilities:
security-check: stage: test script: - | HIGH_VULNS=$(cat gl-dependency-scanning-report.json | jq '[.vulnerabilities[] | select(.severity == "High" or .severity == "Critical")] | length') if [ "$HIGH_VULNS" -gt 0 ]; then echo "Found $HIGH_VULNS high/critical vulnerabilities" exit 1 fi needs: [dependency_scanning] allow_failure: false # Block merge
Pattern 3: Secret Management with OIDC
Use OIDC instead of long-lived tokens:
deploy: stage: deploy id_tokens: AWS_OIDC_TOKEN: aud: https://aws.amazon.com before_script: - ./assume-role-with-oidc.sh script: - ./deploy.sh
Benefit: No long-lived credentials in GitLab variables
Source: GitLab OIDC Authentication
Pattern 4: Approval Rules
Require security team approval:
# Project Settings Merge Requests Approval Rules deploy-production: stage: deploy script: ./deploy.sh environment: name: production rules: - if: $CI_COMMIT_BRANCH == "main" when: manual
Configure in UI: Require approval from "Security" group before merge
Anti-Patterns
Anti-Pattern 1: Redundant Pipelines
Problem: Branch + MR pipelines run simultaneously
# BAD: Runs on every branch workflow: rules: - when: always
Solution: Skip branch pipelines when MR exists
# GOOD: Avoid redundancy workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS == null
Anti-Pattern 2: Stage Bottlenecks
Problem: All test jobs wait for all build jobs
# BAD: test-A waits for build-B unnecessarily stages: - build - test build-A: stage: build script: ./build-a.sh build-B: stage: build script: ./build-b.sh # Takes 20 minutes test-A: stage: test script: ./test-a.sh # Waits 20 min for build-B
Solution: Use needs for explicit dependencies
# GOOD: test-A starts as soon as build-A completes test-A: stage: test needs: [build-A] script: ./test-a.sh
Anti-Pattern 3: No Caching
Problem: Reinstall dependencies on every run
# BAD: No caching test: script: - npm ci # Downloads 500MB every time - npm test
Solution: Cache dependencies
# GOOD: Cache dependencies test: cache: key: files: - package-lock.json paths: - node_modules/ script: - npm ci - npm test
Anti-Pattern 4: Large Artifacts
Problem: Unnecessary artifacts waste storage and transfer time
# BAD: Saves everything build: script: npm run build artifacts: paths: - dist/ - node_modules/ # 500MB not needed downstream - logs/ # 1GB debug logs
Solution: Only save necessary artifacts
# GOOD: Only production build build: script: npm run build artifacts: paths: - dist/ expire_in: 1 day
Anti-Pattern 5: Hardcoded Secrets
Problem: Secrets in .gitlab-ci.yml
# DANGER: Secret in plain text deploy: script: - API_TOKEN=abc123secret ./deploy.sh
Solution: Use GitLab CI/CD variables
# GOOD: Use protected variables deploy: script: - ./deploy.sh variables: API_TOKEN: $DEPLOY_API_TOKEN # Set in GitLab UI
Anti-Pattern 6: Long-Running Jobs
Problem: Single job takes 30 minutes
# BAD: Monolithic test job test: script: - npm run test:all # 30 minutes
Solution: Parallelize
# GOOD: Split into 10 parallel jobs test: parallel: 10 script: - npm run test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL # 3 min each
Anti-Pattern 7: No Fail-Fast
Problem: Run all jobs even after critical failure
# BAD: Expensive tests run even if build fails stages: - build - test build: stage: build script: ./build.sh allow_failure: true # Continues even if build fails expensive-tests: stage: test script: ./expensive-tests.sh # Wastes 20 min on broken build
Solution: Use needs and remove allow_failure
# GOOD: Stop immediately on build failure build: stage: build script: ./build.sh allow_failure: false expensive-tests: stage: test needs: [build] # Won't run if build fails script: ./expensive-tests.sh
Pattern Selection Guide
Choose Pipeline Type Based On
| Scenario | Recommended Pattern |
|---|---|
| Development workflow | Merge Request Pipelines |
| Production releases | Tag Pipelines |
| Scheduled tasks | Scheduled Pipelines |
| Multi-environment | Hybrid Workflow |
Choose Testing Strategy Based On
| Scenario | Recommended Pattern |
|---|---|
| Large test suite | Parallel Test Execution |
| Multiple environments | Conditional Testing |
| Fast feedback needed | Layered Testing (fast slow) |
| Microservices | Contract Testing |
Choose Deployment Strategy Based On
| Scenario | Recommended Pattern |
|---|---|
| Zero-downtime required | Blue-Green Deployment |
| Gradual rollout needed | Canary Deployment |
| Per-MR testing | Review Apps |
| Manual approval needed | Manual Gates |
Additional Resources
- Multi-Project Management - Managing 70+ projects
- Cost Optimization - Reduce CI minute usage
- Pipeline Efficiency - Performance optimization
- Components - Reusable pipeline components
- GitLab CI/CD Patterns Blog
Last Updated: 2026-01-08 Priority: MEDIUM - Reference guide for common scenarios