ci-cd — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ci-cd (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Canonical source:examples/CI_CD_ACCESSIBILITY_BEST_PRACTICES.mdinmgifford/ACCESSIBILITY.mdThis skill is derived from that file. When in doubt, the example is authoritative.
Apply these rules when adding, reviewing, or maintaining CI/CD accessibility checks.
Every CI/CD pipeline must prevent accessibility regressions from reaching production. Automated checks are the baseline, not the ceiling — combine rule-based scanning with accessibility tree testing and, where practical, virtual screen reader testing.
Zero-Debt strategy: target 100 % Lighthouse Accessibility and Performance scores on all pages across all devices and user preferences.
| Level | Meaning |
|---|---|
| Critical | Blocks task completion entirely for one or more disability groups |
| Serious | Significantly impairs access; workaround unreasonable to expect |
| Moderate | Creates friction; workaround exists and is not too burdensome |
| Minor | Best-practice gap; marginal impact on access |
Enforce a strict score threshold. A drop to 99 % accessibility or performance must fail the build.
`.lighthouserc.js` (strict gate — use once baseline is clean):
module.exports = {
ci: {
collect: {
staticDistDir: './_site',
numberOfRuns: 1,
settings: { emulatedFormFactor: 'mobile' },
},
assert: {
assertions: {
'categories:accessibility': ['error', { minScore: 1 }],
'categories:performance': ['error', { minScore: 1 }],
},
},
},
};`.lighthouserc.json` (warn-first — use while resolving existing issues):
{
"ci": {
"collect": { "staticDistDir": "./_site", "numberOfRuns": 1 },
"assert": {
"assertions": {
"categories:accessibility": ["warn", { "minScore": 0.9 }]
}
},
"upload": { "target": "filesystem", "outputDir": ".lighthouseci" }
}
}Start with"warn"+minScore: 0.9, then tighten to"error"+minScore: 1once the existing baseline is clean.
Run axe-core via Playwright on every pull request to catch WCAG violations in dynamic content (menus, modals, theme variants).
Run the same accessibility checks in both light and dark color schemes. A PR is not fully covered unless the test suite explicitly emulates each scheme and fails on violations in either mode. If you use visual regression tests, keep the baselines separate for light and dark mode.
#### Dual light/dark mode coverage
Use one test body and run it twice, once for each color scheme:
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
for (const colorScheme of ['light', 'dark'] as const) {
test(`A11y: home page in ${colorScheme} mode`, async ({ page }) => {
await page.emulateMedia({ colorScheme });
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
}If the app has visual regression tests, keep separate baselines for each scheme. That makes theme-specific failures obvious instead of hiding them in one shared snapshot.
// tests/a11y.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
const themes = ['light', 'dark'];
for (const theme of themes) {
test(`A11y: Desktop & Mobile in ${theme} mode`, async ({ page }) => {
await page.emulateMedia({ colorScheme: theme as 'light' | 'dark' });
await page.goto('/');
const menuBtn = page.locator('#main-menu-toggle');
if (await menuBtn.isVisible()) await menuBtn.click();
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
}Missing axe-core checks on PRs is Critical — dynamic violations are invisible to Lighthouse and reach production silently. Missing one of the color-scheme passes is also a coverage gap, because theme-specific regressions often only appear in the scheme that was not exercised.
main# .github/workflows/lighthouse.yml
name: Lighthouse CI
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with: { ruby-version: "3.2", bundler-cache: true }
- run: bundle exec jekyll build
- uses: actions/setup-node@v4
with: { node-version: "22" }
- run: npm install -g @lhci/cli
- run: lhci autorunRun monthly; skip the scan when open accessibility issues already exist so developers are not flooded with duplicate noise.
# .github/workflows/accessibility-scan.yml
name: Accessibility Scan (Scheduled)
on:
schedule:
- cron: "0 0 1 * *" # first day of every month
workflow_dispatch:
permissions:
contents: write
issues: write
pull-requests: write
jobs:
accessibility-scanner:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for existing open accessibility issues
id: check_issues
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
run: |
COUNT=$(gh issue list --label "accessibility" --state open --json number --jq '. | length')
echo "count=$COUNT" >> $GITHUB_OUTPUT
- name: Run GitHub Accessibility Scanner
if: steps.check_issues.outputs.count == '0'
uses: github/accessibility-scanner@v3
with:
urls: ${{ vars.ACCESSIBILITY_SCAN_URL || format('https://{0}.github.io/{1}/', github.repository_owner, github.event.repository.name) }}
repository: ${{ github.repository }}
token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
cache_key: accessibility-scan-resultsSet the ACCESSIBILITY_SCAN_URL repository variable to override the default GitHub Pages URL. Multiple URLs can be provided as a newline-separated list.name: Deep Site Audit
on: workflow_dispatch
jobs:
crawl:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx playwright test --reporter=json > audit-report.json
- uses: actions/upload-artifact@v4
with:
name: a11y-json-report
path: audit-report.jsonAutomated WCAG rule checks verify markup compliance; they cannot verify what a screen reader actually announces. Add accessibility tree tests for complex components: SVG diagrams, custom widgets, live regions, navigation landmarks.
Assert the exact accessible name, role, and structure that assistive technologies consume — distinct from axe-core rule checks.
// tests/a11y-tree.spec.ts
import { test, expect } from '@playwright/test';
test('main navigation is correctly announced', async ({ page }) => {
await page.goto('/');
await expect(page.locator('nav[aria-label="Main navigation"]'))
.toMatchAriaSnapshot(`
- navigation "Main navigation":
- list:
- listitem:
- link "Home"
- listitem:
- link "About"
`);
});
test('SVG diagram is exposed as a labelled image', async ({ page }) => {
await page.goto('/diagrams');
await expect(page.locator('svg[role="img"]').first())
.toMatchAriaSnapshot(`
- img "User Authentication Flowchart":
`);
});Generate baseline snapshots once with --update-snapshots; treat subsequent diffs the same as visual regression diffs.
test('form controls have meaningful accessible names', async ({ page }) => {
await page.goto('/contact');
await expect(page.getByRole('textbox', { name: 'Email address' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Send message' })).toBeEnabled();
});getByRole() fails immediately when accessible names are missing or wrong — earlier feedback than a manual screen reader audit.
For asserting exact spoken output without a real screen reader installed:
import { virtual } from '@guidepup/virtual-screen-reader';
it('announces the dialog title and action buttons', async () => {
document.body.innerHTML = `
<dialog open aria-labelledby="dlg-title">
<h2 id="dlg-title">Confirm deletion</h2>
<button>Delete</button>
<button>Cancel</button>
</dialog>
`;
await virtual.start({ container: document.body });
const spoken = await virtual.spokenPhraseLog();
expect(spoken).toContain('Confirm deletion');
expect(spoken).toContain('Delete, button');
await virtual.stop();
});GitHub Actions setup for Guidepup:
- uses: guidepup/setup-action@v2
- run: npx jest tests/sr.test.tsRun audits locally before pushing — fastest feedback loop, keeps CI noise low.
`package.json` scripts:
{
"scripts": {
"test:a11y": "lhci autorun && npx playwright test",
"test:a11y:local": "lhci collect --url=http://localhost:3000 && lhci assert"
}
}Install once:
npm install -g @lhci/cli
npm install -D @playwright/test @axe-core/playwrightPrevent issues from entering commits in the first place. Catch problems in order from fastest to slowest feedback:
fail. Use pre-commit (Python) or husky + lint-staged (Node). Keep total runtime ≤ 30–60 seconds.
regressions; publish artifact links.
trend metrics over time in ACCESSIBILITY.md.
Suggested Definition of Done addition:
No UI-impacting commit is accepted unless local/pre-commit accessibility checks pass and PR CI accessibility checks are green.
Policy model:
ACCESSIBILITY.md.Close the detect → fix gap using GitHub's accessibility scanner and the Copilot coding agent:
github/accessibility-scanner@v3and creates issues labelled accessibility.
issue to a Copilot coding agent.
draft pull request linked to the original issue.
Copy examples/AGENT_REMEDIATION_WORKFLOW.yml to .github/workflows/accessibility-remediation.yml to enable this loop.
Covered violation types: image-alt, label, link-name, heading-order, color-contrast, aria-required-attr.
Requires a GitHub Copilot subscription with the coding agent feature enabled and Settings → Copilot → "Allow Copilot to create and approve pull requests" turned on.
| Approach | Finds WCAG rule violations | Finds announcement quality issues | Works for SVG / canvas | CI-friendly |
|---|---|---|---|---|
| axe-core | ✅ | ❌ | Limited | ✅ |
| Lighthouse | ✅ | ❌ | ❌ | ✅ |
| Playwright aria snapshots | Partial | ✅ | ✅ | ✅ |
| Guidepup virtual screen reader | ❌ | ✅ | ✅ | ✅ |
| Manual screen reader testing | Partial | ✅ | ✅ | ❌ |
No single tool catches everything — use the approaches together.
If an issue remains open, subsequent scheduled scans are paused (alert-fatigue guard).
main.lighthouserc score threshold set to warn ≥ 0.9 or error ≥ 1.0accessibility and converted to issuestest:a11y script documented in contributing guideaudit-report.json artifact uploaded on manual deep-crawl runACCESSIBILITY.md tableAutomation covers ~30–40 % of WCAG issues. Pair with manual and assistive technology testing to achieve full conformance.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.