.ai/ci-cd.md
This document describes the architecture, patterns, and common tasks for the GitLab project CI/CD configuration.
The CI/CD configuration spans ~58 YAML files and ~12,500 lines across .gitlab-ci.yml and .gitlab/ci/. The pipeline defines 18 stages:
sync > preflight > prepare > build-images > release-environments > fixtures > lint > test-frontend > test > post-test > review > qa > post-qa > pre-merge > pages > notify > benchmark > ai-gateway
.gitlab-ci.yml # Entry point: stages, workflow rules, global variables, includes
.gitlab/ci/
version.yml # Pinned tool versions (Ruby, Go, Node, Chrome, etc.)
global.gitlab-ci.yml # Shared foundations: retry, before_script, DB/Redis services, caches
rules.gitlab-ci.yml # Centralized rules (~3,500 lines): conditions, file patterns, composite rules
rails.gitlab-ci.yml # RSpec jobs (FOSS + EE), predictive pipelines, coverage, artifact collectors
rails/
shared.gitlab-ci.yml # RSpec base job definitions, parallel configs, DB service mixins
rspec-predictive.gitlab-ci.yml.erb # ERB template for dynamically generated predictive RSpec jobs
frontend.gitlab-ci.yml # Jest, Storybook, Webpack, ESLint, frontend fixtures
static-analysis.gitlab-ci.yml # RuboCop, ESLint, Semgrep, Haml-lint
database.gitlab-ci.yml # DB setup, schema validation, migration testing
setup.gitlab-ci.yml # clone-gitlab-repo, setup-test-env, compile-assets, cache warming
qa.gitlab-ci.yml # E2E QA test triggers
qa-common/ # Shared QA config (rules, variables, Allure reporting)
test-on-cng/ # Cloud-Native GitLab E2E tests
test-on-gdk/ # GDK-based E2E tests
test-on-omnibus/ # Omnibus-based E2E tests (internal + external)
cng/ # CNG image build jobs
templates/
gem.gitlab-ci.yml # Reusable gem child pipeline template (uses spec:inputs)
gitlab-gems.gitlab-ci.yml # Child pipelines for gems/ directory
vendored-gems.gitlab-ci.yml # Child pipelines for vendor/gems/ directory
overrides/
skip.yml # No-op pipeline for security-canonical-sync MRs
gem-cache.rails-next.yml # Cache override for rails-next pipelines
README.md # Explains the override pattern
includes/
as-if-jh.gitlab-ci.yml # JiHu (Chinese edition) compatibility testing
gitlab-com/
danger-review.gitlab-ci.yml # Danger bot review (CI component)
as-if-foss.gitlab-ci.yml # FOSS compatibility testing (strips EE code)
docs.gitlab-ci.yml # Documentation linting, review apps
workhorse.gitlab-ci.yml # GitLab Workhorse Go tests
coverage.gitlab-ci.yml # Code coverage collection
caching.gitlab-ci.yml # Cache warming/updating jobs
reports.gitlab-ci.yml # SAST, Secret Detection, Dependency Scanning
notify.gitlab-ci.yml # Slack notifications
releases.gitlab-ci.yml # Release tagging
release-environments.gitlab-ci.yml # Release environment deployments
build-images.gitlab-ci.yml # CI image builds
# ... and more (benchmark, memory, preflight, pages, etc.)
The main .gitlab-ci.yml uses a wildcard include to auto-register all top-level CI files:
include:
- local: .gitlab/ci/*.gitlab-ci.yml
Adding a new .gitlab/ci/foo.gitlab-ci.yml file automatically includes it in the pipeline. No manual registration needed.
Conditional includes handle special cases:
overrides/skip.yml -- only for security-canonical-sync MRs (creates a no-op pipeline)includes/gitlab-com/*.gitlab-ci.yml -- only on gitlab.com or jihulab.comincludes/as-if-jh.gitlab-ci.yml -- only for gitlab.com MRs (not stable branches, not quarantined)overrides/gem-cache.rails-next.yml -- only for rails-next pipelinesSub-files can include other local files (two-level nesting). Key example:
rails.gitlab-ci.yml includes rails/shared.gitlab-ci.ymlrails/shared.gitlab-ci.yml includes global.gitlab-ci.yml and rules.gitlab-ci.ymlrules.gitlab-ci.yml is the single largest file (~3,500 lines) and the single source of truth for all job conditions. It uses three pattern types:
.if-*)Boolean conditions based on CI variables. Referenced via YAML anchors.
.if-merge-request: &if-merge-request
if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ...'
.if-default-branch-refs: &if-default-branch-refs
if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH && ...'
.*-patterns)Glob arrays for detecting which files changed. Referenced via YAML anchors.
.ci-patterns: &ci-patterns
- "{,jh/}.gitlab-ci.yml"
- "{,jh/}.gitlab/ci/**/*"
.backend-patterns: &backend-patterns
- "app/**/*"
- "lib/**/*"
# ...
.category:rules:job-type)Combine conditions + file patterns into complete rules: blocks that jobs reference via extends:.
.rails:rules:ee-and-foss-default-rules:
rules:
- <<: *if-fork-merge-request
changes: *code-backstage-spec-patterns
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
Jobs use these via extends:
rspec unit pg17:
extends:
- .rspec-base-pg17
- .rspec-unit-parallel
- .rails:rules:ee-and-foss-unit
MR pipelines use a tiering system controlled by labels:
pipeline::tier-1 -- minimal (predictive tests only)pipeline::tier-2 -- standardpipeline::tier-3 -- full (all tests including nightly-level jobs)Special labels:
pipeline::expedited -- skip most jobspipeline:run-all-rspec -- force all RSpec jobspipeline:run-all-jest -- force all Jest jobspipeline:run-as-if-foss -- run full FOSS compatibilitypipeline:as-if-foss-run-predictive -- run FOSS compatibility with predictive tests onlypipeline:run-search-tests -- run Elasticsearch/OpenSearch testspipeline:run-praefect-with-db -- run Praefect DB testspipeline:update-cache -- force cache updatesglobal.gitlab-ci.ymlDefines reusable building blocks:
.default-retry -- retry policy (max 2 retries for infra failures).default-before_script -- standard before_script (FOSS mode, GOPATH, utils).repo-from-artifacts -- use cloned repo from artifacts instead of git clone.use-docker-in-docker -- Docker-in-Docker setup with registry mirror.use-pg16, .use-pg17, .use-pg18 -- PostgreSQL service configs with auto-explain.use-pg17-es7-ee, .use-pg17-clickhouse23, etc. -- combined service stacks.ruby-gems-cache, .node-modules-cache, .assets-cache, .rubocop-cache, etc.rails/shared.gitlab-ci.ymlRSpec-specific foundations:
.rspec-base -- base RSpec job (extends retry, before_script, cache; sets stage, needs, script, after_script).rspec-base-pg17 -- RSpec with PG17 services.rspec-ee-base-pg17 -- EE RSpec with PG17 + Elasticsearch.rspec-unit-parallel: parallel: 44, .rspec-system-parallel: parallel: 32, etc..single-db, .single-db-ci-connection, .praefect-with-dbversion.ymlPinned versions for all tools used in CI images. The DEFAULT_CI_IMAGE variable in .gitlab-ci.yml is constructed from these versions.
Child pipelines are triggered via trigger: jobs. Key patterns:
rspec-predictive:pipeline-generate generates YAML from an ERB template based on detected test files, then rspec:predictive:trigger triggers a child pipeline from the generated artifact.
templates/gem.gitlab-ci.yml uses spec:inputs to create parameterized gem pipelines:
# In vendored-gems.gitlab-ci.yml:
include:
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "microsoft_graph_mailer"
gem_path_prefix: "vendor/gems/"
Each gem gets its own child pipeline triggered from its .gitlab-ci.yml.
qa.gitlab-ci.yml, test-on-cng/, test-on-omnibus/, and test-on-gdk/ trigger E2E test child pipelines with shared config from qa-common/.
The overrides/ directory contains files that conditionally redefine job keys. GitLab CI natively merges definitions with the same key name, so conditional include directives can alter job behavior.
Example: overrides/skip.yml is included only for security-canonical-sync MRs. It defines a no-op job that replaces the entire pipeline with a skip message.
RSpec jobs run with high parallelism (e.g., 44 parallel unit jobs). Since needs: has a 50-job limit, intermediate rspec:artifact-collector jobs aggregate artifacts from groups of RSpec jobs, allowing downstream jobs like rspec:coverage to depend on all results.
The workflow:rules: block in .gitlab-ci.yml (~30 rules) determines:
BUNDLE_GEMFILE: Gemfile.next for rails-next)Key pipeline types:
.gitlab/ci/*.gitlab-ci.yml file (or create a new one -- it auto-includes via wildcard).rules.gitlab-ci.yml using existing condition anchors and file patterns..default-retry, .default-before_script, service mixins).rules.gitlab-ci.yml, add a new pattern anchor:
.my-new-patterns: &my-new-patterns
- "path/to/files/**/*"
.gitlab/ci/my-feature/main.gitlab-ci.yml).In rails/shared.gitlab-ci.yml, adjust the .rspec-*-parallel values. Follow the formula in the comments:
parallel_job_count = ceil(current_count * (avg_duration / target_duration))
Target is 30 minutes per job. Snowflake dashboard links are in the comments next to each parallel config.
In gitlab-gems.gitlab-ci.yml or vendored-gems.gitlab-ci.yml, add:
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "my-gem"
gem_path_prefix: "gems/" # or "vendor/gems/"
The gem must have its own .gitlab-ci.yml at its root.
The CI config pulls from external sources:
untamper-my-lockfile (lockfile integrity check)[email protected], [email protected]gitlab-org/quality/pipeline-common*.gitlab-ci.yml at the top level of .gitlab/ci/ is automatically included. Files in subdirectories are NOT auto-included.rules: uses first-match-wins semantics. Order matters. Put when: never exclusions before when: always inclusions.rules.gitlab-ci.yml are available in files that include: it (like rails/shared.gitlab-ci.yml), but NOT in files included via the top-level wildcard. Top-level files use !reference instead.!reference vs YAML anchors: Use !reference [".some-key", rules] to reference keys across files included at the same level. YAML anchors (*anchor) only work within the same file or direct includes.needs:. This is why artifact collector jobs exist..if-merge-request conditions explicitly exclude merge trains ($CI_MERGE_REQUEST_EVENT_TYPE != "merge_train").