Record standard Go benchmark output on one or more GitHub Actions runners,
compare each platform with its matching main history, update one pull request
comment, and publish long-term charts with GitHub Pages.
The recorder does not install Go or Node, run go, or inspect toolchain caches.
Projects keep full control of benchmark execution; this action only parses the
result file and uploads a validated artifact.
The publisher creates one bot comment and updates it for later commits:
GitHub Pages keeps long-term Main, Branches, and Pull requests series. A merged commit extends the Main series:
Pull request history remains available independently for investigation:
Create .github/go-benchmark.yml:
id: my-project
title: My project benchmarks
groups:
core: "^Core.*$"
runtime: "^(Runtime|Scheduler).*$"
parser:
match: "^Parse.*$"
chart: singleRecord benchmarks in .github/workflows/benchmark.yml:
name: Go benchmarks
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
benchmark:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Run benchmarks
run: go test -run '^$' -bench '^Benchmark' -benchmem -count=5 ./... | tee benchmark.txt
- uses: xgo-dev/setup-benchmark-go-action@v1
with:
config: .github/go-benchmark.yml
benchmark-file: benchmark.txtPublish from a trusted workflow on the default branch. Create
.github/workflows/benchmark-publish.yml:
name: Publish Go benchmarks
on:
workflow_run:
workflows: [Go benchmarks]
types: [completed]
permissions:
actions: read
contents: write
issues: write
pull-requests: write
jobs:
publish:
if: github.event.workflow_run.conclusion == 'success'
uses: xgo-dev/setup-benchmark-go-action/.github/workflows/publish.yml@v1
with:
run_id: ${{ github.event.workflow_run.id }}The separate workflow_run publisher is intentional. Pull request code can
produce benchmark artifacts, but only the publisher from the default branch can
write history or comments. It validates every artifact before using it. Before
each remote write, the publisher also verifies that a pull request still points
at the benchmarked commit, so an older run that finishes late cannot replace
newer history or comments.
By default, data and the generated site are committed atomically to a pages
branch in the project repository. In Settings > Pages, select Deploy from
a branch, then choose pages and / (root).
Patterns use RE2 syntax and are matched against all of:
- short name:
CoreRead - benchmark name:
BenchmarkCoreRead - package-qualified short name:
example.org/project/pkg::CoreRead - package-qualified benchmark name:
example.org/project/pkg::BenchmarkCoreRead
The shorthand group form assigns a combined chart:
groups:
core: "^Core.*$"Use the object form for a display title, several patterns, or one chart per benchmark:
version: 1
id: my-project
title: My project benchmarks
site-path: go-benchmarks/my-project
include:
- "^Benchmark"
exclude: "^Experimental"
max-benchmarks: 500
groups:
storage:
title: Storage
match:
- "^(File|Database)"
chart: combined
parser:
match: "^Parse"
chart: single| Field | Default | Meaning |
|---|---|---|
version |
1 |
Configuration schema version. |
id |
required | Stable suite ID using lowercase letters, digits, ., _, or -. |
title |
<id> benchmarks |
Report and site title. |
site-path |
go-benchmarks/<id> |
Site directory within the data branch. |
include |
^Benchmark |
RE2 pattern or pattern list selecting benchmarks. |
exclude |
none | RE2 pattern or pattern list applied after include. |
max-benchmarks |
500 |
Maximum selected benchmarks, from 1 through 5000. |
groups |
none | Named chart groups. Group IDs are lowercase path-safe names. |
groups.<id>.title |
title-cased ID | Display title. |
groups.<id>.match |
required | RE2 pattern or pattern list. |
groups.<id>.chart |
combined |
combined or single. |
views |
none | Optional pivot tables for the pull request comment. |
A benchmark may match at most one group. Overlap is rejected instead of silently selecting a group. Included benchmarks that match no group remain visible under Other, with a separate chart for each benchmark. This lets new benchmarks appear without first changing configuration.
Without views, the pull request comment uses the default long form:
Group | Benchmark | Metric | Current | vs main
Views provide a constrained data pivot for richer reports. They do not change the artifact schema, stored history, or Pages charts. Each measurement is an observation with five available dimensions:
platformpackagegroupbenchmarkmetric
A view selects observations and places dimensions in table rows, columns, or split sections. This layout produces one row per platform and workload, with file size, build time, and run time as columns:
groups:
programs: "^Program/"
core: "^Core"
views:
programs:
title: Program measurements
select:
groups: "^programs$"
table:
rows: [platform, benchmark]
columns: [metric]
missing: error
dimensions:
benchmark:
title: Workload
trim-prefix: BenchmarkProgram/
metrics:
binary-bytes:
title: File size
format: bytes
build-ns:
title: Build
format: duration-ns
run-ns:
title: Run
format: duration-ns
core:
title: Core language and compiler benchmarks
select:
groups: "^core$"
table:
rows: [platform, benchmark]
columns: [metric]
collapsed: trueThe corresponding standard benchmark record may contain several metrics:
Unit binary-bytes better=lower assume=exact
Unit build-ns better=lower
Unit run-ns better=lower
BenchmarkProgram/cprintf 1 18264 binary-bytes 354926000 build-ns 1274000 run-ns
select accepts one RE2 pattern or a pattern list for each dimension:
| Selector | Matched values |
|---|---|
platforms |
Platform ID and display label. |
packages |
Go package path. |
groups |
Group ID and display title. |
benchmarks |
Full key, benchmark name, and name without Benchmark. |
metrics |
Metric or unit name. |
Selectors within one field are ORed; different fields are ANDed. Observations may intentionally appear in more than one view.
| Field | Default | Meaning |
|---|---|---|
rows |
[platform, benchmark] |
Dimensions identifying each table row. |
columns |
[metric] |
Dimensions expanded into value and vs main column pairs. |
split-by |
none | Dimensions that produce separate tables inside the view. |
collapsed |
false |
Put the view in a Markdown <details> section. |
missing |
blank |
Use blank for - cells or error to require a complete matrix. |
empty |
hide |
Use hide or fail with error when selection is empty. |
max-rows |
200 |
Per-table row limit, from 1 through 1000. |
dimensions |
none | Override dimension titles or trim a literal value prefix. |
metrics |
none | Override metric titles, ordering, and display formats. |
Every dimension may be used at most once across rows, columns, and
split-by. If omitted dimensions make two observations resolve to the same
cell, rendering fails instead of choosing one result. missing: error also
turns a sparse pivot into an explicit failure.
Platforms can be columns instead of rows:
table:
rows: [benchmark, metric]
columns: [platform]Or each platform can have its own compact table:
table:
rows: [benchmark]
columns: [metric]
split-by: [platform]Metric configuration is optional. Listed metrics appear first in declaration order; other selected metrics follow alphabetically. Supported formats are:
| Format | Meaning |
|---|---|
auto |
Number followed by its metric name. |
number |
Number without a unit suffix. |
bytes |
Exact byte value with B. |
duration-ns |
Duration whose input unit is nanoseconds. |
duration-us |
Duration whose input unit is microseconds. |
duration-ms |
Duration whose input unit is milliseconds. |
duration-s |
Duration whose input unit is seconds. |
Duration formats choose a readable ns, us, ms, or s display unit. The
underlying value and historical comparison remain in the original metric.
Reports that exceed GitHub's comment size limit fail with a clear error; use
selectors, split views, or Pages for larger suites.
Input is ordinary go test -bench output. Package names, goos, and goarch
are read from the standard header:
goos: linux
goarch: amd64
pkg: example.org/project/parser
BenchmarkParseSmall-8 125000 912.4 ns/op 64 B/op 1 allocs/op
Every reported metric is retained. Repeated samples, such as -count=5, are
stored and the median becomes the displayed and historical value. The action
does not average results from different platforms.
Common Go units have their usual direction automatically: lower is better for
ns/op, sec/op, B/op, and allocs/op; higher is better for MB/s and
B/s. Custom metrics can declare metadata in the benchmark output:
Unit requests/s better=higher
Unit binary-bytes better=lower assume=exact
Supported metadata is:
| Metadata | Values | Meaning |
|---|---|---|
better |
lower, higher |
Marks a PR delta as better or worse. |
assume |
nothing, exact |
Records the benchmark's comparison assumption. |
Units come from benchmark output, not the YAML configuration. A unit is never combined with another unit.
Each recorder job uploads one artifact. shard-id defaults to GITHUB_JOB.
Different jobs may contribute disjoint benchmarks to the same platform; the
publisher merges them before making its single data commit and single PR
comment.
strategy:
fail-fast: false
matrix:
suite: [core, storage]
steps:
- run: go test -run '^$' -bench . -count=5 ./bench/${{ matrix.suite }} | tee benchmark.txt
- uses: xgo-dev/setup-benchmark-go-action@v1
with:
config: .github/go-benchmark.yml
benchmark-file: benchmark.txt
shard-id: ${{ matrix.suite }}The publisher rejects duplicate shard IDs, duplicate benchmarks across shards, configuration differences, unit metadata conflicts, platform label conflicts, and source commit mismatches. This makes partial or ambiguous matrix output a hard failure instead of publishing misleading data.
Platform ID and label normally come from the benchmark goos and goarch.
Set them explicitly for cross compilation, virtual environments, or a Go
version matrix:
- uses: xgo-dev/setup-benchmark-go-action@v1
with:
config: .github/go-benchmark.yml
benchmark-file: benchmark.txt
platform-id: ubuntu-go1.26-amd64
platform-label: Ubuntu / amd64 / Go 1.26
shard-id: ${{ github.job }}-${{ matrix.package }}Two Go versions on the same OS and architecture must use distinct platform IDs. Only equal platform IDs are merged or compared.
The generated Pages site keeps separate Main, Branches, and Pull requests views. A pull request run updates both its PR series and its branch series. History is capped at 500 commits per series.
For a pull request, the publisher creates one bot comment and updates that same
comment on later commits. Each metric is compared only with the newest matching
platform in main. If no main baseline exists yet, including the first setup
PR in a new project, the report succeeds and marks every metric as new.
The publisher also uploads a rendered preview artifact and writes the report to
the job summary. Pull requests from forks use the same history and comment
flow. Their artifacts must report the exact repository and commit from the
trusted workflow_run event, and the publisher confirms the current pull
request head again before writing. Their configuration must also match
config_path on the default branch. The data token is never available to the
pull request workflow.
The default pages branch can live in another repository:
jobs:
publish:
if: github.event.workflow_run.conclusion == 'success'
uses: xgo-dev/setup-benchmark-go-action/.github/workflows/publish.yml@v1
with:
run_id: ${{ github.event.workflow_run.id }}
data_repository: owner/project-benchmark-data
data_dispatch_event: go-benchmarks-published
config_path: .github/project-benchmark.yml
secrets:
data_token: ${{ secrets.BENCHMARK_DATA_TOKEN }}data_token needs contents write access to the data repository. The optional
data_dispatch_event sends a repository_dispatch event to that repository
after its data is ready, using the same token. Configure Pages there from its
pages branch or handle that event with a Pages deployment workflow. If the
Pages URL is nonstandard, set site_base_url.
An external repository and token are optional for pull request reporting. If the token is missing, the repository cannot be checked out for writing, the push fails, or the deployment notification fails, the publisher keeps the rendered preview and adds a warning to its pull request comment instead of failing. Artifact download, trusted configuration, source validation, and rendering errors still fail the workflow.
| Input | Required | Default | Meaning |
|---|---|---|---|
config |
no | .github/go-benchmark.yml |
Grouping configuration path. |
benchmark-file |
yes | go test -bench output path. |
|
platform-id |
no | <goos>-<goarch> |
Stable comparison and merge identity. |
platform-label |
no | derived | Human-readable platform name. |
shard-id |
no | GITHUB_JOB |
Stable shard identity within a platform. |
retention-days |
no | 30 |
Uploaded artifact retention. |
| Output | Meaning |
|---|---|
artifact-name |
Uploaded artifact name. |
platform-id |
Resolved platform ID. |
shard-id |
Resolved shard ID. |
suite-id |
Configuration ID. |
Call
xgo-dev/setup-benchmark-go-action/.github/workflows/publish.yml@v1 as a job.
| Input | Required | Default | Meaning |
|---|---|---|---|
run_id |
yes | Workflow run containing recorder artifacts. | |
data_repository |
no | caller repository | Repository containing the data branch and Pages site. |
data_branch |
no | pages |
Data and Pages branch. |
data_dispatch_event |
no | Event sent to the data repository after data is ready. | |
site_base_url |
no | derived from repository | Public Pages root URL. |
artifact_pattern |
no | go-benchmark-* |
Artifact download glob. |
config_path |
no | .github/go-benchmark.yml |
Trusted default-branch config for fork PRs. |
| Secret | Required | Meaning |
|---|---|---|
data_token |
no | Token with contents write access to an external data repository. |
Recommended publisher permissions are actions: read, contents: write,
issues: write, and pull-requests: write. GitHub may reduce permissions
passed to a reusable workflow, so the caller must grant them.
Parsing and rendering execute through actions/github-script@v8 on the
runner-provided Node 24 runtime. The action is isolated from a consumer's
setup-go, setup-node, Go cache, Node cache, and selected toolchain versions.
It has no production dependency on the Go toolchain.
Artifacts contain JSON data and a configuration snapshot, never executable
code. Before merging shards or writing history, the publisher validates schema
versions, URLs, labels, metric values, sample medians, configuration, layouts,
units, platforms, and size limits. For fork pull requests, the artifact
configuration must exactly match the configured file on the default branch. It
then binds repository, commit, ref, URLs, and timestamp to trusted
workflow_run metadata. This lets fork pull requests publish without trusting
identity or storage layout supplied by their workflow.
The trusted publisher serializes writes per data repository and publishes one commit after all platform artifacts have passed validation.
The implementation requires Node 24:
npm ci
npm run check
npm run build
npm audit --omit=dev
npm run benchmarknpm run build updates both checked-in bundles: dist/index.js and
publish/dist/index.js.


