rules, workflow, Pipeline Sources, Changes, Conditions, and Dynamic Pipeline Creation: Configuration, Design Choices, and Tradeoffs
Choose deterministic workflow and job-rule designs by balancing suppression scope, change-comparison semantics, explicit positive rules, static configuration, and dynamic child-pipeline complexity.
Learning objectives
- Choose workflow-level suppression only when the entire pipeline should not exist.
- Choose job-level rules when the pipeline remains useful but one job is irrelevant.
- Use rules:changes with an explicit understanding of source-specific comparison semantics.
- Avoid broad catch-all rules that create duplicate or future-unrecognized pipeline types.
- Evaluate when dynamic child pipelines improve architecture and when they merely add a second configuration compiler.
workflow:rules, job rules,
rules:if, rules:changes,
rules:exists, parent-child pipelines, and dynamic child
pipelines are core GitLab CI/CD capabilities on Free, Premium, and
Ultimate across GitLab.com, Self-Managed, and Dedicated. Live
execution still requires eligible runner capacity; every mandatory
exercise therefore has a CI Lint/merged-configuration and prediction
path that does not require buying compute. Version-sensitive additions
are labeled: directory matching for rules:exists arrived
in GitLab 18.2, and rules:changes:regexp is new in GitLab
19.2 and is not required for this chapter.
1. Design principle: place policy at the correct creation layer
Rule design is easier when the policy sentence has a clear subject.
“No pipeline should exist for draft MRs” belongs at
workflow:rules. “Run docs validation only when docs
change” belongs on the docs job. Using a job rule to imitate
whole-pipeline suppression leaves an empty or misleading pipeline;
using workflow to suppress a pipeline just because one expensive job
is irrelevant can remove useful fast feedback.
2. Workflow suppression versus per-job exclusion
| Decision | Prefer workflow:rules | Prefer job rules |
|---|---|---|
| Scope | The entire pipeline type/event is disallowed or redundant. | Pipeline should exist, but this job is conditional. |
| Security | No jobs at all should materialize for this source. | Only one sensitive/expensive job must be absent. |
| Cost | All jobs would be waste for this source. | Keep cheap checks; suppress only expensive work. |
| Observability | Absence of pipeline is the intended signal. | A pipeline should still provide status for other checks. |
| Maintenance | One centralized pipeline-type contract. | Policy stays close to the job it governs. |
3. rules:changes: efficient when the comparison is well-defined
Path filtering can save substantial compute in monorepos, but only
when the comparison base matches the event. MR pipelines naturally
compare with the target branch; branch pipelines compare with the
previous commit. New branches and sources without a push require
extra care. If a scheduled pipeline should run tests based on the
current branch versus main, use
compare_to explicitly rather than assuming the last
push supplies a baseline.
backend_tests:
script: ./test-backend.sh
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- backend/**/*
- if: '$CI_PIPELINE_SOURCE == "schedule"'
changes:
paths:
- backend/**/*
compare_to: 'refs/heads/main'
4. Explicit positive rules versus broad catch-all fallbacks
An explicit allow-list makes newly introduced pipeline sources
default to “not included” until reviewed. A final
when: always is convenient but broad: it may include
the job in scheduled, API, trigger, parent, policy-driven, or future
pipeline sources you never intended.
| Pattern | Strength | Risk |
|---|---|---|
| Explicit source rules | Readable, deterministic, least-privilege. | Must be updated when a new legitimate source is added. |
Final when: always |
Convenient default. | Can create duplicate pipelines/jobs and silently opt into new sources. |
| Negative list + final allow | Useful when policy truly is “everything except X.” | Future sources are automatically allowed; requires governance review. |
5. Rule order is part of policy, not formatting
Because the first match wins, putting a broad branch rule above a special default-branch rule can make the special case unreachable. Review rules like firewall policy: from specific deny/override cases to intended general cases, with comments where order is security-significant.
# Wrong for a special default-branch manual gate:
rules:
- if: '$CI_COMMIT_BRANCH' # catches main first
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
# Better:
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
- if: '$CI_COMMIT_BRANCH'
6. Prefer source identity over variable side effects
Do not use presence of a loosely related variable as a proxy for
pipeline type if CI_PIPELINE_SOURCE expresses the
intent directly. Source-specific variables then refine the case.
This makes the configuration more compatible with
web/API/schedule/child contexts and easier to diagnose from pipeline
metadata.
7. Static configuration versus dynamic child pipelines
Static YAML is easier to review, lint, cache mentally, and audit. Dynamic child configuration can be valuable when the job graph itself is data-dependent—for example, generating component-specific pipelines in a large monorepo. But it creates another compiler step: generator inputs → generated YAML artifact → GitLab parser → child jobs.
| Criterion | Static configuration | Dynamic child configuration |
|---|---|---|
| Reviewability | Direct repository diff. | Review generator plus generated output/evidence. |
| Flexibility | Best when topology is known. | Best when topology depends on repository data/change sets. |
| Trust | No generated-code boundary. | Generated YAML becomes executable CI policy. |
| Failure modes | Main config parse/rule failures. | Generator, artifact, child config parse, trigger, child jobs. |
| Cost/complexity | Lower. | Higher; can reduce monorepo job count when designed well. |
8. Dynamic configuration is a code-generation trust boundary
If a generator reads untrusted branch content and emits YAML that can request privileged runners, protected environments, or secret-consuming jobs, the generator has become a policy compiler. Constrain the inputs, validate the generated file, use least-privilege runners/variables, and keep target-project privileged resources inaccessible to untrusted fork pipelines.
9. Offering, tier, and version boundaries
- Free path: all mandatory rule/workflow/change/existence examples and parent-child/dynamic child concepts are available on Free across GitLab.com, Self-Managed, and Dedicated.
- Runner compute: feature availability does not guarantee free hosted execution capacity; use CI Lint and no-runner reasoning when needed.
-
GitLab 18.2+: directory paths are supported by
rules:exists. Use file paths for older installations. -
GitLab 19.2:
rules:changes:regexpis new. This chapter uses established glob/path behavior so older supported installations can follow the mandatory path. - Self-Managed: verify instance version before adopting newer rule syntax or relying on SaaS-current behavior.
10. Worked decision table: monorepo delivery policy
Scenario: a monorepo has fast linting, backend tests, frontend tests, and a nightly full test. Which control belongs where?
| Requirement | Recommended control | Why |
|---|---|---|
| Avoid branch + MR duplicates | workflow:rules with MR preference. |
This is whole-pipeline creation policy. |
| Backend tests only when backend changes | Job rules:changes. |
Only one job is conditional. |
| Nightly full test regardless of changed files | Schedule source rule without path filtering. | Schedule intent is time-based, not push-delta based. |
| Hundreds of independent components discovered at runtime | Consider generated child pipelines. | Dynamic topology may reduce unnecessary jobs, at added complexity. |
| Production deployment never exists for fork/untrusted MR context | Creation-time job/workflow exclusion plus protected-resource policy. | Prevents privileged job materialization; later chapters deepen secrets/environments. |
11. Maintainability, reliability, performance, and cost
Every extra rule branch expands the state space you must test. Prefer small source matrices, centralize whole-pipeline decisions, and keep job-specific conditions local. Path filters can save compute but become a correctness risk when comparison semantics are misunderstood. Dynamic children can reduce monorepo fan-out but add artifact and parsing latency. Optimize only after the deterministic state model is testable.
Knowledge check
Should “skip only docs_test when docs did not change” normally be workflow or job rules?
Job rules; the rest of the pipeline remains useful.
Why can a final when: always be a governance risk?
It opts the job/pipeline into sources that were not explicitly reviewed, including future or triggered contexts.
When is compare_to most valuable?
When change semantics need an explicit base, especially sources without a normal push comparison or when policy requires a stable reference.
What extra trust boundary does dynamic YAML introduce?
The generator and its inputs determine executable CI configuration; the generated artifact must be treated as code/policy.
Why is rule order a design concern?
First match wins, so an early broad rule can make later specific policy unreachable.
Summary
Good rule architecture is intentionally boring: a small pipeline-source contract at workflow level, local job predicates for job-specific work, explicit comparison semantics, minimal fallbacks, and dynamic configuration only when it solves a real topology problem. This reduces both runner waste and policy ambiguity.
Official references
- GitLab Docs — workflow keyword
- GitLab Docs — Specify when jobs run with rules
- GitLab Docs — CI/CD YAML syntax reference
- GitLab Docs — Predefined CI/CD variables
- GitLab Docs — Where variables can be used
- GitLab Docs — Merge request pipelines
- GitLab Docs — Downstream pipelines
- GitLab Docs — Pipeline editor
- GitLab Docs — CI Lint
- GitLab Docs — CI/CD pipelines
- GitLab Docs — Pipelines API
- GitLab Docs — Use CI/CD configuration from other files
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.