QA How-To
Split Test Suites with GitLab Child Pipelines
Build gitlab ci child pipelines test suites using dynamic YAML, parallel jobs, JUnit reports, artifacts, rules, and reliable failure propagation in 2026.
18 min read | 2,740 words
TL;DR
Create one generated child configuration per test domain, trigger each configuration from the parent with strategy: mirror, and publish JUnit plus debugging artifacts in every child. This keeps suites independently maintainable while making child failures visible in the parent pipeline.
Key Takeaways
- Use a parent pipeline to generate and trigger suite-specific child pipeline YAML.
- Choose strategy: mirror so child failures determine the parent trigger job result.
- Use rules:changes to avoid running suites that an isolated change cannot affect.
- Publish JUnit reports and durable debugging artifacts from every suite job.
- Limit parallelism with runner capacity and resource groups, not job count alone.
- Validate generated YAML before expanding the design to many repositories.
GitLab CI child pipelines test suites work best when a small parent pipeline decides what to run and separate child pipelines own how each suite runs. The pattern reduces one oversized .gitlab-ci.yml, lets UI, API, and contract tests evolve independently, and still gives the merge request one trustworthy result.
This tutorial builds that pattern from an empty repository. It is one implementation inside the complete test automation CI/CD guide, which explains the larger pipeline architecture, security, evidence, and release decisions around it.
You will generate child YAML at runtime, trigger two suites, split browser tests across parallel jobs, publish JUnit evidence, and propagate failures back to the parent. The example uses Node.js and Playwright because the commands are easy to reproduce, but the GitLab design works equally well for Java, Python, mobile, and API suites.
TL;DR
| Need | GitLab mechanism | Choice in this tutorial |
|---|---|---|
| Separate suite configuration | Parent-child pipeline | One generated child per domain |
| Conditional suite selection | rules:changes |
Run UI or API based on changed paths |
| Correct parent status | Trigger strategy | strategy: mirror |
| Faster browser coverage | Parallel matrix | Chromium and Firefox jobs |
| Test evidence | artifacts:reports:junit |
JUnit XML from every job |
| Debugging evidence | Artifact paths | HTML reports and Playwright traces |
The essential flow is generate configuration -> upload artifact -> trigger child -> run suite -> publish evidence. Keep the generator deterministic, validate its output, and let each child own only one test domain.
What You Will Build
By the end, your repository will have:
- A parent
.gitlab-ci.ymlwith generation and trigger stages. - A Node.js generator that writes valid child pipeline YAML.
- Separate UI and API child pipelines selected with path rules.
- A browser matrix that runs Playwright on Chromium and Firefox.
- JUnit XML, HTML reports, traces, and screenshots retained as job artifacts.
- Child failure status mirrored to the parent pipeline.
The example intentionally uses two children. Once it works, add accessibility, contract, performance, or mobile children with the same interface.
Prerequisites
Use a GitLab project with CI/CD enabled and at least one runner capable of running Docker jobs. A GitLab.com-hosted runner is sufficient. For local checks, install Git, Node.js 22 LTS or a newer active LTS, and npm 10 or newer.
Create the project locally:
mkdir gitlab-child-test-suites
cd gitlab-child-test-suites
npm init -y
npm install --save-dev @playwright/test@1.61.0 yaml@2.8.1
npx playwright install chromium firefox
mkdir -p scripts tests/ui tests/api
Do not commit node_modules. The CI examples use the Playwright project image, which already contains compatible browser system dependencies. Commit package-lock.json so npm ci resolves the same dependency graph on every runner.
You need permission to push a branch and run pipelines. No GitLab API token is required because the parent includes generated configuration from a job artifact in the same pipeline.
Step 1: Create Runnable UI and API Tests
Start with tests that prove each child can run independently. Add tests/ui/home.spec.ts:
import { expect, test } from '@playwright/test';
test('example domain has the expected heading', async ({ page }) => {
await page.goto('https://example.com/');
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'Example Domain'
);
});
Add tests/api/example.spec.ts:
import { expect, test } from '@playwright/test';
test('example domain responds with HTML', async ({ request }) => {
const response = await request.get('https://example.com/');
expect(response.ok()).toBeTruthy();
expect(response.headers()['content-type']).toContain('text/html');
});
Add playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
const browserName = process.env.BROWSER ?? 'chromium';
export default defineConfig({
testDir: './tests',
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: [
['line'],
['junit', { outputFile: 'test-results/junit.xml' }],
['html', { outputFolder: 'playwright-report', open: 'never' }]
],
use: {
trace: 'retain-on-failure',
screenshot: 'only-on-failure'
},
projects: [
{
name: browserName,
use: browserName === 'firefox'
? { ...devices['Desktop Firefox'] }
: { ...devices['Desktop Chrome'] }
}
]
});
The environment variable selects one browser per matrix job. API tests accept the same project even though they use Playwright's request fixture and do not launch a page.
Verify: Run npx playwright test tests/ui --project=chromium and npx playwright test tests/api --project=chromium. Both commands should report one passed test. Confirm test-results/junit.xml and playwright-report/index.html exist after each run.
Step 2: Generate Child Pipeline Configurations
A generated configuration is useful when suite lists, images, or commands come from repository data. Add scripts/generate-child-pipelines.mjs:
import { writeFile } from 'node:fs/promises';
import YAML from 'yaml';
const image = 'mcr.microsoft.com/playwright:v1.61.0-noble';
const common = {
image,
before_script: ['npm ci'],
artifacts: {
when: 'always',
expire_in: '14 days',
reports: { junit: 'test-results/junit.xml' },
paths: ['playwright-report/', 'test-results/']
}
};
const ui = {
stages: ['test'],
ui_tests: {
...common,
stage: 'test',
parallel: {
matrix: [{ BROWSER: ['chromium', 'firefox'] }]
},
script: ['npx playwright test tests/ui --project=$BROWSER']
}
};
const api = {
stages: ['test'],
api_tests: {
...common,
stage: 'test',
variables: { BROWSER: 'chromium' },
script: ['npx playwright test tests/api --project=chromium']
}
};
await Promise.all([
writeFile('ui-child.yml', YAML.stringify(ui)),
writeFile('api-child.yml', YAML.stringify(api))
]);
This script treats the generated files as build outputs. The child jobs still check out the same commit as the parent, so they can run repository tests without passing source code as artifacts. Pin the container tag and update it together with @playwright/test; mismatched Playwright package and image versions can fail when browser executables are missing.
A static included YAML file is simpler when configuration never varies. Generation becomes valuable when you derive jobs from a manifest, changed modules, tenant list, or supported runtime matrix. Avoid producing YAML by concatenating unescaped strings. A YAML library safely serializes keys and values.
Verify: Run node scripts/generate-child-pipelines.mjs, then sed -n '1,160p' ui-child.yml and sed -n '1,120p' api-child.yml. You should see a stages array, job definitions, scripts, and JUnit artifact configuration. Parse both files with node -e "import('yaml').then(async y => { for (const f of ['ui-child.yml','api-child.yml']) y.parse(await require('node:fs/promises').readFile(f,'utf8')); console.log('valid YAML') })".
Step 3: Build GitLab CI Child Pipelines Test Suites in the Parent
Create .gitlab-ci.yml:
stages:
- generate
- test
generate-child-configs:
stage: generate
image: node:22-bookworm-slim
before_script:
- npm ci
script:
- node scripts/generate-child-pipelines.mjs
artifacts:
paths:
- ui-child.yml
- api-child.yml
expire_in: 1 day
ui-child-pipeline:
stage: test
needs:
- job: generate-child-configs
artifacts: true
trigger:
include:
- artifact: ui-child.yml
job: generate-child-configs
strategy: mirror
api-child-pipeline:
stage: test
needs:
- job: generate-child-configs
artifacts: true
trigger:
include:
- artifact: api-child.yml
job: generate-child-configs
strategy: mirror
GitLab first runs the generator and stores both files. Each trigger job then reads its configuration from that artifact. strategy: mirror makes the trigger job reflect the downstream child status. Without a dependent strategy, a trigger can succeed after merely creating the child, leaving the parent green while tests later fail.
The artifact include is evaluated by GitLab, not by the runner shell. Use repository-relative artifact paths with forward slashes. Keep generation and triggers in separate stages so the files exist before GitLab tries to create either child.
Verify: Commit the files and push a branch. In Build > Pipelines, open the pipeline. The graph should show one generator followed by two downstream pipeline cards. Open each card. The UI child should contain two jobs, while the API child should contain one.
Step 4: Select Suites with Rules
Running every domain for every documentation edit wastes runner capacity. Add matching rules to the generator and trigger jobs so the complete dependency chain is either present or absent. Replace the relevant parent jobs with this pattern:
generate-child-configs:
stage: generate
image: node:22-bookworm-slim
before_script:
- npm ci
script:
- node scripts/generate-child-pipelines.mjs
artifacts:
paths: [ui-child.yml, api-child.yml]
expire_in: 1 day
rules:
- changes:
- tests/**/*
- scripts/generate-child-pipelines.mjs
- playwright.config.ts
- package.json
- package-lock.json
- .gitlab-ci.yml
ui-child-pipeline:
stage: test
needs:
- job: generate-child-configs
artifacts: true
trigger:
include:
- artifact: ui-child.yml
job: generate-child-configs
strategy: mirror
rules:
- changes:
- tests/ui/**/*
- scripts/generate-child-pipelines.mjs
- playwright.config.ts
- package.json
- package-lock.json
- .gitlab-ci.yml
api-child-pipeline:
stage: test
needs:
- job: generate-child-configs
artifacts: true
trigger:
include:
- artifact: api-child.yml
job: generate-child-configs
strategy: mirror
rules:
- changes:
- tests/api/**/*
- scripts/generate-child-pipelines.mjs
- playwright.config.ts
- package.json
- package-lock.json
- .gitlab-ci.yml
The generator uses the union of relevant paths because either trigger needs its artifacts. Each trigger uses narrower suite paths plus shared configuration. If application code lives under src/, add the actual ownership boundaries. For a monolith, both suites may legitimately watch all application files.
Be careful with needs. If the generator rules and trigger rules disagree, GitLab can reject the pipeline because a trigger needs a job that was not added. The broad generator rule prevents that mismatch.
Verify: Push a change only under tests/ui/. The pipeline should contain the generator and UI trigger but no API trigger. Then edit playwright.config.ts; both triggers should appear because the shared configuration affects both suites.
Step 5: Control Parallel Test Suite Capacity
The UI matrix creates one job per browser. Add more dimensions only when runner capacity supports them. For example, change the UI matrix in the generator to test two browser and shard combinations:
parallel: {
matrix: [
{ BROWSER: ['chromium', 'firefox'], SHARD: ['1/2', '2/2'] }
]
},
script: [
'npx playwright test tests/ui --project=$BROWSER --shard=$SHARD'
]
This produces four jobs. Playwright shards files across two groups for each browser. It is real distribution, unlike duplicating the same command across parallel jobs. Use a fixed shard count so job names and historical comparisons remain understandable. Balance changes when the test inventory changes, so periodically inspect durations.
| Technique | Best use | Main caution |
|---|---|---|
| Browser matrix | Cross-browser confidence | Multiplies total work |
| Playwright shards | Reduce suite wall time | Needs enough independent test files |
GitLab parallel |
Identical job replicas | Command must consume the node index |
| Separate children | Domain ownership and isolation | More configuration surfaces |
| Resource group | Serialize shared environments | Can create a queue |
If tests mutate one shared tenant, add resource_group: shared-qa-environment to the generated job or provision an environment per pipeline. Parallel jobs against shared mutable state create nondeterministic failures. For a deeper treatment of environment authentication, use the GitHub Actions OIDC test environment tutorial; the identity principles also apply when designing GitLab runner access.
Verify: Regenerate the YAML locally and confirm the matrix contains BROWSER and SHARD. Push it. The UI child graph should show four jobs with distinct matrix values. Each test should appear once per browser across the two shard reports, not twice in each shard.
Step 6: Preserve Reports and Debugging Evidence
JUnit reports power GitLab test summaries, but XML alone rarely explains a browser failure. The generator already uploads playwright-report/ and test-results/ with when: always. Enhance the job with an artifact name and longer retention for protected branches if your policy needs it:
artifacts: {
name: '$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA',
when: 'always',
expire_in: '14 days',
reports: {
junit: 'test-results/junit.xml'
},
paths: [
'playwright-report/',
'test-results/'
]
}
Playwright stores retained traces and failure screenshots below test-results/. The HTML report links the run narrative. GitLab parses the JUnit file separately for test reports. Always use unique output paths inside a job, especially when one job runs several commands. Otherwise the last command can overwrite earlier XML.
Parent-child boundaries affect aggregation. The child job remains the authoritative place to open artifacts and logs. GitLab can surface downstream test reports in merge request and pipeline views when the trigger uses a status-dependent strategy supported by the current GitLab configuration. Do not assume that arbitrary artifact files automatically move into the parent job. If a release process needs a single evidence bundle, add an explicit collection stage using the GitLab API or object storage.
Read how to publish test evidence as CI artifacts before setting retention. It helps distinguish developer debugging files from audit evidence that requires controlled, longer-lived storage.
Verify: Intentionally change the expected heading to a wrong value and push. The child job should fail, the trigger job should fail, and the parent pipeline should fail. Open the child job artifacts and confirm that JUnit XML, an HTML report, a screenshot, and a trace are available. Revert the assertion after verification.
Step 7: Add Workflow Guards and Validate the Design
Prevent duplicate branch and merge request pipelines with top-level workflow rules in .gitlab-ci.yml:
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_COMMIT_TAG'
- when: never
These rules allow merge request pipelines, default branch pipelines, and tag pipelines. Adjust the policy if you intentionally test every branch push. Child pipelines are created with a parent pipeline source, but their generated configuration does not need this parent workflow block. The workflow controls creation of the top-level pipeline.
Use GitLab's CI Lint in Build > Pipeline editor > Validate for the parent. Generated artifact includes cannot always be fully simulated before the generation job runs, so also parse generated YAML locally and test on a short-lived branch. Keep a small fixture test for the generator if it starts reading manifests or applying conditional logic.
Finally, protect failure semantics. Do not add allow_failure: true to required suite triggers. Quarantine only known unstable tests, attach an owner and expiry, and keep trustworthy tests blocking. The flaky test quarantine pipeline guide shows how to prevent instability from turning the entire child into a permanent warning.
Verify: Push to a branch without a merge request and confirm the workflow matches your intended policy. Open a merge request and confirm exactly one parent pipeline appears. Break one test once more and verify the merge request cannot pass its required pipeline check.
GitLab CI Child Pipelines Test Suites: Design Decisions
Use child pipelines when suites have distinct owners, dependencies, schedules, or scaling needs. Keep jobs in one pipeline when they share nearly all setup and only differ by one matrix value. A child is an architectural boundary, not merely a visual folder.
Prefer generated children when repository data determines the job graph. Prefer static child files when the graph is stable and reviewers benefit from seeing all jobs directly in version control. You can combine both: a static template can hold common defaults while a generated file selects suite-specific jobs. Pin remote includes to immutable versions if you introduce centrally managed templates.
Define the contract between parent and child explicitly:
- The parent selects and triggers a suite.
- The child runs tests from the same commit.
- Each child publishes JUnit and debugging artifacts.
- Required child failures mirror to the parent.
- Secrets are scoped to the smallest set of protected jobs and environments.
This contract lets teams change frameworks inside one child without rewriting orchestration. It also makes reviews easier because a change to selection logic is visibly separate from a change to test execution.
Troubleshooting
Problem: GitLab says the included artifact does not exist -> Confirm the generator job uploaded the exact path, the trigger names the correct generating job, and generation completes before the trigger stage. Artifact paths are case-sensitive.
Problem: The child fails with an unknown key or invalid YAML -> Parse the output locally with the same YAML library, inspect the uploaded artifact, and use CI Lint where possible. Never concatenate user-controlled strings into YAML.
Problem: A child test fails but the parent remains green -> Use strategy: mirror on required trigger jobs and remove allow_failure: true. Also check that the failing job itself does not suppress the test command exit code with || true.
Problem: Playwright cannot find a browser executable -> Align the Playwright npm package with the pinned Playwright container image. If you use a generic Node image, run npx playwright install --with-deps, accepting the extra installation time.
Problem: The pipeline cannot be created because a needed job is missing -> Make the generator's rules cover the union of every trigger's rules, or mark a truly optional need as optional. Test shared-file and suite-only changes separately.
Problem: Parallel jobs fail only against the shared environment -> Give each pipeline isolated test data or an ephemeral environment. If isolation is unavailable, serialize the mutation-heavy jobs with a resource group and keep read-only tests parallel.
Interview Questions and Answers
Q: Why use a child pipeline instead of one large GitLab pipeline?
A child pipeline creates a clear configuration and ownership boundary. It keeps domain-specific jobs, images, and rules separate while the parent retains orchestration. Use it when that boundary adds maintainability, not merely to reduce the visible job count.
Q: What does strategy: mirror do on a trigger job?
It makes the trigger job's status mirror the downstream pipeline status. That allows required child test failures to fail the parent pipeline and block a merge.
Q: How does a dynamic child pipeline receive its configuration?
A parent job generates YAML and uploads it as an artifact. A later trigger job uses trigger:include with the artifact path and generating job name, after which GitLab creates the child from that configuration.
Q: How do you split a Playwright suite correctly?
Use Playwright's --shard=current/total argument so each job receives a distinct portion of the test files. Combine shards with a browser matrix only when cross-browser coverage is required and runners can handle the resulting job count.
Q: Where should JUnit and traces be published?
Publish them from each child job with artifacts:reports:junit and artifact paths using when: always. The child job is the source of truth for its logs and debugging evidence.
Q: How do you avoid unnecessary child pipelines?
Apply rules:changes to triggers based on suite ownership boundaries. Ensure the generator rules are broad enough to exist whenever any trigger needs its generated artifact.
Common Mistakes
- Creating children for tiny job differences that a matrix expresses more clearly.
- Letting trigger jobs succeed immediately without mirroring required child status.
- Generating YAML through string concatenation and discovering quoting errors only in CI.
- Running identical tests in every parallel node instead of passing a shard index.
- Using different path rules for a trigger and its mandatory generator dependency.
- Uploading JUnit only on success, which removes evidence from failed runs.
- Allowing all browser jobs to mutate the same user, tenant, or database records.
- Treating short-lived CI artifacts as a complete compliance archive.
Where To Go Next
Return to the test automation CI/CD complete guide to place child pipelines inside a complete delivery workflow. Then harden the implementation with these focused tutorials:
- Use OIDC for short-lived test environment access and adapt the identity pattern to GitLab CI job tokens or cloud workload identity.
- Build a controlled flaky test quarantine workflow instead of making an entire child nonblocking.
- Define retention and ownership with the CI test evidence artifacts guide.
After the two-child example is stable, move suite definitions into a small manifest and let the generator iterate over it. Add schema validation and a generator test before allowing teams to contribute entries. That gives you scalable configuration without hiding pipeline behavior from reviewers.
Conclusion
GitLab CI child pipelines test suites provide useful isolation when the parent owns selection and the children own execution. Generate deterministic YAML, trigger it from artifacts, mirror required status, split only with real sharding, and publish evidence even when tests fail.
Start with the UI and API children in this tutorial. Prove failure propagation and artifact access before adding more domains. A small, verified contract scales better than a large dynamic graph whose status nobody trusts.
Interview Questions and Answers
When would you choose a GitLab child pipeline for test automation?
I choose a child when a test domain has distinct ownership, dependencies, selection rules, or scaling behavior. The parent decides when the domain runs, while the child owns its jobs and evidence. For small command variations, I prefer a matrix because it is simpler.
How do dynamic child pipelines work in GitLab CI?
A parent job generates YAML and saves it as an artifact. A later trigger job includes that artifact by path and generating job name. GitLab parses the artifact and creates the downstream child pipeline for the same commit.
How do you propagate test failures across a parent-child pipeline boundary?
I configure the trigger with `strategy: mirror` and leave required jobs blocking. I also test the behavior with an intentional assertion failure. The child, trigger job, parent pipeline, and merge check should all show failure.
What is the difference between splitting by child pipeline and sharding?
A child pipeline separates configuration and ownership domains. Sharding divides one suite's test inventory across workers to reduce wall time. A design can use both, but each solves a different problem.
How would you prevent path rules from creating an invalid pipeline?
I ensure a generator dependency is present whenever any trigger that needs it is present. Its change rules normally cover the union of trigger paths. I verify suite-only changes, shared configuration changes, and unrelated changes in separate branch pipelines.
What evidence should every automated test child publish?
At minimum, it should publish a machine-readable test report such as JUnit and enough failure evidence to diagnose the run. For browser tests that usually includes logs, traces, screenshots, and an HTML report. I upload evidence with `when: always` and choose retention based on its purpose.
Frequently Asked Questions
Can GitLab child pipelines run in parallel?
Yes. Trigger jobs in the same parent stage can create separate child pipelines concurrently, subject to runner and GitLab concurrency limits. Jobs inside each child can also use matrices or sharding, so measure total capacity before multiplying both levels.
How do I make a child pipeline failure fail the parent?
Set `strategy: mirror` on the parent trigger job and keep the trigger and required child jobs blocking. Verify the test command returns a nonzero exit status and that `allow_failure` is not enabled.
Should generated child pipeline YAML be committed?
Usually no. Commit the generator and its input manifest, then treat generated YAML as a short-lived artifact. You can generate it locally for validation without checking the output into source control.
Can a child pipeline access files from the parent repository?
Yes. A parent-child pipeline runs for the same project and commit, so child jobs check out the repository normally. Job artifacts are separate and must be downloaded or explicitly passed when a child needs generated files.
How should I split a large test suite in GitLab CI?
First separate stable ownership domains such as UI, API, and contracts into children. Within a large domain, use framework-native sharding or timing-aware distribution so each test runs once per intended environment.
Do JUnit reports automatically move from a child to the parent job?
Do not treat arbitrary child artifacts as parent job artifacts. Publish JUnit in each child and use GitLab's downstream report presentation where supported. Add an explicit collection process when another system requires one consolidated evidence package.
Related Guides
- Selenium Test Web Components Slots Tutorial: Test Web Component Slots with Selenium
- AI test data generation with Faker and LLMs (2026)
- Automating Jira to test case with n8n (2026)
- Building a test case generator with LangChain (2026)
- Connecting Claude to your test suite with MCP (2026)
- Generating test cases from a PRD with AI (2026)