name: 'accessibility-scanner' description: 'Finds potential accessibility gaps, files GitHub issues to track them, and attempts to fix them with Copilot.' inputs: urls: description: 'Newline-delimited list of URLs to check for accessibility issues' required: false multiline: true url_configs: description: "Stringified JSON array of URL config objects, each with a 'url' field and an optional 'excludeSelectors' field (array of CSS selectors to exclude from the Axe scan for that URL). When provided, takes precedence over the 'urls' input." required: false repository: description: 'Repository (with owner) to file issues in' required: true token: description: "Personal access token (PAT) with fine-grained permissions 'contents: write', 'issues: write', and 'pull_requests: write'" required: true base_url: description: "Optional base URL for the GitHub API (for example, 'https://HOSTNAME/api/v3' for GitHub Enterprise Server)" required: false cache_key: description: 'Key for caching results across runs' required: true login_url: description: 'If scanned pages require authentication, the URL of the login page' required: false username: description: 'If scanned pages require authentication, the username to use for login' required: false password: description: 'If scanned pages require authentication, the password to use for login' required: false auth_context: description: "If scanned pages require authentication, a stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session" required: false skip_copilot_assignment: description: 'Whether to skip assigning filed issues to Copilot' required: false default: 'false' include_screenshots: description: 'Whether to capture screenshots and include links to them in the issue' required: false default: 'false' open_grouped_issues: description: "In the 'file' step, also open grouped issues which link to all issues with the same problem" required: false default: 'false' reduced_motion: description: 'Playwright reducedMotion setting: https://playwright.dev/docs/api/class-browser#browser-new-page-option-reduced-motion' required: false color_scheme: description: 'Playwright colorScheme setting: https://playwright.dev/docs/api/class-browser#browser-new-context-option-color-scheme' required: false scans: description: 'Stringified JSON array of scans to perform. If not provided, only Axe will be performed' required: false outputs: results: description: 'List of issues and pull requests filed (and their associated finding(s)), as stringified JSON' value: ${{ steps.results.outputs.results }} results_file: description: 'Path to a JSON file containing the results (use for large datasets to avoid output size limits)' value: ${{ steps.results.outputs.results_file }} runs: using: 'composite' steps: - name: Make sub-actions referenceable working-directory: ${{ github.action_path }} shell: bash run: | RUNNER_WORK_DIR="$(realpath "${GITHUB_WORKSPACE}/../..")" ACTION_DIR="${RUNNER_WORK_DIR}/_actions/github/accessibility-scanner/current" mkdir -p "${ACTION_DIR}/.github/actions" if [ "$(realpath ".github/actions")" != "$(realpath "${ACTION_DIR}/.github/actions")" ]; then cp -a ".github/actions/." "${ACTION_DIR}/.github/actions/" fi mkdir -p "${ACTION_DIR}/.github/scanner-plugins" if [ "$(realpath ".github/scanner-plugins")" != "$(realpath "${ACTION_DIR}/.github/scanner-plugins")" ]; then cp -a ".github/scanner-plugins/." "${ACTION_DIR}/.github/scanner-plugins/." fi - name: Restore cached results id: restore uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/cache with: key: ${{ inputs.cache_key }} token: ${{ inputs.token }} - if: ${{ steps.restore.outputs.value }} name: Normalize cache format id: normalize_cache shell: bash run: | # Migrate cache format from v1 to v2: # If cached data is a list of Finding objects, each with 'issueUrl' keys (i.e. v1), # convert to a list of (partial) Result objects, each with 'findings' and 'issue' keys (i.e. v2). # Otherwise, re-output as-is. # Read directly from the cache file to avoid shell interpolation of large JSON. jq -c 'if (type == "array" and length > 0 and (.[0] | has("issueUrl"))) then map({findings: [del(.issueUrl)], issue: {url: .issueUrl}}) else . end' "${{ inputs.cache_key }}" > "$RUNNER_TEMP/cached_filings.json" echo "cached_filings_file=$RUNNER_TEMP/cached_filings.json" >> $GITHUB_OUTPUT - if: ${{ inputs.login_url && inputs.username && inputs.password && !inputs.auth_context }} name: Authenticate id: auth uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/auth with: login_url: ${{ inputs.login_url }} username: ${{ inputs.username }} password: ${{ inputs.password }} - name: Find id: find uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/find with: urls: ${{ inputs.urls }} url_configs: ${{ inputs.url_configs }} auth_context: ${{ inputs.auth_context || steps.auth.outputs.auth_context }} include_screenshots: ${{ inputs.include_screenshots }} reduced_motion: ${{ inputs.reduced_motion }} color_scheme: ${{ inputs.color_scheme }} scans: ${{ inputs.scans }} - name: File id: file uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/file with: findings_file: ${{ steps.find.outputs.findings_file }} repository: ${{ inputs.repository }} token: ${{ inputs.token }} base_url: ${{ inputs.base_url }} cached_filings_file: ${{ steps.normalize_cache.outputs.cached_filings_file }} screenshot_repository: ${{ github.repository }} open_grouped_issues: ${{ inputs.open_grouped_issues }} - if: ${{ steps.file.outputs.filings_file }} name: Get issues from filings id: get_issues_from_filings shell: bash run: | # Extract open issues from Filing objects and write to a file jq -c '[.[] | select(.issue.state == "open") | .issue]' "${{ steps.file.outputs.filings_file }}" > "$RUNNER_TEMP/issues.json" echo "issues_file=$RUNNER_TEMP/issues.json" >> "$GITHUB_OUTPUT" - if: ${{ inputs.skip_copilot_assignment != 'true' }} name: Fix id: fix uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/fix with: issues_file: ${{ steps.get_issues_from_filings.outputs.issues_file }} repository: ${{ inputs.repository }} token: ${{ inputs.token }} base_url: ${{ inputs.base_url }} - name: Set results output id: results shell: bash env: FILINGS_FILE: ${{ steps.file.outputs.filings_file }} FIXINGS_FILE: ${{ steps.fix.outputs.fixings_file }} run: | node << 'NODE_SCRIPT' const fs = require('fs'); const path = require('path'); const filingsFile = process.env.FILINGS_FILE; const fixingsFile = process.env.FIXINGS_FILE; const filings = filingsFile && fs.existsSync(filingsFile) ? JSON.parse(fs.readFileSync(filingsFile, 'utf8')) : []; const fixings = fixingsFile && fs.existsSync(fixingsFile) ? JSON.parse(fs.readFileSync(fixingsFile, 'utf8')) : []; const fixingsByIssueUrl = fixings.reduce((acc, fixing) => { if (fixing.issue && fixing.issue.url) acc[fixing.issue.url] = fixing; return acc; }, {}); for (const result of filings) { if (result.issue && result.issue.url && fixingsByIssueUrl[result.issue.url]) { result.pullRequest = fixingsByIssueUrl[result.issue.url].pullRequest; } } const resultsPath = path.join(process.env.GITHUB_WORKSPACE, 'scanner-results.json'); fs.writeFileSync(resultsPath, JSON.stringify(filings)); NODE_SCRIPT RESULTS_FILE="$GITHUB_WORKSPACE/scanner-results.json" # Set results output (backward compat) { echo 'results<<__RESULTS_OUTPUT_DELIMITER__' cat "$RESULTS_FILE" echo echo '__RESULTS_OUTPUT_DELIMITER__' } >> "$GITHUB_OUTPUT" # Set results_file output echo "results_file=$RESULTS_FILE" >> "$GITHUB_OUTPUT" - if: ${{ inputs.include_screenshots == 'true' }} name: Save screenshots uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/save with: path: .screenshots token: ${{ inputs.token }} - name: Copy results to cache path shell: bash run: | mkdir -p "$(dirname '${{ inputs.cache_key }}')" cp "$GITHUB_WORKSPACE/scanner-results.json" "${{ inputs.cache_key }}" - name: Save cached results uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/save with: path: ${{ inputs.cache_key }} token: ${{ inputs.token }} branding: icon: 'compass' color: 'blue'