Chapter 12Lesson 02~270 minutes

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.

Disposable labCI LintBranch vs MRFirst matchInclusion evidenceFree path

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.
Availability baseline (verified 2026-08-21). 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
Trust boundary: if generated YAML is built from fork content, external API output, issue text, or user-controlled strings, validate/escape it before GitLab interprets it as pipeline configuration. Do not combine unreviewed generated configuration with protected secrets or privileged runners.

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 workflow after 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?

Why can feature_present run when app/feature.flag did not change?

A push creates no pipeline. Is an unmatched runner the likely cause?

What should child_probe print for CI_PIPELINE_SOURCE?

Why keep the dynamic child example optional?

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

Next lesson

Design the smallest deterministic rule set

Lesson 3 compares workflow suppression, per-job exclusion, change bases, catch-all rules, and static versus dynamic configuration as maintainability and governance choices.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.