Publish from CI
Coverage reports, eval results, benchmark dashboards, release notes — if
your pipeline produces HTML, one curl puts it at a stable URL your team
can open, comment on, and diff against the previous run. No Pages setup, no
artifact-zip downloads.
The pattern
Store a scoped token (just reports:write) in your CI
secret store, then:
curl -fsS -X POST https://commareports.com/api/v1/reports \
-H "Authorization: Bearer $COMMA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile html coverage/index.html \
'{title: "Coverage — \(env.GITHUB_SHA[0:7])", html: $html}')"
To keep one URL per report instead of one per run, create the report
once, save its id, and PATCH it on every run — each push appends a
revision, reviewers can diff any two, and bookmarks never go
stale:
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 coverage/index.html '{html: $html}')"
GitHub Actions
name: coverage-report
on:
push:
branches: [main]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run coverage
- name: Publish coverage to Comma
env:
COMMA_API_TOKEN: ${{ secrets.COMMA_API_TOKEN }}
REPORT_ID: ${{ vars.COMMA_COVERAGE_REPORT_ID }}
run: |
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 coverage/index.html '{html: $html}')"
The same two lines work in GitLab CI, CircleCI, Buildkite, or a cron on a box — it's plain HTTPS.
Notes for pipelines
- HTML limit is 5 MB. Bigger payloads (screenshots, JS bundles) go in as assets — 25 MB per file.
- Rate limits are per token (60/min by default). One report per build is nowhere near it.
- Notify on push: add a webhook notification on
revision.createdand the new revision announces itself in Slack or Discord. - Access control: CI-published reports follow the same sharing model as everything else — keep them team-visible rather than public-by-default.