perfkit ships a reusable workflow, fonoster/perfkit/.github/workflows/run.yaml. A project calls
it by hand (workflow_dispatch) after a significant change; it creates the run's droplets,
waits for the benchmarks, uploads report.json and the raw results as an artifact, and deletes
the droplets.
GitHub stops a job after 6 hours, and a soak runs for longer than that. So the run is detached:
the start job creates the droplets and starts the agent on the load generator, which drives
the load on its own, and later jobs only follow it:
startrunsperfkit run --detachand outputs the run id.wait-1andwait-2each runperfkit collect <id> --wait --timeout 5h50m. Exit 4 (the run goes on) lets the next job wait; exit 0 skips the rest of the waiting. Each uploads the raw results so far as the artifactperfkit-raw-<id>, overwriting it. When the agent fails (exit 3),collectfirst downloads the raw results of the unfinished benchmarks as they are, so the upload keeps them.finishdownloads that artifact and runsperfkit collect <id>: it writesreport.json, deletes the droplets, and uploads.perfkit/<id>/asperfkit-report-<id>. Exit 1 (a benchmark did not pass) still uploads. It then appendsperfkit summaryof the report to the workflow run's summary page: the verdicts, the capacity steps and the soak rules.cleanupalways runsperfkit sweep --run <id>, so a cancelled or failed workflow leaves nothing billing. It needs every job before it, so it starts only after their uploads.
Where a run failed before its report, the failed job's summary says so and names the artifact that holds the raw results.
Every job has permissions: { contents: read }. Each job runs on a new machine, so collect
adds that machine's IP to the run's firewall before it logs in, with PERFKIT_SSH_KEY.
| Input | Default | Meaning |
|---|---|---|
perfkit-ref |
required | The perfkit tag the workflow is called at, like v0.1.0; the jobs run that perfkit (npx -y github:fonoster/perfkit#<perfkit-ref>). A reusable workflow can't see its own ref, so pass the same tag as in uses:. |
config |
perf/perfkit.yaml |
The project's perfkit.yaml. |
target-version |
required | The image tag under test, like 2.31.5. |
commit |
required | The commit under test, written to report.json. The perf/ config is read from the commit that called the workflow (github.sha), because a release tag under test can predate the config. |
benchmarks |
empty | Comma-separated benchmark ids; empty runs them all. |
| Secret | Meaning |
|---|---|
DIGITALOCEAN_TOKEN |
The DigitalOcean token, with the scopes in the README. It never reaches a droplet, a log or the report. |
PERFKIT_SSH_KEY |
An ed25519 private key in OpenSSH format, without a passphrase. Every job logs in to the droplets with it, registered in the account as perfkit-shared. Make one with ssh-keygen -t ed25519 -N "" -f perfkit. |
NGROK_AUTHTOKEN |
Optional. The agent opens an ngrok tunnel from live.domain of perfkit.yaml to its live.json server, so the benchmarks page shows the run live (see "Live results" in the README). It goes into the agent container's environment on the load generator and nowhere else: not run.json, a log or the report. Without it the run has no tunnel. |
The workflow takes its secrets only through secrets:, so a caller has to name them: secrets: inherit would hand it every secret of the repository.
| Output | Meaning |
|---|---|
run-id |
The run id, like 20261103T141210Z-bd3254a. |
verdict |
Like 2 of 2 benchmarks passed. |
artifact |
perfkit-report-<run id>: report.json and raw/. |
.github/workflows/benchmarks.yaml in the project:
name: Benchmarks
on:
workflow_dispatch:
inputs:
target-version:
description: The image tag to benchmark, like 2.31.5
required: true
benchmarks:
description: Comma-separated benchmark ids (empty runs them all)
default: ""
permissions:
contents: read
jobs:
run:
uses: fonoster/perfkit/.github/workflows/run.yaml@v0.1.0
with:
perfkit-ref: v0.1.0
target-version: ${{ inputs.target-version }}
commit: ${{ github.sha }}
benchmarks: ${{ inputs.benchmarks }}
secrets:
DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
PERFKIT_SSH_KEY: ${{ secrets.PERFKIT_SSH_KEY }}
NGROK_AUTHTOKEN: ${{ secrets.NGROK_AUTHTOKEN }} # optional: live results
publish:
needs: run
if: always() && needs.run.outputs.run-id != ''
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ needs.run.outputs.artifact }}
path: run
- uses: actions/checkout@v4
with:
ref: gh-pages
path: pages
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm install -g npm@11
- run: >-
npx -y github:fonoster/perfkit#v0.1.0 render --report run/report.json
--history pages/benchmarks/data/runs.json --out pages/benchmarks
- working-directory: pages
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add benchmarks
git commit -m "benchmarks: ${{ needs.run.outputs.run-id }} (${{ needs.run.outputs.verdict }})"
git pushPublishing to gh-pages is the caller's job; perfkit never pushes. Run perfkit sweep on a
schedule too, as the backstop for anything a lost run left.