Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
95 changes: 95 additions & 0 deletions .github/actions/bench-checks/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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,
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:
- name: Check infrastructure trust policies offline
shell: bash
working-directory: bench
run: |
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}"

- 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

- name: Install dependencies
shell: bash
working-directory: bench/web
run: pnpm install --frozen-lockfile

- name: Format check
shell: bash
working-directory: bench/web
run: pnpm format:check

- name: Lint
shell: bash
working-directory: bench/web
run: pnpm lint

# `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

# 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
# Postgres integration coverage, so fail loudly if the daemon is missing.
- name: Require Docker for the integration suite
shell: bash
run: docker info > /dev/null

# The migration-runner tests and the web integration suite both run
# bench/scripts/migrate-schema.py through uv.
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: false

# uv resolves the runner's Python dependencies when it runs. Give them the
# repository's 14-day cooldown (see CLAUDE.md "Supply chain hardening").
- name: Set the uv cooldown
shell: bash
run: echo "UV_EXCLUDE_NEWER=$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ)" >> "${GITHUB_ENV}"

- name: Test migration runner
shell: bash
working-directory: bench
run: uv run --no-project --with 'psycopg[binary]>=3.2' python -m unittest discover -s scripts/tests -v

- name: Test deployment verification
shell: bash
working-directory: bench
run: node --test scripts/tests/verify-web-deploy.test.mjs

- name: Test
shell: bash
working-directory: bench/web
run: pnpm test
63 changes: 4 additions & 59 deletions .github/workflows/bench-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,69 +47,14 @@ jobs:
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.bench == 'true') }}
runs-on: ubuntu-24.04
timeout-minutes: 30
defaults:
run:
working-directory: bench/web
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Check infrastructure trust policies offline
working-directory: bench
run: |
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
run: npm install -g "pnpm@${PNPM_VERSION}"

- 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

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Format check
run: pnpm format:check

- name: Lint
run: pnpm lint

# `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
run: pnpm 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
# Postgres integration coverage, so fail loudly if the daemon is missing.
- name: Require Docker for the integration suite
run: docker info > /dev/null

# The migration-runner tests and the web integration suite both run
# bench/scripts/migrate-schema.py through uv.
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
# The same checks bench-deploy runs on the commit it deploys.
- name: Bench checks
uses: ./.github/actions/bench-checks
with:
enable-cache: false

# uv resolves the runner's Python dependencies when it runs. Give them the
# repository's 14-day cooldown (see CLAUDE.md "Supply chain hardening").
- name: Set the uv cooldown
run: echo "UV_EXCLUDE_NEWER=$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ)" >> "${GITHUB_ENV}"

- name: Test migration runner
working-directory: bench
run: uv run --no-project --with 'psycopg[binary]>=3.2' python -m unittest discover -s scripts/tests -v

- name: Test
run: pnpm test
pnpm-version: ${{ env.PNPM_VERSION }}
65 changes: 60 additions & 5 deletions .github/workflows/bench-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ on:
default: preview

concurrency:
# Include the event name so a manual workflow_dispatch (e.g. a preview deploy) on
# main does NOT land in the same group as — and thus cancel — an in-flight
# push-triggered production deploy on the same ref.
group: bench-deploy-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
# Push and manual production runs share one queue and never cancel an active
# run, so each promotion finishes its verification before the next starts. A
# newer preview run can cancel an older preview of the same event and ref.
group: ${{ (github.event_name == 'push' || inputs.environment == 'production') && 'bench-deploy-production' || format('bench-deploy-preview-{0}-{1}', github.event_name, github.ref) }}
cancel-in-progress: ${{ github.event_name != 'push' && inputs.environment != 'production' }}

permissions:
contents: read
Expand All @@ -64,8 +64,35 @@ env:
VERCEL_CLI_VERSION: "64.0.2"

jobs:
# Run bench's checks on the exact commit this run deploys. main takes squash
# merges and does not require a branch to be up to date before it merges, so
# the pushed commit itself is never checked before it lands, and Bench CI on
# main runs beside this workflow, not before it.
checks:
name: Check the deployed commit
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
if: >-
vars.BENCH_VERCEL_PROJECT_ID != '' && (
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
)
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

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

deploy:
name: Build & deploy to Vercel
needs: checks
runs-on: ubuntu-24.04
timeout-minutes: 20
# Fork PRs have no access to VERCEL_TOKEN, and we do not auto-deploy untrusted
Expand Down Expand Up @@ -116,6 +143,14 @@ jobs:
--before="$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
"vercel@${VERCEL_CLI_VERSION}"

# Production verification probes the public domain, so it has to be set.
- name: Require a public production verification URL
if: env.TARGET == 'production'
env:
PROD_URL: ${{ vars.BENCH_SITE_BASE_URL }}
run: |
[[ "${PROD_URL}" == https://* ]] || { echo "::error::Set BENCH_SITE_BASE_URL to the public HTTPS production URL."; exit 1; }

- name: Pull Vercel project settings
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
Expand All @@ -124,6 +159,8 @@ jobs:
- name: Build
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
# next.config.js embeds this, so /api/health reports the deployed commit.
BENCH_BUILD_SHA: ${{ github.sha }}
run: |
set -Eeuo pipefail
if [ "${TARGET}" = "production" ]; then
Expand Down Expand Up @@ -155,3 +192,21 @@ jobs:
with:
header: bench-preview
message: "Bench preview for `${{ github.event.pull_request.head.sha }}`: ${{ steps.deploy.outputs.url }}"

# Check the build commit, the read paths and CDN caching. Production is
# probed on the public domain, and any failure fails the run. Previews sit
# behind Vercel Authentication, so their verification reports as blocked.
- name: Verify deployed build and read paths
id: verify
timeout-minutes: 4
Comment thread
lwwmanning marked this conversation as resolved.
Resolved
env:
DEPLOY_URL: ${{ steps.deploy.outputs.url }}
PROD_URL: ${{ vars.BENCH_SITE_BASE_URL }}
EXPECTED_SHA: ${{ github.sha }}
run: |
set -Eeuo pipefail
if [ "${TARGET}" = "production" ]; then
node bench/scripts/verify-web-deploy.mjs "${PROD_URL}" "${EXPECTED_SHA}" production
else
node bench/scripts/verify-web-deploy.mjs "${DEPLOY_URL}" "${EXPECTED_SHA}" preview
fi
38 changes: 34 additions & 4 deletions bench/docs/architecture/deploy-and-infra.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,20 @@ scripts/migrate-schema.py ──IAM token, verify-full TLS──▶ RDS as `mi

```
push to main ──▶ production deploy
workflow_dispatch ──▶ preview or production (input)
workflow_dispatch ──▶ preview or production (production from main only)
same-repo PR ──▶ preview (event merge commit)
│
▼
runner: pnpm install + `vercel build [--prod]` (builds web/ ON THE RUNNER)
shared checks on exact SHA: format + lint + build + test
│
▼
`vercel deploy --prebuilt [--prod]` (uploads the prebuilt output)
runner: `vercel build [--prod]` on the same SHA
│
▼
`vercel deploy --prebuilt [--prod]`
│
▼
verify build SHA + read paths + CDN HIT (bounded retries)
```

- The Vercel project `benchmarks-website` is keyed by `VERCEL_ORG_ID` +
Expand All @@ -112,10 +119,33 @@ runner: pnpm install + `vercel build [--prod]` (builds web/ ON THE RUNNER)
prebuilt output is then uploaded, eliminating a build race.
- `main` **is** production — there is no staging gate. The per-PR CI
(including a testcontainer migration test) is the gate; a merge to `main`
ships straight to the v4 production domain.
ships to the v4 production domain after the deploy workflow checks the same commit.
- Push and manual production runs share one concurrency group without cancelling an active run.
This serializes promotion and verification across both triggers. Previews can cancel stale runs.
- Production verification uses `BENCH_SITE_BASE_URL`. A protected preview reports verification
as blocked. A production authentication error or failed probe fails the workflow.
- A `bench-keep-warm` scheduled workflow pings the production deployment so the RDS
connection pool and Data Cache stay warm between visits.

### Required merge check

The `main` ruleset of `vortex-data/vortex.dev` requires `Bench: format, lint, build, test` from
**GitHub Actions** (app ID `15368`), beside the marketing site's checks. It does not require
branches to be up to date before merging, so a merged commit is never checked before it lands.
That is why `bench-deploy.yml` runs the same checks (the `bench-checks` composite action) on the
exact commit it deploys, and deploys only if they pass.

To see the check for a commit:

```bash
checked_sha="$(git rev-parse HEAD)"
gh api "repos/vortex-data/vortex.dev/commits/${checked_sha}/check-runs" \
--jq '.check_runs[] | select(.name == "Bench: format, lint, build, test") | {name, conclusion, app_id: .app.id}'
```

A commit that changes bench must report `success` and app ID `15368`. The composite action keeps
the check name the same when its steps change.

## Legacy v3 host deploy (`ops/`) — decommissioned 2026-07-08

The v3 Rust server ran on an EC2 host under systemd, deployed by a **polling**
Expand Down
4 changes: 3 additions & 1 deletion bench/docs/runbooks/deploy-secrets-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ variable.
| `VERCEL_TOKEN` (secret) | `web-deploy.yml` |
| `VERCEL_ORG_ID` | `web-deploy.yml` |
| `VERCEL_PROJECT_ID` | `web-deploy.yml` |
| `BENCH_SITE_BASE_URL` | `web-keep-warm.yml` |
| `BENCH_SITE_BASE_URL` | `web-keep-warm.yml`, production verification in `web-deploy.yml` |
| `GH_BENCH_SCHEMA_ROLE_ARN` | `schema-deploy.yml` |
| `RDS_BENCH_REGION` | `schema-deploy.yml` |
| `RDS_BENCH_INSTANCE_ENDPOINT` | `schema-deploy.yml` |
Expand Down Expand Up @@ -355,6 +355,8 @@ export VERCEL_ORG_ID="<org id from Section A>" # the value you set as t
export VERCEL_PROJECT_ID="<project id from Section A>"
read -rs VERCEL_TOKEN && export VERCEL_TOKEN # paste at the prompt; never echoed, never recorded in shell history
# Run from the REPO ROOT (not web/) to match the workflow's project-resolution path:
BENCH_BUILD_SHA="$(git rev-parse HEAD)"
export BENCH_BUILD_SHA
vercel pull --yes --environment=preview --token="$VERCEL_TOKEN" && vercel build --token="$VERCEL_TOKEN"
unset VERCEL_TOKEN # drop the secret from the environment when done
```
Expand Down
Loading
Loading