Two gates:
void linton every push. It's fast, needs no browser or build, and catches Tailwind v4, Next 16, React and a11y markup bugs.void auditon pull requests against a production build (started by void) or the preview deployment URL.
Run perf measurements on a quiet runner, one job at a time. Parallel Chromium instances on one machine distort CPU-throttled numbers. Use runs: 3 or more and trust medians.
1. GitHub Actions: lint + audit a local production build
# .github/workflows/void.yml
name: void
on:
pull_request:
push:
branches: [main]
concurrency:
group: void-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bunx void lint --format md --fail-on error
audit:
runs-on: ubuntu-latest
needs: lint
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- name: Install Chromium
run: bunx playwright-core install --with-deps chromium # or: sudo apt-get install -y chromium
- run: bunx void doctor
- name: Build
run: bun run build
env:
NEXT_PUBLIC_SITE_URL: https://www.example.com # canonical/OG URLs use the production origin
- name: Audit
run: |
bunx void audit \
--start "bun run start -p 3100" --port 3100 \
--routes /,/pricing,/blog/hello-world,/this-page-does-not-exist \
--format md --out void-report.json > void-report.md
- name: Job summary
if: always()
run: cat void-report.md >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@v4
if: always()
with:
name: void-report
path: |
void-report.json
void-report.mdNotes:
- The audit step fails the job with exit 1 on errors or exceeded budgets. That's the gate. Exit 3 (server didn't start) and 4 (browser) mean infrastructure problems: read the printed server log tail or
void doctor. - When
void.config.tsalready definesstart,routesandbudgets(the template does), the step is justbunx void audit --format md --out void-report.json > void-report.md. bunx voidassumes@void/cliis a dev dependency. In this monorepo usebun run void …(rootpackage.jsonscript).- Don't set
VERCEL_ENVin CI builds you audit, or the template'srobots.tsservesDisallow: /andseo/robots-blocks-allfires. That's correct behavior for previews, wrong for this job.
2. Audit the Vercel preview deployment
# .github/workflows/void-preview.yml
name: void preview
on: deployment_status
jobs:
audit:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bunx playwright-core install --with-deps chromium
- name: Audit preview (perf, smooth, a11y, design)
run: bunx void audit "${{ github.event.deployment_status.environment_url }}" --only perf,smooth,a11y,design --format md > void-report.md
- if: always()
run: cat void-report.md >> "$GITHUB_STEP_SUMMARY"Previews correctly send Disallow: / / noindex, so run the seo and geo gates against the local production build (§1) or against production after deploy. Vercel preview protection must allow the runner, e.g. with a protection-bypass header or a public preview.
3. Baselines and regressions
# main branch: save a baseline artifact
bunx void audit --start "bun run start -p 3100" --port 3100 --json > baseline.json
# PR: compare
bunx void audit --start "bun run start -p 3100" --port 3100 --json > pr.json
jq -n --slurpfile a baseline.json --slurpfile b pr.json \
'[$a[0].scores, $b[0].scores] | {before: .[0], after: .[1]}'
jq -n --slurpfile a baseline.json --slurpfile b pr.json \
'($a[0].findings | map(.fingerprint)) as $old | $b[0].findings | map(select(.fingerprint as $f | $old | index($f) | not)) | map({id, severity, message})'The second jq lists findings that are new in the PR (by fingerprint = rule id + target).
4. Other CI systems
Any runner with Bun and a Chromium works:
curl -fsSL https://bun.sh/install | bash
apt-get update && apt-get install -y chromium # Debian/Ubuntu images
export VOID_CHROMIUM=/usr/bin/chromium
bun install && bun run build
bunx void audit --start "bun run start -p 3100" --port 3100 --format mdContainers: Chromium runs with --no-sandbox inside void. Give the job ≥ 2 vCPU and don't run other heavy jobs on the same machine during perf audits.
5. Recommended gate policy
| Branch / event | Command | failOn |
|---|---|---|
| Every push | void lint |
error |
| Pull request | void audit (local production build) |
error + budgets |
| Preview deploy | void audit |
error |
| Nightly on production | void audit https://www.example.com --runs 5 --out nightly.json |
warn (report only, notify on regressions) |
Loosen severities in void.config.ts rules with a comment and an owner. Never turn off gate rules (seo/http-status, seo/noindex, seo/canonical-*, seo/robots-blocks-all, seo/sitemap-url-status, geo/js-dependency, geo/jsonld-parse-error) or set failOn: "never" on PR checks.