rules, workflow, Pipeline Sources, Changes, Conditions, and Dynamic Pipeline Creation: Guided Hands-On Workflow and Core Operations
Build a disposable Free-compatible pipeline matrix for branch and merge-request events, prove rules:changes and rules:exists inclusion, suppress one pipeline intentionally, and inspect the resulting pipeline/job evidence.
Learning objectives
- Validate configuration and inspect current pipeline sources before editing.
- Create one deterministic workflow that prefers MR pipelines over duplicate branch pipelines.
- Use rules:changes and rules:exists on synthetic files and predict inclusion before pushing.
- Create one intentionally absent pipeline and diagnose it without weakening the rule.
- Capture structured pipeline/job evidence while keeping the lab Free-compatible and secret-free.
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. Disposable scenario and preflight
Use a disposable project or a branch such as
ch12/rules-lab. The lab changes only synthetic files
and CI configuration. It does not register runners, create tokens,
deploy environments, or touch registries. If no runner capacity is
available, you can still validate configuration, inspect whether
GitLab creates a pipeline object, and reason about job inclusion;
actual job logs are optional evidence.
| Preflight | Record before change |
|---|---|
| Project/ref | Project path, default branch, lab branch, current HEAD SHA. |
| Existing pipeline policy |
Current workflow, job rules,
includes, and project “pipelines must succeed” setting if
relevant.
|
| Recent evidence | For the last few pipelines: source, ref, SHA, status. |
| MR state | Whether the lab branch already has an open MR; this affects duplicate-pipeline logic. |
| Runner capacity | Eligible runner exists or static/creation-only path will be used. |
2. Create tiny synthetic paths whose state is obvious
mkdir -p app docs
printf 'print("hello")
' > app/main.py
printf '# Guide
' > docs/guide.md
printf 'enabled=true
' > app/feature.flag
git status --short
These files let us distinguish change state from
existence state. docs/guide.md can be changed
independently, while app/feature.flag can simply exist.
3. Build a deterministic branch/MR workflow
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_PIPELINE_SOURCE == "push"'
when: never
- if: '$CI_COMMIT_BRANCH'
- if: '$CI_COMMIT_TAG'
- if: '$CI_PIPELINE_SOURCE == "web"'
stages: [inspect, test]
context_probe:
stage: inspect
script:
- printf 'source=%s
' "$CI_PIPELINE_SOURCE"
- printf 'sha=%s
' "$CI_COMMIT_SHA"
- printf 'ref=%s
' "$CI_COMMIT_REF_NAME"
rules:
- when: on_success
docs_check:
stage: test
script: echo "docs job exists because docs changed"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- docs/**/*
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH'
changes:
- docs/**/*
feature_present:
stage: test
script: echo "feature marker exists"
rules:
- exists:
- app/feature.flag
4. Validate and inspect expansion before triggering
Use Build → Pipeline editor or CI Lint. Validate GitLab semantics, not only YAML syntax. Inspect merged configuration if includes are involved. Confirm that the workflow has a deliberate finite set of positive cases rather than an accidental broad fallback.
Write an expected matrix before pushing:
| Scenario | Pipeline? | docs_check? | feature_present? |
|---|---|---|---|
| Branch push, no open MR, docs changed | Yes — branch pipeline | Yes | Yes |
| Branch push, open MR | Branch pipeline suppressed; MR pipeline is preferred | Evaluate in MR context | Evaluate in MR context |
| MR pipeline, only app file changed | Yes | No | Yes |
| Tag push | Yes | No with current job rules | Yes |
5. Commit the lab and bind evidence to an exact SHA
git switch -c ch12/rules-lab
git add -- .gitlab-ci.yml app/main.py app/feature.flag docs/guide.md
git diff --cached --check
git diff --cached
git commit -m "ch12: add deterministic pipeline rules lab"
BASE_SHA="$(git rev-parse HEAD)"
printf 'base_sha=%s
' "$BASE_SHA"
git push -u origin ch12/rules-lab
In GitLab, record the created pipeline’s source/ref/SHA. If it is a
branch pipeline, verify source=push. Do not print the
entire environment; the three allowlisted values above are
sufficient.
6. Open a disposable MR and prove duplicate suppression
Open an MR from ch12/rules-lab to the default branch.
Push one harmless additional commit. With the workflow above, GitLab
should create an MR pipeline and suppress the duplicate branch
pipeline for the branch push while an MR is open.
printf '
MR note
' >> docs/guide.md
git add -- docs/guide.md
git commit -m "ch12: change docs under open MR"
MR_SHA="$(git rev-parse HEAD)"
git push
printf 'mr_sha=%s
' "$MR_SHA"
Verify independently: the MR pipeline has source
merge_request_event, its SHA matches the pushed commit,
and there is not a second branch-push pipeline for the same SHA. If
you do see both, preserve both IDs before changing anything.
7. Prove changes and exists are different predicates
Now change only app/main.py. In the MR pipeline,
docs_check should be absent because no docs path
changed relative to the MR target branch.
feature_present should still exist because
app/feature.flag remains in the repository.
printf '# harmless change
' >> app/main.py
git add -- app/main.py
git commit -m "ch12: app-only change"
APP_SHA="$(git rev-parse HEAD)"
git push
That observation proves why “changed” and “exists” must not be used interchangeably.
8. Intentionally create no pipeline and diagnose the rule
Use a temporary branch rule that denies one clearly named lab branch before the normal branch rule:
workflow:
rules:
- if: '$CI_COMMIT_BRANCH == "ch12/no-pipeline"'
when: never
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH'
Validate it, create/push ch12/no-pipeline, and observe
that no pipeline object appears for that push. Diagnose from
configuration plus the branch/source context: this is not “no
runner,” because a runner is consulted only after a job exists.
Restore the normal workflow after capturing evidence.
9. Manual/web pipeline: inspect source before trusting changes
If your role allows a harmless manual run from
Build → Pipelines → New pipeline, run against the
disposable branch. The source should be web. Do not
infer file-delta behavior from the last push. For non-push sources,
either avoid changes or use an explicit
compare_to when a defined comparison is required.
10. Optional Free dynamic child fixture
This optional example generates a fixed child configuration from reviewed literals. It demonstrates configuration creation without using external/untrusted input.
stages: [generate, child]
generate_child:
stage: generate
script:
- |
cat > generated-child.yml <<'YAML'
child_probe:
script:
- echo "child_source=$CI_PIPELINE_SOURCE"
- test "$CI_PIPELINE_SOURCE" = "parent_pipeline"
YAML
artifacts:
paths: [generated-child.yml]
run_child:
stage: child
trigger:
include:
- artifact: generated-child.yml
job: generate_child
11. Challenge: choose the control surface
You need expensive integration tests only for MR pipelines when
app/** changes. Should you suppress all non-MR
pipelines in workflow:rules, or keep branch pipelines
for lightweight jobs and put the path/source condition on the
expensive job? Justify the answer from project needs. The key is to
place a condition at the narrowest layer that matches the policy:
whole-pipeline policy belongs in workflow; job-specific
policy belongs in job rules.
12. Cleanup and verification
-
Restore the intended
workflowafter the no-pipeline demonstration. - Close the synthetic MR if you created one and delete only the lab branches after confirming the evidence SHAs remain reachable in your local clone/notes as needed.
-
Remove synthetic
app/,docs/, and CI changes or delete the disposable project. - Verify no token, secret, environment deployment, package, or external resource was created.
Knowledge check
A push to a branch with an open MR creates both branch and MR pipelines. Where should you look first?
workflow:rules and overlapping pipeline-creation conditions, before editing individual job scripts.
Why can feature_present run when app/feature.flag did not change?
rules:exists checks whether the path exists in repository state; it is not a change detector.
A push creates no pipeline. Is an unmatched runner the likely cause?
No. Runner eligibility is evaluated only after a pipeline and job exist; inspect workflow/source/configuration first.
What should child_probe print for CI_PIPELINE_SOURCE?
parent_pipeline.
Why keep the dynamic child example optional?
The chapter goal is creation-time reasoning. Dynamic generation adds artifact/configuration and trust complexity that should not be required to learn deterministic rules.
Summary
The guided lab proved creation-time causality with before/after
evidence: workflow chooses pipeline type, job rules choose job
existence, changes and exists answer
different questions, and a deliberately absent pipeline is diagnosed
before runner concerns enter the model.
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.