GitLab’s parent-child pipelines are actually a way to hide complexity, not just split it.
Let’s watch a typical scenario. Imagine a monolithic application with a frontend, a backend, and some shared libraries. Building and testing all of this sequentially in one giant .gitlab-ci.yml file is a nightmare. Dependencies get tangled, test suites take hours, and a small frontend change triggers a full backend rebuild.
Here’s a simplified gitlab-ci.yml for this monolith:
stages:
- build
- test
- deploy
variables:
DOCKER_IMAGE_TAG: $CI_COMMIT_SHORT_SHA
build_backend:
stage: build
script:
- echo "Building backend..."
- docker build -t myapp/backend:$DOCKER_IMAGE_TAG ./backend
- docker push myapp/backend:$DOCKER_IMAGE_TAG
build_frontend:
stage: build
script:
- echo "Building frontend..."
- docker build -t myapp/frontend:$DOCKER_IMAGE_TAG ./frontend
- docker push myapp/frontend:$DOCKER_IMAGE_TAG
test_backend:
stage: test
needs: ["build_backend"]
script:
- echo "Testing backend..."
- docker run myapp/backend:$DOCKER_IMAGE_TAG npm test
test_frontend:
stage: test
needs: ["build_frontend"]
script:
- echo "Testing frontend..."
- docker run myapp/frontend:$DOCKER_IMAGE_TAG npm test
deploy_production:
stage: deploy
needs: ["test_backend", "test_frontend"]
script:
- echo "Deploying to production..."
- # deployment logic here
This works, but it’s brittle. A change in build_backend might require updating test_backend’s script, and the whole pipeline grows with every new microservice or component.
Now, let’s split this using parent-child pipelines. The "parent" pipeline will orchestrate the "children."
Parent Pipeline (.gitlab-ci.yml):
stages:
- build
- test
- deploy
variables:
DOCKER_IMAGE_TAG: $CI_COMMIT_SHORT_SHA
build_all_components:
stage: build
trigger:
project: my-group/my-project # Or use 'strategy: depend' for same project
branch: main
file: .gitlab-ci/build-pipeline.yml # Path to child pipeline config
test_all_components:
stage: test
needs: ["build_all_components"]
trigger:
project: my-group/my-project
branch: main
file: .gitlab-ci/test-pipeline.yml
deploy_production:
stage: deploy
needs: ["test_all_components"]
script:
- echo "Deploying to production..."
- # deployment logic here
Child Pipeline for Building (.gitlab-ci/build-pipeline.yml):
stages:
- build
variables:
DOCKER_IMAGE_TAG: $CI_COMMIT_SHORT_SHA
build_backend_job:
stage: build
script:
- echo "Building backend..."
- docker build -t myapp/backend:$DOCKER_IMAGE_TAG ./backend
- docker push myapp/backend:$DOCKER_IMAGE_TAG
build_frontend_job:
stage: build
script:
- echo "Building frontend..."
- docker build -t myapp/frontend:$DOCKER_IMAGE_TAG ./frontend
- docker push myapp/frontend:$DOCKER_IMAGE_TAG
Child Pipeline for Testing (.gitlab-ci/test-pipeline.yml):
stages:
- test
variables:
DOCKER_IMAGE_TAG: $CI_COMMIT_SHORT_SHA
test_backend_job:
stage: test
needs: [] # Dependencies are handled by the parent pipeline's trigger
script:
- echo "Testing backend..."
- docker run myapp/backend:$DOCKER_IMAGE_TAG npm test
test_frontend_job:
stage: test
needs: []
script:
- echo "Testing frontend..."
- docker run myapp/frontend:$DOCKER_IMAGE_TAG npm test
In this setup, the parent pipeline’s trigger keyword is key. It tells GitLab to start a new pipeline based on the specified file and branch. The needs keyword on the parent’s trigger jobs ensures that the child pipelines run only after their predecessors in the parent’s stage complete.
The real power comes from strategy: depend. If your child pipelines are in the same project as the parent, you can omit project and branch from the trigger and use:
trigger:
file: .gitlab-ci/build-pipeline.yml
strategy: depend
This makes the parent pipeline’s jobs wait for the triggered child pipeline to complete successfully before proceeding. If any job in the child pipeline fails, the parent pipeline job that triggered it will also be marked as failed.
The parent-child relationship isn’t just about code organization; it’s a fundamental shift in how pipeline dependencies are managed. Instead of explicit needs between jobs in a single file, you’re now defining dependencies between pipelines. The parent pipeline acts as a scheduler, and each trigger job represents a dependency on a whole separate pipeline’s success.
The one thing most people don’t realize is that the needs relationship on the trigger job in the parent is not about the jobs within the child pipeline. It’s about ensuring the entire triggered child pipeline finishes before the parent moves to its next stage. If the child pipeline has multiple jobs, they all run concurrently (or as GitLab’s runner capacity allows), and the parent only cares about the overall success of that triggered pipeline.
This pattern is incredibly useful for monorepos, complex applications, or when you want to enforce distinct build, test, and deploy phases that can be managed and versioned independently.
The next hurdle you’ll face is managing complex variable passing and artifact sharing between these independent child pipelines.