Skip to content

Commit f9f4010

Browse files
Add url_configs input to skip CSS selectors per URL from Axe scan (#213)
Tracking issue with context: #212 Embedded iframes, third-party widgets, and user-generated content often should not be scanned. This adds a `url_configs` input — a stringified JSON array of objects each with a `url` field and an optional `excludeSelectors` field — that allows per-URL CSS selector exclusion via `AxeBuilder.exclude()` before `analyze()` is called. When `url_configs` is provided, it takes precedence over the `urls` input. ## Changes - **`action.yml` (root)** — New `url_configs` input; `urls` is now optional (required when `url_configs` is not provided); both forwarded to the `find` step - **`.github/actions/find/action.yml`** — New `url_configs` input declared; `urls` made optional - **`find/src/types.d.ts`** — New `UrlConfig` type: `{ url: string; excludeSelectors?: string[] }` - **`find/src/index.ts`** — Parses and validates `url_configs` JSON; when present, uses it instead of `urls`; passes each URL's `excludeSelectors` to `findForUrl` - **`find/src/findForUrl.ts`** — `findForUrl` and `runAxeScan` accept `exclude?: string[]`; selectors applied via `axeBuilder.exclude()` before `analyze()` - **`README.md`** — `url_configs` input documented in the inputs table and getting-started example; `urls` marked as conditionally required ## Usage ```yaml - uses: github/accessibility-scanner@v1 with: url_configs: '[{"url":"https://example.com","excludeSelectors":["iframe","#third-party-widget"]},{"url":"https://example.com/about"}]' repository: owner/repo token: ${{ secrets.GH_TOKEN }} cache_key: cached_results.json ``` The `urls` input continues to work as before when `url_configs` is not provided.
2 parents 588b3a8 + 0f88383 commit f9f4010

6 files changed

Lines changed: 116 additions & 49 deletions

File tree

.github/actions/find/action.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ description: 'Finds potential accessibility gaps.'
44
inputs:
55
urls:
66
description: 'Newline-delimited list of URLs to check for accessibility issues'
7-
required: true
7+
required: false
88
multiline: true
9+
url_configs:
10+
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."
11+
required: false
912
auth_context:
1013
description: "Stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session"
1114
required: false

.github/actions/find/src/findForUrl.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {ColorSchemePreference, Finding, ReducedMotionPreference} from './types.d.js'
1+
import type {ColorSchemePreference, Finding, ReducedMotionPreference, UrlConfig} from './types.d.js'
22
import {AxeBuilder} from '@axe-core/playwright'
33
import playwright from 'playwright'
44
import {AuthContext} from './AuthContext.js'
@@ -8,12 +8,13 @@ import {getScansContext} from './scansContextProvider.js'
88
import * as core from '@actions/core'
99

1010
export async function findForUrl(
11-
url: string,
11+
urlConfig: UrlConfig,
1212
authContext?: AuthContext,
1313
includeScreenshots: boolean = false,
1414
reducedMotion?: ReducedMotionPreference,
1515
colorScheme?: ColorSchemePreference,
1616
): Promise<Finding[]> {
17+
const {url, excludeSelectors} = urlConfig
1718
const browser = await playwright.chromium.launch({
1819
headless: true,
1920
executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined,
@@ -56,7 +57,7 @@ export async function findForUrl(
5657
}
5758

5859
if (scansContext.shouldPerformAxeScan) {
59-
await runAxeScan({page, addFinding})
60+
await runAxeScan({page, addFinding, excludeSelectors})
6061
}
6162
} catch (e) {
6263
core.error(`Error during accessibility scan: ${e}`)
@@ -69,13 +70,18 @@ export async function findForUrl(
6970
async function runAxeScan({
7071
page,
7172
addFinding,
73+
excludeSelectors,
7274
}: {
7375
page: playwright.Page
7476
addFinding: (findingData: Finding, options?: {includeScreenshots?: boolean}) => Promise<void>
77+
excludeSelectors?: string[]
7578
}) {
7679
const url = page.url()
7780
core.info(`Scanning ${url}`)
78-
const rawFindings = await new AxeBuilder({page}).analyze()
81+
const axeBuilder = new AxeBuilder({page})
82+
excludeSelectors?.forEach(selector => axeBuilder.exclude(selector))
83+
84+
const rawFindings = await axeBuilder.analyze()
7985

8086
if (rawFindings) {
8187
for (const violation of rawFindings.violations) {

.github/actions/find/src/index.ts

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {AuthContextInput, ColorSchemePreference, ReducedMotionPreference} from './types.js'
1+
import type {AuthContextInput, ColorSchemePreference, ReducedMotionPreference, UrlConfig} from './types.js'
22
import fs from 'node:fs'
33
import path from 'node:path'
44
import * as core from '@actions/core'
@@ -7,37 +7,22 @@ import {findForUrl} from './findForUrl.js'
77

88
export default async function () {
99
core.info("Starting 'find' action")
10-
const urls = core.getMultilineInput('urls', {required: true})
11-
core.debug(`Input: 'urls: ${JSON.stringify(urls)}'`)
10+
const urlConfigs = loadUrlConfigs()
11+
const urls = loadUrls({urlConfigs})
12+
const reducedMotion = loadReducedMotion()
13+
const colorScheme = loadColorScheme()
14+
15+
const actualUrls = urlConfigs || urls || []
16+
1217
const authContextInput: AuthContextInput = JSON.parse(core.getInput('auth_context', {required: false}) || '{}')
1318
const authContext = new AuthContext(authContextInput)
14-
1519
const includeScreenshots = core.getInput('include_screenshots', {required: false}) !== 'false'
16-
const reducedMotionInput = core.getInput('reduced_motion', {required: false})
17-
let reducedMotion: ReducedMotionPreference | undefined
18-
if (reducedMotionInput) {
19-
if (!['reduce', 'no-preference', null].includes(reducedMotionInput)) {
20-
throw new Error(
21-
"Input 'reduced_motion' must be one of: 'reduce', 'no-preference', or null per Playwright documentation.",
22-
)
23-
}
24-
reducedMotion = reducedMotionInput as ReducedMotionPreference
25-
}
26-
const colorSchemeInput = core.getInput('color_scheme', {required: false})
27-
let colorScheme: ColorSchemePreference | undefined
28-
if (colorSchemeInput) {
29-
if (!['light', 'dark', 'no-preference', null].includes(colorSchemeInput)) {
30-
throw new Error(
31-
"Input 'color_scheme' must be one of: 'light', 'dark', 'no-preference', or null per Playwright documentation.",
32-
)
33-
}
34-
colorScheme = colorSchemeInput as ColorSchemePreference
35-
}
3620

3721
const findings = []
38-
for (const url of urls) {
22+
for (const urlConfig of actualUrls) {
23+
const {url} = urlConfig
3924
core.info(`Preparing to scan ${url}`)
40-
const findingsForUrl = await findForUrl(url, authContext, includeScreenshots, reducedMotion, colorScheme)
25+
const findingsForUrl = await findForUrl(urlConfig, authContext, includeScreenshots, reducedMotion, colorScheme)
4126
if (findingsForUrl.length === 0) {
4227
core.info(`No accessibility gaps were found on ${url}`)
4328
continue
@@ -54,3 +39,65 @@ export default async function () {
5439
core.info(`Found ${findings.length} findings in total`)
5540
core.info("Finished 'find' action")
5641
}
42+
43+
function loadUrlConfigs() {
44+
const urlConfigInput = core.getInput('url_configs', {required: false})
45+
if (!urlConfigInput) return
46+
47+
try {
48+
const parsed = JSON.parse(urlConfigInput)
49+
50+
if (!Array.isArray(parsed)) {
51+
throw new Error("Input 'url_configs' must be a JSON array.")
52+
}
53+
54+
for (const item of parsed) {
55+
if (typeof item !== 'object' || item === null || typeof item.url !== 'string') {
56+
throw new Error("Each entry in 'url_configs' must be an object with a 'url' string field.")
57+
}
58+
}
59+
60+
return parsed as UrlConfig[]
61+
} catch (e) {
62+
throw new Error(`Invalid 'url_configs' input: ${(e as Error).message}`)
63+
}
64+
}
65+
66+
function loadUrls({urlConfigs}: {urlConfigs?: UrlConfig[]} = {}) {
67+
// - no need to process this input if url_configs is provided
68+
if (urlConfigs) return
69+
70+
const urls: string[] = core.getMultilineInput('urls', {required: false})
71+
core.debug(`Input: 'urls: ${JSON.stringify(urls)}'`)
72+
73+
if (urls.length === 0) {
74+
throw new Error("Either 'urls' or 'url_configs' input must be provided.")
75+
}
76+
77+
return urls.map(url => ({url})) as UrlConfig[]
78+
}
79+
80+
function loadReducedMotion() {
81+
const reducedMotionInput = core.getInput('reduced_motion', {required: false})
82+
if (!reducedMotionInput) return
83+
84+
if (!['reduce', 'no-preference', null].includes(reducedMotionInput)) {
85+
throw new Error(
86+
"Input 'reduced_motion' must be one of: 'reduce', 'no-preference', or null per Playwright documentation.",
87+
)
88+
}
89+
return reducedMotionInput as ReducedMotionPreference
90+
}
91+
92+
function loadColorScheme() {
93+
const colorSchemeInput = core.getInput('color_scheme', {required: false})
94+
if (!colorSchemeInput) return
95+
96+
if (!['light', 'dark', 'no-preference', null].includes(colorSchemeInput)) {
97+
throw new Error(
98+
"Input 'color_scheme' must be one of: 'light', 'dark', 'no-preference', or null per Playwright documentation.",
99+
)
100+
}
101+
102+
return colorSchemeInput as ColorSchemePreference
103+
}

.github/actions/find/src/types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,8 @@ export type AuthContextInput = {
3737
export type ReducedMotionPreference = 'reduce' | 'no-preference' | null
3838

3939
export type ColorSchemePreference = 'light' | 'dark' | 'no-preference' | null
40+
41+
export type UrlConfig = {
42+
url: string
43+
excludeSelectors?: string[]
44+
}

README.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
# reduced_motion: no-preference # Optional: Playwright reduced motion configuration option
5959
# color_scheme: light # Optional: Playwright color scheme configuration option
6060
# scans: '["axe","reflow-scan"]' # Optional: An array of scans (or plugins) to be performed. If not provided, only Axe will be performed.
61+
# url_configs: '[{"url":"https://example.com","excludeSelectors":["iframe","#widget"]}]' # Optional: Per-URL config with CSS selectors to exclude from the Axe scan. When provided, takes precedence over 'urls'.
6162
```
6263

6364
> 👉 Update all `REPLACE_THIS` placeholders with your actual values. See [Action Inputs](#action-inputs) for details.
@@ -113,23 +114,24 @@ Trigger the workflow manually or automatically based on your configuration. The
113114

114115
## Action inputs
115116

116-
| Input | Required | Description | Example |
117-
| ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
118-
| `urls` | Yes | Newline-delimited list of URLs to scan | `https://primer.style`<br>`https://primer.style/octicons` |
119-
| `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` |
120-
| `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` |
121-
| `cache_key` | Yes | Key for caching results across runs<br>Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` |
122-
| `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` |
123-
| `login_url` | No | If scanned pages require authentication, the URL of the login page | `https://github.com/login` |
124-
| `username` | No | If scanned pages require authentication, the username to use for login | `some-user` |
125-
| `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` |
126-
| `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":[...]}` |
127-
| `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` |
128-
| `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` |
129-
| `open_grouped_issues` | No | Whether to create a tracking issue which groups filed issues together by violation type. Default: `false` | `true` |
130-
| `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` |
131-
| `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` |
132-
| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` |
117+
| Input | Required | Description | Example |
118+
| ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
119+
| `urls` | No\* | Newline-delimited list of URLs to scan. Required unless `url_configs` is provided. | `https://primer.style`<br>`https://primer.style/octicons` |
120+
| `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` |
121+
| `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` |
122+
| `cache_key` | Yes | Key for caching results across runs<br>Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` |
123+
| `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` |
124+
| `login_url` | No | If scanned pages require authentication, the URL of the login page | `https://github.com/login` |
125+
| `username` | No | If scanned pages require authentication, the username to use for login | `some-user` |
126+
| `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` |
127+
| `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":[...]}` |
128+
| `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` |
129+
| `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` |
130+
| `open_grouped_issues` | No | Whether to create a tracking issue which groups filed issues together by violation type. Default: `false` | `true` |
131+
| `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` |
132+
| `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` |
133+
| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` |
134+
| `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"]}]'` |
133135

134136
---
135137

0 commit comments

Comments
 (0)