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
How it works
- 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.
- Write a baseline. Run
scanwith--update-baselineagainst staging or production. Commit the resulting JSON: that freezes today's backlog. - Gate PRs on regressions. CI runs
scanwith--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.jsononce, snapshot today's defects
npx personaudit scan https://app.example.com --session ./session.json \
--baseline personaudit-baseline.json --update-baselinein 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 seriousGitHub 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
- Full CLI docs: install, auth, and every flag this guide uses.
- WCAG 2.2 AA checklist: what the rules the gate cites actually require.