Blog

GitHub Accessibility Integration: CI/CD Pipeline Testing Guide

TestParty
TestParty
April 8, 2025

Accessibility issues discovered in production cost more to fix than issues caught during development. Integrating accessibility testing into CI/CD pipelines prevents issues from reaching production, enforces standards automatically, and shifts accessibility left in the development lifecycle.

This guide covers implementing accessibility testing in GitHub workflows, from basic integration to enterprise-scale enforcement.


Why CI/CD Accessibility Testing

Shift-Left Benefits

Cost reduction: Issues caught during development cost 5-10x less to fix than issues discovered in production.

Developer education: Immediate feedback teaches developers accessibility requirements through practice.

Consistent standards: Automated checks enforce the same rules for every pull request.

Prevention over detection: Stop issues before they affect users rather than fixing them after.

Pipeline Integration Points

Accessibility testing can integrate at multiple points:

Pre-commit: Run checks before code is committed locally.

Pull request: Check accessibility when PRs are opened or updated.

Pre-deployment: Gate deployments on accessibility status.

Post-deployment: Verify production accessibility after deployment.


Implementation Approaches

Tool Options

TestParty Bouncer: Purpose-built GitHub integration for accessibility checks.

axe-core based tools:

  • axe CLI
  • Pa11y
  • jest-axe

Custom implementations: Scripted solutions using accessibility testing libraries.

Integration Patterns

GitHub Actions: Native GitHub workflow integration running in GitHub infrastructure.

Status checks: Required checks that block PR merging.

Check runs: Detailed results visible in PR interface.

Commit status: Simple pass/fail on commits.


Basic GitHub Actions Setup

Simple axe-core Integration

# .github/workflows/accessibility.yml
name: Accessibility Check

on:
  pull_request:
    branches: [main]

jobs:
  accessibility:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build

      - name: Start server
        run: npm start &

      - name: Wait for server
        run: npx wait-on http://localhost:3000

      - name: Run accessibility tests
        run: npx axe http://localhost:3000 --exit

Pa11y Integration

name: Accessibility Check

on: pull_request

jobs:
  pa11y:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install Pa11y
        run: npm install -g pa11y-ci

      - name: Build and start server
        run: |
          npm ci
          npm run build
          npm start &
          npx wait-on http://localhost:3000

      - name: Run Pa11y
        run: pa11y-ci --config .pa11yci.json
// .pa11yci.json
{
  "defaults": {
    "standard": "WCAG2AA",
    "timeout": 30000
  },
  "urls": [
    "http://localhost:3000/",
    "http://localhost:3000/products",
    "http://localhost:3000/checkout"
  ]
}

TestParty Bouncer Integration

TestParty Bouncer provides streamlined accessibility checking:

# .github/workflows/accessibility.yml
name: Accessibility Check

on:
  pull_request:
    branches: [main, develop]

jobs:
  accessibility:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run accessibility check
        uses: testparty/bouncer-action@v1
        with:
          api-key: ${{ secrets.TESTPARTY_API_KEY }}
          urls: |
            http://localhost:3000/
            http://localhost:3000/products
            http://localhost:3000/cart
          threshold: 0

Bouncer Features

Automatic PR comments: Issues appear directly in pull request comments with remediation guidance.

Dashboard integration: Results visible in TestParty Spotlight dashboard.

Historical tracking: Track accessibility trends across PRs.

Threshold configuration: Set acceptable issue counts for gradual improvement.


Required Status Checks

Make accessibility checks required for merging:

GitHub Configuration

  1. Navigate to repository Settings → Branches
  2. Add branch protection rule for main
  3. Enable "Require status checks to pass"
  4. Select accessibility check as required

Enforcement Levels

Strict: Zero issues required to pass. Best for new projects or specific routes.

threshold: 0

Gradual improvement: Allow some issues, reduce over time.

threshold: 10  # Reduce monthly

Critical only: Block only on critical issues.

severity: critical

Testing Strategies

Component Testing

Test individual components in isolation:

// component.test.js
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
import { Button } from './Button';

expect.extend(toHaveNoViolations);

describe('Button accessibility', () => {
  it('should have no accessibility violations', async () => {
    const { container } = render(<Button>Click me</Button>);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

Page-Level Testing

Test rendered pages:

// pages.test.js
const { chromium } = require('playwright');
const { AxeBuilder } = require('@axe-core/playwright');

test('homepage accessibility', async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('http://localhost:3000');

  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toHaveLength(0);

  await browser.close();
});

User Flow Testing

Test complete user journeys:

// checkout-flow.test.js
test('checkout flow accessibility', async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  // Test each step of checkout
  const urls = [
    'http://localhost:3000/cart',
    'http://localhost:3000/checkout/shipping',
    'http://localhost:3000/checkout/payment',
    'http://localhost:3000/checkout/review'
  ];

  for (const url of urls) {
    await page.goto(url);
    const results = await new AxeBuilder({ page }).analyze();
    expect(results.violations).toHaveLength(0);
  }

  await browser.close();
});

Handling Dynamic Content

Single Page Applications

SPAs require waiting for content:

// Wait for specific element
await page.waitForSelector('#main-content');

// Or wait for network idle
await page.goto(url, { waitUntil: 'networkidle' });

// Then run accessibility check
const results = await new AxeBuilder({ page }).analyze();

Authenticated Routes

Test pages requiring authentication:

- name: Setup test user
  run: npm run seed:test-user

- name: Run authenticated tests
  env:
    TEST_USER: ${{ secrets.TEST_USER }}
    TEST_PASS: ${{ secrets.TEST_PASS }}
  run: npm run test:accessibility:auth

Multiple States

Test components in different states:

// Test modal in open state
await page.click('#open-modal');
await page.waitForSelector('.modal-open');
const openResults = await new AxeBuilder({ page }).analyze();

// Test modal closed state
await page.click('#close-modal');
await page.waitForSelector('.modal-open', { state: 'hidden' });
const closedResults = await new AxeBuilder({ page }).analyze();

Results Reporting

PR Comments

Post results directly to pull requests:

- name: Post accessibility results
  uses: actions/github-script@v7
  if: always()
  with:
    script: |
      const fs = require('fs');
      const results = JSON.parse(fs.readFileSync('a11y-results.json'));

      const body = results.violations.length > 0
        ? `## Accessibility Issues Found\n\n${formatViolations(results)}`
        : '## ✅ No accessibility issues found';

      await github.rest.issues.createComment({
        owner: context.repo.owner,
        repo: context.repo.repo,
        issue_number: context.issue.number,
        body
      });

Artifact Storage

Store detailed results:

- name: Upload accessibility results
  uses: actions/upload-artifact@v4
  if: always()
  with:
    name: accessibility-results
    path: a11y-results.json

Dashboard Integration

Send results to monitoring dashboards:

- name: Report to TestParty
  run: |
    curl -X POST https://api.testparty.ai/results \
      -H "Authorization: Bearer ${{ secrets.TESTPARTY_API_KEY }}" \
      -d @a11y-results.json

Best Practices

Baseline and Improve

Don't require zero issues immediately on legacy projects:

  1. Establish baseline issue count
  2. Require no new issues
  3. Reduce threshold incrementally
  4. Celebrate progress

Focus on Changed Files

For large projects, focus on modified code:

- name: Get changed files
  id: changes
  uses: dorny/paths-filter@v2
  with:
    filters: |
      frontend:
        - 'src/**'

- name: Run accessibility tests
  if: steps.changes.outputs.frontend == 'true'
  run: npm run test:a11y

Test Critical Paths First

Prioritize high-impact pages:

  • Homepage
  • Product pages
  • Checkout flow
  • Account pages
  • Contact/support

Fail Fast

Run accessibility checks early in pipeline:

jobs:
  lint:
    # Quick checks first
  accessibility:
    needs: lint
    # Before expensive tests
  e2e:
    needs: accessibility

Enterprise Considerations

Multi-Repository Setup

Standardize across repositories:

# In shared workflow repo
# .github/workflows/accessibility-template.yml
on:
  workflow_call:
    inputs:
      urls:
        required: true
        type: string

jobs:
  accessibility:
    runs-on: ubuntu-latest
    steps:
      - uses: testparty/bouncer-action@v1
        with:
          urls: ${{ inputs.urls }}
# In application repo
jobs:
  accessibility:
    uses: org/shared-workflows/.github/workflows/accessibility-template.yml@main
    with:
      urls: |
        http://localhost:3000/
        http://localhost:3000/checkout

Secrets Management

Use organization-level secrets:

with:
  api-key: ${{ secrets.TESTPARTY_API_KEY }}  # Org secret

Reporting Aggregation

Aggregate results across repositories in TestParty Spotlight or similar dashboard.


Taking Action

CI/CD accessibility integration prevents issues from reaching production, educates developers, and enforces consistent standards. The investment in pipeline integration pays dividends in reduced remediation costs and improved accessibility outcomes.

TestParty Bouncer provides streamlined GitHub integration purpose-built for accessibility enforcement.

Schedule a TestParty demo and get a 14-day compliance implementation plan.


Related Resources

Contact Us

Automate the software work for accessibility compliance, end-to-end.

Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.

Book a Demo