Series Context: Part 9 of 10 in GitLab Core Engineering: Architecture, MCP & CI/CD Deep Dives.

Grounded in live pipeline runs across: !269060 (Tier-3 345 jobs / 45 bridges), !269061 (56 jobs spec-only), !264856 (89 jobs), !262103 (Tier-2), !269059.

Why MRs With Green RSpec Specs Still Fail Review Velocity

In gitlab-org/gitlab, passing rspec locally is only half the battle. GitLab's Dangerfile and tiered MR pipeline architecture enforce repository-wide invariants on commit history, Enterprise Edition (EE) boundaries, as-if-foss compatibility, labels, and RuboCop god-class baselines (Gitlab/OmniscientClassMethods).

Here is the exact checklist of CI/Danger traps we resolved while taking five concurrent MRs from maintainer feedback to 100% green pipelines.

1. Commit Hygiene & Git Trailers (Changelog: & EE: true)

Danger evaluates every commit in an MR branch (not just the MR title!). Leaving "fix review comments" or "wip" in a commit series triggers immediate Danger warnings and blocks merge until squashed or amended.

Rules That Keep Danger Zero-Warning

  1. Squash to 1 clean commit (or intentionally atomic commits) where the subject line is <= 72 characters (imperative mood, capitalized, no trailing period) and body lines wrap cleanly at <= 72 characters.
  2. Declare Changelog: accurately for user-facing changes, bug fixes, or structural refactors (added, fixed, other)—skip Changelog: on spec-only MRs (!269061).
  3. Never forget EE: true when touching ee/! Even a 6-line deletion inside ee/spec/helpers/secrets_helper_spec.rb (!269061) lives under ee/. On our first rebase of !269061, dropping EE: true wouldn't trigger in some branches until Danger inspects ee/ paths—adding EE: true to commit 745ca762b3 kept Danger completely clean.

2. How danger/specs/Dangerfile Guards FOSS vs EE Boundaries

Look inside danger/specs/Dangerfile:

all_changed_files = helper.all_changed_files has_app_changes = all_changed_files.grep(%r{\A(app|lib|db/(geo/)?(post_)?migrate)/}).any? has_ee_app_changes = all_changed_files.grep(%r{\Aee/(app|lib|db/(geo/)?(post_)?migrate)/}).any? spec_changes = specs.changed_specs_files(ee: :exclude) has_spec_changes = spec_changes.any? has_ee_spec_changes = specs.changed_specs_files(ee: :only).any? new_specs_needed = (helper.mr_labels & NO_SPECS_LABELS).empty? 

Look closely at two rules in those 40 lines that surprise engineers every week:

  1. Pure refactoring MRs without new spec files (NO_SPECS_LABELS): If you touch app/ or lib/ without adding tests, Danger warns unless the MR carries one of maintenance::refactor, maintenance::pipelines, maintenance::workflow, maintenance::performance, documentation, or QA.
  2. Changing ee/app/... while only modifying spec/... (not ee/spec/...): Danger triggers EE_CHANGE_WITH_FOSS_SPEC_CHANGE_MESSAGE—a red flag that EE-only behavior may break Community Edition (FOSS_ONLY=1) builds! GitLab verifies this via:
    • Local FOSS_ONLY=1 bin/rspec -- <spec_files>
    • Downstream rspec:predictive:trigger inside the as-if-foss cross-project child pipeline (pipeline:as-if-foss-run-predictive).

Notice in !269060, touching ee/app/ and ee/lib/ alongside ee/spec/ (no CE spec/ leak) kept FOSS boundaries crisp, allowing its Tier-3 pipeline (#3053354161) and as-if-foss predictive bridges to pass all 345 jobs and 45 downstream triggers cleanly.

3. danger/spec_only/Dangerfile & Stale Labels After Rebases

Look at danger/spec_only/Dangerfile: it manages ~"pipeline:spec-only" automatically when an MR only edits spec files. Why does that matter?

  • On !269061, once upstream commit 5350f0e843 removed the Haml/feature-flag files, only ee/spec/helpers/secrets_helper_spec.rb remained.
  • Applying ~"pipeline:spec-only" (and requesting removal of stale ~"frontend" / ~UX labels) trimmed the pipeline from a full asset/Jest/backend suite down to 56 targeted jobs (#3053445498), all green in a fraction of the compute cost.

4. Don't Forget legacy_methods.yml When Shrinking Project, Group, or User

In rubocop/cop/gitlab/omniscient_class_methods.rb:

def external_dependency_checksum @external_dependency_checksum ||= Digest::SHA256.hexdigest(...) end 

Notice TWO clever RuboCop internals there:

  1. It caches an external_dependency_checksum across rubocop/cop/gitlab/config/omniscient_classes/*/*.yml so editing legacy_methods.yml automatically invalidates RuboCop's cache!
  2. Its on_investigation_end hook fails both ways: if you add an unlisted method to EE::Project, OR if you delete a listed method from EE::Project without deleting its name from ee_project/legacy_methods.yml! Checking RuboCop locally catches this in 2 seconds before pushing a doomed pipeline.

5. Understanding GitLab's Tiered Pipelines (tier-1 -> tier-2 -> tier-3)

Contributors are often surprised when a pipeline passes on Monday (tier-1), gets approved, and suddenly runs 345+ jobs on Wednesday (tier-3):

  • pipeline::tier-1 (Initial MR push): Runs predictive RSpec subsets, Danger, RuboCop, Docs lint, and fast static checks (~56–89 jobs). Fast feedback loop.
  • pipeline::tier-2 (First review / pipeline:mr-approved pending): Expands coverage across predictive suites.
  • pipeline::tier-3 (pipeline:mr-approved applied by maintainer): Runs full predictive matrices + as-if-foss downstream child bridges (45 bridges on !269060). Always wait for Tier-3 completion (never assume Tier-1 == merge-safe), and note that Tier-3 pipelines must be fresh against recent master before merge train triggers!

6. Clean Worktree & CodeRabbit Pre-Push Workflow

When juggling 5 concurrent MRs in gitlab-org/gitlab (a multi-gigabyte monorepo), avoid checking branches out back-and-forth in your primary working copy:

  1. Use isolated git worktree add directories per MR branch so rebasing !264856 never invalidates Bootsnap/Sorbet caches or mixes untracked scratch artifacts into !269060.
  2. Rebase directly onto upstream/master (git fetch upstream master && git rebase upstream/master), never fork-stale origin/master.
  3. Run an automated diff review (coderabbit review --plain -t uncommitted / commit review) before pushing to catch stray comments, unused helper leftovers, or spec regressions with 0 findings.
  4. Prune and remove temporary worktrees (git worktree remove + git worktree prune) once branches are pushed and green.

Series Navigation