Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
49c3e2c
feat: add ignore_https_errors input for self-signed certificates
mvanhorn Jun 7, 2026
0060cc3
chore(deps): Bump the github-actions group across 1 directory with 2 …
dependabot[bot] Oct 16, 2026
95a12e6
chore(deps): Bump the github-actions group across 1 directory with 2 …
JoyceZhu Oct 16, 2026
6d84bfe
chore(deps-dev): Bump vite from 8.2.2 to 8.3.5 (#286)
dependabot[bot] Oct 21, 2026
b5c9678
chore(deps-dev): Bump undici from 6.29.0 to 6.29.1 (#287)
dependabot[bot] Oct 21, 2026
b37e9e1
Handle long accessibility rule labels
helen Oct 23, 2026
07b343c
Handle long accessibility rule labels (#289)
helen Oct 23, 2026
23fa5ee
chore(deps): Bump ruby/setup-ruby
dependabot[bot] Oct 23, 2026
e442d9b
chore(deps): Bump ruby/setup-ruby from 1.328.0 to 1.329.0 in the gith…
abdulahmad307 Oct 26, 2026
d46305d
Add accesslint to the list of scans in test workflow
helen Oct 27, 2026
13fe126
Update site test for AccessLint results
Copilot Oct 28, 2026
31e250a
Add accesslint to the list of scans in test workflow (#291)
helen Oct 28, 2026
a436263
Merge remote-tracking branch 'upstream/main' into fix/150-ignore-http…
mvanhorn Oct 29, 2026
d3018b0
fix: forward ignore_https_errors to auth and update find index test
mvanhorn Oct 30, 2026
babe902
Update README with warning on HTTPS errors
abdulahmad307 Oct 30, 2026
7425cce
chore(deps): Bump ruby/setup-ruby
dependabot[bot] Oct 30, 2026
67da339
chore(deps): Bump ruby/setup-ruby from 1.329.0 to 1.334.0 in the gith…
helen Nov 4, 2026
5d80611
feat: add ignore_https_errors input for scanning self-signed SSL envi…
helen Nov 4, 2026
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/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ Authenticate with Playwright and save session state. Supports HTTP Basic and for
> [!IMPORTANT]
> Don’t put passwords in your workflow as plain text; instead reference a [repository secret](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository).

#### `ignore_https_errors`

**Optional** Bool - whether to ignore HTTPS errors when logging in to hosts with self-signed certificates.

[!WARNING]
Enabling this disables TLS certificate validation for the authentication step itself. If an attacker can intercept traffic, they can present an untrusted certificate and capture submitted credentials (including HTTP Basic auth and form passwords) and resulting session cookies/storage.

Use this only in trusted test/staging environments. For shared workflows, prefer a separate explicit opt-in for auth flows (for example, `auth_ignore_https_errors`) so certificate checks are not unintentionally disabled during credentialed requests.

### Outputs

#### `auth_context`
Expand Down
4 changes: 4 additions & 0 deletions .github/actions/auth/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ inputs:
password:
description: "Password"
required: true
ignore_https_errors:
description: "Whether to ignore HTTPS errors when logging in to hosts with self-signed certificates"
required: false
default: "false"

outputs:
auth_context:
Expand Down
2 changes: 2 additions & 0 deletions .github/actions/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default async function () {
const username = core.getInput('username', {required: true})
const password = core.getInput('password', {required: true})
core.setSecret(password)
const ignoreHttpsErrors = core.getInput('ignore_https_errors', {required: false}).toLowerCase() === 'true'

// Launch a headless browser
browser = await playwright.chromium.launch({
Expand All @@ -27,6 +28,7 @@ export default async function () {
username,
password,
},
...(ignoreHttpsErrors ? {ignoreHTTPSErrors: true} : {}),

@kendallgassner kendallgassner Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non blocking question should we have just kept the name ignoreHttpsErrors

})
page = await context.newPage()

Expand Down
1 change: 1 addition & 0 deletions .github/actions/file/src/generateIssueBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ ${standardsLine}
const body = `${categoryNotice}## What
${describeFinding(finding)}

${finding.ruleId ? `**Rule:** \`${finding.scannerType} rule: ${finding.ruleId}\`\n\n` : ''}
${screenshotSection ?? ''}
To fix this, ${finding.solutionShort}.
${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''}
Expand Down
19 changes: 18 additions & 1 deletion .github/actions/file/src/openIssue.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import type {Octokit} from '@octokit/core'
import type {Finding} from './types.d.js'
import {generateIssueBody} from './generateIssueBody.js'
import {createHash} from 'node:crypto'
import * as url from 'node:url'
const URL = url.URL

/** Max length for GitHub issue titles */
const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256
/** Max length for GitHub label names */
const GITHUB_LABEL_NAME_MAX_LENGTH = 50
const RULE_LABEL_HASH_LENGTH = 4

function createRuleLabel(scannerType: string, ruleId: string): string {
const label = `${scannerType} rule: ${ruleId}`
const characters = Array.from(label)

if (characters.length <= GITHUB_LABEL_NAME_MAX_LENGTH) {
return label
}

const hash = createHash('sha256').update(label).digest('hex').slice(0, RULE_LABEL_HASH_LENGTH)
const prefixLength = GITHUB_LABEL_NAME_MAX_LENGTH - hash.length - 1
return `${characters.slice(0, prefixLength).join('')}-${hash}`
}

/**
* Truncates text to a maximum length, adding an ellipsis if truncated.
Expand All @@ -25,7 +42,7 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding
const labels = [`${primary.scannerType}-scanning-issue`]
// Only include a ruleId label when it's defined
if (primary.ruleId) {
labels.push(`${primary.scannerType} rule: ${primary.ruleId}`)
labels.push(createRuleLabel(primary.scannerType, primary.ruleId))
}
// Flag non-WCAG findings so they can be filtered or triaged separately
if (primary.category && primary.category !== 'wcag') {
Expand Down
14 changes: 14 additions & 0 deletions .github/actions/file/tests/generateIssueBody.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,26 @@ describe('generateIssueBody', () => {
const body = generateIssueBody(baseFinding, 'github/accessibility-scanner')

expect(body).toContain('## What')
expect(body).toContain('**Rule:** `axe rule: color-contrast`')
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.2 guidelines OR')
expect(body).not.toContain('Specifically:')
})

it('omits the rule line when the finding has no ruleId', () => {
const body = generateIssueBody(findingWithEmptyOptionalFields, 'github/accessibility-scanner')

expect(body).not.toContain('**Rule:**')
})

it('includes the full rule ID when its label would be shortened', () => {
const ruleId = 'keyboard-accessible/scrollable-region-with-an-extra-long-description'
const body = generateIssueBody({...baseFinding, ruleId}, 'github/accessibility-scanner')

expect(body).toContain(`**Rule:** \`axe rule: ${ruleId}\``)
})

it('formats solutionLong lines into bullets while preserving Fix any/Fix all lines', () => {
const body = generateIssueBody(
{
Expand Down
28 changes: 28 additions & 0 deletions .github/actions/file/tests/openIssue.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {describe, it, expect, vi} from 'vitest'
import {createHash} from 'node:crypto'

// Mock generateIssueBody so we can inspect what screenshotRepo is passed
vi.mock('../src/generateIssueBody.js', () => ({
Expand Down Expand Up @@ -65,6 +66,33 @@ describe('openIssue', () => {
)
})

it('shortens long rule labels with a deterministic hash suffix', async () => {
const octokit = mockOctokit()
const ruleId = 'keyboard-accessible/scrollable-region-with-an-extra-long-description'
const fullLabel = `axe rule: ${ruleId}`
await openIssue(octokit, 'org/repo', [{...baseFinding, ruleId}])

const labels = octokit.request.mock.calls[0][1].labels
const ruleLabel = labels[1]
const hash = createHash('sha256').update(fullLabel).digest('hex').slice(0, 4)

expect(ruleLabel).toBe(`${fullLabel.slice(0, 45)}-${hash}`)
expect(Array.from(ruleLabel)).toHaveLength(50)
})

it('produces distinct labels for different long rule IDs', async () => {
const firstOctokit = mockOctokit()
const secondOctokit = mockOctokit()
const commonPrefix = 'keyboard-accessible/scrollable-region-with-a-long-description-'
await openIssue(firstOctokit, 'org/repo', [{...baseFinding, ruleId: `${commonPrefix}first`}])
await openIssue(secondOctokit, 'org/repo', [{...baseFinding, ruleId: `${commonPrefix}second`}])

const firstLabel = firstOctokit.request.mock.calls[0][1].labels[1]
const secondLabel = secondOctokit.request.mock.calls[0][1].labels[1]
expect(firstLabel.slice(0, 45)).toBe(secondLabel.slice(0, 45))
expect(firstLabel).not.toBe(secondLabel)
})

it('adds a category label for non-WCAG findings', async () => {
const octokit = mockOctokit()
await openIssue(octokit, 'org/repo', [{...baseFinding, category: 'best-practice'}])
Expand Down
4 changes: 4 additions & 0 deletions .github/actions/find/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ configuration option.
[`colorScheme`](https://playwright.dev/docs/api/class-browser#browser-new-context-option-color-scheme)
configuration option.

#### `ignore_https_errors`

**Optional** Bool - whether to ignore HTTPS errors when scanning URLs with self-signed certificates.

#### `include_screenshots`

**Optional** Bool - whether to capture screenshots of scanned pages and include links to them in the issue
Expand Down
5 changes: 5 additions & 0 deletions .github/actions/find/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ inputs:
color_scheme:
description: 'Playwright colorScheme setting: https://playwright.dev/docs/api/class-browser#browser-new-context-option-color-scheme'
required: false
ignore_https_errors:
description: 'Whether to ignore HTTPS errors when scanning URLs with self-signed certificates'
required: false
default: 'false'

outputs:
findings_file:
description: 'Path to a JSON file containing the list of potential accessibility gaps'
Expand Down
2 changes: 2 additions & 0 deletions .github/actions/find/src/findForUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export async function findForUrl(
includeScreenshots: boolean = false,
reducedMotion?: ReducedMotionPreference,
colorScheme?: ColorSchemePreference,
ignoreHttpsErrors: boolean = false,
): Promise<Finding[]> {
const {url, excludeSelectors, waitForSelectors} = urlConfig
const browser = await playwright.chromium.launch({
Expand All @@ -33,6 +34,7 @@ export async function findForUrl(
...(authContext?.toPlaywrightBrowserContextOptions() ?? {}),
...(reducedMotion ? {reducedMotion} : {}),
...(colorScheme ? {colorScheme} : {}),
...(ignoreHttpsErrors ? {ignoreHTTPSErrors: true} : {}),
}
let context: playwright.BrowserContext | undefined
try {
Expand Down
14 changes: 13 additions & 1 deletion .github/actions/find/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default async function () {
const urls = loadUrls({urlConfigs})
const reducedMotion = loadReducedMotion()
const colorScheme = loadColorScheme()
const ignoreHttpsErrors = loadIgnoreHttpsErrors()

const actualUrls = urlConfigs || urls || []

Expand All @@ -22,7 +23,14 @@ export default async function () {
for (const urlConfig of actualUrls) {
const {url} = urlConfig
core.info(`Preparing to scan ${url}`)
const findingsForUrl = await findForUrl(urlConfig, authContext, includeScreenshots, reducedMotion, colorScheme)
const findingsForUrl = await findForUrl(
urlConfig,
authContext,
includeScreenshots,
reducedMotion,
colorScheme,
ignoreHttpsErrors,
)
if (findingsForUrl.length === 0) {
core.info(`No accessibility gaps were found on ${url}`)
continue
Expand Down Expand Up @@ -108,3 +116,7 @@ function loadColorScheme() {

return colorSchemeInput as ColorSchemePreference
}

function loadIgnoreHttpsErrors() {
return core.getInput('ignore_https_errors', {required: false}).toLowerCase() === 'true'
}
31 changes: 31 additions & 0 deletions .github/actions/find/tests/findForUrl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,4 +392,35 @@ describe('findForUrl', () => {
expect(await categoryFor(['wcag2a', 'experimental'])).toBe('experimental')
})
})

describe('when ignoreHttpsErrors is enabled', () => {
it('forwards ignoreHTTPSErrors to the browser context', async () => {
actionInput = ''
clearAll()

await findForUrl({url: 'test.com'}, undefined, false, undefined, undefined, true)

expect(playwrightMocks.browserNewContext).toHaveBeenCalledWith({ignoreHTTPSErrors: true})
})
})

describe('when ignoreHttpsErrors is not enabled', () => {
it('does not forward ignoreHTTPSErrors by default', async () => {
actionInput = ''
clearAll()

await findForUrl({url: 'test.com'})

expect(playwrightMocks.browserNewContext).toHaveBeenCalledWith({})
})

it('does not forward ignoreHTTPSErrors when explicitly disabled', async () => {
actionInput = ''
clearAll()

await findForUrl({url: 'test.com'}, undefined, false, undefined, undefined, false)

expect(playwrightMocks.browserNewContext).toHaveBeenCalledWith({})
})
})
})
2 changes: 1 addition & 1 deletion .github/actions/find/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('url_configs', () => {

await find()

expect(mocks.findForUrl).toHaveBeenCalledWith(urlConfig, expect.anything(), false, undefined, undefined)
expect(mocks.findForUrl).toHaveBeenCalledWith(urlConfig, expect.anything(), false, undefined, undefined, false)
})

it('rejects waitForSelectors values that are not arrays of strings', async () => {
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: npm
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup Ruby
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8
uses: ruby/setup-ruby@26e5b7d197ebf60545ee9667bcbace0be11ccaec
with:
ruby-version: '3.4'
bundler-cache: true
Expand Down Expand Up @@ -80,7 +80,7 @@ jobs:
repository: ${{ env.TESTING_REPOSITORY }}
token: ${{ secrets.GH_TOKEN }}
cache_key: ${{ steps.cache_key.outputs.cache_key }}
scans: '["axe", "reflow-scan"]'
scans: '["axe", "reflow-scan", "accesslint"]'

- name: Retrieve cached results
uses: ./.github/actions/gh-cache/restore
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
# 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
# ignore_https_errors: false # Optional: Set to true to ignore HTTPS errors when scanning URLs with self-signed certificates
# scans: '["axe","accesslint","reflow-scan"]' # Optional: An array of scans (or plugins) to be performed. Built-in engines are 'axe' and 'accesslint'; any other entry is a plugin name. If not provided, only Axe will be performed.
# url_configs: '[{"url":"https://example.com","excludeSelectors":["iframe","#widget"],"waitForSelectors":["#app","[data-ready]"]}]' # Optional: Per-URL config with CSS selectors to exclude from Axe or wait for before scanning. When provided, takes precedence over 'urls'.
```
Expand Down Expand Up @@ -137,6 +138,7 @@ Trigger the workflow manually or automatically based on your configuration. The
| `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` |
| `ignore_https_errors` | No | Whether to ignore HTTPS errors when scanning URLs with self-signed certificates. Default: `false` | `true` |
| `scans` | No | An array of scans (or plugins) to be performed. Built-in engines are `axe` and `accesslint`; any other entry is treated as a plugin name. If not provided, only Axe will be performed. | `'["axe", "accesslint", ...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 `excludeSelectors` (selectors to exclude from Axe) and `waitForSelectors` (selectors that must become visible within 30 seconds before scanning). When provided, takes precedence over the `urls` input. | `'[{"url":"https://example.com","excludeSelectors":["iframe","#widget"],"waitForSelectors":["#app"]}]'` |
Expand Down
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ inputs:
color_scheme:
description: 'Playwright colorScheme setting: https://playwright.dev/docs/api/class-browser#browser-new-context-option-color-scheme'
required: false
ignore_https_errors:
description: 'Whether to ignore HTTPS errors when scanning URLs with self-signed certificates'
required: false
default: 'false'
scans:
description: "Stringified JSON array of scans to perform. Core engines and local plugins use string names. Allowlisted NPM plugins use an object with 'name', 'package', and optional 'version'. If not provided, only Axe will be performed"
required: false
Expand Down Expand Up @@ -123,6 +127,7 @@ runs:
login_url: ${{ inputs.login_url }}
username: ${{ inputs.username }}
password: ${{ inputs.password }}
ignore_https_errors: ${{ inputs.ignore_https_errors }}
Comment thread
helen marked this conversation as resolved.
Resolved
Comment thread
helen marked this conversation as resolved.
Resolved
- name: Find
id: find
uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/find
Expand All @@ -133,6 +138,7 @@ runs:
include_screenshots: ${{ inputs.include_screenshots }}
reduced_motion: ${{ inputs.reduced_motion }}
color_scheme: ${{ inputs.color_scheme }}
ignore_https_errors: ${{ inputs.ignore_https_errors }}
scans: ${{ inputs.scans }}
- name: File
id: file
Expand Down
Loading
Loading