Publish HTML reports from CI
Your pipeline already generates the report. Playwright writes one, so does
pytest --html, so does genhtml, so does every scanner you run. Then
the job uploads it as an artifact and the report effectively ceases to
exist: it needs a CI login, a download, an unzip and a local file open
before anyone reads a word of it.
Nobody does that. They screenshot the failure into Slack.
The pattern, once
Every provider guide below is the same three lines with different syntax around them:
# 1. generate — and survive a failing build
run-the-thing || true
# 2. publish to a saved report id
curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
-H "Authorization: Bearer $COMMA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile html report.html \
--arg title "Run $(git rev-parse --short HEAD)" \
'{title: $title, html: $html}')"
# 3. there is no step 3
Three things make it work in practice:
PATCHa saved id, don'tPOST. POSTing creates a new report every build — a spray of orphan links. PATCHing one id gives the suite a permanent URL and a revision per run, which is also what makes two runs diffable.- Make it unconditional.
if: always(),after_script,post { always { … } }. A failing build is when the report matters. - Use a scoped token.
reports:writeonly, stored as a CI secret, revocable without touching anything else.
By provider
- GitHub Actions —
if: always(), and why the artifact upload isn't the same thing - GitLab CI —
after_script, and where Pages falls short for per-run reports - Jenkins —
post { always { … } }, and the HTML Publisher CSP problem - CircleCI —
store_artifactsversus a URL - Azure DevOps — publishing without a custom extension
- Bitbucket Pipelines — the
after-scriptstep
Multi-file reports
Most tools that say "HTML report" emit a directory — index.html plus
CSS, JavaScript and JSON. Playwright, Allure, genhtml, nbconvert,
Storybook and Gradle all do.
The entry HTML goes in the PATCH body; the siblings go up through
POST /api/v1/reports/$REPORT_ID/assets as base64, one call per file.
Worth scripting once and forgetting. Relative references are rewritten to
the uploaded copies, so the drill-down links resolve. See the
API reference.
What you get that an artifact never gave you
- A link in the PR that a reviewer clicks instead of reconstructing.
- Threads on the failure. Select the failing test, leave a comment, and it stays anchored there across runs — so a flaky test carries its own history. See commenting on HTML.
- Diffable revisions. Two runs, one diff, which tests changed state.
- Access control per report — private, team, domain-gated, or a link. See sharing & access control.
By report type
Framework-specific guides, each with the exact generate command:
pytest · Jest and Vitest · Playwright · Cypress · JUnit · Gradle · Go · PHPUnit · RSpec · Allure · coverage · JaCoCo · Lighthouse · k6 · Trivy · Semgrep · SonarQube
The full library is on the blog index.
Scheduled, not just triggered
Some reports shouldn't wait for a push. A routine re-runs the job on a cron and posts the refreshed output as a new revision at the same URL — no pipeline required for the weekly digest that currently exists as a recurring calendar reminder to someone.
Try it
Comma is free — unlimited reports, unlimited commenters, unlimited revision history.