---
title: "Running void in CI (audit)"
description: "Two gates: 1. void lint on every push. It's fast, needs no browser or build, and catches Tailwind v4, Next 16, React and a11y markup bugs. 2."
canonical: https://void-design.vercel.app/docs/audit/ci
lastModified: 2026-09-16
---

# Running void in CI

Two gates:
1. **`void lint`** on every push. It's fast, needs no browser or build, and catches Tailwind v4, Next 16, React and a11y markup bugs.
2. **`void audit`** on 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

```yaml
# .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.md
```

Notes:
- 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.ts` already defines `start`, `routes` and `budgets` (the template does), the step is just `bunx void audit --format md --out void-report.json > void-report.md`.
- `bunx void` assumes `@void/cli` is a dev dependency. In this monorepo use `bun run void …` (root `package.json` script).
- Don't set `VERCEL_ENV` in CI builds you audit, or the template's `robots.ts` serves `Disallow: /` and `seo/robots-blocks-all` fires. That's correct behavior for previews, wrong for this job.

## 2. Audit the Vercel preview deployment

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

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

```bash
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 md
```

Containers: 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 <preview> --only perf,smooth,a11y,design` | 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.
