View an HTML report from GitLab CI

Your pipeline produces coverage/index.html. You add it to artifacts:paths. You open the job, click Browse, click the file — and GitLab downloads it instead of showing it.

That's not a misconfiguration. GitLab.com deliberately does not render HTML artifacts inline: serving arbitrary user HTML from a GitLab domain would run that HTML in GitLab's own origin, which is an obvious way to hand attackers a session. The documented answer is GitLab Pages — which is a real answer, and also a whole separate deployment: another job, another artifact contract, one site per project, and its own access model to reason about.

For a report that ten people need to read and discuss twice a week, that's a lot of infrastructure for a document.

One curl in .gitlab-ci.yml

Store a scoped token (reports:write is enough) as a masked CI/CD variable, create the report once, and PATCH it from every pipeline so there is one URL per report rather than one per run:

publish_report:
  stage: report
  image: alpine:latest
  when: always
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$COMMA_REPORT_ID" \
        -H "Authorization: Bearer $COMMA_API_TOKEN" \
        -H "Content-Type: application/json" \
        -d "$(jq -n --rawfile html coverage/index.html \
              --arg title "Coverage — $CI_COMMIT_SHORT_SHA" \
              '{title: $title, html: $html}')"
    - echo "Report → https://commareports.com/p/$COMMA_REPORT_ID"

when: always matters — the pipelines you most want to look at are the failed ones, and the default on_success skips the publish exactly then.

Posting the link on the merge request

- |
  [ -n "$CI_MERGE_REQUEST_IID" ] && curl -fsS -X POST \
    "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" \
    -H "PRIVATE-TOKEN: $GITLAB_API_TOKEN" \
    --data-urlencode "body=📊 [Coverage report](https://commareports.com/p/$COMMA_REPORT_ID) — updated for $CI_COMMIT_SHORT_SHA"

Because the report URL never changes, the note stays accurate as commits land — no stale links accumulating down the MR thread.

What this buys over the artifact browser

GitLab artifact GitLab Pages Comma report
Renders HTML in browser No (download) Yes Yes
Stable URL across runs No Yes Yes
Setup artifacts:paths Pages job + config one curl
Expiry 30 days default until redeployed revisions kept
Comments on the report anchored threads
History you can diff revision per run
Readers without a seat No Public or nothing private / team / domain / link

The row that usually decides it is the last one. GitLab artifacts require project membership; Pages on a private project is public-or-nothing unless you're on a tier with access control. A report that a PM, a customer or an auditor needs to open wants identity-based visibility, not a seat.

Details before you wire it up

  • HTML body: 5 MB. Screenshots, videos and trace bundles go in as assets — 25 MB per file, 250 MB per report.
  • Scripts are stripped on write. If your report is a JavaScript app (Allure, Playwright's default reporter), publish a static digest and attach the archive — see sharing an Allure report.
  • Mask the token. reports:write only, masked variable, revocable from API tokens without touching the pipeline.
  • Announce the run. A webhook on revision.created posts each new revision into Slack or Discord.

Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision history. Add the job above to the pipeline that currently uploads an artifact nobody opens.

Create your first report →

Related