Parent-Child Pipelines, Dynamic Child Pipelines, Generated Configuration, and Monorepo Decomposition: Guided Hands-On Workflow and Core Operations
This guided workflow builds a disposable toy monorepo with two static child pipelines, then adds a safely generated child configuration from allowlisted metadata. You will trigger only the intended subtree, inspect parent/child IDs and shared SHA, and compare default trigger behavior with status mirroring.
Learning objectives
- Create two static child pipeline files for separate monorepo subtrees and trigger them intentionally.
- Generate a small child YAML artifact only from allowlisted metadata and validate the generated content before GitLab consumes it.
- Use rules:changes to trigger only the intended subtree and explain the pipeline-creation consequences.
- Record parent/child pipeline IDs, source/ref/SHA, trigger-job status, child pipeline source, and observed job/artifact evidence.
- Use a free/disposable path and complete a challenge that selects the correct orchestration/dataflow/security layer.
1. Guided scenario: a two-area disposable monorepo
Create a throwaway project with two synthetic areas:
services/api/ and services/web/. Each area
has a text file and its own child pipeline. Nothing deploys,
publishes packages, touches cloud infrastructure, or requires
privileged runners.
.
├── .gitlab-ci.yml
├── ci/
│ ├── children/
│ │ ├── api.yml
│ │ └── web.yml
│ └── generate_child.py
├── services/
│ ├── api/app.txt
│ └── web/app.txt
└── generated/
2. Preflight and assumptions
- Use a disposable GitLab project you are authorized to modify.
- Use GitLab Free-compatible parent-child pipelines. No catalog, protected environment, cloud, Kubernetes, or admin control is required.
- A normal non-privileged runner is sufficient for jobs that execute scripts. Trigger jobs themselves do not use runners.
- Record the GitLab offering/version visible to you and the Runner version/executor if runtime jobs execute.
- Do not create or print secrets. All variables in this lab are synthetic identifiers.
3. Static child A: API
stages: [verify]
api-verify:
stage: verify
image: alpine:3.20.3
rules:
- if: '$CI_PIPELINE_SOURCE == "parent_pipeline"'
script:
- test -f services/api/app.txt
- mkdir -p evidence
- printf 'area=api\nsha=%s\nchild_pipeline=%s\nsource=%s\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" "$CI_PIPELINE_SOURCE" > evidence/api.txt
artifacts:
expire_in: 1 day
paths: [evidence/api.txt]
4. Static child B: web
stages: [verify]
web-verify:
stage: verify
image: alpine:3.20.3
rules:
- if: '$CI_PIPELINE_SOURCE == "parent_pipeline"'
script:
- test -f services/web/app.txt
- mkdir -p evidence
- printf 'area=web\nsha=%s\nchild_pipeline=%s\nsource=%s\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" "$CI_PIPELINE_SOURCE" > evidence/web.txt
artifacts:
expire_in: 1 day
paths: [evidence/web.txt]
5. Parent pipeline: trigger only changed subtrees
stages: [children]
api-child:
stage: children
rules:
- changes:
- services/api/**/*
variables:
PARENT_PIPELINE_ID: "$CI_PIPELINE_ID"
MONOREPO_AREA: "api"
trigger:
include:
- local: ci/children/api.yml
strategy: mirror
web-child:
stage: children
rules:
- changes:
- services/web/**/*
variables:
PARENT_PIPELINE_ID: "$CI_PIPELINE_ID"
MONOREPO_AREA: "web"
trigger:
include:
- local: ci/children/web.yml
strategy: mirror
Make an API-only change. Prediction: the parent contains the API
trigger but omits the web trigger; the created child has the same
SHA as the parent and its jobs report parent_pipeline.
Preserve the graph and IDs before changing the project again.
6. Evidence after the static run
| Observation | Expected proof |
|---|---|
| Parent source/ref/SHA | Record from pipeline details/job-safe predefined variables |
| Parent trigger jobs | API present; web omitted for API-only change |
| Child ID | Visible from downstream card/details; API can also query with appropriate access |
| Child source | parent_pipeline |
| Child SHA | Exactly equals parent CI_COMMIT_SHA |
| Trigger status | Mirrors child because strategy: mirror is used |
| Artifact | evidence/api.txt belongs to child job and contains only synthetic metadata |
7. Add a bounded generator instead of templating raw user input
The generator accepts exactly one area from a hard-coded allowlist and emits a fixed job shape. It does not accept raw YAML, shell fragments, image names, includes, or arbitrary job counts.
from pathlib import Path
import os, sys
allowed = {"api", "web"}
area = os.environ.get("AREA", "")
if area not in allowed:
raise SystemExit(f"AREA must be one of {sorted(allowed)}")
job = f"""{area}-generated-verify:
image: alpine:3.20.3
script:
- test -f services/{area}/app.txt
- mkdir -p evidence
- printf 'area={area}\\nsha=%s\\nchild_pipeline=%s\\nsource=%s\\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" "$CI_PIPELINE_SOURCE" > evidence/{area}-generated.txt
artifacts:
expire_in: 1 day
paths:
- evidence/{area}-generated.txt
"""
Path("generated").mkdir(exist_ok=True)
Path("generated/child.yml").write_text(job, encoding="utf-8")
8. Generate, inspect, then trigger the dynamic child
stages: [generate, children]
generate-api-child:
stage: generate
image: python:3.13.7-alpine3.22
variables:
AREA: "api"
script:
- python ci/generate_child.py
- test "$(grep -c '^[a-z].*-generated-verify:' generated/child.yml)" -eq 1
- sed -n '1,120p' generated/child.yml
artifacts:
name: "generated-child-$CI_PIPELINE_ID-$CI_JOB_ID"
expire_in: 1 day
paths:
- generated/child.yml
run-generated-api:
stage: children
trigger:
include:
- artifact: generated/child.yml
job: generate-api-child
strategy: mirror
The printed YAML contains no secret. In a production generator, prefer a dedicated validation step and preserve a digest of the generated artifact. The trigger artifact path uses GitLab server path syntax; do not assume the runner operating system controls this path.
9. Dynamic configuration constraints to observe
- The generated artifact must fit instance artifact/configuration limits.
-
The dynamic child’s
includesection cannot interpolate CI/CD variables. - The generator job runs on a runner; the trigger job does not run a script.
- Generated configuration is compiled after the generator succeeds, so syntax or semantic errors are child-creation failures, not generator-runtime success.
- Keep generation deterministic for the same validated metadata and source SHA.
10. Challenge: which layer should solve each requirement?
| Requirement | Correct layer | Why |
|---|---|---|
| Run API checks only when API files changed | Parent trigger job rules:changes | Controls whether the child is created |
| Select a fixed safe child shape from known metadata | Validated generator | Creates bounded configuration from allowlisted inputs |
| Make parent wait for child outcome | trigger:strategy: mirror | Controls status coupling |
| Pass a non-secret area name | Trigger input/explicit variable | Makes the child contract visible |
| Give child a deployment credential | Secret/identity mechanism in authorized child job | Do not forward broad parent secrets |
| Fetch parent artifact in child | Optional cross-pipeline artifact mechanism | Separate dataflow from orchestration; paid feature may apply |
11. Cleanup
Export only the evidence you need, then delete the disposable branch/project resources you created. Do not delete unrelated pipelines or artifacts by ambiguous “latest” selection. If the project is shared, remove only the lab files/branch after confirming the exact ref.
Knowledge check
Why does the trigger job not need a runner?
GitLab itself creates the downstream pipeline from trigger configuration. Only ordinary script jobs need runner execution.
What should happen on an API-only commit in the static example?
Only api-child should be included by rules:changes; web-child should be omitted.
Why is AREA validated against an allowlist before generating YAML?
The generator is emitting executable CI configuration. All generator inputs must be constrained so untrusted text cannot become jobs, scripts, includes, or unbounded fan-out.
What should you compare between the parent and dynamic child?
Project, ref, and especially exact CI_COMMIT_SHA should match; also record the distinct pipeline IDs and source=parent_pipeline in the child.
Does the mandatory lab require needs:pipeline:job?
No. Each child uses repository source and its own outputs. Cross-boundary artifact download is optional and tier-sensitive.
Version and compatibility note
GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.
Official references and version notes
Documentation verification date: 2026-09-11.
Parent/child pipeline syntax, downstream status behavior, report
visibility, hierarchy limits, inputs, and variable forwarding are
version-sensitive. Re-check the deployed GitLab version before
relying on newer syntax such as strategy: mirror.
- Downstream pipelines — parent-child semantics, dynamic child pipelines, nesting, pipeline source, reports, variables, inputs, and status behavior.
-
CI/CD YAML syntax reference
—
trigger,trigger:include,trigger:strategy,trigger:forward, andneeds:pipeline:job. - Use CI/CD configuration from other files — include rules and dynamic-child configuration limitations.
- Troubleshooting downstream pipelines — empty child pipelines, permission/configuration failures, and variable forwarding issues.
- CI/CD limits — downstream hierarchy sizing and resource-protection rationale.
-
Pipelines API
— listing pipelines and querying child pipelines with
source=parent_pipeline.
CI_PIPELINE_SOURCE=parent_pipeline. Nested child
pipelines are limited to two child levels; the default pipeline
hierarchy limit is 1000 downstream pipelines. One child trigger can
combine up to three child configuration files. Dynamic child
configuration can come from a generated artifact; the artifact path
is interpreted by the GitLab server, and CI/CD variables cannot be
used in an include section inside the dynamic child
configuration. strategy: mirror was introduced in
GitLab 18.2 and is the recommended status-coupling strategy; without
a strategy the trigger job succeeds once the child is created.
YAML-defined trigger variables forward by default, while pipeline
variables do not unless
trigger:forward:pipeline_variables: true is set.
needs:pipeline:job for cross-parent/child artifact
download is currently Premium/Ultimate, so mandatory labs do not
depend on it.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.