Skip to main content

Field guide · CI

Fail the build only on new defects

Snapshot today's backlog once. After that every pull request fails only when axe-core finds a new critical or serious violation, including states behind a saved login session. Deterministic, no API key, and the existing backlog stays ignored until you clear it.

Last reviewed 26 September 2026

On this page

How it works

  1. Capture a session (optional). For a behind-login site, authenticate once locally and save the session. Cookies never need to live in the repo: store them as a CI secret.
  2. Write a baseline. Run scan with --update-baseline against staging or production. Commit the resulting JSON: that freezes today's backlog.
  3. Gate PRs on regressions. CI runs scan with --fail-on serious. The build fails only when a new defect appears at or above that severity.

CLI

once, authenticate and save a session
npx personaudit auth https://app.example.com --save ./session.json
once, snapshot today's defects
npx personaudit scan https://app.example.com --session ./session.json \
  --baseline personaudit-baseline.json --update-baseline
in ci, fail only on new defects at or above serious
npx personaudit scan https://app.example.com --session ./session.json \
  --baseline personaudit-baseline.json --fail-on serious

GitHub Action

A ready-to-use workflow lives in the repo at examples/github-actions/accessibility-gate.yml. It restores an optional session secret, scans vars.PERSONAUDIT_TARGET_URL, gates on new critical/serious defects, and uploads the Markdown report as a build artifact.

examples/github-actions/accessibility-gate.yml
name: Accessibility gate
on: pull_request

# pull-requests: write is only needed for the sticky comment (packaged action input
# `comment: "true"`). Drop it if you do not use that input.
permissions:
  contents: read
  pull-requests: write

jobs:
  a11y:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    env:
      TARGET_URL: ${{ vars.PERSONAUDIT_TARGET_URL }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          node-version: 22
      - name: Install Personaudit and Chromium
        run: |
          npm install -g personaudit
          npx playwright install --with-deps chromium

      # Secrets cannot be tested in `if:`, so the step always runs and writes
      # an empty file when the secret is unset. Remove it for a public site.
      - name: Restore session
        env:
          SESSION_JSON: ${{ secrets.PERSONAUDIT_SESSION }}
        run: |
          if [ -n "$SESSION_JSON" ]; then
            printf '%s' "$SESSION_JSON" > session.json
            chmod 600 session.json
          fi

      - name: Scan and gate on new serious defects
        run: |
          args=( scan "$TARGET_URL" --baseline personaudit-baseline.json --fail-on serious )
          if [ -s session.json ]; then args+=( --session session.json ); fi
          personaudit "${args[@]}"

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: accessibility-report
          path: personaudit-report/scan.md

What the gate is (and isn't)

Note

The gate runs on axe-core only: deterministic verdicts. Persona notes measure task success in the product loop; they never enter the CI baseline and never fail the build. Passing this gate does not replace testing with disabled people.

Related

Try it on a client site

A letter grade and every failing axe rule, free, no signup.

Grade a site free