Skip to main content

Documentation · GitHub Action v2

Accessibility testing in GitHub Actions

The Accessibility Pro Action scans a URL against WCAG 2.2 on every pull request with five engines and live-DOM verification, posts one comment that updates itself, and fails the build when the result crosses the threshold you set. This page is the complete reference. For what the product does beyond CI, see Accessibility Pro for developers.

Install

Add one workflow file. There is no checkout, no setup-node and no browser install: the scan runs on Accessibility Pro's infrastructure, and the Action is a dependency-free Node script that calls it. The Action is listed on the GitHub Marketplace and its source is MIT licensed.

# .github/workflows/accessibility.yml
name: Accessibility
on: pull_request

permissions:
  pull-requests: write   # post the results comment
  id-token: write        # count free-tier quota against this repo

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: accessibility-pro/action@v2
        with:
          url: ${{ secrets.STAGING_URL }}

The URL must be reachable from the internet, because the scanner loads it the way a visitor's browser would. A preview deployment per pull request works well; store its address as a secret if it is not public.

Permissions

pull-requests: write lets the Action post or update its comment. Without it you get a warning and the results still appear in the job summary. id-token: write lets the Action prove, with a signed GitHub OIDC token, which repository the free-tier quota belongs to, so no other workflow can spend it. Without it the scan still runs and quota is bucketed on the repository name the workflow reports, which nothing verifies.

Pull requests from forks receive a read-only token by design, so the comment is skipped there. That never fails the build.

Inputs

InputDefaultWhat it does
urlrequiredURL to scan. One per line to scan several pages in one step. Store preview URLs that need authentication as repository secrets.
wcag-levelAAConformance level to scan against: A, AA or AAA.
fail-onerrorerror fails on any critical or high violation; warning adds medium; wcag fails on any Level A or AA success-criterion failure at any severity; none reports only.
thresholds(empty)JSON object of per-severity allowances that overrides fail-on, for example {"critical": 0, "high": 3}. Severities you leave out are unlimited.
fail-on-unrepresentativetrueFail when the scanner could not see the real page: a bot challenge, a full-page consent wall, an HTTP block, or filtering that did not complete. Ignored when fail-on is none.
enginesall fiveComma-separated subset of axe-core, lighthouse, pa11y, ibm-equal-access and arc-style. Fewer engines is faster but gives less cross-engine corroboration.
commentstickysticky updates one pull-request comment in place, new adds one per run, off disables it.
top-issues5How many findings to list inline, ranked by measured impact (1 to 25).
annotationstrueEmit build-failing findings as workflow annotations.
sarif-file(empty)Path to write a SARIF 2.1.0 report to, for the Security tab.
results-file(empty)Path to write the raw scan payload (JSON) to, for your own tooling.
accessibility-pro-token(empty)API token. Scans are attributed to your account, appear in your dashboard and draw on your plan's CI allowance instead of the free tier.
github-tokengithub.tokenToken used to post the pull-request comment.
timeout-minutes15Per-attempt budget for one scan request (1 to 60).
retries1Retries for transient backend or network failures (0 to 3).
backend-urlproductionOverride for a self-hosted backend.
report-domainproductionOverride for a self-hosted frontend.
oidc-audiencehttps://api.accessibilitypro.appAudience for the GitHub OIDC token. Change only when self-hosting.

Outputs

With several URLs, counts are summed across pages, and score, scan-id and report-url describe the worst page, the one to open first.

OutputValue
passedtrue when every scanned URL cleared the gate.
scoreScore from 0 to 100 of the lowest-scoring URL.
scan-idScan id of the lowest-scoring URL.
report-urlHosted report for the lowest-scoring URL, including its share token. Store the whole URL: the id alone does not open a private report.
violations-critical, -high, -medium, -lowViolation counts summed across every scanned URL.
total-issuesTotal findings reported as violations.
manual-review-countFindings that need a human look rather than counting as violations.
warnings-countScan warnings that make the result unrepresentative; 0 for a clean scan.
engines-usedSources that produced findings: the engines that ran plus any in-house analyzers.
plan-tierPlan the scan was billed to. Empty on the anonymous free tier.
ci-scans-remainingCI scans left in this month's allowance. Empty when anonymous or unlimited.
sarif-file, results-fileAbsolute paths of the files written, when requested.

Sample workflows

Gate on conformance, not severity

fail-on: wcag fails the build for any Level A or AA success-criterion failure, whatever severity it carries. Use it when you are working to a compliance deadline such as the European Accessibility Act or ADA.

      - uses: accessibility-pro/action@v2
        with:
          url: https://staging.example.com
          wcag-level: AA
          fail-on: wcag

Scan several pages in one step

Each page is a separate scan against your quota.

      - uses: accessibility-pro/action@v2
        with:
          url: |
            https://staging.example.com/
            https://staging.example.com/pricing
            https://staging.example.com/checkout

Track findings in the Security tab

SARIF fingerprints match the scanner's own baseline keys, so code scanning tracks a finding across runs as fixed or reintroduced. Results appear in the Security tab, not on source lines, because a URL scan has no source file to annotate.

      - uses: accessibility-pro/action@v2
        id: a11y
        with:
          url: https://staging.example.com
          sarif-file: a11y.sarif
          fail-on: none          # let code scanning own the verdict

      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: a11y.sarif
          category: accessibility

Pay down a backlog without a red build every day

No new criticals, and no more than five high-severity findings.

      - uses: accessibility-pro/action@v2
        with:
          url: https://staging.example.com
          thresholds: '{"critical": 0, "high": 5}'

Use the results in later steps

      - uses: accessibility-pro/action@v2
        id: a11y
        with:
          url: https://staging.example.com
          fail-on: none

      - name: Block release below 90
        if: steps.a11y.outputs.score < 90
        run: |
          echo "Score ${{ steps.a11y.outputs.score }}, report ${{ steps.a11y.outputs.report-url }}"
          exit 1

Watch production overnight

A scheduled run has no pull request to comment on, so results go to the job summary and the workflow's failure notification.

on:
  schedule:
    - cron: '0 6 * * *'

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: accessibility-pro/action@v2
        with:
          url: https://www.example.com
          fail-on: wcag

Exit codes

The step exits with 0 when every URL was scanned and cleared the gate, and always when fail-on: none is set and every URL was scanned. It exits with 1, with the reason in the log, in four cases:

  • the gate failed: a threshold was crossed on at least one URL, with a link to the worst report;
  • one or more URLs could not be scanned at all; results for the others are still in the job summary;
  • the result was unrepresentative and fail-on-unrepresentative is true;
  • the scan could not run: a rate limit, a rejected token, a blocked URL or a backend error after the retries.

The verdict is computed by the scanner, not by the Action, so the build result and the hosted report always agree. Two kinds of finding appear in the report but never gate a build: markup your code does not own (component-library portals, consent widgets, reCAPTCHA and Turnstile frames) and rules that are not WCAG success criteria, including 4.1.1, which WCAG 2.2 removed. Both are disclosed in the comment.

Free tier and tokens

Without a token, a repository gets 10 scans a day and 10 an hour, so an accidental loop cannot drain the day's allowance in minutes. With an accessibility-pro-token, scans are attributed to your account and draw on your plan's CI allowance, which is separate from interactive scans: 300 a month signed in on the free plan, 1,000 on Developer, 10,000 on Team and 40,000 on Business. See plans and CI allowances.

      - uses: accessibility-pro/action@v2
        with:
          url: ${{ secrets.STAGING_URL }}
          accessibility-pro-token: ${{ secrets.ACCESSIBILITY_PRO_TOKEN }}

The Action prints the remaining allowance on each run and warns at 90%. Copy-as-PR fixes are generated in the hosted report while you are signed in there; the token does not carry into that surface.

When your staging site blocks the scanner

Bot protection, a web application firewall or an IP allow-list can refuse the scan before it sees a page. The Action then fails as unrepresentative instead of passing on an error page. The guide to allowing the scanner through your firewall lists the fixed addresses, the User-Agent token and the per-site request header to allow, with a ready-made Cloudflare rule.

How accurate the gate is

Findings carry their evidence level, from verified by interaction down to a single engine's report, and AI-written text is labelled as such. The scanner's precision and recall on annotated fixtures and a field audit of real websites are published.