Share a Buildkite HTML report
Buildkite's artifact story is deliberate: artifact_paths uploads the file,
and Buildkite serves it back as a download. That is exactly right for a
.zip, a core dump, or a binary. It is exactly wrong for index.html.
Click the coverage report in the Artifacts tab and you get a file in your Downloads folder. Open that file and the stylesheet is missing, because the CSS it references was a sibling in a directory that no longer exists around it. The reviewer sees unstyled text and gives up.
The usual fallback is an annotation:
buildkite-agent annotate --style error --context tests < summary.md
Annotations are genuinely good, and they are not a report. They take a
restricted subset of markup — no scripts, no external stylesheets — and they
are size-capped. A failure summary fits. A Playwright HTML report, a coverage
tree, or a pytest --html page does not.
Publish the report from the step
Mint a scoped token with reports:write and nothing else,
expose it to the agent the way you already expose secrets, create the report
once, then PATCH that id every build:
steps:
- label: ":test_tube: tests"
command: .buildkite/test.sh
artifact_paths:
- "playwright-report/**/*"
#!/bin/bash
# .buildkite/test.sh
set -uo pipefail
npx playwright test
EXIT=$?
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 playwright-report/index.html \
--arg title "Tests — ${BUILDKITE_BRANCH} @ ${BUILDKITE_COMMIT:0:7}" \
'{title: $title, html: $html}')"
buildkite-agent annotate --style info \
"Report → https://commareports.com/p/$COMMA_REPORT_ID"
exit $EXIT
Two details carry the weight:
- Capture the exit code, publish, then re-exit. Buildkite halts a command
list at the first non-zero exit, so a naive
&&chain publishes only the green builds — the ones nobody needed to read. Apre-exitagent hook does the same job pipeline-wide if you would rather not touch each script. - Annotate with the link, not the report. The annotation is now one line and always fits, and it is the fastest path from the build page to something a human can read.
artifact_paths stays. Keeping the artifact costs nothing and engineers who
are already inside Buildkite may prefer it; the published report is for
everyone who is not.
What the published copy adds
- It renders. Report HTML is stored verbatim and served inside a sandboxed iframe with scripts enabled, so an interactive report stays interactive instead of arriving as a download prompt.
- One URL across every build. Revisions accumulate at the same address and any two can be diffed, which turns "what changed since the last green build" into a question with an answer.
- Readers with no Buildkite seat. Visibility is private, team, domain-gated, or link, decided per report and independent of your pipeline's ACL.
- Comments anchored to the content. The reviewer highlights the assertion that regressed and pins a thread to it, and the thread survives the next hundred builds — see commenting on HTML.
Limits
- HTML body: 5 MB. Traces, videos and archives go in as assets at 25 MB per file, 250 MB per report.
- Self-contained reports stay interactive. A report that fetches sibling data files at view time can't, because the frame has an opaque origin — publish a static digest and attach the archive, as in sharing an Allure report.
- Rate limit: 60 requests/minute per token. One publish per build is nowhere near it.
Try it
Comma is free — unlimited reports, unlimited commenters, unlimited revision history.
Related
- Publish from CI — the general pipeline pattern
- GitHub Actions HTML reports · CircleCI HTML reports
- Share a Playwright report · Share a coverage report