Skip to content

Commit 765d845

Browse files
committed
add plugin docs
- restructure findForUrl code to accound for upcoming changes
1 parent 6ffffc7 commit 765d845

4 files changed

Lines changed: 108 additions & 47 deletions

File tree

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

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {AuthContext} from './AuthContext.js'
55
import {generateScreenshots} from './generateScreenshots.js'
66
import {loadPlugins, invokePlugin} from './pluginManager.js'
77
import {getScansContext} from './scansContextProvider.js'
8-
import axe from 'axe-core'
98
import core from '@actions/core'
109

1110
export async function findForUrl(
@@ -27,7 +26,6 @@ export async function findForUrl(
2726
const context = await browser.newContext(contextOptions)
2827
const page = await context.newPage()
2928
await page.goto(url)
30-
core.info(`Scanning ${page.url()}`)
3129

3230
const findings: Finding[] = []
3331
const addFinding = (findingData: Finding) => {
@@ -37,44 +35,66 @@ export async function findForUrl(
3735
try {
3836
const scansContext = getScansContext()
3937

40-
let rawFindings: axe.AxeResults | undefined
41-
if (scansContext.shouldPerformAxeScan) {
42-
rawFindings = await new AxeBuilder({page}).analyze()
43-
}
44-
4538
if (scansContext.shouldRunPlugins) {
4639
const plugins = await loadPlugins()
4740
for (const plugin of plugins) {
4841
if (scansContext.scansToPerform.includes(plugin.name)) {
4942
core.info(`Running plugin: ${plugin.name}`)
50-
await invokePlugin({plugin, page, addFinding, url})
43+
await invokePlugin({
44+
plugin,
45+
page,
46+
addFinding,
47+
// - this will be coming soon
48+
// runAxeScan: () => runAxeScan({page, includeScreenshots, findings}),
49+
})
5150
} else {
5251
core.info(`Skipping plugin ${plugin.name} because it is not included in the 'scans' input`)
5352
}
5453
}
5554
}
5655

57-
let screenshotId: string | undefined
58-
if (includeScreenshots) {
59-
screenshotId = await generateScreenshots(page)
56+
if (scansContext.shouldPerformAxeScan) {
57+
runAxeScan({
58+
includeScreenshots,
59+
page,
60+
findings,
61+
})
6062
}
61-
62-
const axeFindings = rawFindings?.violations.map(violation => ({
63-
scannerType: 'axe',
64-
url,
65-
html: violation.nodes[0].html.replace(/'/g, '''),
66-
problemShort: violation.help.toLowerCase().replace(/'/g, '''),
67-
problemUrl: violation.helpUrl.replace(/'/g, '''),
68-
ruleId: violation.id,
69-
solutionShort: violation.description.toLowerCase().replace(/'/g, '''),
70-
solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, '''),
71-
screenshotId,
72-
}))
73-
findings.push(...(axeFindings || []))
7463
} catch (e) {
7564
core.error(`Error during accessibility scan: ${e}`)
7665
}
7766
await context.close()
7867
await browser.close()
7968
return findings
8069
}
70+
71+
async function runAxeScan({
72+
includeScreenshots,
73+
page,
74+
findings,
75+
}: {
76+
includeScreenshots: boolean
77+
page: playwright.Page
78+
findings: Finding[]
79+
}) {
80+
const url = page.url()
81+
core.info(`Scanning ${url}`)
82+
const rawFindings = await new AxeBuilder({page}).analyze()
83+
let screenshotId: string | undefined
84+
if (includeScreenshots) {
85+
screenshotId = await generateScreenshots(page)
86+
}
87+
88+
const axeFindings = rawFindings?.violations.map(violation => ({
89+
scannerType: 'axe',
90+
url,
91+
html: violation.nodes[0].html.replace(/'/g, '''),
92+
problemShort: violation.help.toLowerCase().replace(/'/g, '''),
93+
problemUrl: violation.helpUrl.replace(/'/g, '''),
94+
ruleId: violation.id,
95+
solutionShort: violation.description.toLowerCase().replace(/'/g, '''),
96+
solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, '''),
97+
screenshotId,
98+
}))
99+
findings.push(...(axeFindings || []))
100+
}

.github/actions/find/src/pluginManager.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,16 @@ import core from '@actions/core'
1010
const __filename = fileURLToPath(import.meta.url)
1111
const __dirname = path.dirname(__filename)
1212

13+
type PluginDefaultParams = {
14+
page: playwright.Page
15+
addFinding: (findingData: Finding) => void
16+
// - this will be coming soon
17+
// runAxeScan: (options: {includeScreenshots: boolean; page: playwright.Page; findings: Finding[]}) => Promise<void>
18+
}
19+
1320
export type Plugin = {
1421
name: string
15-
default: (options: {page: playwright.Page; addFinding: (findingData: Finding) => void; url: string}) => Promise<void>
22+
default: (options: PluginDefaultParams) => Promise<void>
1623
}
1724

1825
const plugins: Plugin[] = []
@@ -88,12 +95,9 @@ export async function loadPluginsFromPath({readPath, importPath}: {readPath: str
8895
}
8996
}
9097

91-
type InvokePluginParams = {
98+
type InvokePluginParams = PluginDefaultParams & {
9299
plugin: Plugin
93-
page: playwright.Page
94-
addFinding: (findingData: Finding) => void
95-
url: string
96100
}
97-
export function invokePlugin({plugin, page, addFinding, url}: InvokePluginParams) {
98-
return plugin.default({page, addFinding, url})
101+
export function invokePlugin({plugin, page, addFinding}: InvokePluginParams) {
102+
return plugin.default({page, addFinding})
99103
}

.github/actions/find/tests/findForUrl.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {describe, it, expect, vi} from 'vitest'
22
import core from '@actions/core'
33
import {findForUrl} from '../src/findForUrl.js'
4-
import AxeBuilder from '@axe-core/playwright'
4+
import {AxeBuilder} from '@axe-core/playwright'
55
import axe from 'axe-core'
66
import * as pluginManager from '../src/pluginManager.js'
77
import {clearCache} from '../src/scansContextProvider.js'
@@ -28,7 +28,7 @@ vi.mock('@axe-core/playwright', () => {
2828
const AxeBuilderMock = vi.fn()
2929
const rawFinding = {violations: []} as unknown as axe.AxeResults
3030
AxeBuilderMock.prototype.analyze = vi.fn(() => Promise.resolve(rawFinding))
31-
return {default: AxeBuilderMock}
31+
return {AxeBuilder: AxeBuilderMock}
3232
})
3333

3434
let actionInput: string = ''

README.md

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ jobs:
6565
- Admin access to add repository secrets
6666

6767
📚 Learn more
68+
6869
- [Quickstart for GitHub Actions](https://docs.github.com/en/actions/get-started/quickstart)
6970
- [Understanding GitHub Actions](https://docs.github.com/en/actions/get-started/understand-github-actions)
7071
- [Writing workflows](https://docs.github.com/en/actions/how-tos/write-workflows)
@@ -89,6 +90,7 @@ The a11y scanner requires a Personal Access Token (PAT) as a repository secret:
8990
> 👉 GitHub Actions' default [GITHUB_TOKEN](https://docs.github.com/en/actions/tutorials/authenticate-with-github_token) cannot be used here.
9091
9192
📚 Learn more
93+
9294
- [Creating a fine-grained PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)
9395
- [Creating repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository)
9496

@@ -99,6 +101,7 @@ The a11y scanner requires a Personal Access Token (PAT) as a repository secret:
99101
Trigger the workflow manually or automatically based on your configuration. The a11y scanner will run and create issues for any accessibility findings. When issues are assigned to GitHub Copilot, always review proposed fixes before merging.
100102

101103
📚 Learn more
104+
102105
- [View workflow run history](https://docs.github.com/en/actions/how-tos/monitor-workflows/view-workflow-run-history)
103106
- [Running a workflow manually](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow#running-a-workflow)
104107
- [Re-run workflows and jobs](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/re-run-workflows-and-jobs)
@@ -107,20 +110,21 @@ Trigger the workflow manually or automatically based on your configuration. The
107110

108111
## Action inputs
109112

110-
| Input | Required | Description | Example |
111-
|-------|----------|-------------|---------|
112-
| `urls` | Yes | Newline-delimited list of URLs to scan | `https://primer.style`<br>`https://primer.style/octicons` |
113-
| `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` |
114-
| `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` |
115-
| `cache_key` | Yes | Key for caching results across runs<br>Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` |
116-
| `login_url` | No | If scanned pages require authentication, the URL of the login page | `https://github.com/login` |
117-
| `username` | No | If scanned pages require authentication, the username to use for login | `some-user` |
118-
| `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` |
119-
| `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":[...]}` |
120-
| `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` |
121-
| `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` |
122-
| `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` |
123-
| `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` |
113+
| Input | Required | Description | Example |
114+
| ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
115+
| `urls` | Yes | Newline-delimited list of URLs to scan | `https://primer.style`<br>`https://primer.style/octicons` |
116+
| `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` |
117+
| `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` |
118+
| `cache_key` | Yes | Key for caching results across runs<br>Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` |
119+
| `login_url` | No | If scanned pages require authentication, the URL of the login page | `https://github.com/login` |
120+
| `username` | No | If scanned pages require authentication, the username to use for login | `some-user` |
121+
| `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` |
122+
| `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":[...]}` |
123+
| `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` |
124+
| `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` |
125+
| `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` |
126+
| `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` |
127+
| `scans` | No | A list of scans (or plugins) to be performed. If not provided, only axe will be performed. | `['axe', 'reflow']` |
124128

125129
---
126130

@@ -143,13 +147,46 @@ The a11y scanner leverages GitHub Copilot coding agent, which can be configured
143147
- **Directory/file-scoped:** `.github/instructions/*.instructions.md`
144148

145149
📚 Learn more
150+
146151
- [Adding repository custom instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions)
147152
- [Optimizing GitHub Copilot for accessibility](https://accessibility.github.com/documentation/guide/copilot-instructions)
148153
- [GitHub Copilot .instructions.md support](https://github.blog/changelog/2025-07-23-github-copilot-coding-agent-now-supports-instructions-md-custom-instructions/)
149154
- [GitHub Copilot agents.md support](https://github.blog/changelog/2025-08-28-copilot-coding-agent-now-supports-agents-md-custom-instructions)
150155

151156
---
152157

158+
## Plugins
159+
160+
The plugin system allows teams to create custom scans/test to run on their pages. An example of this is axe interaction tests. In some cases, it might be desirable to perform specific interactions on elements of a given page before doing an axe scan. These interactions are usually unique to each page that is scanned, so it would require the owning team to write a custom plugin that can interact with the page and run the axe scan when ready. See the example under `./.github/scanner-plugins/test-plugin` (this is not an axe interaction test, but should give a general understanding of how plugins look like).
161+
162+
Some plugins come built-in with the scanner and can be enabled via actions inputs.
163+
164+
### How Plugins Work
165+
166+
Plugins are dynamically loaded by the scanner when it runs. The scanner will look into the `./.github` folder in your repo (where you run the workflow from) and search for a `scanner-plugins` folder. If it finds it, it will assume each folder under that is a plugin, and attempt to load the `index.js` file inside it. Once loaded, the scanner will invoke the exported default function from the `index.js` file.
167+
168+
#### Default Function Api
169+
170+
When the default function is invoked, the following arguments are passed to the function:
171+
172+
- page: this is the [playwright page](https://playwright.dev/docs/api/class-page) instance. See the linked docs for information on how to interact with the page.
173+
- addFinding: this is a function that will add a finding to the list. Findings are used to generate issues and 'filings'. See here for the [types](https://github.com/github/accessibility-scanner/blob/main/tests/types.d.ts). This function expects a single object as an argument, and it should match the `Finding` type defined in the types linked above.
174+
175+
### How To Create Plugins
176+
177+
As mentioned above, plugins need to live under `./.github/scanner-plugins`. For a plugin to work, it needs to meet the following criteria:
178+
179+
- Each seperate plugin should live in a separate folder under `./.github/scanner-plugins`. So `./.github/scanner-plugins/plugin-1` would be 1 plugin loaded by the scanner
180+
- Each plugin should have one `index.js` file inside its folder
181+
- The `index.js` file must export a `name` field. This is the name used to pass to the `scans` input. So the following: `scans: ['my-custom-plugin']` would cause the scanner to only run that plugin
182+
- The `index.js` file must export a default function. This is the function that the scanner uses to run the plugin.
183+
184+
### Things To Lookout For
185+
186+
- Plugin names should be unique. If multiple plugins have the same name, and the `scans` input passes this name, all the plugins with that name _will_ run. However, this is not advised because if you want to turn off one plugin, you'll have to go back and change that plugin name.
187+
188+
---
189+
153190
## Feedback
154191

155192
💬 We welcome your feedback! To submit feedback or report issues, please create an issue in this repository. For more information on contributing, please refer to the [CONTRIBUTING](./CONTRIBUTING.md) file.

0 commit comments

Comments
 (0)