Merge pull request #36 from seanmac5291/copilot/fix-35 #65
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: '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 |