Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for GitLab CI/CD By James Joyner IV · · 11 min read

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: always is 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.

Free download · 368-page PDF

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.