Skip to content

Merge pull request #36 from seanmac5291/copilot/fix-35 #65

Merge pull request #36 from seanmac5291/copilot/fix-35

Merge pull request #36 from seanmac5291/copilot/fix-35 #65

name: 'Enterprise Accessibility Scanner'
on:
schedule:
# Run every Monday at 9:00 AM UTC (weekly)
- cron: '0 9 * * 1'
workflow_dispatch:
inputs:
target_url:
description: 'URL to scan (leave empty for default)'
required: false
type: string
standards:
description: 'Accessibility standards to test'
required: false
default: 'WCAG2AA'
type: choice
options:
- 'WCAG2A'
- 'WCAG2AA'
- 'WCAG2AAA'
- 'Section508'
- 'EN301549'
environment:
description: 'Environment to test'
required: false
default: 'production'
type: choice
options:
- 'development'
- 'staging'
- 'production'
tools:
description: 'Tools to run (comma-separated)'
required: false
default: 'axe,pa11y,lighthouse,playwright'
type: string
fail_on_issues:
description: 'Fail workflow if accessibility issues found'
required: false
default: false
type: boolean
pull_request:
branches: [main, develop]
types: [opened, synchronize, reopened]
push:
branches: [main]
# Global environment variables
env:
ACCESSIBILITY_CONFIG_PATH: '.github/accessibility-config.yml'
DEFAULT_TARGET_URL: 'https://ncaa-d1-softball.netlify.app/'
NODE_VERSION: '20'
REPORT_DIR: 'accessibility-reports'
jobs:
# Job 1: Setup and Configuration
setup:
name: 'Setup and Configuration'
runs-on: ubuntu-latest
outputs:
target-url: ${{ steps.config.outputs.target-url }}
standards: ${{ steps.config.outputs.standards }}
tools: ${{ steps.config.outputs.tools }}
environment: ${{ steps.config.outputs.environment }}
thresholds: ${{ steps.config.outputs.thresholds }}
matrix-tools: ${{ steps.config.outputs.matrix-tools }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Parse configuration and inputs
id: config
run: |
echo "Parsing accessibility configuration..."
# Set default values
TARGET_URL="${{ github.event.inputs.target_url || env.DEFAULT_TARGET_URL }}"
STANDARDS="${{ github.event.inputs.standards || 'WCAG2AA' }}"
ENVIRONMENT="${{ github.event.inputs.environment || 'production' }}"
TOOLS="${{ github.event.inputs.tools || 'axe,pa11y,lighthouse,playwright' }}"
# Override with environment-specific settings if config exists
if [ -f "$ACCESSIBILITY_CONFIG_PATH" ]; then
echo "Loading configuration from $ACCESSIBILITY_CONFIG_PATH"
# Override target URL based on environment
if [ "$ENVIRONMENT" = "development" ]; then
TARGET_URL=$(yq eval '.environments.development.target_url // env(TARGET_URL)' $ACCESSIBILITY_CONFIG_PATH)
elif [ "$ENVIRONMENT" = "staging" ]; then
TARGET_URL=$(yq eval '.environments.staging.target_url // env(TARGET_URL)' $ACCESSIBILITY_CONFIG_PATH)
elif [ "$ENVIRONMENT" = "production" ]; then
TARGET_URL=$(yq eval '.environments.production.target_url // env(TARGET_URL)' $ACCESSIBILITY_CONFIG_PATH)
fi
fi
# Create matrix for parallel execution
MATRIX_TOOLS=$(echo "$TOOLS" | sed 's/,/","/g' | sed 's/^/["/' | sed 's/$/"]/')
# Extract thresholds (will be used for quality gates)
THRESHOLDS="{}"
if [ -f "$ACCESSIBILITY_CONFIG_PATH" ]; then
THRESHOLDS=$(yq eval -o=json '.thresholds' $ACCESSIBILITY_CONFIG_PATH 2>/dev/null || echo "{}")
fi
echo "=== Configuration Summary ==="
echo "Target URL: $TARGET_URL"
echo "Standards: $STANDARDS"
echo "Environment: $ENVIRONMENT"
echo "Tools: $TOOLS"
echo "Matrix Tools: $MATRIX_TOOLS"
echo "Thresholds: $THRESHOLDS"
# Set outputs
echo "target-url=$TARGET_URL" >> $GITHUB_OUTPUT
echo "standards=$STANDARDS" >> $GITHUB_OUTPUT
echo "environment=$ENVIRONMENT" >> $GITHUB_OUTPUT
echo "tools=$TOOLS" >> $GITHUB_OUTPUT
echo "matrix-tools=$MATRIX_TOOLS" >> $GITHUB_OUTPUT
# Output thresholds as multiline string using EOF delimiter
echo "thresholds<<EOF" >> $GITHUB_OUTPUT
echo "$THRESHOLDS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Job 2: Tool Setup (runs once, cached for all scan jobs)
setup-tools:
name: 'Setup Accessibility Tools'
runs-on: ubuntu-latest
needs: setup
outputs:
tools-cache-hit: ${{ steps.setup.outputs.tools-cache-hit }}
report-dir: ${{ steps.setup.outputs.report-dir }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup accessibility tools
id: setup
uses: ./.github/actions/setup-accessibility-tools
with:
node-version: ${{ env.NODE_VERSION }}
tools: ${{ needs.setup.outputs.tools }}
playwright-browsers: 'true'
report-dir: ${{ env.REPORT_DIR }}
# Job 3: Parallel Accessibility Scans (Matrix Strategy)
scan:
name: 'Accessibility Scan'
runs-on: ubuntu-latest
needs: [setup, setup-tools]
strategy:
matrix:
tool: ${{ fromJson(needs.setup.outputs.matrix-tools) }}
fail-fast: false # Continue other tools even if one fails
max-parallel: 4 # Run up to 4 tools in parallel
outputs:
axe-violations: ${{ steps.axe-scan.outputs.violations-count }}
axe-status: ${{ steps.axe-scan.outputs.scan-status }}
pa11y-issues: ${{ steps.pa11y-scan.outputs.issues-count }}
pa11y-status: ${{ steps.pa11y-scan.outputs.scan-status }}
lighthouse-desktop-score: ${{ steps.lighthouse-scan.outputs.desktop-score }}
lighthouse-mobile-score: ${{ steps.lighthouse-scan.outputs.mobile-score }}
lighthouse-status: ${{ steps.lighthouse-scan.outputs.scan-status }}
playwright-violations: ${{ steps.playwright-scan.outputs.violations-count }}
playwright-status: ${{ steps.playwright-scan.outputs.scan-status }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup accessibility tools
uses: ./.github/actions/setup-accessibility-tools
with:
node-version: ${{ env.NODE_VERSION }}
tools: ${{ matrix.tool }}
playwright-browsers: ${{ matrix.tool == 'playwright' }}
report-dir: ${{ env.REPORT_DIR }}
# Conditional tool execution based on matrix
- name: Run Axe scan
if: matrix.tool == 'axe'
id: axe-scan
uses: ./.github/actions/axe-scan
with:
target-url: ${{ needs.setup.outputs.target-url }}
standards: 'wcag2a,wcag2aa,wcag21aa'
timeout: 30000
report-dir: ${{ env.REPORT_DIR }}
fail-on-violations: false
- name: Run Pa11y scan
if: matrix.tool == 'pa11y'
id: pa11y-scan
uses: ./.github/actions/pa11y-scan
with:
target-url: ${{ needs.setup.outputs.target-url }}
standard: ${{ needs.setup.outputs.standards }}
timeout: 30000
reporters: 'json,html,csv'
report-dir: ${{ env.REPORT_DIR }}
fail-on-issues: false
- name: Run Lighthouse scan
if: matrix.tool == 'lighthouse'
id: lighthouse-scan
uses: ./.github/actions/lighthouse-scan
with:
target-url: ${{ needs.setup.outputs.target-url }}
form-factors: 'desktop,mobile'
timeout: 30000
report-dir: ${{ env.REPORT_DIR }}
fail-on-score: false
- name: Run Playwright scan
if: matrix.tool == 'playwright'
id: playwright-scan
uses: ./.github/actions/playwright-scan
with:
target-url: ${{ needs.setup.outputs.target-url }}
browsers: 'chromium'
workers: 1
timeout: 30000
report-dir: ${{ env.REPORT_DIR }}
fail-on-violations: false
- name: Run additional accessibility scans
if: matrix.tool == 'playwright'
run: |
echo "Running additional accessibility scans..."
# Create keyboard navigation test script
cat > keyboard-test.cjs << 'EOF'
const puppeteer = require('puppeteer');
const { writeFileSync } = require('fs');
async function testKeyboardNavigation() {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-dev-shm-usage']
});
const page = await browser.newPage();
const issues = [];
try {
const targetUrl = process.env.TARGET_URL || '${{ needs.setup.outputs.target-url }}';
console.log(`Testing keyboard navigation on: ${targetUrl}`);
await page.goto(targetUrl, { waitUntil: 'networkidle0', timeout: 30000 });
// Test tab navigation
console.log('Testing focusable elements...');
const focusableElements = await page.$$eval('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', elements => {
return elements.map(el => ({
tagName: el.tagName,
id: el.id,
className: el.className,
tabIndex: el.tabIndex,
textContent: el.textContent ? el.textContent.trim().substring(0, 50) : ''
}));
});
console.log(`Found ${focusableElements.length} focusable elements`);
// Test each focusable element (limit to 20 for performance)
const elementsToTest = Math.min(focusableElements.length, 20);
for (let i = 0; i < elementsToTest; i++) {
await page.keyboard.press('Tab');
const activeElement = await page.evaluate(() => {
const el = document.activeElement;
if (!el) return null;
const computedStyle = window.getComputedStyle(el);
const pseudoStyle = window.getComputedStyle(el, ':focus');
return {
tagName: el.tagName,
id: el.id,
className: el.className,
textContent: el.textContent ? el.textContent.trim().substring(0, 30) : '',
hasVisibleFocus: (
computedStyle.outline !== 'none' ||
computedStyle.outlineWidth !== '0px' ||
computedStyle.outlineStyle !== 'none' ||
computedStyle.boxShadow !== 'none' ||
pseudoStyle.outline !== 'none' ||
pseudoStyle.outlineWidth !== '0px' ||
pseudoStyle.boxShadow !== 'none'
)
};
});
if (activeElement && !activeElement.hasVisibleFocus) {
issues.push({
type: 'keyboard-navigation',
severity: 'moderate',
message: `Element ${activeElement.tagName} lacks visible focus indicator`,
element: activeElement
});
}
}
// Test skip links
console.log('Testing skip links...');
const skipLinks = await page.$$eval('a[href^="#"]', links => {
return links.filter(link => {
const text = link.textContent.toLowerCase();
return text.includes('skip') || text.includes('main');
}).length;
});
if (skipLinks === 0) {
issues.push({
type: 'keyboard-navigation',
severity: 'moderate',
message: 'No skip links found for keyboard navigation',
element: null
});
}
} catch (error) {
console.error('Keyboard navigation test error:', error);
issues.push({
type: 'keyboard-navigation',
severity: 'critical',
message: `Keyboard navigation test failed: ${error.message}`,
element: null
});
}
await browser.close();
writeFileSync('${{ env.REPORT_DIR }}/keyboard-navigation.json',
JSON.stringify(issues, null, 2));
console.log(`Keyboard navigation test completed. Found ${issues.length} issues.`);
return issues.length;
}
testKeyboardNavigation().then(count => {
console.log(`Found ${count} keyboard navigation issues`);
process.exit(0);
}).catch(err => {
console.error('Keyboard test error:', err);
process.exit(1);
});
EOF
# Create screen reader simulation test
cat > screen-reader-test.cjs << 'EOF'
const puppeteer = require('puppeteer');
const { writeFileSync } = require('fs');
async function testScreenReaderCompatibility() {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-dev-shm-usage']
});
const page = await browser.newPage();
const issues = [];
try {
const targetUrl = process.env.TARGET_URL || '${{ needs.setup.outputs.target-url }}';
console.log(`Testing screen reader compatibility on: ${targetUrl}`);
await page.goto(targetUrl, { waitUntil: 'networkidle0', timeout: 30000 });
// Test for main landmark
const landmarks = await page.$$eval('[role="main"], main', els => els.length);
if (landmarks === 0) {
issues.push({
type: 'screen-reader',
severity: 'moderate',
message: 'No main landmark found for screen reader navigation',
count: 1
});
}
// Test heading structure
const headings = await page.$$eval('h1, h2, h3, h4, h5, h6', headings => {
return headings.map(h => ({
tagName: h.tagName,
text: h.textContent.trim().substring(0, 50),
level: parseInt(h.tagName[1])
}));
});
if (headings.length === 0) {
issues.push({
type: 'screen-reader',
severity: 'moderate',
message: 'No heading structure found for screen reader navigation',
count: 1
});
} else {
// Check heading hierarchy
let previousLevel = 0;
for (const heading of headings) {
if (heading.level > previousLevel + 1) {
issues.push({
type: 'screen-reader',
severity: 'moderate',
message: `Heading hierarchy skip detected (h${previousLevel} to h${heading.level})`,
count: 1
});
break;
}
previousLevel = heading.level;
}
}
// Test for ARIA landmarks
const ariaLandmarks = await page.$$eval('[role="navigation"], [role="banner"], [role="contentinfo"], nav, header, footer', els => els.length);
if (ariaLandmarks === 0) {
issues.push({
type: 'screen-reader',
severity: 'moderate',
message: 'No ARIA landmarks found for screen reader navigation',
count: 1
});
}
// Test page title
const pageTitle = await page.title();
if (!pageTitle || pageTitle.trim() === '') {
issues.push({
type: 'screen-reader',
severity: 'moderate',
message: 'Page missing descriptive title',
count: 1
});
}
// Test language attribute
const langAttribute = await page.evaluate(() => {
return document.documentElement.getAttribute('lang');
});
if (!langAttribute) {
issues.push({
type: 'screen-reader',
severity: 'moderate',
message: 'HTML element missing lang attribute',
count: 1
});
}
} catch (error) {
console.error('Screen reader test error:', error);
issues.push({
type: 'screen-reader',
severity: 'critical',
message: `Screen reader test failed: ${error.message}`,
count: 1
});
}
await browser.close();
writeFileSync('${{ env.REPORT_DIR }}/screen-reader.json',
JSON.stringify(issues, null, 2));
console.log(`Screen reader test completed. Found ${issues.length} issues.`);
return issues.length;
}
testScreenReaderCompatibility().then(count => {
console.log(`Found ${count} screen reader issues`);
process.exit(0);
}).catch(err => {
console.error('Screen reader test error:', err);
process.exit(1);
});
EOF
# Install puppeteer for the additional tests
npm install puppeteer --save-dev || echo "Puppeteer installation failed, skipping additional tests"
# Run the additional tests
echo "Running keyboard navigation test..."
node keyboard-test.cjs || echo "Keyboard test failed"
echo "Running screen reader compatibility test..."
node screen-reader-test.cjs || echo "Screen reader test failed"
# Upload individual tool reports as artifacts
- name: Upload scan reports
uses: actions/upload-artifact@v4
if: always()
with:
name: accessibility-scan-${{ matrix.tool }}
path: ${{ env.REPORT_DIR }}/
retention-days: 30
# Job 4: Quality Gates and Aggregation
quality-gates:
name: 'Quality Gates & Aggregation'
runs-on: ubuntu-latest
needs: [setup, scan]
if: always() # Run even if some scans failed
outputs:
total-issues: ${{ steps.aggregate.outputs.total-issues }}
overall-status: ${{ steps.aggregate.outputs.overall-status }}
quality-gate-passed: ${{ steps.quality-gates.outputs.passed }}
dashboard-path: ${{ steps.aggregate.outputs.dashboard-path }}
summary-path: ${{ steps.aggregate.outputs.summary-path }}
pr-comment-path: ${{ steps.aggregate.outputs.pr-comment-path }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Download all scan artifacts
- name: Download scan reports
uses: actions/download-artifact@v4
with:
path: downloaded-reports
pattern: accessibility-scan-*
merge-multiple: true
- name: Consolidate reports
run: |
echo "Consolidating scan reports..."
mkdir -p ${{ env.REPORT_DIR }}
# Move all downloaded reports to the main report directory
if [ -d "downloaded-reports" ]; then
find downloaded-reports -type f -exec cp {} ${{ env.REPORT_DIR }}/ \;
fi
echo "Report directory contents:"
ls -la ${{ env.REPORT_DIR }}/
# Extract results from scan outputs (with fallbacks for failed scans)
- name: Extract scan results
id: extract
run: |
echo "Extracting scan results..."
# Initialize with defaults
AXE_VIOLATIONS=0
PA11Y_ISSUES=0
LIGHTHOUSE_DESKTOP=0
LIGHTHOUSE_MOBILE=0
PLAYWRIGHT_VIOLATIONS=0
# Extract from report files if they exist
if [ -f "${{ env.REPORT_DIR }}/axe-report.json" ]; then
AXE_VIOLATIONS=$(jq 'if type=="array" then .[0].violations else .violations end | length' ${{ env.REPORT_DIR }}/axe-report.json 2>/dev/null || echo "0")
fi
if [ -f "${{ env.REPORT_DIR }}/pa11y-report.json" ]; then
PA11Y_ISSUES=$(jq 'length' ${{ env.REPORT_DIR }}/pa11y-report.json 2>/dev/null || echo "0")
fi
if [ -f "${{ env.REPORT_DIR }}/lighthouse-accessibility-desktop.json" ]; then
LIGHTHOUSE_DESKTOP=$(jq '.categories.accessibility.score * 100' ${{ env.REPORT_DIR }}/lighthouse-accessibility-desktop.json 2>/dev/null || echo "0")
fi
if [ -f "${{ env.REPORT_DIR }}/lighthouse-accessibility-mobile.json" ]; then
LIGHTHOUSE_MOBILE=$(jq '.categories.accessibility.score * 100' ${{ env.REPORT_DIR }}/lighthouse-accessibility-mobile.json 2>/dev/null || echo "0")
fi
if [ -f "${{ env.REPORT_DIR }}/playwright-results.json" ]; then
PLAYWRIGHT_VIOLATIONS=$(jq '[.suites[].specs[].tests[] | select(.results[0].status == "failed")] | length' ${{ env.REPORT_DIR }}/playwright-results.json 2>/dev/null || echo "0")
fi
echo "=== Extracted Results ==="
echo "Axe violations: $AXE_VIOLATIONS"
echo "Pa11y issues: $PA11Y_ISSUES"
echo "Lighthouse desktop: $LIGHTHOUSE_DESKTOP%"
echo "Lighthouse mobile: $LIGHTHOUSE_MOBILE%"
echo "Playwright violations: $PLAYWRIGHT_VIOLATIONS"
# Set outputs for next steps
echo "axe-violations=$AXE_VIOLATIONS" >> $GITHUB_OUTPUT
echo "pa11y-issues=$PA11Y_ISSUES" >> $GITHUB_OUTPUT
echo "lighthouse-desktop=$LIGHTHOUSE_DESKTOP" >> $GITHUB_OUTPUT
echo "lighthouse-mobile=$LIGHTHOUSE_MOBILE" >> $GITHUB_OUTPUT
echo "playwright-violations=$PLAYWRIGHT_VIOLATIONS" >> $GITHUB_OUTPUT
# Apply quality gates based on thresholds
- name: Apply quality gates
id: quality-gates
run: |
echo "Applying quality gates..."
# Get results
AXE_VIOLATIONS=${{ steps.extract.outputs.axe-violations }}
PA11Y_ISSUES=${{ steps.extract.outputs.pa11y-issues }}
LIGHTHOUSE_DESKTOP=${{ steps.extract.outputs.lighthouse-desktop }}
LIGHTHOUSE_MOBILE=${{ steps.extract.outputs.lighthouse-mobile }}
PLAYWRIGHT_VIOLATIONS=${{ steps.extract.outputs.playwright-violations }}
# Default thresholds (can be overridden by config)
MAX_CRITICAL_VIOLATIONS=0
MAX_SERIOUS_VIOLATIONS=5
MIN_LIGHTHOUSE_SCORE=90
MAX_PA11Y_ISSUES=5
MAX_PLAYWRIGHT_VIOLATIONS=3
# Calculate average Lighthouse score
if [ "$LIGHTHOUSE_DESKTOP" != "0" ] && [ "$LIGHTHOUSE_MOBILE" != "0" ]; then
AVG_LIGHTHOUSE=$(echo "($LIGHTHOUSE_DESKTOP + $LIGHTHOUSE_MOBILE) / 2" | bc -l)
elif [ "$LIGHTHOUSE_DESKTOP" != "0" ]; then
AVG_LIGHTHOUSE=$LIGHTHOUSE_DESKTOP
elif [ "$LIGHTHOUSE_MOBILE" != "0" ]; then
AVG_LIGHTHOUSE=$LIGHTHOUSE_MOBILE
else
AVG_LIGHTHOUSE=0
fi
# Check thresholds
QUALITY_GATE_PASSED=true
FAILURES=()
if [ "$AXE_VIOLATIONS" -gt "$MAX_CRITICAL_VIOLATIONS" ]; then
QUALITY_GATE_PASSED=false
FAILURES+=("Axe violations ($AXE_VIOLATIONS) exceed threshold ($MAX_CRITICAL_VIOLATIONS)")
fi
if [ "$PA11Y_ISSUES" -gt "$MAX_PA11Y_ISSUES" ]; then
QUALITY_GATE_PASSED=false
FAILURES+=("Pa11y issues ($PA11Y_ISSUES) exceed threshold ($MAX_PA11Y_ISSUES)")
fi
if [ "$(echo "$AVG_LIGHTHOUSE < $MIN_LIGHTHOUSE_SCORE" | bc -l)" -eq "1" ] && [ "$AVG_LIGHTHOUSE" != "0" ]; then
QUALITY_GATE_PASSED=false
FAILURES+=("Lighthouse score ($AVG_LIGHTHOUSE) below threshold ($MIN_LIGHTHOUSE_SCORE)")
fi
if [ "$PLAYWRIGHT_VIOLATIONS" -gt "$MAX_PLAYWRIGHT_VIOLATIONS" ]; then
QUALITY_GATE_PASSED=false
FAILURES+=("Playwright violations ($PLAYWRIGHT_VIOLATIONS) exceed threshold ($MAX_PLAYWRIGHT_VIOLATIONS)")
fi
echo "=== Quality Gate Results ==="
echo "Quality Gate Passed: $QUALITY_GATE_PASSED"
if [ "$QUALITY_GATE_PASSED" = "false" ]; then
echo "❌ Quality gate failures:"
for failure in "${FAILURES[@]}"; do
echo " - $failure"
done
else
echo "✅ All quality gates passed"
fi
echo "passed=$QUALITY_GATE_PASSED" >> $GITHUB_OUTPUT
# Generate comprehensive reports
- name: Generate accessibility reports
id: aggregate
uses: ./.github/actions/generate-accessibility-report
with:
report-dir: ${{ env.REPORT_DIR }}
target-url: ${{ needs.setup.outputs.target-url }}
standards: ${{ needs.setup.outputs.standards }}
scan-tools: ${{ needs.setup.outputs.tools }}
axe-violations: ${{ steps.extract.outputs.axe-violations }}
pa11y-issues: ${{ steps.extract.outputs.pa11y-issues }}
lighthouse-desktop-score: ${{ steps.extract.outputs.lighthouse-desktop }}
lighthouse-mobile-score: ${{ steps.extract.outputs.lighthouse-mobile }}
playwright-violations: ${{ steps.extract.outputs.playwright-violations }}
workflow-run-id: ${{ github.run_id }}
workflow-run-number: ${{ github.run_number }}
generate-dashboard: 'true'
generate-summary: 'true'
generate-pr-comment: 'true'
# Upload comprehensive reports
- name: Upload accessibility reports
uses: actions/upload-artifact@v4
if: always()
with:
name: accessibility-evaluation-comprehensive
path: ${{ env.REPORT_DIR }}/
retention-days: 90
# Fail workflow if quality gates failed and fail-on-issues is true
- name: Check quality gate enforcement
if: github.event.inputs.fail_on_issues == 'true' && steps.quality-gates.outputs.passed == 'false'
run: |
echo "❌ Quality gates failed and fail_on_issues is enabled"
echo "Total issues found: ${{ steps.aggregate.outputs.total-issues }}"
echo "This workflow will fail to enforce accessibility standards."
exit 1
# Job 5: PR Comment (only for pull requests)
pr-comment:
name: 'Update PR Comment'
runs-on: ubuntu-latest
needs: [quality-gates]
if: github.event_name == 'pull_request'
permissions:
pull-requests: write
steps:
- name: Download reports
uses: actions/download-artifact@v4
with:
name: accessibility-evaluation-comprehensive
path: ${{ env.REPORT_DIR }}
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = './${{ env.REPORT_DIR }}/pr-comment.md';
if (fs.existsSync(path)) {
const comment = fs.readFileSync(path, 'utf8');
// Find existing comment to update
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existingComment = comments.find(comment =>
comment.body.includes('🔍 Accessibility Scan Results')
);
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: comment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}
}
# Job 6: Workflow Summary
summary:
name: 'Workflow Summary'
runs-on: ubuntu-latest
needs: [setup, quality-gates]
if: always()
steps:
- name: Download reports
uses: actions/download-artifact@v4
with:
name: accessibility-evaluation-comprehensive
path: ${{ env.REPORT_DIR }}
- name: Generate workflow summary
run: |
echo "# 🔍 Enterprise Accessibility Scan Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Target URL:** ${{ needs.setup.outputs.target-url }}" >> $GITHUB_STEP_SUMMARY
echo "**Standards:** ${{ needs.setup.outputs.standards }}" >> $GITHUB_STEP_SUMMARY
echo "**Environment:** ${{ needs.setup.outputs.environment }}" >> $GITHUB_STEP_SUMMARY
echo "**Tools:** ${{ needs.setup.outputs.tools }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Extract values for detailed breakdown
AXE_VIOLATIONS=${{ needs.quality-gates.outputs.total-issues }}
if [ -f "${{ env.REPORT_DIR }}/axe-report.json" ]; then
AXE_VIOLATIONS=$(jq 'if type=="array" then .[0].violations else .violations end | length' ${{ env.REPORT_DIR }}/axe-report.json 2>/dev/null || echo "0")
fi
PA11Y_ISSUES=0
if [ -f "${{ env.REPORT_DIR }}/pa11y-report.json" ]; then
PA11Y_ISSUES=$(jq 'length' ${{ env.REPORT_DIR }}/pa11y-report.json 2>/dev/null || echo "0")
fi
KEYBOARD_ISSUES=0
if [ -f "${{ env.REPORT_DIR }}/keyboard-navigation.json" ]; then
KEYBOARD_ISSUES=$(jq 'length' ${{ env.REPORT_DIR }}/keyboard-navigation.json 2>/dev/null || echo "0")
fi
SCREENREADER_ISSUES=0
if [ -f "${{ env.REPORT_DIR }}/screen-reader.json" ]; then
SCREENREADER_ISSUES=$(jq 'length' ${{ env.REPORT_DIR }}/screen-reader.json 2>/dev/null || echo "0")
fi
LIGHTHOUSE_SCORE=100
if [ -f "${{ env.REPORT_DIR }}/lighthouse-accessibility-desktop.json" ]; then
LIGHTHOUSE_SCORE=$(jq '.categories.accessibility.score * 100' ${{ env.REPORT_DIR }}/lighthouse-accessibility-desktop.json 2>/dev/null || echo "100")
fi
# Calculate structural issues from Lighthouse
STRUCTURAL_ISSUES=0
if [ -f "${{ env.REPORT_DIR }}/lighthouse-accessibility-desktop.json" ]; then
# Check for missing main landmark
if jq -r '.audits["landmark-one-main"].score' ${{ env.REPORT_DIR }}/lighthouse-accessibility-desktop.json 2>/dev/null | grep -q "null\|0"; then
STRUCTURAL_ISSUES=$((STRUCTURAL_ISSUES + 1))
fi
# Check for missing skip navigation
if jq -r '.audits.bypass.score' ${{ env.REPORT_DIR }}/lighthouse-accessibility-desktop.json 2>/dev/null | grep -q "null\|0"; then
STRUCTURAL_ISSUES=$((STRUCTURAL_ISSUES + 1))
fi
fi
TOTAL_ISSUES=$((AXE_VIOLATIONS + PA11Y_ISSUES + KEYBOARD_ISSUES + SCREENREADER_ISSUES + STRUCTURAL_ISSUES))
# Generate comprehensive summary matching the comprehensive workflow format
echo "**Scan completed:** $(date)" >> $GITHUB_STEP_SUMMARY
echo "**Workflow Run:** ${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Critical issues section
if [ "$AXE_VIOLATIONS" -gt "0" ]; then
echo "🚨 **CRITICAL:** $AXE_VIOLATIONS WCAG violations require immediate fixes" >> $GITHUB_STEP_SUMMARY
else
echo "✅ **WCAG Compliance:** No critical violations found" >> $GITHUB_STEP_SUMMARY
fi
# High priority sections
if [ "$STRUCTURAL_ISSUES" -gt "0" ]; then
echo "🏗️ **HIGH PRIORITY:** $STRUCTURAL_ISSUES structural foundation issues" >> $GITHUB_STEP_SUMMARY
fi
echo "💡 **Lighthouse Score:** $LIGHTHOUSE_SCORE%" >> $GITHUB_STEP_SUMMARY
if [ "$KEYBOARD_ISSUES" -gt "0" ]; then
echo "⌨️ **HIGH PRIORITY:** $KEYBOARD_ISSUES keyboard navigation issues" >> $GITHUB_STEP_SUMMARY
fi
if [ "$SCREENREADER_ISSUES" -gt "0" ]; then
echo "🔊 **HIGH PRIORITY:** $SCREENREADER_ISSUES screen reader compatibility issues" >> $GITHUB_STEP_SUMMARY
fi
# Medium priority
if [ "$PA11Y_ISSUES" -gt "0" ]; then
echo "📝 **MEDIUM PRIORITY:** $PA11Y_ISSUES content and markup issues" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "## 📊 Comprehensive Issue Breakdown" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Priority | Category | Issues | Impact |" >> $GITHUB_STEP_SUMMARY
echo "|----------|----------|--------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| 🚨 Critical | WCAG Violations | $AXE_VIOLATIONS | Blocks users with disabilities |" >> $GITHUB_STEP_SUMMARY
echo "| 🏗️ High | Structural Issues | $STRUCTURAL_ISSUES | Affects screen reader navigation |" >> $GITHUB_STEP_SUMMARY
echo "| ⌨️ High | Keyboard Navigation | $KEYBOARD_ISSUES | Blocks keyboard-only users |" >> $GITHUB_STEP_SUMMARY
echo "| 🔊 High | Screen Reader Support | $SCREENREADER_ISSUES | Impacts assistive technology |" >> $GITHUB_STEP_SUMMARY
echo "| 📝 Medium | Content & Markup | $PA11Y_ISSUES | General accessibility quality |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Total Issues**: $TOTAL_ISSUES" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Generate action plan
if [ "$TOTAL_ISSUES" -eq "0" ]; then
# Success case
echo "🎉 **CONGRATULATIONS!** Your app passes all accessibility tests!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ **WCAG 2.1 AA Compliance**: No violations detected" >> $GITHUB_STEP_SUMMARY
echo "✅ **Keyboard Navigation**: All elements accessible" >> $GITHUB_STEP_SUMMARY
echo "✅ **Screen Reader Support**: Proper structure and labels" >> $GITHUB_STEP_SUMMARY
echo "✅ **Content Quality**: No markup issues found" >> $GITHUB_STEP_SUMMARY
echo "✅ **Structural Foundation**: Landmarks and navigation present" >> $GITHUB_STEP_SUMMARY
else
# Issues found case
echo "## 🎯 Priority-Based Action Plan" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Phase 1: Critical WCAG Fixes
if [ "$AXE_VIOLATIONS" -gt "0" ]; then
echo "### 🚨 Phase 1: Critical WCAG Fixes (⏰ 2-3 hours MANUAL)" >> $GITHUB_STEP_SUMMARY
echo "- **$AXE_VIOLATIONS WCAG violations** must be fixed immediately" >> $GITHUB_STEP_SUMMARY
echo "- These directly violate accessibility standards" >> $GITHUB_STEP_SUMMARY
echo "- See axe-report.json for specific elements and fixes" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
# Phase 2: High Priority Issues
HIGH_PRIORITY_COUNT=$((STRUCTURAL_ISSUES + KEYBOARD_ISSUES + SCREENREADER_ISSUES))
if [ "$HIGH_PRIORITY_COUNT" -gt "0" ]; then
echo "### 🏗️ Phase 2: High Priority Issues (⏰ 2-4 hours MANUAL)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Structural Foundation
if [ "$STRUCTURAL_ISSUES" -gt "0" ]; then
echo "**Structural Foundation ($STRUCTURAL_ISSUES issues):**" >> $GITHUB_STEP_SUMMARY
echo "- Add \`<main>\` landmark to App.jsx" >> $GITHUB_STEP_SUMMARY
echo "- Add skip navigation for keyboard users" >> $GITHUB_STEP_SUMMARY
echo "- Improves screen reader page navigation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
# Keyboard Navigation
if [ "$KEYBOARD_ISSUES" -gt "0" ]; then
echo "**Keyboard Navigation ($KEYBOARD_ISSUES issues):**" >> $GITHUB_STEP_SUMMARY
echo "- Add visible focus indicators to interactive elements" >> $GITHUB_STEP_SUMMARY
echo "- Ensure all buttons respond to Enter/Space keys" >> $GITHUB_STEP_SUMMARY
echo "- Test tab order flows logically through content" >> $GITHUB_STEP_SUMMARY
echo "- Critical for users who cannot use a mouse" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
# Screen Reader Support
if [ "$SCREENREADER_ISSUES" -gt "0" ]; then
echo "**Screen Reader Support ($SCREENREADER_ISSUES issues):**" >> $GITHUB_STEP_SUMMARY
echo "- Fix heading hierarchy (h1 → h2 → h3)" >> $GITHUB_STEP_SUMMARY
echo "- Add alt text to images" >> $GITHUB_STEP_SUMMARY
echo "- Add proper form labels" >> $GITHUB_STEP_SUMMARY
echo "- Add ARIA landmarks for page regions" >> $GITHUB_STEP_SUMMARY
echo "- Essential for blind and low-vision users" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
fi
# Phase 3: Content Quality
if [ "$PA11Y_ISSUES" -gt "0" ]; then
echo "### 📝 Phase 3: Content Quality (⏰ 2-4 hours MANUAL)" >> $GITHUB_STEP_SUMMARY
echo "- **$PA11Y_ISSUES content and markup issues detected**" >> $GITHUB_STEP_SUMMARY
echo "- Review pa11y-report.html for specific element locations" >> $GITHUB_STEP_SUMMARY
echo "- Focus areas: images, headings, and form elements" >> $GITHUB_STEP_SUMMARY
echo "- Impacts overall accessibility quality score" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
# Phase 4: Validation
echo "### ✅ Phase 4: Validation (⏰ 30 minutes)" >> $GITHUB_STEP_SUMMARY
echo "- Re-run this accessibility workflow" >> $GITHUB_STEP_SUMMARY
echo "- Test keyboard navigation manually (Tab through page)" >> $GITHUB_STEP_SUMMARY
echo "- Test with screen reader if possible (NVDA/VoiceOver)" >> $GITHUB_STEP_SUMMARY
echo "- Verify all issues are resolved" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Automated analysis section
echo "---" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## 📊 Automated Analysis Available" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Calculate time savings
TOTAL_MANUAL_HOURS=8
AUTOMATED_HOURS=1
TIME_SAVED=$((TOTAL_MANUAL_HOURS - AUTOMATED_HOURS))
EFFICIENCY_GAIN=$((TIME_SAVED * 100 / TOTAL_MANUAL_HOURS))
echo "### ⚡ Automated Implementation Option" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Leverage automated tools for implementation:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "1. **Download** the \`accessibility-evaluation-comprehensive.zip\` artifact" >> $GITHUB_STEP_SUMMARY
echo "2. **Review** the detailed analysis in \`accessibility-analysis.md\`" >> $GITHUB_STEP_SUMMARY
echo "3. **Create Issue for Tracking:**" >> $GITHUB_STEP_SUMMARY
echo " - Go to [Issues tab](../../issues/new)" >> $GITHUB_STEP_SUMMARY
echo " - Title: \`📊 Accessibility Analysis - $TOTAL_ISSUES issues identified\`" >> $GITHUB_STEP_SUMMARY
echo " - Copy the comprehensive analysis report" >> $GITHUB_STEP_SUMMARY
echo "4. **Implement Solutions:**" >> $GITHUB_STEP_SUMMARY
echo " - Use automated tools and agents" >> $GITHUB_STEP_SUMMARY
echo " - Follow the prioritized implementation plan" >> $GITHUB_STEP_SUMMARY
echo " - Validate changes with this workflow" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### ⏱️ Time Comparison" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Approach | Time Required | Efficiency |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------------|------------|" >> $GITHUB_STEP_SUMMARY
echo "| 👨‍💻 **Manual Fixes** | $TOTAL_MANUAL_HOURS hours | Baseline |" >> $GITHUB_STEP_SUMMARY
echo "| 🤖 **Copilot SWE Agent** | $COPILOT_HOURS hour | **${EFFICIENCY_GAIN}% faster** |" >> $GITHUB_STEP_SUMMARY
echo "| 💰 **Time Saved** | **$TIME_SAVED hours** | **Ready in minutes** |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🎯 What Copilot SWE Will Do" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **Add main landmark** and skip navigation to App.jsx" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **Fix heading hierarchy** across all components" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **Add focus indicators** and keyboard navigation" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **Implement ARIA labels** and semantic structure" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **Add alt text** to images and fix color contrast" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **Structure data tables** with proper headers" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **Test and validate** all changes automatically" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Result:** Professional accessibility implementation in ~1 hour vs $TOTAL_MANUAL_HOURS hours manual work" >> $GITHUB_STEP_SUMMARY
fi