Accessibility Audit #45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Accessibility Audit | |
| # Runs a full WCAG 2.1 AA accessibility audit against the live site. | |
| # Crawls https://community-access.github.io, runs axe-core WCAG 2.1 AA checks, | |
| # and creates or updates GitHub issues for each unique violation found. | |
| on: | |
| schedule: | |
| # 9:00 AM Central Time (UTC-6) = 15:00 UTC | |
| - cron: "0 15 * * *" | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| issues: write | |
| jobs: | |
| audit: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| - name: Install Playwright browsers | |
| run: npx playwright install --with-deps chromium | |
| - name: Install audit dependencies | |
| run: npm install --no-save @axe-core/playwright playwright | |
| - name: Run accessibility audit | |
| run: | | |
| mkdir -p screenshots | |
| node scripts/a11y-crawl.js https://community-access.github.io ./screenshots > accessibility-report.json 2> crawl-log.txt | |
| cat crawl-log.txt | |
| - name: Upload screenshots | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: accessibility-screenshots | |
| path: screenshots/ | |
| retention-days: 30 | |
| - name: Upload report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: accessibility-report | |
| path: accessibility-report.json | |
| retention-days: 30 | |
| - name: Upload crawl log | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: crawl-log | |
| path: crawl-log.txt | |
| retention-days: 7 | |
| - name: Push screenshots to a11y-screenshots branch | |
| if: always() | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| if ls screenshots/*.png 1>/dev/null 2>&1; then | |
| git fetch origin a11y-screenshots 2>/dev/null || true | |
| git worktree add /tmp/a11y-branch a11y-screenshots 2>/dev/null || \ | |
| git worktree add --orphan -b a11y-screenshots /tmp/a11y-branch | |
| rm -rf /tmp/a11y-branch/screenshots | |
| cp -r screenshots /tmp/a11y-branch/screenshots | |
| cd /tmp/a11y-branch | |
| git add screenshots/ | |
| git diff --cached --quiet || git commit -m "Update a11y screenshots [skip ci]" | |
| git push origin a11y-screenshots --force | |
| fi | |
| - name: Create or update GitHub issues for violations | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const screenshotBase = `https://raw.githubusercontent.com/${context.repo.owner}/${context.repo.repo}/a11y-screenshots/screenshots`; | |
| let report; | |
| try { | |
| report = JSON.parse(fs.readFileSync('accessibility-report.json', 'utf-8')); | |
| } catch (err) { | |
| console.log('No report file found or invalid JSON. Skipping issue creation.'); | |
| return; | |
| } | |
| if (!report.violations || report.violations.length === 0) { | |
| console.log('No violations found. No issues to create.'); | |
| return; | |
| } | |
| // Deduplicate: one issue per unique (ruleId, page) pair | |
| const seen = new Map(); | |
| for (const v of report.violations) { | |
| const key = `${v.ruleId}::${v.page}`; | |
| if (!seen.has(key)) { | |
| seen.set(key, { ...v, nodes: [v] }); | |
| } else { | |
| seen.get(key).nodes.push(v); | |
| } | |
| } | |
| const labels = ['accessibility', 'automated']; | |
| // Ensure labels exist | |
| for (const label of labels) { | |
| try { | |
| await github.rest.issues.getLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label, | |
| }); | |
| } catch { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label, | |
| color: label === 'accessibility' ? 'd93f0b' : '0e8a16', | |
| description: label === 'accessibility' | |
| ? 'WCAG accessibility violation' | |
| : 'Created by automated workflow', | |
| }); | |
| } | |
| } | |
| let created = 0; | |
| let updated = 0; | |
| for (const [key, violation] of seen) { | |
| const title = `[A11y] ${violation.ruleId} - ${violation.page}`; | |
| const elementsSection = violation.nodes | |
| .map((n, i) => { | |
| const screenshotUrl = n.screenshotFile | |
| ? `\n\n` | |
| : ''; | |
| return `**Element ${i + 1}:**\n\`\`\`html\n${n.html}\n\`\`\`\n**Selector:** \`${n.selector}\`${screenshotUrl}`; | |
| }) | |
| .join('\n\n'); | |
| const body = [ | |
| `### Accessibility Violation: \`${violation.ruleId}\``, | |
| '', | |
| `**Impact:** ${violation.impact}`, | |
| `**Page:** ${violation.page}`, | |
| `**URL:** ${violation.pageUrl}`, | |
| '', | |
| `**Description:** ${violation.description}`, | |
| '', | |
| `**Help:** [${violation.help}](${violation.helpUrl})`, | |
| '', | |
| '---', | |
| '', | |
| elementsSection, | |
| '', | |
| '---', | |
| '', | |
| `Full report and logs available in the [workflow run artifacts](${runUrl}).`, | |
| '', | |
| `*Last detected: ${report.auditDate}*`, | |
| '', | |
| '---', | |
| '', | |
| '> **Note:** This issue was created by an automated accessibility audit using axe-core. Automated tools like this can catch many common WCAG violations but they do not replace manual testing by human testers. Automated tools typically catch around 30-40% of real-world accessibility issues. Issues like unclear focus order, confusing screen reader announcements, cognitive load problems, and complex interaction patterns require hands-on testing with assistive technologies such as VoiceOver, NVDA, and keyboard-only navigation. Please treat this as a starting point, not a complete accessibility review.', | |
| ].join('\n'); | |
| const existing = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'accessibility', | |
| per_page: 100, | |
| }); | |
| const match = existing.data.find((issue) => issue.title === title); | |
| if (match) { | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: match.number, | |
| body: body, | |
| labels: labels, | |
| }); | |
| console.log(`Updated issue #${match.number}: ${title}`); | |
| updated++; | |
| } else { | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: title, | |
| body: body, | |
| labels: labels, | |
| }); | |
| console.log(`Created issue: ${title}`); | |
| created++; | |
| } | |
| } | |
| console.log(`Done. Created ${created} issues, updated ${updated} issues.`); |