Skip to main content
Technology & EngineeringCicd Patterns244 lines

Gitlab CI

GitLab CI/CD pipelines including multi-stage builds, includes, rules, and Auto DevOps configuration

Quick Summary18 lines
You are an expert in GitLab CI/CD for continuous integration and deployment.

## Key Points

- local: .gitlab/ci/build.yml
- local: .gitlab/ci/test.yml
- project: 'my-group/ci-templates'
- template: Security/SAST.gitlab-ci.yml
- Use `rules` instead of `only/except` for clearer, more predictable pipeline control.
- Define `default` settings (image, cache, retry) to reduce duplication across jobs.
- Use `needs` (DAG) to allow jobs to start as soon as their actual dependencies finish, not waiting for the entire stage.
- Set `artifacts:expire_in` to avoid filling up storage with old build outputs.
- Use `include` to break large `.gitlab-ci.yml` files into manageable pieces.
- Leverage GitLab Environments with deployment tracking and rollback.
- Use `interruptible: true` on jobs that can be safely cancelled when new commits are pushed.
- Use `resource_group` to prevent concurrent deployments to the same environment.
skilldb get cicd-patterns-skills/Gitlab CIFull skill: 244 lines
Paste into your CLAUDE.md or agent config

GitLab CI — CI/CD

You are an expert in GitLab CI/CD for continuous integration and deployment.

Overview

GitLab CI/CD is configured through a .gitlab-ci.yml file at the repository root. Pipelines consist of stages that run sequentially, with jobs within a stage running in parallel. GitLab provides shared runners or supports self-managed runners. It offers deep integration with GitLab's merge requests, environments, container registry, and package registry.

Setup & Configuration

The pipeline is defined in .gitlab-ci.yml. GitLab automatically detects this file and runs pipelines on push events.

Basic pipeline:

stages:
  - build
  - test
  - deploy

default:
  image: node:20-alpine
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/

build:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

test:
  stage: test
  script:
    - npm ci
    - npm test
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'

deploy-staging:
  stage: deploy
  script:
    - ./deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

Core Patterns

Rules-Based Pipeline Control

rules replaces the older only/except syntax for controlling when jobs run:

deploy-production:
  stage: deploy
  script:
    - ./deploy.sh production
  environment:
    name: production
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
      when: manual
    - if: $CI_PIPELINE_SOURCE == "schedule"
      when: never
    - when: never

Includes and Templates

Split large configs into reusable files:

include:
  - local: .gitlab/ci/build.yml
  - local: .gitlab/ci/test.yml
  - project: 'my-group/ci-templates'
    ref: main
    file: '/templates/deploy.yml'
  - template: Security/SAST.gitlab-ci.yml

Define reusable job templates with extends:

.deploy-template:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache curl
  script:
    - ./deploy.sh $ENVIRONMENT

deploy-staging:
  extends: .deploy-template
  variables:
    ENVIRONMENT: staging
  environment:
    name: staging

deploy-production:
  extends: .deploy-template
  variables:
    ENVIRONMENT: production
  environment:
    name: production
  when: manual

Multi-Project Pipelines

Trigger pipelines in other projects:

trigger-downstream:
  stage: deploy
  trigger:
    project: my-group/downstream-project
    branch: main
    strategy: depend
  variables:
    UPSTREAM_VERSION: $CI_COMMIT_TAG

Parent-Child Pipelines

Generate dynamic pipeline configurations:

generate-config:
  stage: build
  script:
    - python generate-pipeline.py > child-pipeline.yml
  artifacts:
    paths:
      - child-pipeline.yml

trigger-child:
  stage: test
  trigger:
    include:
      - artifact: child-pipeline.yml
        job: generate-config
    strategy: depend

Parallel and Matrix Jobs

test:
  stage: test
  parallel:
    matrix:
      - RUBY_VERSION: ["3.1", "3.2", "3.3"]
        DB: ["postgres", "mysql"]
  image: ruby:${RUBY_VERSION}
  script:
    - bundle install
    - bundle exec rspec
  services:
    - name: ${DB}:latest

DAG (Directed Acyclic Graph) Pipelines

Control job dependencies independent of stage ordering:

build-frontend:
  stage: build
  script: npm run build:frontend

build-backend:
  stage: build
  script: npm run build:backend

test-frontend:
  stage: test
  needs: [build-frontend]
  script: npm run test:frontend

test-backend:
  stage: test
  needs: [build-backend]
  script: npm run test:backend

Core Philosophy

GitLab CI's strength is its deep integration with the entire GitLab platform — merge requests, environments, container registry, package registry, and security scanning all connect through the pipeline without third-party plugins or marketplace actions. This integration means the pipeline is not just a build system but a unified delivery platform where every stage from code to production is visible in a single interface. The trade-off is vendor coupling: the more GitLab-specific features you use, the harder it becomes to migrate. Use the integration where it adds genuine value (environments, merge request pipelines, security templates) and keep the core build logic portable.

The rules system is GitLab CI's most important configuration primitive and the one most teams underuse. Rules replace the older only/except keywords with a declarative, composable way to control when jobs run. The critical mental model is that rules are evaluated top-to-bottom and the first matching rule wins. This means you can express complex conditional logic — run on tags but not schedules, run manually on main but automatically on feature branches — without nested conditionals or scripting. Mastering rules is the difference between a pipeline that does what you expect and one that surprises you.

Parent-child pipelines and includes transform GitLab CI from a single-file configuration into a modular system. For monorepos, a parent pipeline can detect which components changed and trigger only the relevant child pipelines, keeping build times proportional to the change rather than the repository size. For multi-team organizations, shared CI templates published in a central project let platform teams provide vetted, secure pipeline components that product teams consume without copying YAML. This separation of concerns — platform teams own the how, product teams own the what — scales CI governance across the organization.

Anti-Patterns

  • Using only/except instead of rules. The legacy only/except syntax is less expressive, harder to reason about, and cannot be combined with rules in the same job. Standardize on rules for all pipeline control logic to avoid confusing evaluation behavior.

  • Stage-based parallelism without needs. By default, GitLab runs all jobs in a stage after every job in the previous stage finishes. Without needs (DAG mode), a fast frontend test waits for a slow backend build to complete even though they are independent. Use needs to express actual dependencies so jobs start as soon as their prerequisites finish.

  • Monolithic .gitlab-ci.yml files. A single configuration file with hundreds of jobs is unmaintainable and hard to review. Use include to split configuration by component, team, or concern, and use extends with hidden jobs (.template-name) to reduce duplication.

  • Cache-dependent correctness. GitLab CI caches are best-effort and per-runner by default. A pipeline that fails on cache miss is a pipeline that will fail unpredictably. Use artifacts for files that must be passed between jobs; use caches only for speeding up dependency installation.

  • Unprotected manual production deploys. Using when: manual on a production deploy job without allow_failure: false means the pipeline shows as passed even if no one clicks the deploy button. Combine manual triggers with allow_failure: false and protected branch/variable restrictions to enforce the deployment gate.

Best Practices

  • Use rules instead of only/except for clearer, more predictable pipeline control.
  • Define default settings (image, cache, retry) to reduce duplication across jobs.
  • Use needs (DAG) to allow jobs to start as soon as their actual dependencies finish, not waiting for the entire stage.
  • Set artifacts:expire_in to avoid filling up storage with old build outputs.
  • Use include to break large .gitlab-ci.yml files into manageable pieces.
  • Leverage GitLab Environments with deployment tracking and rollback.
  • Use interruptible: true on jobs that can be safely cancelled when new commits are pushed.
  • Use resource_group to prevent concurrent deployments to the same environment.
  • Configure retry on flaky jobs with specific failure reasons: retry: { max: 2, when: [runner_system_failure] }.
  • Use protected branches and protected variables to restrict secret access.

Common Pitfalls

  • Jobs in the same stage run in parallel by default; assuming sequential execution within a stage causes race conditions.
  • only/except and rules cannot be mixed in the same job; this causes a validation error.
  • Cache is best-effort and not guaranteed; never rely on cache for correctness—use artifacts for required files.
  • Variables defined in variables: at the job level override project-level CI/CD variables of the same name.
  • when: manual jobs do not block the pipeline by default; use allow_failure: false to make them blocking.
  • Not setting GIT_DEPTH or GIT_STRATEGY wastes time cloning full history when only shallow clones are needed.
  • Using dependencies: [] to skip artifact downloads but forgetting that needs also controls artifact downloads.
  • Dynamic child pipelines require the generated YAML to be a valid artifact before triggering.

Install this skill directly: skilldb add cicd-patterns-skills

Get CLI access →