From cc8e4e25f38440ad2ee5e60bf9e975f6103f3ffd Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 14:08:54 -0700 Subject: [PATCH 1/9] Classify Axe findings by conformance tier --- .github/actions/find/src/findForUrl.ts | 11 +++++- .github/actions/find/src/types.d.ts | 3 ++ .github/actions/find/tests/findForUrl.test.ts | 36 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index d9f1ea87..ea4f1e18 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -1,4 +1,4 @@ -import type {ColorSchemePreference, Finding, ReducedMotionPreference, UrlConfig} from './types.d.js' +import type {ColorSchemePreference, Finding, FindingCategory, ReducedMotionPreference, UrlConfig} from './types.d.js' import {AxeBuilder} from '@axe-core/playwright' import playwright from 'playwright' import {AuthContext} from './AuthContext.js' @@ -87,6 +87,7 @@ async function runAxeScan({ for (const violation of rawFindings.violations) { await addFinding({ scannerType: 'axe', + category: categorizeAxeViolation(violation.tags), url, html: violation.nodes[0].html.replace(/'/g, '''), problemShort: violation.help.toLowerCase().replace(/'/g, '''), @@ -98,3 +99,11 @@ async function runAxeScan({ } } } + +// Maps an Axe violation's tags to a conformance tier. Experimental is checked +// first because some experimental rules also carry a wcag* tag. +function categorizeAxeViolation(tags: string[]): FindingCategory { + if (tags.includes('experimental')) return 'experimental' + if (tags.includes('best-practice')) return 'best-practice' + return 'wcag' +} diff --git a/.github/actions/find/src/types.d.ts b/.github/actions/find/src/types.d.ts index dcbc8600..a2f4a534 100644 --- a/.github/actions/find/src/types.d.ts +++ b/.github/actions/find/src/types.d.ts @@ -1,5 +1,8 @@ +export type FindingCategory = 'wcag' | 'best-practice' | 'experimental' + export type Finding = { scannerType: string + category?: FindingCategory url: string html?: string problemShort: string diff --git a/.github/actions/find/tests/findForUrl.test.ts b/.github/actions/find/tests/findForUrl.test.ts index 85299c5c..54244885 100644 --- a/.github/actions/find/tests/findForUrl.test.ts +++ b/.github/actions/find/tests/findForUrl.test.ts @@ -117,4 +117,40 @@ describe('findForUrl', () => { expect(loadedPlugins[1].default).toHaveBeenCalledTimes(0) }) }) + + describe('axe finding categorization', () => { + function axeViolation(tags: string[]) { + return { + id: 'some-rule', + help: 'Help', + helpUrl: 'https://example.com', + description: 'Description', + tags, + nodes: [{html: '
', failureSummary: 'summary'}], + } + } + + async function categoryFor(tags: string[]) { + clearAll() + actionInput = JSON.stringify(['axe']) + vi.mocked(AxeBuilder.prototype.analyze).mockResolvedValueOnce({ + violations: [axeViolation(tags)], + } as unknown as axe.AxeResults) + + const findings = await findForUrl('test.com') + return findings[0].category + } + + it('categorizes a violation with only wcag tags as wcag', async () => { + expect(await categoryFor(['wcag2a', 'wcag111'])).toBe('wcag') + }) + + it('categorizes a violation with a best-practice tag as best-practice', async () => { + expect(await categoryFor(['cat.semantics', 'best-practice'])).toBe('best-practice') + }) + + it('categorizes a violation with an experimental tag as experimental, even alongside wcag tags', async () => { + expect(await categoryFor(['wcag2a', 'experimental'])).toBe('experimental') + }) + }) }) From a753177c4ca4a950a75a6da7bf7cb9b209eb7a53 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 14:09:15 -0700 Subject: [PATCH 2/9] Surface finding category in issue body and label --- .github/actions/file/src/generateIssueBody.ts | 9 +++++++- .github/actions/file/src/openIssue.ts | 4 ++++ .github/actions/file/src/types.d.ts | 3 +++ .../file/tests/generateIssueBody.test.ts | 23 +++++++++++++++++++ .github/actions/file/tests/openIssue.test.ts | 22 ++++++++++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 18e25d31..8ca8a675 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -18,13 +18,20 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str ` } + const categoryNotice = + finding.category && finding.category !== 'wcag' + ? `**Note:** This is ${ + finding.category === 'experimental' ? 'an experimental check' : 'a best-practice recommendation' + }, not a hard WCAG failure.\n\n` + : '' + const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific violation reported in this issue is no longer reproducible. - [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization. - [ ] A test SHOULD be added to ensure this specific violation does not regress. - [ ] This PR MUST NOT introduce any new accessibility issues or regressions.` - const body = `## What + const body = `${categoryNotice}## What An accessibility scan ${finding.html ? `flagged the element \`${finding.html}\`` : `found an issue on ${finding.url}`} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. ${screenshotSection ?? ''} diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 937f06cf..03161c2c 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -26,6 +26,10 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding if (finding.ruleId) { labels.push(`${finding.scannerType} rule: ${finding.ruleId}`) } + // Flag non-WCAG findings so they can be filtered or triaged separately + if (finding.category && finding.category !== 'wcag') { + labels.push(finding.category) + } const title = truncateWithEllipsis( `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index ee91bc67..ba36ecc3 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -1,5 +1,8 @@ +export type FindingCategory = 'wcag' | 'best-practice' | 'experimental' + export type Finding = { scannerType: string + category?: FindingCategory ruleId?: string url: string html?: string diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 167ee5f8..048441b4 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -76,4 +76,27 @@ describe('generateIssueBody', () => { expect(body).toContain(`found an issue on ${findingWithEmptyOptionalFields.url}`) expect(body).not.toContain('flagged the element') }) + + it('omits the category notice for WCAG findings', () => { + expect(generateIssueBody(baseFinding, 'github/accessibility-scanner')).not.toContain('**Note:**') + expect(generateIssueBody({...baseFinding, category: 'wcag'}, 'github/accessibility-scanner')).not.toContain( + '**Note:**', + ) + }) + + it('includes a best-practice notice for best-practice findings', () => { + const body = generateIssueBody({...baseFinding, category: 'best-practice'}, 'github/accessibility-scanner') + + expect(body).toContain('**Note:**') + expect(body).toContain('best-practice recommendation') + expect(body).toContain('not a hard WCAG failure') + }) + + it('includes an experimental notice for experimental findings', () => { + const body = generateIssueBody({...baseFinding, category: 'experimental'}, 'github/accessibility-scanner') + + expect(body).toContain('**Note:**') + expect(body).toContain('an experimental check') + expect(body).toContain('not a hard WCAG failure') + }) }) diff --git a/.github/actions/file/tests/openIssue.test.ts b/.github/actions/file/tests/openIssue.test.ts index 77a184c3..e9cb46ff 100644 --- a/.github/actions/file/tests/openIssue.test.ts +++ b/.github/actions/file/tests/openIssue.test.ts @@ -65,6 +65,28 @@ describe('openIssue', () => { ) }) + it('adds a category label for non-WCAG findings', async () => { + const octokit = mockOctokit() + await openIssue(octokit, 'org/repo', {...baseFinding, category: 'best-practice'}) + + expect(octokit.request).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + labels: ['axe-scanning-issue', 'axe rule: color-contrast', 'best-practice'], + }), + ) + }) + + it('does not add a category label for WCAG findings', async () => { + const octokit = mockOctokit() + await openIssue(octokit, 'org/repo', {...baseFinding, category: 'wcag'}) + + const labels = octokit.request.mock.calls[0][1].labels + expect(labels).not.toContain('wcag') + expect(labels).not.toContain('best-practice') + expect(labels).not.toContain('experimental') + }) + it('truncates long titles with ellipsis', async () => { const octokit = mockOctokit() const longFinding = { From 7973e5f22f9df1e6525bbbcf7bc13479f4db0702 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 14:09:29 -0700 Subject: [PATCH 3/9] Add switches to skip filing best-practice and experimental issues --- .github/actions/file/action.yml | 8 ++++++++ .github/actions/file/src/index.ts | 33 ++++++++++++++++++++++++++++++- action.yml | 10 ++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index 7d76af7a..6d229250 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -28,6 +28,14 @@ inputs: description: "When true, log the issues that would be filed without opening, closing, or reopening any issues." required: false default: "false" + file_best_practice_issues: + description: "File issues for best-practice findings (accessibility recommendations that are not hard WCAG failures). Disabling only suppresses new issues; existing ones are left untouched." + required: false + default: "true" + file_experimental_issues: + description: "File issues for experimental findings (checks that are not yet stable). Disabling only suppresses new issues; existing ones are left untouched." + required: false + default: "true" outputs: filings_file: diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index fd16b68d..62700634 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -16,6 +16,13 @@ import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' import {OctokitResponse} from '@octokit/types' const OctokitWithThrottling = Octokit.plugin(throttling) +// core.getBooleanInput throws when an input is missing, so default unset switches. +function getBooleanInputWithDefault(name: string, defaultValue: boolean): boolean { + const raw = core.getInput(name) + if (!raw) return defaultValue + return raw.toLowerCase() === 'true' +} + export default async function () { core.info("Started 'file' action") const findingsFile = core.getInput('findings_file', {required: true}) @@ -30,6 +37,8 @@ export default async function () { : [] const shouldOpenGroupedIssues = core.getBooleanInput('open_grouped_issues') const dryRun = core.getBooleanInput('dry_run') + const fileBestPracticeIssues = getBooleanInputWithDefault('file_best_practice_issues', true) + const fileExperimentalIssues = getBooleanInputWithDefault('file_experimental_issues', true) core.debug(`Input: 'findings_file: ${findingsFile}'`) core.debug(`Input: 'repository: ${repoWithOwner}'`) core.debug(`Input: 'base_url: ${baseUrl ?? '(default)'}'`) @@ -37,6 +46,8 @@ export default async function () { core.debug(`Input: 'cached_filings_file: ${cachedFilingsFile}'`) core.debug(`Input: 'open_grouped_issues: ${shouldOpenGroupedIssues}'`) core.debug(`Input: 'dry_run: ${dryRun}'`) + core.debug(`Input: 'file_best_practice_issues: ${fileBestPracticeIssues}'`) + core.debug(`Input: 'file_experimental_issues: ${fileExperimentalIssues}'`) const octokit = new OctokitWithThrottling({ auth: token, @@ -60,6 +71,10 @@ export default async function () { }) const filings = updateFilingsWithNewFindings(cachedFilings, findings) + // Suppressed new filings are kept out of the cache so they aren't seen as + // resolved (and auto-closed) on the next run. + const suppressedFilings = new Set() + // Track new issues for grouping const newIssuesByProblemShort: Record = {} const trackingIssueUrls: Record = {} @@ -68,6 +83,21 @@ export default async function () { for (const filing of filings) { let response: OctokitResponse | undefined try { + // Category switches gate only NEW issues; existing ones are reconciled normally. + if (isNewFiling(filing)) { + const category = filing.findings[0].category ?? 'wcag' + if ( + (category === 'best-practice' && !fileBestPracticeIssues) || + (category === 'experimental' && !fileExperimentalIssues) + ) { + core.info( + `Skipping new ${category} issue (filing disabled for this category): ${filing.findings[0].problemShort}`, + ) + suppressedFilings.add(filing) + continue + } + } + if (dryRun) { if (isResolvedFiling(filing)) { dryRunCounts.close++ @@ -170,7 +200,8 @@ export default async function () { } const filingsPath = path.join(process.env.RUNNER_TEMP || '/tmp', `filings-${crypto.randomUUID()}.json`) - fs.writeFileSync(filingsPath, JSON.stringify(filings)) + const outputFilings = suppressedFilings.size > 0 ? filings.filter(f => !suppressedFilings.has(f)) : filings + fs.writeFileSync(filingsPath, JSON.stringify(outputFilings)) core.setOutput('filings_file', filingsPath) core.debug(`Output: 'filings_file: ${filingsPath}'`) diff --git a/action.yml b/action.yml index 1b852a5c..9c3052b0 100644 --- a/action.yml +++ b/action.yml @@ -45,6 +45,14 @@ inputs: description: "In the 'file' step, also open grouped issues which link to all issues with the same problem" required: false default: 'false' + file_best_practice_issues: + description: 'File issues for best-practice findings (accessibility recommendations that are not hard WCAG failures). Disabling suppresses new issues while existing ones are left untouched.' + required: false + default: 'true' + file_experimental_issues: + description: 'File issues for experimental findings (checks that are not yet stable). Disabling suppresses new issues while existing ones are left untouched.' + required: false + default: 'true' reduced_motion: description: 'Playwright reducedMotion setting: https://playwright.dev/docs/api/class-browser#browser-new-page-option-reduced-motion' required: false @@ -134,6 +142,8 @@ runs: screenshot_repository: ${{ github.repository }} open_grouped_issues: ${{ inputs.open_grouped_issues }} dry_run: ${{ inputs.dry_run }} + file_best_practice_issues: ${{ inputs.file_best_practice_issues }} + file_experimental_issues: ${{ inputs.file_experimental_issues }} - if: ${{ steps.file.outputs.filings_file }} name: Get issues from filings id: get_issues_from_filings From eedc7725b252a31f3ed26ed70ce2e9ba24be27df Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 14:53:16 -0700 Subject: [PATCH 4/9] Set finding category in integration test expectations --- tests/site-with-errors.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index 8c7981bd..cf216880 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -58,6 +58,7 @@ describe('site-with-errors', () => { const expected = [ { scannerType: 'axe', + category: 'wcag', url: 'http://127.0.0.1:4000/', html: '', problemShort: 'elements must meet minimum color contrast ratio thresholds', @@ -67,6 +68,7 @@ describe('site-with-errors', () => { }, { scannerType: 'axe', + category: 'best-practice', url: 'http://127.0.0.1:4000/', html: '', problemShort: 'page should contain a level-one heading', @@ -75,6 +77,7 @@ describe('site-with-errors', () => { }, { scannerType: 'axe', + category: 'wcag', url: 'http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html', html: ``, @@ -85,6 +88,7 @@ describe('site-with-errors', () => { }, { scannerType: 'axe', + category: 'wcag', url: 'http://127.0.0.1:4000/about/', html: 'jekyllrb.com', problemShort: 'elements must meet minimum color contrast ratio thresholds', @@ -94,6 +98,7 @@ describe('site-with-errors', () => { }, { scannerType: 'axe', + category: 'wcag', url: 'http://127.0.0.1:4000/404.html', html: '
  • Accessibility Scanner Demo
  • ', problemShort: 'elements must meet minimum color contrast ratio thresholds', @@ -103,6 +108,7 @@ describe('site-with-errors', () => { }, { scannerType: 'axe', + category: 'best-practice', url: 'http://127.0.0.1:4000/404.html', html: '

    ', problemShort: 'headings should not be empty', From a95cc310dcbe673adf403b37d9879de5ece15ae0 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 14:53:16 -0700 Subject: [PATCH 5/9] Address Copilot feedback on input validation and acceptance criteria --- .github/actions/file/src/generateIssueBody.ts | 7 ++++++- .github/actions/file/src/index.ts | 8 ++++++-- .github/actions/file/tests/generateIssueBody.test.ts | 5 +++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 8ca8a675..eb75ab60 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -25,9 +25,14 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str }, not a hard WCAG failure.\n\n` : '' + const standardsLine = + finding.category && finding.category !== 'wcag' + ? '- [ ] The fix MUST meet the accessibility standards specified by the repository or organization (WCAG 2.1 if applicable).' + : '- [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization.' + const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific violation reported in this issue is no longer reproducible. -- [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization. +${standardsLine} - [ ] A test SHOULD be added to ensure this specific violation does not regress. - [ ] This PR MUST NOT introduce any new accessibility issues or regressions.` diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 62700634..1f4d7d9b 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -16,11 +16,15 @@ import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' import {OctokitResponse} from '@octokit/types' const OctokitWithThrottling = Octokit.plugin(throttling) -// core.getBooleanInput throws when an input is missing, so default unset switches. +// core.getBooleanInput throws when an input is unset, so this defaults unset +// switches while still rejecting values that aren't a valid boolean. function getBooleanInputWithDefault(name: string, defaultValue: boolean): boolean { const raw = core.getInput(name) if (!raw) return defaultValue - return raw.toLowerCase() === 'true' + const normalized = raw.trim().toLowerCase() + if (normalized === 'true') return true + if (normalized === 'false') return false + throw new TypeError(`Invalid boolean input '${name}': '${raw}'. Expected 'true' or 'false'.`) } export default async function () { diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 048441b4..7a6ec421 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -26,6 +26,7 @@ describe('generateIssueBody', () => { expect(body).toContain('## What') expect(body).toContain('## Acceptance Criteria') expect(body).toContain('The specific violation reported in this issue is no longer reproducible.') + expect(body).toContain('The fix MUST meet WCAG 2.1 guidelines OR') expect(body).not.toContain('Specifically:') }) @@ -90,6 +91,8 @@ describe('generateIssueBody', () => { expect(body).toContain('**Note:**') expect(body).toContain('best-practice recommendation') expect(body).toContain('not a hard WCAG failure') + expect(body).toContain('WCAG 2.1 if applicable') + expect(body).not.toContain('The fix MUST meet WCAG 2.1 guidelines OR') }) it('includes an experimental notice for experimental findings', () => { @@ -98,5 +101,7 @@ describe('generateIssueBody', () => { expect(body).toContain('**Note:**') expect(body).toContain('an experimental check') expect(body).toContain('not a hard WCAG failure') + expect(body).toContain('WCAG 2.1 if applicable') + expect(body).not.toContain('The fix MUST meet WCAG 2.1 guidelines OR') }) }) From 3b62b40f9a86c4e299e9b7a995a39a8af76d19b8 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 15:04:51 -0700 Subject: [PATCH 6/9] Trimming verbose comments --- .github/actions/file/src/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 1f4d7d9b..1d25cedd 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -16,7 +16,7 @@ import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' import {OctokitResponse} from '@octokit/types' const OctokitWithThrottling = Octokit.plugin(throttling) -// core.getBooleanInput throws when an input is unset, so this defaults unset +// Throws when an input is unset, so this defaults unset // switches while still rejecting values that aren't a valid boolean. function getBooleanInputWithDefault(name: string, defaultValue: boolean): boolean { const raw = core.getInput(name) @@ -75,8 +75,7 @@ export default async function () { }) const filings = updateFilingsWithNewFindings(cachedFilings, findings) - // Suppressed new filings are kept out of the cache so they aren't seen as - // resolved (and auto-closed) on the next run. + // Suppressed new filings are kept out of the cache const suppressedFilings = new Set() // Track new issues for grouping @@ -87,7 +86,7 @@ export default async function () { for (const filing of filings) { let response: OctokitResponse | undefined try { - // Category switches gate only NEW issues; existing ones are reconciled normally. + // Category switches gate only new issues if (isNewFiling(filing)) { const category = filing.findings[0].category ?? 'wcag' if ( From 9d668aaa908fc92c2569fff4397881ba3f7465b8 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 17:20:15 -0700 Subject: [PATCH 7/9] Document best-practice and experimental issue inputs in README --- README.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a20261e7..fa8cf0e5 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ jobs: # skip_copilot_assignment: false # Optional: Set to true to skip assigning issues to GitHub Copilot (or if you don't have GitHub Copilot) # include_screenshots: false # Optional: Set to true to capture screenshots and include links to them in filed issues # open_grouped_issues: false # Optional: Set to true to open an issue grouping individual issues per violation + # file_best_practice_issues: true # Optional: Set to false to stop filing new issues for best-practice findings (recommendations that are not hard WCAG failures) + # file_experimental_issues: true # Optional: Set to false to stop filing new issues for experimental findings (checks that are not yet stable) # dry_run: false # Optional: Set to true to scan and log what would be filed without creating/closing issues or writing the cache # reduced_motion: no-preference # Optional: Playwright reduced motion configuration option # color_scheme: light # Optional: Playwright color scheme configuration option @@ -115,25 +117,27 @@ Trigger the workflow manually or automatically based on your configuration. The ## Action inputs -| Input | Required | Description | Example | -| ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `urls` | No\* | Newline-delimited list of URLs to scan. Required unless `url_configs` is provided. | `https://primer.style`
    `https://primer.style/octicons` | -| `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` | -| `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` | -| `cache_key` | Yes | Key for caching results across runs
    Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` | -| `base_url` | No | GitHub API base URL used by Octokit. Set this for GitHub Enterprise Server (format: `https://HOSTNAME/api/v3`). Defaults to `https://api.github.com` | `https://ghe.example.com/api/v3` | -| `login_url` | No | If scanned pages require authentication, the URL of the login page | `https://github.com/login` | -| `username` | No | If scanned pages require authentication, the username to use for login | `some-user` | -| `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` | -| `auth_context` | No | If scanned pages require authentication, a stringified JSON object containing username, password, cookies, and/or localStorage from an authenticated session | `{"username":"some-user","password":"***","cookies":[...]}` | -| `skip_copilot_assignment` | No | Whether to skip assigning filed issues to GitHub Copilot. Set to `true` if you don't have GitHub Copilot or prefer to handle issues manually | `true` | -| `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | -| `open_grouped_issues` | No | Whether to create a tracking issue which groups filed issues together by violation type. Default: `false` | `true` | -| `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | -| `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | -| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | -| `dry_run` | No | When `true`, scan and log the issues that _would_ be filed without opening, closing, reopening, or assigning any issues — and without writing to the `gh-cache` branch. Useful for safely previewing results. Default: `false` | `true` | -| `url_configs` | No | A stringified JSON array of URL config objects. Each object must have a `url` field and may have 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. | `'[{"url":"https://example.com","excludeSelectors":["iframe","#widget"]}]'` | +| Input | Required | Description | Example | +| --------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `urls` | No\* | Newline-delimited list of URLs to scan. Required unless `url_configs` is provided. | `https://primer.style`
    `https://primer.style/octicons` | +| `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` | +| `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` | +| `cache_key` | Yes | Key for caching results across runs
    Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` | +| `base_url` | No | GitHub API base URL used by Octokit. Set this for GitHub Enterprise Server (format: `https://HOSTNAME/api/v3`). Defaults to `https://api.github.com` | `https://ghe.example.com/api/v3` | +| `login_url` | No | If scanned pages require authentication, the URL of the login page | `https://github.com/login` | +| `username` | No | If scanned pages require authentication, the username to use for login | `some-user` | +| `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` | +| `auth_context` | No | If scanned pages require authentication, a stringified JSON object containing username, password, cookies, and/or localStorage from an authenticated session | `{"username":"some-user","password":"***","cookies":[...]}` | +| `skip_copilot_assignment` | No | Whether to skip assigning filed issues to GitHub Copilot. Set to `true` if you don't have GitHub Copilot or prefer to handle issues manually | `true` | +| `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | +| `open_grouped_issues` | No | Whether to create a tracking issue which groups filed issues together by violation type. Default: `false` | `true` | +| `file_best_practice_issues` | No | Whether to file issues for best-practice findings (accessibility recommendations that are not hard WCAG failures). Set to `false` to suppress new best-practice issues; existing ones are left untouched. Default: `true` | `false` | +| `file_experimental_issues` | No | Whether to file issues for experimental findings (checks that are not yet stable). Set to `false` to suppress new experimental issues; existing ones are left untouched. Default: `true` | `false` | +| `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | +| `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | +| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | +| `dry_run` | No | When `true`, scan and log the issues that _would_ be filed without opening, closing, reopening, or assigning any issues — and without writing to the `gh-cache` branch. Useful for safely previewing results. Default: `false` | `true` | +| `url_configs` | No | A stringified JSON array of URL config objects. Each object must have a `url` field and may have 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. | `'[{"url":"https://example.com","excludeSelectors":["iframe","#widget"]}]'` | --- From f898bda75b9d3ad498c0e5551a5a4f4a9f5c7323 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Wed, 24 Jun 2026 13:18:57 -0700 Subject: [PATCH 8/9] Wording changes Co-authored-by: Joyce Zhu --- .github/actions/file/src/generateIssueBody.ts | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index eb75ab60..a7e3dd3f 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -22,7 +22,7 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str finding.category && finding.category !== 'wcag' ? `**Note:** This is ${ finding.category === 'experimental' ? 'an experimental check' : 'a best-practice recommendation' - }, not a hard WCAG failure.\n\n` + }, not a definite WCAG failure.\n\n` : '' const standardsLine = diff --git a/README.md b/README.md index fa8cf0e5..f8a5ff42 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Trigger the workflow manually or automatically based on your configuration. The | Input | Required | Description | Example | | --------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `urls` | No\* | Newline-delimited list of URLs to scan. Required unless `url_configs` is provided. | `https://primer.style`
    `https://primer.style/octicons` | +| `urls` | No | Newline-delimited list of URLs to scan. Required unless `url_configs` is provided. | `https://primer.style`
    `https://primer.style/octicons` | | `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` | | `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` | | `cache_key` | Yes | Key for caching results across runs
    Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` | From 10248572a137781df5a30359bd2d00ba7039ca21 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Wed, 24 Jun 2026 14:14:50 -0700 Subject: [PATCH 9/9] Use GFM note alert, WCAG 2.2, and core.getBooleanInput --- .github/actions/file/src/generateIssueBody.ts | 6 ++--- .github/actions/file/src/index.ts | 11 +++------- .../file/tests/generateIssueBody.test.ts | 22 +++++++++---------- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index a7e3dd3f..f0167ffb 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -20,15 +20,15 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str const categoryNotice = finding.category && finding.category !== 'wcag' - ? `**Note:** This is ${ + ? `> [!NOTE]\n> This is ${ finding.category === 'experimental' ? 'an experimental check' : 'a best-practice recommendation' }, not a definite WCAG failure.\n\n` : '' const standardsLine = finding.category && finding.category !== 'wcag' - ? '- [ ] The fix MUST meet the accessibility standards specified by the repository or organization (WCAG 2.1 if applicable).' - : '- [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization.' + ? '- [ ] The fix MUST meet the accessibility standards specified by the repository or organization (WCAG 2.2 if applicable).' + : '- [ ] The fix MUST meet WCAG 2.2 guidelines OR the accessibility standards specified by the repository or organization.' const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific violation reported in this issue is no longer reproducible. diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 1d25cedd..f3ad2b98 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -16,15 +16,10 @@ import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' import {OctokitResponse} from '@octokit/types' const OctokitWithThrottling = Octokit.plugin(throttling) -// Throws when an input is unset, so this defaults unset -// switches while still rejecting values that aren't a valid boolean. +// core.getBooleanInput throws on unset inputs, so apply the default first. function getBooleanInputWithDefault(name: string, defaultValue: boolean): boolean { - const raw = core.getInput(name) - if (!raw) return defaultValue - const normalized = raw.trim().toLowerCase() - if (normalized === 'true') return true - if (normalized === 'false') return false - throw new TypeError(`Invalid boolean input '${name}': '${raw}'. Expected 'true' or 'false'.`) + if (!core.getInput(name)) return defaultValue + return core.getBooleanInput(name) } export default async function () { diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 7a6ec421..7fbabe5d 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -26,7 +26,7 @@ describe('generateIssueBody', () => { expect(body).toContain('## What') expect(body).toContain('## Acceptance Criteria') expect(body).toContain('The specific violation reported in this issue is no longer reproducible.') - expect(body).toContain('The fix MUST meet WCAG 2.1 guidelines OR') + expect(body).toContain('The fix MUST meet WCAG 2.2 guidelines OR') expect(body).not.toContain('Specifically:') }) @@ -79,29 +79,29 @@ describe('generateIssueBody', () => { }) it('omits the category notice for WCAG findings', () => { - expect(generateIssueBody(baseFinding, 'github/accessibility-scanner')).not.toContain('**Note:**') + expect(generateIssueBody(baseFinding, 'github/accessibility-scanner')).not.toContain('> [!NOTE]') expect(generateIssueBody({...baseFinding, category: 'wcag'}, 'github/accessibility-scanner')).not.toContain( - '**Note:**', + '> [!NOTE]', ) }) it('includes a best-practice notice for best-practice findings', () => { const body = generateIssueBody({...baseFinding, category: 'best-practice'}, 'github/accessibility-scanner') - expect(body).toContain('**Note:**') + expect(body).toContain('> [!NOTE]') expect(body).toContain('best-practice recommendation') - expect(body).toContain('not a hard WCAG failure') - expect(body).toContain('WCAG 2.1 if applicable') - expect(body).not.toContain('The fix MUST meet WCAG 2.1 guidelines OR') + expect(body).toContain('not a definite WCAG failure') + expect(body).toContain('WCAG 2.2 if applicable') + expect(body).not.toContain('The fix MUST meet WCAG 2.2 guidelines OR') }) it('includes an experimental notice for experimental findings', () => { const body = generateIssueBody({...baseFinding, category: 'experimental'}, 'github/accessibility-scanner') - expect(body).toContain('**Note:**') + expect(body).toContain('> [!NOTE]') expect(body).toContain('an experimental check') - expect(body).toContain('not a hard WCAG failure') - expect(body).toContain('WCAG 2.1 if applicable') - expect(body).not.toContain('The fix MUST meet WCAG 2.1 guidelines OR') + expect(body).toContain('not a definite WCAG failure') + expect(body).toContain('WCAG 2.2 if applicable') + expect(body).not.toContain('The fix MUST meet WCAG 2.2 guidelines OR') }) })