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:writeonly, masked variable, revocable from API tokens without touching the pipeline. - Announce the run. A webhook on
revision.createdposts 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.
Related
- Publish from CI — the general pipeline pattern
- GitHub Actions HTML reports
- Jenkins HTML reports
- Share a coverage report