Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ab73b5b
Plan bench's upgrade onto the site's toolchain
lwwmanning Oct 28, 2026
45e667a
Plan bench PR 1: the toolchain move
lwwmanning Oct 28, 2026
13609d9
Add a local parity check for bench builds
lwwmanning Oct 28, 2026
80602cd
Set the bench parity bar to no noticeable change
lwwmanning Oct 28, 2026
e53da05
Make the bench parity check fail on any non-200 and stale caches
lwwmanning Oct 28, 2026
b8562a0
Submit bun.lock to the dependency graph
lwwmanning Oct 28, 2026
5827ac4
Retry the snapshot submission once, group main pushes by commit
lwwmanning Oct 28, 2026
88afe40
Make bench a member of the site's Bun workspace
lwwmanning Oct 28, 2026
cb722e6
Upgrade bench to Next.js 16 and React 19.3
lwwmanning Oct 28, 2026
b9d9698
Format bench/scripts to bench's 100-column style
lwwmanning Oct 28, 2026
d92b74f
Lint and format bench with Biome
lwwmanning Oct 28, 2026
58e2900
Run Biome over all of bench in bench CI
lwwmanning Oct 28, 2026
6977969
Type-check bench with TypeScript 7 and a shared base config
lwwmanning Oct 28, 2026
091a757
Apply the site's a11y rules to bench, with inline exceptions
lwwmanning Oct 28, 2026
c9cca56
Compare bench's pages, error responses and dark theme in the parity c…
lwwmanning Oct 28, 2026
a810be6
Correct the linker and JSX comments for the shared toolchain
lwwmanning Oct 28, 2026
83d27f9
Cache Bun's packages and test the parity check in bench CI
lwwmanning Oct 28, 2026
bfd712c
Fail dependency review when the bun.lock snapshot is missing
lwwmanning Oct 28, 2026
f92f33a
Correct two bench notes in CLAUDE.md
lwwmanning Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 38 additions & 28 deletions .github/actions/bench-checks/action.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
name: Bench checks
description: >-
bench's full check suite: infrastructure trust policies, format, lint, build,
the migration-runner and deployment-verification tests, and the web tests with
their Postgres integration suite. Bench CI runs it on every change to bench,
bench's full check suite: infrastructure trust policies, the parity check's
tests, format, lint, build, the migration-runner and deployment-verification
tests, and the web tests with their Postgres integration suite. Bench CI runs it on every change to bench,
and bench-deploy runs it on the exact commit it deploys. Run it after a
checkout.

inputs:
pnpm-version:
description: Keep in lockstep with `packageManager` in bench/web/package.json.
required: true

runs:
using: composite
steps:
Expand All @@ -21,43 +16,59 @@ runs:
bash -n infra/provision.sh
python3 -m unittest discover -s infra/tests -v

# pnpm must exist before setup-node so its `cache: pnpm` can resolve the store.
- name: Install pnpm
shell: bash
env:
PNPM_VERSION: ${{ inputs.pnpm-version }}
run: npm install -g "pnpm@${PNPM_VERSION}"
# bench/web is a member of the root Bun workspace: Bun installs both apps
# from the one bun.lock and runs bench's scripts.
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.4.3

# Next.js, Vitest and the deployment tests run on Node.
- name: Setup Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
# The repository-wide Node pin (see CLAUDE.md "Node version"). Keep
# @types/node in bench/web/package.json on the same major.
node-version-file: .nvmrc
cache: pnpm
cache-dependency-path: bench/web/pnpm-lock.yaml

# Bun's package store, keyed on the lockfile, as in ci.yml. The restore
# key falls back to any earlier cache from this OS.
- name: Cache Bun install cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-

# At the root, where bun.lock and bunfig.toml live. This installs both apps.
- name: Install dependencies
shell: bash
working-directory: bench/web
run: pnpm install --frozen-lockfile
run: bun install --frozen-lockfile

- name: Format check
# The parity check's own logic: request planning, the expected statuses
# and the comparisons. The explicit path keeps `bun test` from also
# collecting bench's Vitest files.
- name: Test the parity check
shell: bash
working-directory: bench/web
run: pnpm format:check
run: bun test bench/scripts/parity/compare-api.test.ts

- name: Lint
# Biome over all of bench (web, scripts, parity harness) with the root
# biome.json, in CI mode: format and lint, no writes. Import sorting is off
# for bench (see bench/AGENTS.md). It replaces the bench/web `format:check`
# and `lint` scripts, which cover only bench/web and would leave
# bench/scripts unchecked on bench-only changes, where ci.yml skips the
# root `check:ci`. `bun run` uses the workspace's pinned Biome and never
# downloads one.
- name: Format and lint
shell: bash
working-directory: bench/web
run: pnpm lint
run: bun run biome ci bench

# `next build` must succeed without a reachable database (every page and route
# is request-rendered), so no BENCH_DB_* env vars are provided here on purpose.
- name: Build
shell: bash
working-directory: bench/web
run: pnpm build
run: bun run --cwd bench/web build

# testcontainers self-skips the Postgres integration suite when no Docker daemon
# is available (a local-dev convenience); in CI that would silently drop the only
Expand Down Expand Up @@ -91,5 +102,4 @@ runs:

- name: Test
shell: bash
working-directory: bench/web
run: pnpm test
run: bun run --cwd bench/web test
5 changes: 4 additions & 1 deletion .github/actions/changed-paths/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ outputs:
description: '"true" if anything outside bench/ changed.'
value: ${{ steps.classify.outputs.site }}
bench:
description: '"true" if bench/, a bench workflow, a shared action or .nvmrc changed.'
description: '"true" if bench/, a bench workflow, a shared action, .nvmrc or a root workspace file changed.'
value: ${{ steps.classify.outputs.bench }}

runs:
Expand Down Expand Up @@ -55,6 +55,9 @@ runs:
# Shared by both apps: these actions, and the Node pin that
# every workflow reads.
.github/actions/* | .nvmrc) site=true; bench=true ;;
# The root files of the Bun workspace that both apps install
# and build from.
package.json | bun.lock | bunfig.toml | biome.json | tsconfig*.json) site=true; bench=true ;;
*) site=true ;;
esac
done <<< "${files}"
Expand Down
6 changes: 0 additions & 6 deletions .github/workflows/bench-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ concurrency:
group: bench-ci-${{ github.ref }}
cancel-in-progress: true

env:
# Keep in lockstep with `packageManager` in bench/web/package.json.
PNPM_VERSION: "11.5.2"

jobs:
changes:
name: Detect changed paths
Expand Down Expand Up @@ -56,5 +52,3 @@ jobs:
# The same checks bench-deploy runs on the commit it deploys.
- name: Bench checks
uses: ./.github/actions/bench-checks
with:
pnpm-version: ${{ env.PNPM_VERSION }}
45 changes: 26 additions & 19 deletions .github/workflows/bench-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
# runs from the repository root and uses the pulled project settings to find
# the app.
#
# Triggers: a production deploy on every push to main that touches bench/; a
# preview deploy on every same-repo pull request that does, with the preview URL
# posted back to the PR as a sticky comment; and `workflow_dispatch` for manual
# preview or production runs. Fork PRs are skipped, since they can't read
# VERCEL_TOKEN.
# Triggers: a production deploy on every push to main that touches bench/ or
# the root files bench/web builds from (package.json, bun.lock, bunfig.toml,
# tsconfig.base.json); a preview deploy on every same-repo pull request that
# does, with the preview URL posted back to the PR as a sticky comment; and
# `workflow_dispatch` for manual preview or production runs. Fork PRs are
# skipped, since they can't read VERCEL_TOKEN.
#
# The job runs in the `bench-production` or `bench-preview` GitHub environment.
# Each holds VERCEL_TOKEN, a token scoped to the `benchmarks-website` project,
Expand All @@ -27,11 +28,19 @@ on:
branches: [main]
paths:
- 'bench/**'
- 'package.json'
- 'bun.lock'
- 'bunfig.toml'
- 'tsconfig.base.json'
- '.github/workflows/bench-deploy.yml'
pull_request:
branches: [main]
paths:
- 'bench/**'
- 'package.json'
- 'bun.lock'
- 'bunfig.toml'
- 'tsconfig.base.json'
- '.github/workflows/bench-deploy.yml'
workflow_dispatch:
inputs:
Expand All @@ -56,10 +65,6 @@ permissions:
env:
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ vars.BENCH_VERCEL_PROJECT_ID }}
# `vercel build` runs on the runner (not Vercel's builders), so it shells out to
# pnpm to install + build bench/web. Keep in lockstep with `packageManager` in
# bench/web/package.json.
PNPM_VERSION: "11.5.2"
# renovate: datasource=npm depName=vercel
VERCEL_CLI_VERSION: "64.0.2"

Expand Down Expand Up @@ -87,8 +92,6 @@ jobs:

- name: Bench checks
uses: ./.github/actions/bench-checks
with:
pnpm-version: ${{ env.PNPM_VERSION }}

deploy:
name: Build & deploy to Vercel
Expand Down Expand Up @@ -117,19 +120,18 @@ jobs:
with:
persist-credentials: false

# `vercel build` below builds ON THE RUNNER (not Vercel's infra), so it shells out
# to pnpm to install bench/web's deps and run `next build`. Without pnpm + Node on
# PATH it fails with `spawn pnpm ENOENT`. Mirror bench-ci.yml's setup.
# pnpm must exist before setup-node so its `cache: pnpm` can resolve the store.
- name: Install pnpm
run: npm install -g "pnpm@${PNPM_VERSION}"
# `vercel build` below builds ON THE RUNNER (not Vercel's infra). It runs
# bench/web's install command from vercel.json and `next build`, so Bun and
# Node must be on PATH. Mirror the bench-checks setup.
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.4.3

- name: Setup Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
cache: pnpm
cache-dependency-path: bench/web/pnpm-lock.yaml

# An exact version, and `--before` gives the CLI's whole dependency tree
# the repository's 14-day cooldown, so this job, which holds VERCEL_TOKEN,
Expand All @@ -151,6 +153,11 @@ jobs:
run: |
[[ "${PROD_URL}" == https://* ]] || { echo "::error::Set BENCH_SITE_BASE_URL to the public HTTPS production URL."; exit 1; }

# bench/web is a member of the root Bun workspace, so install from the
# root: bun.lock is there, and bunfig.toml's cooldown and linker apply.
- name: Install dependencies
run: bun install --frozen-lockfile

- name: Pull Vercel project settings
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,19 +226,96 @@ jobs:
# `bun audit` job is informational, so this is the one advisory check
# that blocks merge, and it is scoped to what the PR itself introduces
# (severity `high` and above) rather than the whole pre-existing tree.
#
# GitHub's dependency graph does not read bun.lock. The dependency-snapshot
# workflow submits it, and the action would pass without it, so a guard
# runs first (see its comment). The retry inputs then cover the short time
# GitHub takes to process a submitted snapshot. `fail-on-scopes` stays at
# the action's default, `runtime`: a development-only advisory does not
# block merge and stays with `bun audit` and CLAUDE.md "Audit advisories".
dependency-review:
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
permissions:
contents: read
# Implies read, which the guard uses to list a fork's changed files.
pull-requests: write
# The guard reads the snapshot job's check run.
checks: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# The action reviews whatever graph it finds. If the snapshot job
# failed, was skipped or finished late, that graph lacks bun.lock's
# packages and the review passes without them. So: for a pull request
# from this repository, wait for that job on the head commit and fail
# unless it succeeded. A fork's pull request gets no snapshot (its token
# cannot submit one), so fail it if it changes a bun.lock; if it does
# not, it adds no locked package.
- name: Require the bun.lock snapshot
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
CHANGED_FILES: ${{ github.event.pull_request.changed_files }}
FROM_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
# The `name:` of the job in dependency-snapshot.yml.
SNAPSHOT_CHECK: Submit bun.lock
WAIT_SECONDS: 600
run: |
if [ "$FROM_FORK" = true ]; then
files=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files?per_page=100" --jq '.[].filename')
# The API lists at most 3000 files; a longer pull request could hide a bun.lock.
listed=$(grep -c . <<<"$files" || true)
if [ "$listed" -lt "$CHANGED_FILES" ]; then
echo "::error::Listed $listed of the pull request's $CHANGED_FILES changed files, so a bun.lock change could go unseen."
exit 1
fi
if grep -Eq '(^|/)bun\.lock$' <<<"$files"; then
echo "::error::This pull request comes from a fork and changes bun.lock. A fork gets no dependency snapshot, so dependency review cannot see the locked packages. Make the lock change from a branch in this repository."
exit 1
fi
echo "Fork pull request without a bun.lock change: it adds no locked package."
exit 0
fi

name=$(jq -rn --arg name "$SNAPSHOT_CHECK" '$name | @uri')
deadline=$((SECONDS + WAIT_SECONDS))
while :; do
# The newest run of the job (highest id), so a successful rerun counts.
if run=$(gh api "repos/$REPO/commits/$HEAD_SHA/check-runs?check_name=$name&per_page=100" \
--jq '(.check_runs | max_by(.id)) // {} | "\(.status // "missing") \(.conclusion // "none")"'); then
read -r status conclusion <<<"$run"
if [ "$status" = completed ]; then
if [ "$conclusion" = success ]; then
echo "The '$SNAPSHOT_CHECK' job submitted the snapshot for $HEAD_SHA."
exit 0
fi
echo "::error::The '$SNAPSHOT_CHECK' job concluded '$conclusion', so dependency review would not see bun.lock's packages. Rerun it, then rerun this job."
exit 1
fi
echo "'$SNAPSHOT_CHECK' on $HEAD_SHA: $status"
else
echo "::warning::Could not read the check runs of $HEAD_SHA; trying again."
fi
if [ "$SECONDS" -ge "$deadline" ]; then
echo "::error::The '$SNAPSHOT_CHECK' job did not finish on $HEAD_SHA within $WAIT_SECONDS seconds."
exit 1
fi
sleep 15
done

- name: Dependency Review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: high
comment-summary-in-pr: on-failure
retry-on-snapshot-warnings: true
retry-on-snapshot-warnings-timeout: 300

# Defense-in-depth on top of GitHub's push protection. Push protection
# covers high-entropy / known-provider patterns at push time; TruffleHog
Expand Down
Loading
Loading