Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/actions/find/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ configuration option.
Allowlisted NPM plugins use an object with `name`, `package`, and optional `version`. If not provided, only Axe
will be performed. See [the plugin docs](../../../PLUGINS.md#loading-plugins-from-npm-packages) for an example.

#### `plugin_failure_policy`

**Optional** `log-and-continue` (default) or `abort`. Applies to built-in, local, and NPM plugins, not
Axe or AccessLint. The default logs scan errors and returns partial findings, preserving existing behavior
(a thrown plugin error skips the remaining scans on that URL; subsequent URLs still run).
`abort` rejects failed or unavailable requested plugins, stops subsequent scans and URLs, and writes no
findings file or output for the run. See [plugin failure behavior](../../../PLUGINS.md#plugin-failure-behavior)
for limitations.

### Outputs

#### `findings_file`
Expand Down
4 changes: 4 additions & 0 deletions .github/actions/find/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ inputs:
scans:
description: "Stringified JSON array of scans to perform. Core engines are 'axe' and 'accesslint'. Any other entry is either a plugin name (string) or, for a first-party NPM-published plugin, an object with 'name', 'package', and optional 'version'. If not provided, only Axe will be performed"
required: false
plugin_failure_policy:
description: "Plugin failure behavior: 'log-and-continue' preserves partial findings (default); 'abort' fails before publishing findings when a requested plugin fails or is unavailable. Does not change core engine failure behavior."
required: false
default: 'log-and-continue'
reduced_motion:
description: 'Playwright reducedMotion setting: https://playwright.dev/docs/api/class-browser#browser-new-page-option-reduced-motion'
required: false
Expand Down
16 changes: 16 additions & 0 deletions .github/actions/find/src/findForUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ import * as core from '@actions/core'

const SELECTOR_WAIT_TIMEOUT = 30000

export type PluginFailurePolicy = 'log-and-continue' | 'abort'

export async function findForUrl(
urlConfig: UrlConfig,
authContext?: AuthContext,
includeScreenshots: boolean = false,
reducedMotion?: ReducedMotionPreference,
colorScheme?: ColorSchemePreference,
ignoreHttpsErrors: boolean = false,
pluginFailurePolicy: PluginFailurePolicy = 'log-and-continue',
): Promise<Finding[]> {
const {url, excludeSelectors, waitForSelectors} = urlConfig
const browser = await playwright.chromium.launch({
Expand Down Expand Up @@ -56,13 +59,22 @@ export async function findForUrl(
findings.push({...findingData, screenshotId, originalUrl: url})
}

let pluginStage: string | undefined
try {
const scansContext = getScansContext()

if (scansContext.shouldRunPlugins) {
pluginStage = 'loading requested plugins'
const plugins = await loadPlugins()
if (pluginFailurePolicy === 'abort') {
const unavailable = scansContext.pluginScans.filter(name => !plugins.some(plugin => plugin.name === name))
if (unavailable.length > 0) {
throw new Error(`Requested plugins are unavailable: ${unavailable.join(', ')}`)
}
}
for (const plugin of plugins) {
if (scansContext.scansToPerform.includes(plugin.name)) {
pluginStage = `running plugin '${plugin.name}'`
core.info(`Running plugin: ${plugin.name}`)
await invokePlugin({
plugin,
Expand All @@ -73,6 +85,7 @@ export async function findForUrl(
core.info(`Skipping plugin ${plugin.name} because it is not included in the 'scans' input`)
}
}
pluginStage = undefined
}

if (scansContext.shouldPerformAxeScan) {
Expand All @@ -84,6 +97,9 @@ export async function findForUrl(
}
} catch (e) {
core.error(`Error during accessibility scan: ${e}`)
if (pluginFailurePolicy === 'abort' && pluginStage) {
throw new Error(`Plugin failure while ${pluginStage} on ${url}: ${e}`, {cause: e})
}
}
return findings
} finally {
Expand Down
10 changes: 10 additions & 0 deletions .github/actions/find/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default async function () {
const reducedMotion = loadReducedMotion()
const colorScheme = loadColorScheme()
const ignoreHttpsErrors = loadIgnoreHttpsErrors()
const pluginFailurePolicy = loadPluginFailurePolicy()

const actualUrls = urlConfigs || urls || []

Expand All @@ -30,6 +31,7 @@ export default async function () {
reducedMotion,
colorScheme,
ignoreHttpsErrors,
pluginFailurePolicy,
)
if (findingsForUrl.length === 0) {
core.info(`No accessibility gaps were found on ${url}`)
Expand Down Expand Up @@ -120,3 +122,11 @@ function loadColorScheme() {
function loadIgnoreHttpsErrors() {
return core.getInput('ignore_https_errors', {required: false}).toLowerCase() === 'true'
}

function loadPluginFailurePolicy() {
const value = core.getInput('plugin_failure_policy', {required: false}) || 'log-and-continue'
if (value !== 'log-and-continue' && value !== 'abort') {
throw new Error("Input 'plugin_failure_policy' must be one of: 'log-and-continue', 'abort'.")
}
return value
}
2 changes: 2 additions & 0 deletions .github/actions/find/src/scansContextProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {NpmPluginRequest} from './pluginManager/types.js'
type ScansContext = {
scansToPerform: Array<string>
npmPlugins: NpmPluginRequest[]
pluginScans: string[]
shouldPerformAxeScan: boolean
shouldPerformAccesslintScan: boolean
shouldRunPlugins: boolean
Expand Down Expand Up @@ -54,6 +55,7 @@ export function getScansContext() {
scansContext = {
scansToPerform,
npmPlugins,
pluginScans,
// - if no 'scans' input is provided, we default to the existing behavior
// (only axe scan) for backwards compatability.
// - we can enforce using the 'scans' input in a future major release and
Expand Down
10 changes: 9 additions & 1 deletion .github/actions/find/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ describe('url_configs', () => {

await find()

expect(mocks.findForUrl).toHaveBeenCalledWith(urlConfig, expect.anything(), false, undefined, undefined, false)
expect(mocks.findForUrl).toHaveBeenCalledWith(
urlConfig,
expect.anything(),
false,
undefined,
undefined,
false,
'log-and-continue',
)
})

it('rejects waitForSelectors values that are not arrays of strings', async () => {
Expand Down
Loading
Loading