Browse the Knowledge Hub211 resources
GitLab CI/CD review: what it is, tutorial, pros and cons
GitLab CI/CD is the pipeline engine built into GitLab. Jobs are defined in a .gitlab-ci.yml file in the repository and run by GitLab Runner, on GitLab-hosted machines or on your own. For QA teams its distinguishing features are JUnit test reports shown in merge requests, built-in parallel and matrix jobs, and retries scoped to the type of failure.
QAble has evaluated this tool and works alongside it. The factual sections are complete; our view is bounded to what we have actually seen rather than a long-term verdict.
- last reviewed
- 13 October 2026
- version covered
- GitLab 19.4
- licence
- Freemium
At a glance
GitLab CI/CD at a glance
- Vendor
- GitLab
- Category
- CI/CD
- Licence
- Freemium (MIT for the core and GitLab Runner; Enterprise Edition features under a proprietary licence)
- Platforms
- Cloud, Self-hosted
- Languages
- YAML
- Integrates with
- Docker, Kubernetes, Terraform, SonarQube
- First released
- 2015
- Version reviewed
- GitLab 19.4
Verdict
Our verdict on GitLab CI/CD
Strengths
- JUnit results appear in the merge request, compared with the target branch, with no plugin or marketplace action
- Failed tests show how often they failed on the default branch in the last 14 days, a built-in flakiness hint
- parallel: N splits a job into up to 200 copies with CI_NODE_INDEX and CI_NODE_TOTAL set
- retry can be limited to runner or infrastructure failures, so real test failures are not retried
- Self-managed GitLab and self-hosted runners keep pipelines inside a private network
- Pipelines, registry, issues and security scanning share one product and one set of permissions
Weaknesses
- The Free tier on GitLab.com includes 400 compute minutes a month, little for browser suites
- retry allows at most two retries, and only per job, not per test
- Jobs without an image on hosted runners get ruby:3.1, a common first-run surprise
- Test reports do not change job status; the script must still exit non-zero on failures
- macOS hosted runners are in beta and limited to Premium, Ultimate and open source programmes
Our take
GitLab CI/CD fits best where a team wants source, pipelines, test results and issue tracking in one product with one permission model, and where self-managed hosting is a requirement rather than a preference. Its test reporting is a genuine step ahead of GitHub Actions: JUnit results appear in the merge request, compared with the target branch, with a count of recent failures on the default branch that acts as a cheap flakiness signal. QAble has evaluated it rather than maintained a long-running suite on it, so treat this as a bounded take. Watch three things: the 400 compute minutes on the Free tier disappear quickly with browser suites, retry is capped at two, and the default hosted image is Ruby, so every test job should name its own image.
We have evaluated this tool rather than maintained a long-running suite on it, so this is a bounded view rather than a long-term verdict. The rest of this page is factual.
How it works
What is GitLab CI/CD and how does it work?
Pipelines defined in .gitlab-ci.yml
A pipeline is a set of jobs declared in .gitlab-ci.yml at the root of the repository. Jobs belong to stages that run in order, and jobs in the same stage run in parallel. The needs keyword lets a job start as soon as the jobs it depends on finish, ignoring stage order, which shortens pipelines where a fast API suite should not wait for a slow build of something unrelated. rules decide whether a job is added to a pipeline at all, based on branch, changed files or variables. include and CI/CD components pull shared configuration from other files and projects, so a QA team can publish one test job template that many projects reuse.
Runners and executors
Jobs are executed by GitLab Runner, an MIT-licensed agent that polls GitLab for work. Each runner uses an executor, most often Docker or Kubernetes, which starts the image named in the job and runs its script inside it. Runners are scoped to the instance, a group or a project. On GitLab.com, instance runners hosted by GitLab are available on Linux, with Windows and macOS in beta, and a job with no image falls back to ruby:3.1. Your own runners are registered with a runner authentication token prefixed glrt-; the older registration tokens have been disabled by default since GitLab 17.0 and are scheduled for removal in 20.0.
Artifacts and reports
A job keeps files through artifacts:paths, uploaded when the job ends and downloadable from the job page. artifacts:reports is a separate channel that GitLab parses: reports:junit takes JUnit XML files and turns them into the Tests tab of the pipeline and the test summary in the merge request. By default artifacts upload only for successful jobs, so test jobs need artifacts:when: always. On GitLab.com, artifacts expire after 30 days unless expire_in says otherwise, although artifacts from the latest successful pipeline on each ref are kept by default. Reports must be files of up to 30 MB each and 100 MB per job in total.
Parallel jobs and matrices
parallel: N creates N copies of a job, named test 1/N to test N/N, each with CI_NODE_INDEX and CI_NODE_TOTAL set, and N can be from 1 to 200. A runner that understands sharding, such as Playwright with --shard, uses those variables directly; for others, a short script picks every Nth spec file. parallel:matrix instead creates one job per combination of variable values, such as browser and environment, up to 200 permutations. In both cases jobs beyond the available runner capacity wait as pending, so parallelism is bounded by runners, not by the YAML.
Strengths
What it does well
Test results inside the merge request
The test summary panel compares the source branch with a baseline pipeline on the target branch and groups results into newly failed tests, newly encountered errors, existing failures and resolved failures. Reviewers see what this change broke rather than every failure in the suite, and can copy failed test names to rerun them locally.
A built-in flakiness signal
For a test that failed in the default branch in the last 14 days, the report shows a message such as Failed 3 times in main in the last 14 days. It is not a full flaky test system, but it tells a reviewer at a glance whether a failure is probably theirs or a known unstable test, with no extra tooling.
Screenshots attached to failures
If a JUnit test case’s system-out contains an attachment tag pointing at a file saved as an artefact, the failure details in the merge request show that screenshot. UI suites that already save a screenshot on failure only need to write the tag and keep the file in artifacts:paths.
Retries scoped to the cause
retry takes a maximum of 0, 1 or 2 and an optional when list of failure types, such as runner_system_failure, api_failure or the newer runner_configuration_error, plus exit_codes. A suite can retry jobs that died because a runner vanished without retrying jobs whose assertions failed, which keeps flaky tests visible.
Cancelling superseded pipelines
Jobs marked interruptible: true can be cancelled automatically when a newer pipeline starts on the same branch. Long browser jobs on busy merge requests stop consuming compute minutes the moment they are out of date, which matters on a quota.
Results from child pipelines
Large test suites are often split into child pipelines, one per area or platform. The pipeline’s Tests tab includes results from child pipelines, so the parent still gives one place to read the outcome of the whole run.
Pricing
GitLab CI/CD pricing and licensing
GitLab is open core. The core, including CI/CD and GitLab Runner, is MIT licensed and can be self-managed at no licence cost; Premium and Ultimate add features under a proprietary licence. On GitLab.com, plans are per user and each includes a monthly quota of compute minutes on GitLab-hosted instance runners. Runners you register to your own projects or groups do not draw on that quota; you pay for their machines instead.
- Free: $0, up to 5 users per top-level group, 400 compute minutes a month and 10 GiB of storage on GitLab.com.
- Premium: list price $29 per user per month billed annually on 13 October 2026, with 10,000 compute minutes a month.
- Ultimate: custom pricing, with 50,000 compute minutes a month and security, compliance and portfolio features.
- Extra compute minutes are sold at a list price of $10 per 1,000, and minutes are counted as job duration multiplied by a cost factor: 1 for small Linux, 2 for medium, 3 for large, and 6 or 12 for the beta macOS runners.
- Self-hosted runners use no compute minutes but bring server, image and maintenance costs, which for device labs or private networks are unavoidable anyway.
Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.
Getting started
GitLab CI/CD tutorial: getting started
Setup, step by step
- Make sure the suite runs headless from one command and writes JUnit XML.
- Create .gitlab-ci.yml in the repository root with an explicit image for the test job.
- Add artifacts:reports:junit pointing at the XML files, with artifacts:when: always.
- Commit and push; open the pipeline and check the Tests tab after the job finishes.
- Open a merge request with a deliberately failing test and confirm the test summary shows it.
- If tests must reach a private network, register a project or group runner with an authentication token.
Install
# Hosted runners need no install: commit .gitlab-ci.yml and push.
# For your own runner, create it in the UI, then register it with its glrt- token:
sudo gitlab-runner register --non-interactive \
--url "https://gitlab.com/" --token "$RUNNER_TOKEN" \
--executor "docker" --docker-image alpine:latestA first example
# .gitlab-ci.yml
default:
# Without an image, hosted runners use ruby:3.1
image: maven:3.10.0-eclipse-temurin-21
interruptible: true # cancelled when a newer pipeline starts
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
cache:
key:
files: [pom.xml]
paths: [.m2/repository]
api-tests:
stage: test
script:
- mvn -B verify
artifacts:
when: always # upload reports even when tests fail
expire_in: 2 weeks
paths:
- target/screenshots/
reports:
junit:
- target/surefire-reports/TEST-*.xml
- target/failsafe-reports/TEST-*.xmlWhat trips people up first
Reports that only appear when everything passes. Artifacts upload on success by default, so the first failing run shows a red job and no test results, exactly when they are needed. Add artifacts:when: always. Then remember that the junit report never changes job status: the job fails only because the test command exits non-zero. A script that swallows the exit code, for example by piping the output through another command, produces green jobs with failures inside. The third early surprise is the image: on GitLab.com hosted runners a job with no image runs in ruby:3.1, which has neither Java nor Node.
In practice
Using GitLab CI/CD in practice
Organising test jobs in a pipeline
Order jobs by cost and use needs so fast feedback is not held up: unit tests and linting first, then API tests that need only the build, then UI suites that need a deployed review environment. Use rules to run the full regression on the default branch and on a schedule, and a smaller smoke set on merge requests, with rules:changes to skip suites unaffected by a change where that is safe. Put the common test job in a template or CI/CD component, with the image, cache, retry policy and report paths, and extend it per suite. Keep credentials in masked and protected CI/CD variables, scoped to environments, never in the YAML.
Sharding a browser suite with parallel
parallel creates numbered copies of a job, and Playwright’s shard option reads the index and total directly. Each copy uploads its own JUnit file under a unique name, and GitLab merges every copy’s results into one Tests tab. Use the official Playwright image whose version matches the package, so browsers and library agree. Keep traces and screenshots in artifacts:paths with a short expiry, since only failing runs need them, and size N to the runners you actually have: copies beyond capacity simply queue.
e2e:
stage: test
image: mcr.microsoft.com/playwright:v1.64.0-noble
parallel: 4 # creates e2e 1/4 to e2e 4/4
script:
- npm ci
# One JUnit file per copy, so no copy overwrites another
- PLAYWRIGHT_JUNIT_OUTPUT_NAME=results-$CI_NODE_INDEX.xml npx playwright test --reporter=junit --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
artifacts:
when: always
expire_in: 1 week
paths:
- test-results/ # traces and screenshots
reports:
junit: results-*.xmlRetrying infrastructure, not assertions
A bare retry: 2 retries every failure type, which means a genuinely failing test can pass on the second or third attempt and the merge request goes green. Restrict retries to failures that are not about the code: runner_system_failure, api_failure and, since GitLab 19.1, runner_configuration_error for problems such as an image that cannot be pulled. Handle test-level flakiness inside the test runner, which can record that a test needed a retry, and watch the 14-day failure count in the test report to find the worst offenders.
.test-defaults:
retry:
max: 2
when:
- runner_system_failure
- api_failure
- runner_configuration_error # added as a failure type in 19.1
timeout: 45m
ui-tests:
extends: .test-defaults
image: maven:3.10.0-eclipse-temurin-21
script:
# Surefire reruns failed tests and records them as flaky in the XML
- mvn -B -Dsurefire.rerunFailingTestsCount=2 verify
artifacts:
when: always
reports:
junit: target/failsafe-reports/TEST-*.xmlChoosing between hosted and your own runners
Hosted Linux runners suit most API and browser suites that test a publicly reachable environment, and larger machine sizes trade a higher cost factor for shorter jobs. Register your own runners when tests must reach systems inside a private network, need physical devices, or would exhaust the compute quota; on a Kubernetes executor each job gets a fresh pod, which keeps runs isolated. Tag runners by capability, such as android or gpu, and select them with tags in the job. Self-managed GitLab instances can disable instance runners entirely and run everything on their own infrastructure.
Keeping pipelines readable and cheap
Pipelines grow by copying jobs, so set conventions early: one template per kind of test job, every job with an explicit image and timeout, interruptible on anything that can safely be cancelled, and expire_in on artefacts so storage does not grow without limit. Name JUnit files uniquely per parallel copy so nothing is overwritten. Review compute usage on the usage quotas page monthly, since a nightly suite moved from small to large runners triples its minutes. Use the pipeline editor’s validation before pushing YAML changes, which catches most syntax mistakes without a wasted run.
Pitfalls
Common pitfalls
Test reports missing for failed jobs
Why it happens: artifacts are uploaded on success by default, and reports:junit is an artifact, so the runs with failures are the ones without a report.
The fix: Set artifacts:when: always on every test job, ideally in a shared template so new jobs inherit it.
Green jobs that contain failing tests
Why it happens: The JUnit report never affects job status. If the script hides the test command’s exit code, for example through a pipe or a wrapper that always exits 0, the job passes.
The fix: Let the test command’s exit code reach the shell, use set -o pipefail where output is piped, and treat the report as display only.
Retrying every failure
Why it happens: retry: 2 without when retries assertion failures as well as infrastructure problems, so flaky or genuinely broken tests turn green on a later attempt.
The fix: Use retry:when with runner_system_failure, api_failure and runner_configuration_error, and leave script failures to the test runner’s own retry, which records them.
Relying on retry types that changed in 19.1
Why it happens: GitLab 19.1 deprecated stuck_or_timeout_failure in favour of more specific values, and reclassified some failures, such as image pull errors, from script_failure to runner_configuration_error. Older retry rules can stop matching.
The fix: Review retry:when lists against the current YAML reference and add runner_configuration_error where infrastructure retries are intended.
Registering runners with registration tokens
Why it happens: Older tutorials use a registration token from the settings page. These have been disabled by default since GitLab 17.0 and are scheduled for removal in GitLab 20.0.
The fix: Create the runner in the UI or through the API, then register it with the glrt- authentication token it gives you.
Burning the Free tier quota on browser suites
Why it happens: Free namespaces get 400 compute minutes a month, and larger runners multiply usage by their cost factor. A sharded UI suite on every push can use that in days.
The fix: Mark test jobs interruptible, run full regression on a schedule rather than every push, and move heavy suites to your own runners.
Compared
How GitLab CI/CD compares
| Tool | Hosting | Free allowance | Test report in code review | Retry options | Parallel test runs | Licence |
|---|---|---|---|---|---|---|
| GitLab CI/CD | GitLab.com, self-managed or Dedicated | 400 compute minutes a month on Free | Yes, compared with the target branch | retry: up to 2, by failure type | parallel: 1 to 200, plus matrix | Open core: MIT core, proprietary tiers |
| GitHub Actions | GitHub-hosted or self-hosted runners | 2,000 minutes a month on Free | Via marketplace actions creating check runs | Re-run failed jobs; no retry keyword | Matrix, up to 256 jobs per run | Proprietary service; runner is MIT |
| Jenkins | Self-hosted only | All free; you pay for servers | Checks API publishing through plugins | retry step, with agent conditions | Matrix, parallel stages, timing-based splitting | MIT |
| Azure DevOps | Microsoft-hosted or self-hosted agents | One hosted job, 1,800 minutes, enable first | Tests tab on the pipeline run | Flaky detection with VSTest reruns | parallel strategy, up to 99 agents | Proprietary service |
FAQ
Frequently asked questions
What is GitLab CI/CD?
GitLab CI/CD is the pipeline engine built into GitLab. Jobs are defined in a .gitlab-ci.yml file in the repository and run by GitLab Runner, on GitLab-hosted machines or on your own. For QA teams its distinguishing features are JUnit test reports shown in merge requests, built-in parallel and matrix jobs, and retries scoped to the type of failure.
Is GitLab CI/CD free?
Yes, within limits. CI/CD is part of GitLab’s Free tier, which on GitLab.com includes 400 compute minutes a month on GitLab-hosted runners for up to 5 users per top-level group. Runners you register yourself do not use those minutes. The self-managed core is MIT licensed, so you can host GitLab and its runners yourself without licence fees. Premium and Ultimate add more minutes and features.
GitLab CI/CD or GitHub Actions for test automation?
Mostly it follows where the code lives. On test reporting specifically, GitLab is ahead: JUnit results appear in the merge request compared with the target branch, without third-party actions. GitHub Actions has a far larger marketplace, a more generous free minute allowance for private repositories, and hosted macOS on every plan. For self-managed hosting inside a private network, GitLab is the more complete option.
How do I show test results in GitLab merge requests?
Make the test runner write JUnit XML, then add artifacts:reports:junit with the file path or a glob to the job, plus artifacts:when: always so the report uploads when tests fail. Results then appear in the merge request’s test summary panel and the pipeline’s Tests tab. Directories are not accepted, only files and glob patterns, and each file must be under 30 MB.
How do I run tests in parallel in GitLab?
Add parallel: N to the job. GitLab creates N copies and sets CI_NODE_INDEX and CI_NODE_TOTAL in each, which runners such as Playwright accept directly through --shard; for others, select a subset of spec files in a script. Use parallel:matrix to run the same job across browsers or environments instead. Enough runner capacity must exist, or the extra jobs wait as pending.
Does GitLab detect flaky tests?
Partly. The unit test report shows, for each failed test, how many times it failed on the default branch in the last 14 days, which separates known unstable tests from new breakage. There is no automatic quarantine or rerun of individual tests in GitLab itself. Retries at test level come from the test runner, and job-level retry can be restricted to infrastructure failure types.
Why does my GitLab job say java: command not found?
Because the job has no image. On GitLab.com hosted runners, a job without image runs in ruby:3.1, which has no Java, Maven or Node. Set image at job level or in the default section, using an image that matches your toolchain, such as an official Maven or Playwright image pinned to a version.
What changed for CI users in recent GitLab releases?
GitLab 19.4 was released on 17 September 2026, and 19.5 is listed as the upcoming release. Relevant recent changes include retry failure types reworked in 19.1, with stuck_or_timeout_failure deprecated and runner_configuration_error added, and Fastzip becoming the default archiver for caches and artifacts in GitLab Runner 19.4. Runner registration tokens remain scheduled for removal in GitLab 20.0.
Alternatives
GitLab CI/CD alternatives worth weighing
Resources
Where to go next
Picking the wrong tool costs more than picking late.
QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.