Skip to content

AccessLint/audit

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AccessLint Audit Action

Run a WCAG 2.2 accessibility audit against a live URL on every PR or push, emit JSON and Markdown reports, and pipe the output to whatever downstream step you want — fail the build, comment on the PR, upload an artifact, open an issue.

Backed by @accesslint/core. For audit + auto-fix workflows in your editor, see the Claude Code plugin.

Quick start

- uses: AccessLint/audit@v1
  id: a11y
  with:
    url: ${{ steps.preview.outputs.url }}

Permissions

This action requires no special permissions. It only navigates the URL you point it at, runs the audit in a headless browser, and writes report files to $GITHUB_WORKSPACE. It does not read or write git refs, push to the repository, or call the GitHub API.

If you compose with downstream actions that do need permissions (peter-evans/create-pull-request, actions/github-script, sticky PR comments, etc.), set those permissions on the workflow or job, not on this action.

Inputs

Name Default Description
url Single URL to audit. One of url or urls is required.
urls One URL per line — audits all and aggregates into one report. Wins over url when both set.
compare-against Baseline URL for regression mode: only violations new in url relative to this baseline are reported. Single-URL only.
wcag-level AA Conformance level. One of A, AA, AAA.
min-impact serious Drops anything below this from the report. One of critical, serious, moderate, minor.
fail-on never Exit non-zero when violations at this level or worse exist. One of never (compose your own gate), any, critical, serious, moderate, minor.
rules "" Comma- or whitespace-separated rule IDs to include (allowlist). Empty runs all rules.
rules-exclude "" Comma- or whitespace-separated rule IDs to exclude. Example: landmarks/region,navigable/bypass.
wait-for networkidle What to wait for after navigation: load, domcontentloaded, networkidle, or a CSS selector like #app-ready.
auth-headers "" JSON object of HTTP headers (e.g. '{"Authorization":"Bearer ${{ secrets.PREVIEW_TOKEN }}"}').

Reports always land at $GITHUB_WORKSPACE/accesslint-report.json and accesslint-report.md. The browser is installed automatically; cache ~/.cache/ms-playwright between runs to skip the download (see Cache the browser).

Outputs

Name Description
violation-count Total violations after the min-impact filter.
critical-count Critical-impact count.
serious-count Serious-impact count.
annotated-count Number of violations that landed as inline ::warning file=…:: PR-diff annotations (those with a workspace-relative source path).
failed true when violation-count > 0. Independent of fail-onfail-on controls the action's exit code; this output reports detection.
report-json-path Absolute path to accesslint-report.json.
report-markdown-path Absolute path to accesslint-report.md.
report-sarif-path Absolute path to accesslint-report.sarif (SARIF v2.1.0).

PR-diff annotations

When the audited URL is a React dev build (CRA, Next dev, Vite + React), each violation is mapped back to its source line via React DevTools fibers + sourcemaps. The action emits a ::warning file=src/Card.tsx,line=42,col=7::Insufficient color contrast 3.74:1 per violation, which GitHub renders inline on the PR diff at the matching line. No more "scroll up to find the sticky comment" — the squiggle is right where you're reading.

Production builds and non-React pages don't carry source metadata; those violations land in the report's "Unmapped" group and the Source column reads .

Composition examples

The action exits 0 by default — every common gating pattern is a downstream step. For the most common gate, use fail-on: directly.

Fail the build on critical violations (one-liner)

- uses: AccessLint/audit@v1
  with:
    url: ${{ steps.preview.outputs.url }}
    fail-on: critical

fail-on accepts never (default, composition-friendly), any, critical, serious, moderate, minor. Choose the threshold; the action does the rest.

Custom gating via the step output

- uses: AccessLint/audit@v1
  id: a11y
  with:
    url: ${{ steps.preview.outputs.url }}

- name: Custom gate
  if: steps.a11y.outputs.critical-count > 5
  run: exit 1

Audit multiple URLs in one job

- uses: AccessLint/audit@v1
  with:
    urls: |
      ${{ steps.preview.outputs.url }}/
      ${{ steps.preview.outputs.url }}/dashboard
      ${{ steps.preview.outputs.url }}/settings
    fail-on: critical

The Markdown report renders a per-URL ## section; outputs (violation-count, critical-count, etc.) are totals across the run.

Regression mode — only fail on new violations

- uses: AccessLint/audit@v1
  with:
    url: ${{ steps.preview.outputs.url }}        # the PR build
    compare-against: https://www.your-app.com    # current production
    fail-on: any

Audits both URLs and reports only violations present in the candidate but not the baseline. Match key is ruleId + selector — works well when the DOM structure is stable between the two pages, less well when selectors are content-dependent.

Upload SARIF for code scanning

- uses: AccessLint/audit@v1
  id: a11y
  with:
    url: ${{ steps.preview.outputs.url }}

- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: ${{ steps.a11y.outputs.report-sarif-path }}
    category: accesslint

Violations show up in the repo's Security → Code scanning tab and as inline annotations on the PR diff (independently of the ::warning:: annotations the action also emits).

Sticky comment on the PR with the report

- uses: AccessLint/audit@v1
  with:
    url: ${{ steps.preview.outputs.url }}

- uses: marocchino/sticky-pull-request-comment@v2
  if: github.event_name == 'pull_request'
  with:
    header: a11y
    path: accesslint-report.md

Upload as a build artifact

- uses: AccessLint/audit@v1
  with: { url: ${{ steps.preview.outputs.url }} }

- uses: actions/upload-artifact@v6
  if: always()
  with:
    name: accesslint-report
    path: |
      accesslint-report.json
      accesslint-report.md

Open an issue when a scheduled audit finds regressions

on:
  schedule: [{ cron: "0 9 * * 1" }]   # every Monday 09:00 UTC

jobs:
  weekly-a11y:
    runs-on: ubuntu-latest
    permissions: { issues: write }
    steps:
      - uses: AccessLint/audit@v1
        id: a11y
        with:
          url: https://www.example.com
          min-impact: critical

      - if: steps.a11y.outputs.failed == 'true'
        uses: actions/github-script@v8
        with:
          script: |
            const md = require('fs').readFileSync('accesslint-report.md', 'utf8');
            github.rest.issues.create({
              ...context.repo,
              title: `a11y regressions on ${new Date().toISOString().slice(0, 10)}`,
              body: md,
              labels: ['accessibility'],
            });

Auth headers for a protected preview

- uses: AccessLint/audit@v1
  with:
    url: ${{ steps.vercel.outputs.preview-url }}
    auth-headers: |
      {"x-vercel-protection-bypass": "${{ secrets.VERCEL_BYPASS }}"}

Cache the browser between runs

- uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: playwright-${{ runner.os }}

- uses: AccessLint/audit@v1
  with:
    url: ${{ steps.preview.outputs.url }}
    install-browser: false   # cache hit ⇒ skip download

What it does

  1. Installs chromium-headless-shell via Playwright (skip with install-browser: false).
  2. Launches headless Chromium and navigates to url.
  3. Injects the @accesslint/core IIFE and runs runAudit(document) plus attachReactFiberSource to map violations back to source files when the page is a React dev build.
  4. Filters by min-impact, sorts by severity, writes JSON + Markdown reports, sets step outputs, and adds the markdown to the run summary.
  5. Always exits 0 — composition decides whether the build fails.

React source mapping

When auditing a React dev build that ships sourcemaps (CRA, Next dev, Vite + React), each violation's Source: column points at the actual .tsx/.jsx line that produced the element — read from React DevTools fibers in-page. On production builds, non-React pages, or pages without sourcemaps, the column is and consumers fall back to the selector.

Errors and how to fix them

The action surfaces actionable messages for the most common failures:

Symptom Likely fix
Failed to load <url>: net::ERR_… Local dev server not started, wrong port, or a typo in the URL.
Failed to load <url>: …401/403… Protected preview — set auth-headers with the right Authorization or bypass token.
wait-for timed out: … The wait condition wasn't met inside the budget. Switch wait-for to a more specific selector, e.g. #app-ready.

Development

npm install
npm run typecheck
npm run build         # bundles dist/index.js via esbuild
npm test              # vitest

The bundled dist/ is committed; CI verifies it's regenerated on every PR.

License

MIT

About

WCAG 2.2 accessibility audit GitHub Action — source-mapped to React/JSX lines, with inline PR annotations, regression mode, and SARIF output for Code Scanning.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors