GitLab CI Artifacts and Reports: Surfacing Results Right in the Merge Request
JUnit, coverage, code quality, accessibility — GitLab can render all of it inline on the MR. Here is how to wire up every report type, with AI writing the glue.
- #gitlab
- #ci-cd
- #artifacts
- #testing
For the longest time my team’s merge requests just said “pipeline passed” with a green check, and to find out what passed you clicked into the job log and scrolled through ten thousand lines. The whole point of GitLab’s report artifacts is to pull the signal out of those logs and put it on the MR where reviewers actually look. Once I wired them up properly, code review got noticeably faster — and AI turned out to be great at generating the slightly fiddly report config. Here’s the complete tour.
Artifacts vs. reports: the distinction that matters
Plain artifacts:paths saves files you can download later. artifacts:reports: is different — it tells GitLab to parse a file of a known format and render it in the UI. Same artifacts: key, completely different superpower:
test:
stage: test
script:
- pytest --junitxml=report.xml
artifacts:
when: always
paths:
- report.xml # downloadable file
reports:
junit: report.xml # parsed and rendered on the MR
Note when: always. Test reports are most useful when tests fail, and the default on_success would discard them on a failure. This single line is the most common omission I fix in other people’s pipelines.
JUnit: the test summary widget
The junit report drives the “Tests” tab and the test-summary widget on the MR. It compares against the target branch and highlights newly failing and newly fixed tests. Almost every test framework can emit JUnit XML:
unit-tests:
stage: test
script:
- jest --ci --reporters=default --reporters=jest-junit
artifacts:
when: always
reports:
junit: junit.xml
variables:
JEST_JUNIT_OUTPUT_NAME: junit.xml
You can supply a glob (junit: "test-results/**/*.xml") to merge reports from a matrix or parallel run. That’s a genuinely nice touch when you shard tests.
Pro Tip: If your framework outputs a non-standard JUnit dialect, GitLab may show garbled test names. Ask AI to “write a small post-processing step that normalizes this JUnit XML to standard format” rather than fighting the framework’s reporter flags for an hour.
Coverage: the visual diff
Coverage has two halves. The number in the job output comes from a regex you configure; the inline source annotations come from a Cobertura-format coverage_report:
coverage-job:
stage: test
script:
- pytest --cov=app --cov-report=xml --cov-report=term
coverage: '/TOTAL.+?(\d+\%)$/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
The coverage: regex captures the percentage for the badge and MR widget. The coverage_report with Cobertura format is what paints the red/green lines directly in the MR’s changed-files view, so reviewers see which new lines lack tests. Getting that regex right is exactly the kind of finicky task AI nails — give it your tool’s terminal output and ask for the matching regex.
Code Quality: inline degradations
The Code Quality report flags complexity and maintainability changes right on the diff:
include:
- template: "Jobs/Code-Quality.gitlab-ci.yml"
That GitLab template emits a codequality report automatically. If you run your own linter, you can produce the same CodeClimate JSON yourself and wire it up:
eslint:
stage: test
script:
- eslint --format gitlab .
artifacts:
reports:
codequality: gl-code-quality-report.json
Many linters ship a GitLab/CodeClimate formatter (eslint-formatter-gitlab, for instance). When they don’t, AI can write a converter from the linter’s JSON to CodeClimate format quickly — it’s a well-documented schema.
Accessibility, load performance, and the rest
GitLab supports a surprisingly long list of report types. A few I actually use:
a11y:
script:
- pa11y-ci --reporter cli > /dev/null
artifacts:
reports:
accessibility: gl-accessibility.json
browser-perf:
script:
- lighthouse-ci
artifacts:
reports:
browser_performance: browser-performance.json
The accessibility report compares pa11y results against the target branch and flags new violations on the MR. The browser/load performance reports do the same for Lighthouse and load-test metrics. None of these require a paid tier for the basic comparison widget on most setups — check your tier, but the config is identical.
Dotenv: passing values between jobs
One report type isn’t about humans at all. dotenv artifacts pass variables from one job to a later one:
build:
stage: build
script:
- echo "IMAGE_TAG=$(date +%s)-$CI_COMMIT_SHORT_SHA" >> build.env
artifacts:
reports:
dotenv: build.env
deploy:
stage: deploy
needs: ["build"]
script:
- echo "Deploying $IMAGE_TAG"
The deploy job inherits IMAGE_TAG from the build job’s dotenv. This is the clean way to share computed values without committing them or stuffing them into the cache. One caveat: dotenv variables are not masked, so never write a secret into a build.env file. Treat them as build metadata only.
Expiry, and not blowing up your storage
Reports are artifacts, and artifacts cost storage. Set expiry deliberately:
.report-defaults:
artifacts:
when: always
expire_in: 1 week
expose_as: "Test Report"
expire_in keeps storage bounded; expose_as adds a download link to the MR widget for humans who want the raw file. I extend .report-defaults across test jobs so the policy is set once.
Dependencies, needs, and artifact flow
Artifacts don’t just render — they flow between jobs, and controlling that flow keeps pipelines fast. By default a job downloads artifacts from all jobs in earlier stages, which on a big pipeline means pulling megabytes you don’t need. Use dependencies: or needs: to be explicit:
package:
stage: package
needs:
- job: build
artifacts: true
- job: test
artifacts: false
script:
- ./package.sh dist/
Here package pulls the build job’s artifacts but skips the test job’s — it doesn’t need the JUnit XML to build a tarball. On a pipeline with large artifacts, declaring exactly what each job consumes shaves real time off every run. An empty dependencies: [] says “download nothing,” which is the right call for a job that only needs the source checkout.
There’s a correctness angle too: if package implicitly downloaded a stale artifact from an unrelated job, you could ship the wrong file. Being explicit about artifact flow is partly a speed optimization and partly a guard against subtle “wrong bytes” bugs. AI is good at generating the needs: graph, but it tends to default to pulling everything; I tighten the artifacts: true/false flags by hand because only I know which jobs genuinely consume which outputs.
How AI fits in
This is fast-junior-engineer territory all the way down. AI reliably:
- Writes the coverage regex from sample terminal output.
- Generates report converters (linter JSON to CodeClimate, etc.).
- Remembers the exact
artifacts:reports:key names, which are easy to typo.
It gets wrong:
- Whether
when: alwaysis needed (it often omits it, and your fail-case reports vanish). - Which report types your GitLab tier actually renders.
So I draft with it and verify against a real failing pipeline — the only true test of report config is seeing it render on an MR. And the standing rule: I never paste API tokens or registry credentials into a chat to debug a job; I share the YAML and the log, not the secrets. Dotenv artifacts especially are unmasked, so keep secrets out of them entirely. For reviewing whether the surfaced reports actually caught regressions, our code review dashboard pairs nicely with these MR widgets.
My standard prompt: “Here’s the terminal output of my coverage tool and my test runner. Generate the GitLab CI artifacts:reports: config to surface JUnit results and Cobertura coverage on the MR, including the coverage: regex.” Variants live in my prompt library and the testing-focused prompt packs.
Conclusion
Report artifacts turn a green checkmark into actionable, inline feedback — test failures, coverage gaps, complexity creep, all on the MR where reviewers live. The config is fiddly but well-suited to AI drafting; just remember when: always, mind your tier, keep secrets out of dotenv, and verify against a real pipeline run. More guides await in the GitLab CI/CD category.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.