├── .gitignore
├── .github
├── CODEOWNERS
├── actions
│ ├── fix
│ │ ├── src
│ │ │ ├── types.d.ts
│ │ │ ├── retry.ts
│ │ │ ├── Issue.ts
│ │ │ ├── getLinkedPR.ts
│ │ │ ├── index.ts
│ │ │ └── assignIssue.ts
│ │ ├── tsconfig.json
│ │ ├── package.json
│ │ ├── action.yml
│ │ ├── README.md
│ │ ├── bootstrap.js
│ │ └── package-lock.json
│ ├── auth
│ │ ├── tsconfig.json
│ │ ├── src
│ │ │ ├── types.d.ts
│ │ │ └── index.ts
│ │ ├── package.json
│ │ ├── action.yml
│ │ ├── README.md
│ │ ├── bootstrap.js
│ │ └── package-lock.json
│ ├── file
│ │ ├── tsconfig.json
│ │ ├── src
│ │ │ ├── isNewFiling.ts
│ │ │ ├── isResolvedFiling.ts
│ │ │ ├── isRepeatedFiling.ts
│ │ │ ├── closeIssue.ts
│ │ │ ├── reopenIssue.ts
│ │ │ ├── types.d.ts
│ │ │ ├── updateFilingsWithNewFindings.ts
│ │ │ ├── Issue.ts
│ │ │ ├── openIssue.ts
│ │ │ └── index.ts
│ │ ├── package.json
│ │ ├── action.yml
│ │ ├── README.md
│ │ ├── bootstrap.js
│ │ └── package-lock.json
│ ├── find
│ │ ├── tsconfig.json
│ │ ├── package.json
│ │ ├── action.yml
│ │ ├── src
│ │ │ ├── types.d.ts
│ │ │ ├── index.ts
│ │ │ ├── findForUrl.ts
│ │ │ └── AuthContext.ts
│ │ ├── README.md
│ │ ├── bootstrap.js
│ │ └── package-lock.json
│ └── gh-cache
│ │ ├── save
│ │ ├── README.md
│ │ └── action.yml
│ │ ├── delete
│ │ ├── README.md
│ │ └── action.yml
│ │ ├── restore
│ │ ├── README.md
│ │ └── action.yml
│ │ └── cache
│ │ ├── README.md
│ │ └── action.yml
├── dependabot.yml
└── workflows
│ └── test.yml
├── sites
└── site-with-errors
│ ├── .gitignore
│ ├── index.markdown
│ ├── 404.html
│ ├── about.markdown
│ ├── _posts
│ └── 2025-07-30-welcome-to-jekyll.markdown
│ ├── Gemfile
│ ├── config.ru
│ ├── _config.yml
│ └── Gemfile.lock
├── tests
├── types.d.ts
└── site-with-errors.test.ts
├── package.json
├── LICENSE
├── SUPPORT.md
├── CONTRIBUTING.md
├── SECURITY.md
├── CODE_OF_CONDUCT.md
├── action.yml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | test-results
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @github/accessibility-reviewers
2 |
--------------------------------------------------------------------------------
/sites/site-with-errors/.gitignore:
--------------------------------------------------------------------------------
1 | _site
2 | .sass-cache
3 | .jekyll-cache
4 | .jekyll-metadata
5 | vendor
6 |
--------------------------------------------------------------------------------
/sites/site-with-errors/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | # Feel free to add content and custom Front Matter to this file.
3 | # To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
4 |
5 | layout: home
6 | ---
7 |
--------------------------------------------------------------------------------
/.github/actions/fix/src/types.d.ts:
--------------------------------------------------------------------------------
1 | export type Issue = {
2 | url: string;
3 | nodeId?: string;
4 | };
5 |
6 | export type PullRequest = {
7 | url: string;
8 | nodeId?: string;
9 | };
10 |
11 | export type Fixing = {
12 | issue: Issue;
13 | pullRequest: PullRequest;
14 | };
15 |
--------------------------------------------------------------------------------
/.github/actions/auth/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "nodenext",
5 | "moduleResolution": "nodenext",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "rootDir": "src",
9 | "outDir": "dist"
10 | },
11 | "include": [
12 | "src/**/*.ts"
13 | ]
14 | }
--------------------------------------------------------------------------------
/.github/actions/file/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "nodenext",
5 | "moduleResolution": "nodenext",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "rootDir": "src",
9 | "outDir": "dist"
10 | },
11 | "include": [
12 | "src/**/*.ts"
13 | ]
14 | }
--------------------------------------------------------------------------------
/.github/actions/find/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "nodenext",
5 | "moduleResolution": "nodenext",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "rootDir": "src",
9 | "outDir": "dist"
10 | },
11 | "include": [
12 | "src/**/*.ts"
13 | ]
14 | }
--------------------------------------------------------------------------------
/.github/actions/fix/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "nodenext",
5 | "moduleResolution": "nodenext",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "rootDir": "src",
9 | "outDir": "dist"
10 | },
11 | "include": [
12 | "src/**/*.ts"
13 | ]
14 | }
--------------------------------------------------------------------------------
/.github/actions/file/src/isNewFiling.ts:
--------------------------------------------------------------------------------
1 | import type { Filing, NewFiling } from "./types.d.js";
2 |
3 | export function isNewFiling(filing: Filing): filing is NewFiling {
4 | // A Filing without an issue is new
5 | return (
6 | (!("issue" in filing) || !filing.issue?.url) &&
7 | "findings" in filing &&
8 | filing.findings.length > 0
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/.github/actions/file/src/isResolvedFiling.ts:
--------------------------------------------------------------------------------
1 | import type { Filing, ResolvedFiling } from "./types.d.js";
2 |
3 | export function isResolvedFiling(filing: Filing): filing is ResolvedFiling {
4 | // A Filing without findings is resolved
5 | return (
6 | (!("findings" in filing) || filing.findings.length === 0) &&
7 | "issue" in filing &&
8 | !!filing.issue?.url
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/.github/actions/file/src/isRepeatedFiling.ts:
--------------------------------------------------------------------------------
1 | import type { Filing, RepeatedFiling } from "./types.d.js";
2 |
3 | export function isRepeatedFiling(filing: Filing): filing is RepeatedFiling {
4 | // A Filing with an issue and findings is a repeated filing
5 | return (
6 | "findings" in filing &&
7 | filing.findings.length > 0 &&
8 | "issue" in filing &&
9 | !!filing.issue?.url
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/.github/actions/gh-cache/save/README.md:
--------------------------------------------------------------------------------
1 | # save
2 |
3 | Commit a file or directory to an orphaned 'gh-cache' branch.
4 |
5 | ## Usage
6 |
7 | ### Inputs
8 |
9 | #### `path`
10 |
11 | **Required** Relative path to a file or directory to save. Allowed characters are `A-Za-z0-9._/-`. For example: `findings.json`.
12 |
13 | #### `token`
14 |
15 | **Required** Token with fine-grained permissions 'contents: write'.
16 |
--------------------------------------------------------------------------------
/.github/actions/file/src/closeIssue.ts:
--------------------------------------------------------------------------------
1 | import type { Octokit } from '@octokit/core';
2 | import { Issue } from './Issue.js';
3 |
4 | export async function closeIssue(octokit: Octokit, { owner, repository, issueNumber }: Issue) {
5 | return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, {
6 | owner,
7 | repository,
8 | issue_number: issueNumber,
9 | state: 'closed'
10 | });
11 | }
--------------------------------------------------------------------------------
/.github/actions/gh-cache/delete/README.md:
--------------------------------------------------------------------------------
1 | # delete
2 |
3 | Delete a file or directory from the orphaned 'gh-cache' branch
4 |
5 | ## Usage
6 |
7 | ### Inputs
8 |
9 | #### `path`
10 |
11 | **Required** Relative path to a file or directory to delete. Allowed characters are `A-Za-z0-9._/-`. For example: `findings.json`.
12 |
13 | #### `token`
14 |
15 | **Required** Token with fine-grained permissions 'contents: write'.
16 |
--------------------------------------------------------------------------------
/.github/actions/file/src/reopenIssue.ts:
--------------------------------------------------------------------------------
1 | import type { Octokit } from '@octokit/core';
2 | import type { Issue } from './Issue.js';
3 |
4 | export async function reopenIssue(octokit: Octokit, { owner, repository, issueNumber}: Issue) {
5 | return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, {
6 | owner,
7 | repository,
8 | issue_number: issueNumber,
9 | state: 'open'
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/.github/actions/auth/src/types.d.ts:
--------------------------------------------------------------------------------
1 | export type Cookie = {
2 | name: string;
3 | value: string;
4 | domain: string;
5 | path: string;
6 | expires?: number;
7 | httpOnly?: boolean;
8 | secure?: boolean;
9 | sameSite?: "Strict" | "Lax" | "None";
10 | };
11 |
12 | export type LocalStorage = {
13 | [origin: string]: {
14 | [key: string]: string;
15 | };
16 | };
17 |
18 | export type AuthContextOutput = {
19 | username?: string;
20 | password?: string;
21 | cookies?: Cookie[];
22 | localStorage?: LocalStorage;
23 | };
24 |
--------------------------------------------------------------------------------
/sites/site-with-errors/404.html:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /404.html
3 | layout: page
4 | ---
5 |
6 |
19 |
20 |
21 |
404
22 |
23 |
Page not found :(
24 |
The requested page could not be found.
25 |
26 |
--------------------------------------------------------------------------------
/.github/actions/gh-cache/restore/README.md:
--------------------------------------------------------------------------------
1 | # restore
2 |
3 | Checkout a file or directory from an orphaned 'gh-cache' branch.
4 |
5 | ## Usage
6 |
7 | ### Inputs
8 |
9 | #### `path`
10 |
11 | **Required** Relative path to a file or directory to restore. Allowed characters are `A-Za-z0-9._/-`. For example: `findings.json`.
12 |
13 | #### `token`
14 |
15 | **Required** Token with fine-grained permissions 'contents: read'.
16 |
17 | #### `fail_on_cache_miss`
18 |
19 | **Optional** Fail the workflow if cached item is not found. For example: `'true'`. Default: `'false'`.
20 |
--------------------------------------------------------------------------------
/tests/types.d.ts:
--------------------------------------------------------------------------------
1 | export type Finding = {
2 | scannerType: string;
3 | ruleId: string;
4 | url: string;
5 | html: string;
6 | problemShort: string;
7 | problemUrl: string;
8 | solutionShort: string;
9 | solutionLong?: string;
10 | };
11 |
12 | export type Issue = {
13 | id: number;
14 | nodeId: string;
15 | url: string;
16 | title: string;
17 | state?: "open" | "reopened" | "closed";
18 | };
19 |
20 | export type PullRequest = {
21 | url: string;
22 | nodeId: string;
23 | };
24 |
25 | export type Result = {
26 | findings: Finding[];
27 | issue: Issue;
28 | pullRequest: PullRequest;
29 | };
30 |
--------------------------------------------------------------------------------
/.github/actions/find/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "find",
3 | "version": "1.0.0",
4 | "description": "Finds potential accessibility gaps.",
5 | "main": "dist/index.js",
6 | "module": "dist/index.js",
7 | "scripts": {
8 | "start": "node bootstrap.js",
9 | "build": "tsc"
10 | },
11 | "keywords": [],
12 | "author": "GitHub",
13 | "license": "MIT",
14 | "type": "module",
15 | "dependencies": {
16 | "@actions/core": "^2.0.1",
17 | "@axe-core/playwright": "^4.11.0",
18 | "playwright": "^1.57.0"
19 | },
20 | "devDependencies": {
21 | "@types/node": "^25.0.2",
22 | "typescript": "^5.9.3"
23 | }
24 | }
--------------------------------------------------------------------------------
/sites/site-with-errors/about.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: About
4 | permalink: /about/
5 | ---
6 |
7 | This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](https://jekyllrb.com/)
8 |
9 | You can find the source code for Minima at GitHub:
10 | [jekyll][jekyll-organization] /
11 | [minima](https://github.com/jekyll/minima)
12 |
13 | You can find the source code for Jekyll at GitHub:
14 | [jekyll][jekyll-organization] /
15 | [jekyll](https://github.com/jekyll/jekyll)
16 |
17 |
18 | [jekyll-organization]: https://github.com/jekyll
19 |
--------------------------------------------------------------------------------
/.github/actions/fix/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fix",
3 | "version": "1.0.0",
4 | "description": "Attempts to fix issues with Copilot.",
5 | "main": "dist/index.js",
6 | "module": "dist/index.js",
7 | "scripts": {
8 | "start": "node bootstrap.js",
9 | "build": "tsc"
10 | },
11 | "keywords": [],
12 | "author": "GitHub",
13 | "license": "MIT",
14 | "type": "module",
15 | "dependencies": {
16 | "@actions/core": "^2.0.1",
17 | "@octokit/core": "^7.0.6",
18 | "@octokit/plugin-throttling": "^11.0.3"
19 | },
20 | "devDependencies": {
21 | "@types/node": "^25.0.2",
22 | "typescript": "^5.9.3"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.github/actions/auth/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "auth",
3 | "version": "1.0.0",
4 | "description": "Authenticate with Playwright and save session state. Supports HTTP Basic and form authentication.",
5 | "main": "dist/index.js",
6 | "module": "dist/index.js",
7 | "scripts": {
8 | "start": "node bootstrap.js",
9 | "build": "tsc"
10 | },
11 | "keywords": [],
12 | "author": "GitHub",
13 | "license": "MIT",
14 | "type": "module",
15 | "dependencies": {
16 | "@actions/core": "^2.0.1",
17 | "playwright": "^1.57.0"
18 | },
19 | "devDependencies": {
20 | "@types/node": "^25.0.2",
21 | "typescript": "^5.9.3"
22 | }
23 | }
--------------------------------------------------------------------------------
/.github/actions/file/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "file",
3 | "version": "1.0.0",
4 | "description": "Files GitHub issues to track potential accessibility gaps.",
5 | "main": "dist/index.js",
6 | "module": "dist/index.js",
7 | "scripts": {
8 | "start": "node bootstrap.js",
9 | "build": "tsc"
10 | },
11 | "keywords": [],
12 | "author": "GitHub",
13 | "license": "MIT",
14 | "type": "module",
15 | "dependencies": {
16 | "@actions/core": "^2.0.1",
17 | "@octokit/core": "^7.0.6",
18 | "@octokit/plugin-throttling": "^11.0.3"
19 | },
20 | "devDependencies": {
21 | "@types/node": "^25.0.2",
22 | "typescript": "^5.9.3"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.github/actions/find/action.yml:
--------------------------------------------------------------------------------
1 | name: "Find"
2 | description: "Finds potential accessibility gaps."
3 |
4 | inputs:
5 | urls:
6 | description: "Newline-delimited list of URLs to check for accessibility issues"
7 | required: true
8 | multiline: true
9 | auth_context:
10 | description: "Stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session"
11 | required: false
12 |
13 | outputs:
14 | findings:
15 | description: "List of potential accessibility gaps, as stringified JSON"
16 |
17 | runs:
18 | using: "node24"
19 | main: "bootstrap.js"
20 |
21 | branding:
22 | icon: "compass"
23 | color: "blue"
24 |
--------------------------------------------------------------------------------
/.github/actions/auth/action.yml:
--------------------------------------------------------------------------------
1 | name: "Auth"
2 | description: "Authenticate with Playwright and save session state. Supports HTTP Basic and form authentication."
3 |
4 | inputs:
5 | login_url:
6 | description: "Login page URL"
7 | required: true
8 | username:
9 | description: "Username"
10 | required: true
11 | password:
12 | description: "Password"
13 | required: true
14 |
15 | outputs:
16 | auth_context:
17 | description: "Stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session"
18 |
19 | runs:
20 | using: "node24"
21 | main: "bootstrap.js"
22 |
23 | branding:
24 | icon: "lock"
25 | color: "blue"
26 |
--------------------------------------------------------------------------------
/.github/actions/find/src/types.d.ts:
--------------------------------------------------------------------------------
1 | export type Finding = {
2 | url: string;
3 | html: string;
4 | problemShort: string;
5 | problemUrl: string;
6 | solutionShort: string;
7 | solutionLong?: string;
8 | };
9 |
10 | export type Cookie = {
11 | name: string;
12 | value: string;
13 | domain: string;
14 | path: string;
15 | expires?: number;
16 | httpOnly?: boolean;
17 | secure?: boolean;
18 | sameSite?: "Strict" | "Lax" | "None";
19 | };
20 |
21 | export type LocalStorage = {
22 | [origin: string]: {
23 | [key: string]: string;
24 | };
25 | };
26 |
27 | export type AuthContextInput = {
28 | username?: string;
29 | password?: string;
30 | cookies?: Cookie[];
31 | localStorage?: LocalStorage;
32 | };
33 |
--------------------------------------------------------------------------------
/.github/actions/fix/action.yml:
--------------------------------------------------------------------------------
1 | name: "Fix"
2 | description: "Attempts to fix issues with Copilot."
3 |
4 | inputs:
5 | issues:
6 | description: "List of issues to attempt to fix, as stringified JSON"
7 | required: true
8 | repository:
9 | description: "Repository (with owner) containing issues"
10 | required: true
11 | token:
12 | description: "Personal access token (PAT) with fine-grained permissions 'issues: write' and 'pull_requests: write'"
13 | required: true
14 |
15 | outputs:
16 | fixings:
17 | description: "List of pull requests filed (and their associated issues), as stringified JSON"
18 |
19 | runs:
20 | using: "node24"
21 | main: "bootstrap.js"
22 |
23 | branding:
24 | icon: "compass"
25 | color: "blue"
26 |
--------------------------------------------------------------------------------
/.github/actions/gh-cache/cache/README.md:
--------------------------------------------------------------------------------
1 | # cache
2 |
3 | Cache/uncache strings using an orphaned 'gh-cache' branch.
4 |
5 | ## Usage
6 |
7 | ### Inputs
8 |
9 | #### `key`
10 |
11 | **Required** Identifier for the string to cache/uncache.
12 |
13 | #### `token`
14 |
15 | **Required** Token with fine-grained permissions 'contents: write'.
16 |
17 | #### `value`
18 |
19 | **Optional** String to cache. If provided, the existing (cached) value will be overwritten and then outputted. If not provided, the existing (cached) value will be outputted.
20 |
21 | #### `fail_on_cache_miss`
22 |
23 | **Optional** Fail the workflow if cached item is not found. For example: `'true'`. Default: `'false'`.
24 |
25 | ### Outputs
26 |
27 | #### `value`
28 |
29 | Cached string.
30 |
--------------------------------------------------------------------------------
/.github/actions/file/src/types.d.ts:
--------------------------------------------------------------------------------
1 | export type Finding = {
2 | scannerType: string;
3 | ruleId: string;
4 | url: string;
5 | html: string;
6 | problemShort: string;
7 | problemUrl: string;
8 | solutionShort: string;
9 | solutionLong?: string;
10 | };
11 |
12 | export type Issue = {
13 | id: number;
14 | nodeId: string;
15 | url: string;
16 | title: string;
17 | state?: "open" | "reopened" | "closed";
18 | };
19 |
20 | export type ResolvedFiling = {
21 | findings: never[];
22 | issue: Issue;
23 | };
24 |
25 | export type NewFiling = {
26 | findings: Finding[];
27 | issue?: never;
28 | };
29 |
30 | export type RepeatedFiling = {
31 | findings: Finding[];
32 | issue: Issue;
33 | };
34 |
35 | export type Filing = ResolvedFiling | NewFiling | RepeatedFiling;
36 |
--------------------------------------------------------------------------------
/.github/actions/file/action.yml:
--------------------------------------------------------------------------------
1 | name: "File"
2 | description: "Files GitHub issues to track potential accessibility gaps."
3 |
4 | inputs:
5 | findings:
6 | description: "List of potential accessibility gaps, as stringified JSON"
7 | required: true
8 | repository:
9 | description: "Repository (with owner) to file issues in"
10 | required: true
11 | token:
12 | description: "Token with fine-grained permission 'issues: write'"
13 | required: true
14 | cached_filings:
15 | description: "Cached filings from previous runs, as stringified JSON. Without this, duplicate issues may be filed."
16 | required: false
17 |
18 | outputs:
19 | filings:
20 | description: "List of issues filed (and their associated finding(s)), as stringified JSON"
21 |
22 | runs:
23 | using: "node24"
24 | main: "bootstrap.js"
25 |
26 | branding:
27 | icon: "compass"
28 | color: "blue"
29 |
--------------------------------------------------------------------------------
/.github/actions/find/README.md:
--------------------------------------------------------------------------------
1 | # find
2 |
3 | Finds potential accessibility gaps.
4 |
5 | ## Usage
6 |
7 | ### Inputs
8 |
9 | #### `urls`
10 |
11 | **Required** Newline-delimited list of URLs to check for accessibility issues. For example:
12 |
13 | ```txt
14 | https://primer.style
15 | https://primer.style/octicons/
16 | ```
17 |
18 | #### `auth_context`
19 |
20 | **Optional** Stringified JSON object containing `username`, `password`, `cookies`, and/or `localStorage` from an authenticated session. For example: `{"username":"some-user","password":"correct-horse-battery-staple","cookies":[{"name":"theme-preference","value":"light","domain":"primer.style","path":"/"}],"localStorage":{"https://primer.style":{"theme-preference":"light"}}}`
21 |
22 | ### Outputs
23 |
24 | #### `findings`
25 |
26 | List of potential accessibility gaps, as stringified JSON. For example:
27 |
28 | ```JS
29 | '[]'
30 | ```
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "accessibility-scanner",
3 | "version": "0.0.0-development",
4 | "description": "Finds potential accessibility gaps, files GitHub issues to track them, and attempts to fix them with Copilot",
5 | "scripts": {
6 | "test": "vitest run tests/*.test.ts"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/github/accessibility-scanner.git"
11 | },
12 | "author": "GitHub Inc.",
13 | "license": "MIT",
14 | "bugs": {
15 | "url": "https://github.com/github/accessibility-scanner/issues"
16 | },
17 | "homepage": "https://github.com/github/accessibility-scanner#readme",
18 | "devDependencies": {
19 | "@actions/core": "^2.0.1",
20 | "@octokit/core": "^7.0.6",
21 | "@octokit/plugin-throttling": "^11.0.3",
22 | "@octokit/types": "^16.0.0",
23 | "@types/node": "^25.0.2",
24 | "vitest": "^4.0.15"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.github/actions/auth/README.md:
--------------------------------------------------------------------------------
1 | # auth
2 |
3 | Authenticate with Playwright and save session state. Supports HTTP Basic and form authentication.
4 |
5 | ## Usage
6 |
7 | ### Inputs
8 |
9 | #### `login_url`
10 |
11 | **Required** Login page URL. For example, `https://github.com/login`
12 |
13 | #### `username`
14 |
15 | **Required** Username.
16 |
17 | #### `password`
18 |
19 | **Required** Password.
20 |
21 | > [!IMPORTANT]
22 | > 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).
23 |
24 | ### Outputs
25 |
26 | #### `auth_context`
27 |
28 | Stringified JSON object containing `username`, `password`, `cookies`, and/or `localStorage` from an authenticated session. For example: `{"username":"some-user","password":"correct-horse-battery-staple","cookies":[{"name":"theme-preference","value":"light","domain":"primer.style","path":"/"}],"localStorage":{"https://primer.style":{"theme-preference":"light"}}}`
29 |
--------------------------------------------------------------------------------
/.github/actions/file/README.md:
--------------------------------------------------------------------------------
1 | # file
2 |
3 | Files GitHub issues to track potential accessibility gaps.
4 |
5 | ## Usage
6 |
7 | ### Inputs
8 |
9 | #### `findings`
10 |
11 | **Required** List of potential accessibility gaps, as stringified JSON. For example:
12 |
13 | ```JS
14 | '[]'
15 | ```
16 |
17 | #### `repository`
18 |
19 | **Required** Repository (with owner) to file issues in. For example: `primer/primer-docs`.
20 |
21 | #### `token`
22 |
23 | **Required** Token with fine-grained permission 'issues: write'.
24 |
25 | #### `cached_filings`
26 |
27 | **Optional** Cached filings from previous runs, as stringified JSON. Without this, duplicate issues may be filed. For example: `'[{"findings":[],"issue":{"id":1,"nodeId":"SXNzdWU6MQ==","url":"https://github.com/github/docs/issues/123","title":"Accessibility issue: 1"}}]'`
28 |
29 | ### Outputs
30 |
31 | #### `filings`
32 |
33 | List of issues filed (and their associated finding(s)), as stringified JSON. For example: `'[{"findings":[],"issue":{"id":1,"nodeId":"SXNzdWU6MQ==","url":"https://github.com/github/docs/issues/123","title":"Accessibility issue: 1"}}]'`
34 |
--------------------------------------------------------------------------------
/.github/actions/fix/README.md:
--------------------------------------------------------------------------------
1 | # fix
2 |
3 | Attempts to fix issues with Copilot.
4 |
5 | ## Usage
6 |
7 | ### Inputs
8 |
9 | #### `issues`
10 |
11 | **Required** List of issues to attempt to fix—including, at a minimum, their `url`s—as stringified JSON. For example: `'[{"url":"https://github.com/github/docs/issues/123"},{"nodeId":"SXNzdWU6Mg==","url":"https://github.com/github/docs/issues/124"},{"id":4,"nodeId":"SXNzdWU6NA==","url":"https://github.com/github/docs/issues/126","title":"Accessibility issue: 4"}]'`.
12 |
13 | #### `repository`
14 |
15 | **Required** Repository (with owner) to file issues in. For example: `primer/primer-docs`.
16 |
17 | #### `token`
18 |
19 | **Required** Personal access token (PAT) with fine-grained permissions 'issues: write' and 'pull_requests: write'.
20 |
21 | ### Outputs
22 |
23 | #### `fixings`
24 |
25 | List of pull requests filed (and their associated issues), as stringified JSON. For example: `'[{"issue":{"id":1,"nodeId":"SXNzdWU6MQ==","url":"https://github.com/github/docs/issues/123","title":"Accessibility issue: 1"},"pullRequest":{"url":"https://github.com/github/docs/pulls/124"}}]'`
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 GitHub
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/actions/fix/src/retry.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Sleep for a given number of milliseconds.
3 | * @param ms Time to sleep, in milliseconds.
4 | */
5 | function sleep(ms: number): Promise {
6 | return new Promise((resolve) => setTimeout(() => resolve(), ms));
7 | }
8 |
9 | /**
10 | * Retry a function with exponential backoff.
11 | * @param fn The function to retry.
12 | * @param maxAttempts The maximum number of retry attempts.
13 | * @param baseDelay The base delay between attempts.
14 | * @param attempt The current attempt number.
15 | * @returns The result of the function or undefined if all attempts fail.
16 | */
17 | export async function retry(
18 | fn: () => Promise | T | null | undefined,
19 | maxAttempts = 6,
20 | baseDelay = 2000,
21 | attempt = 1
22 | ): Promise {
23 | const value = await fn();
24 | if (value != null) return value;
25 | if (attempt >= maxAttempts) return undefined;
26 | /** Exponential backoff, capped at 30s */
27 | const delay = Math.min(30000, baseDelay * 2 ** (attempt - 1));
28 | /** ±10% jitter */
29 | const jitter = 1 + (Math.random() - 0.5) * 0.2;
30 | await sleep(Math.round(delay * jitter));
31 | return retry(fn, maxAttempts, baseDelay, attempt + 1);
32 | }
33 |
--------------------------------------------------------------------------------
/.github/actions/find/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { AuthContextInput } from "./types.js";
2 | import core from "@actions/core";
3 | import { AuthContext } from "./AuthContext.js";
4 | import { findForUrl } from "./findForUrl.js";
5 |
6 | export default async function () {
7 | core.info("Starting 'find' action");
8 | const urls = core.getMultilineInput("urls", { required: true });
9 | core.debug(`Input: 'urls: ${JSON.stringify(urls)}'`);
10 | const authContextInput: AuthContextInput = JSON.parse(
11 | core.getInput("auth_context", { required: false }) || "{}"
12 | );
13 | const authContext = new AuthContext(authContextInput);
14 |
15 | let findings = [];
16 | for (const url of urls) {
17 | core.info(`Preparing to scan ${url}`);
18 | const findingsForUrl = await findForUrl(url, authContext);
19 | if (findingsForUrl.length === 0) {
20 | core.info(`No accessibility gaps were found on ${url}`);
21 | continue;
22 | }
23 | findings.push(...findingsForUrl);
24 | core.info(`Found ${findingsForUrl.length} findings for ${url}`);
25 | }
26 |
27 | core.setOutput("findings", JSON.stringify(findings));
28 | core.debug(`Output: 'findings: ${JSON.stringify(findings)}'`);
29 | core.info(`Found ${findings.length} findings in total`);
30 | core.info("Finished 'find' action");
31 | }
32 |
--------------------------------------------------------------------------------
/.github/actions/fix/src/Issue.ts:
--------------------------------------------------------------------------------
1 | import { Issue as IssueInput } from "./types.d.js";
2 |
3 | export class Issue implements IssueInput {
4 | /**
5 | * Extracts owner, repository, and issue number from a GitHub issue URL.
6 | * @param issueUrl A GitHub issue URL (e.g. `https://github.com/owner/repo/issues/42`).
7 | * @returns An object with `owner`, `repository`, and `issueNumber` keys.
8 | * @throws The provided URL is unparseable due to its unexpected format.
9 | */
10 | static parseIssueUrl(issueUrl: string): { owner: string; repository: string; issueNumber: number } {
11 | const { owner, repository, issueNumber } = /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec(issueUrl)?.groups || {};
12 | if (!owner || !repository || !issueNumber) {
13 | throw new Error(`Could not parse issue URL: ${issueUrl}`);
14 | }
15 | return { owner, repository, issueNumber: Number(issueNumber) }
16 | }
17 |
18 | url: string;
19 | nodeId?: string;
20 |
21 | get owner(): string {
22 | return Issue.parseIssueUrl(this.url).owner;
23 | }
24 |
25 | get repository(): string {
26 | return Issue.parseIssueUrl(this.url).repository;
27 | }
28 |
29 | get issueNumber(): number {
30 | return Issue.parseIssueUrl(this.url).issueNumber;
31 | }
32 |
33 | constructor({url, nodeId}: IssueInput) {
34 | this.url = url;
35 | this.nodeId = nodeId;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # Support
2 |
3 | ## How to get help
4 |
5 | This project uses GitHub Issues to track bugs, feature requests, and support inquiries. Please search the existing issues before filing new issues to avoid duplicates.
6 |
7 | For help or questions about using this project, please open an issue for support requests, usage questions, or general inquiries. At this time, GitHub Discussions are not enabled. All communication should occur via issues.
8 |
9 | For information about contributing to this project, including how to create issues and what types of contributions we accept, please refer to the [CONTRIBUTING](./CONTRIBUTING.md) file.
10 |
11 | ## Project maintenance status
12 |
13 | This repo `github/accessibility-scanner` is under active development and maintained by GitHub staff during the public preview state. We will do our best to respond to support, feature requests, and community questions in a timely manner.
14 |
15 | ## Important notice
16 |
17 | The a11y scanner is currently in public preview and feature development work is still ongoing. This project cannot guarantee that code fixes or suggestions produced by GitHub Copilot will be fully accessible. Please use caution when applying the suggestions it provides. Always confirm or verify GitHub Copilot's recommendations with an accessibility subject matter expert before using them in production.
18 |
19 | ## GitHub support policy
20 |
21 | Support for this project is limited to the resources listed above (GitHub Issues).
22 |
--------------------------------------------------------------------------------
/sites/site-with-errors/_posts/2025-07-30-welcome-to-jekyll.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Welcome to Jekyll!"
4 | date: 2025-07-30 13:32:33 -0400
5 | categories: jekyll update
6 | ---
7 | You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated.
8 |
9 | Jekyll requires blog post files to be named according to the following format:
10 |
11 | `YEAR-MONTH-DAY-title.MARKUP`
12 |
13 | Where `YEAR` is a four-digit number, `MONTH` and `DAY` are both two-digit numbers, and `MARKUP` is the file extension representing the format used in the file. After that, include the necessary front matter. Take a look at the source for this post to get an idea about how it works.
14 |
15 | Jekyll also offers powerful support for code snippets:
16 |
17 | {% highlight ruby %}
18 | def print_hi(name)
19 | puts "Hi, #{name}"
20 | end
21 | print_hi('Tom')
22 | #=> prints 'Hi, Tom' to STDOUT.
23 | {% endhighlight %}
24 |
25 | Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk].
26 |
27 | [jekyll-docs]: https://jekyllrb.com/docs/home
28 | [jekyll-gh]: https://github.com/jekyll/jekyll
29 | [jekyll-talk]: https://talk.jekyllrb.com/
--------------------------------------------------------------------------------
/sites/site-with-errors/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | # Hello! This is where you manage which Jekyll version is used to run.
3 | # When you want to use a different version, change it below, save the
4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
5 | #
6 | # bundle exec jekyll serve
7 | #
8 | # This will help ensure the proper Jekyll version is running.
9 | # Happy Jekylling!
10 | gem "jekyll", "~> 4.4.1"
11 | # This is the default theme for new Jekyll sites. You may change this to anything you like.
12 | gem "minima", "~> 2.5"
13 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
14 | # uncomment the line below. To upgrade, run `bundle update github-pages`.
15 | # gem "github-pages", group: :jekyll_plugins
16 | # If you have any plugins, put them here!
17 | group :jekyll_plugins do
18 | gem "jekyll-feed", "~> 0.12"
19 | end
20 |
21 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
22 | # and associated library.
23 | platforms :mingw, :x64_mingw, :mswin, :jruby do
24 | gem "tzinfo", ">= 1", "< 3"
25 | gem "tzinfo-data"
26 | end
27 |
28 | # Performance-booster for watching directories on Windows
29 | gem "wdm", "~> 0.1", :platforms => [:mingw, :x64_mingw, :mswin]
30 |
31 | # Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem
32 | # do not have a Java counterpart.
33 | gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
34 |
35 | # Web server
36 | gem "rack", "~> 3.2"
37 | gem "puma", "~> 7.1"
--------------------------------------------------------------------------------
/.github/actions/find/src/findForUrl.ts:
--------------------------------------------------------------------------------
1 | import type { Finding } from './types.d.js';
2 | import AxeBuilder from '@axe-core/playwright'
3 | import playwright from 'playwright';
4 | import { AuthContext } from './AuthContext.js';
5 |
6 | export async function findForUrl(url: string, authContext?: AuthContext): Promise {
7 | const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined });
8 | const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {};
9 | const context = await browser.newContext(contextOptions);
10 | const page = await context.newPage();
11 | await page.goto(url);
12 | console.log(`Scanning ${page.url()}`);
13 |
14 | let findings: Finding[] = [];
15 | try {
16 | const rawFindings = await new AxeBuilder({ page }).analyze();
17 | findings = rawFindings.violations.map(violation => ({
18 | scannerType: 'axe',
19 | url,
20 | html: violation.nodes[0].html.replace(/'/g, "'"),
21 | problemShort: violation.help.toLowerCase().replace(/'/g, "'"),
22 | problemUrl: violation.helpUrl.replace(/'/g, "'"),
23 | ruleId: violation.id,
24 | solutionShort: violation.description.toLowerCase().replace(/'/g, "'"),
25 | solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'")
26 | }));
27 | } catch (e) {
28 | // do something with the error
29 | }
30 | await context.close();
31 | await browser.close();
32 | return findings;
33 | }
34 |
--------------------------------------------------------------------------------
/.github/actions/auth/bootstrap.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | //@ts-check
3 |
4 | import fs from 'node:fs'
5 | import * as url from 'node:url'
6 | import { spawn } from 'node:child_process'
7 |
8 | function spawnPromisified(command, args, { quiet = false, ...options } = {}) {
9 | return new Promise((resolve, reject) => {
10 | const proc = spawn(command, args, options)
11 | proc.stdout.setEncoding('utf8')
12 | proc.stdout.on('data', (data) => {
13 | if (!quiet) {
14 | console.log(data)
15 | }
16 | })
17 | proc.stderr.setEncoding('utf8')
18 | proc.stderr.on('data', (data) => {
19 | console.error(data)
20 | })
21 | proc.on('close', (code) => {
22 | if (code !== 0) {
23 | reject(code)
24 | } else {
25 | resolve(code)
26 | }
27 | })
28 | })
29 | }
30 |
31 | await (async () => {
32 | // If dependencies are not vendored-in, install them at runtime.
33 | try {
34 | await fs.accessSync(
35 | url.fileURLToPath(new URL('./node_modules', import.meta.url)),
36 | fs.constants.R_OK
37 | )
38 | } catch {
39 | try {
40 | await spawnPromisified('npm', ['ci'], {
41 | cwd: url.fileURLToPath(new URL('.', import.meta.url)),
42 | quiet: true
43 | })
44 | } catch {
45 | process.exit(1)
46 | }
47 | } finally {
48 | // Compile TypeScript.
49 | try {
50 | await spawnPromisified('npm', ['run', 'build'], {
51 | cwd: url.fileURLToPath(new URL('.', import.meta.url)),
52 | quiet: true
53 | })
54 | } catch {
55 | process.exit(1)
56 | }
57 | // Run the main script.
58 | const action = await import('./dist/index.js')
59 | await action.default()
60 | }
61 | })()
--------------------------------------------------------------------------------
/.github/actions/file/bootstrap.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | //@ts-check
3 |
4 | import fs from 'node:fs'
5 | import * as url from 'node:url'
6 | import { spawn } from 'node:child_process'
7 |
8 | function spawnPromisified(command, args, { quiet = false, ...options } = {}) {
9 | return new Promise((resolve, reject) => {
10 | const proc = spawn(command, args, options)
11 | proc.stdout.setEncoding('utf8')
12 | proc.stdout.on('data', (data) => {
13 | if (!quiet) {
14 | console.log(data)
15 | }
16 | })
17 | proc.stderr.setEncoding('utf8')
18 | proc.stderr.on('data', (data) => {
19 | console.error(data)
20 | })
21 | proc.on('close', (code) => {
22 | if (code !== 0) {
23 | reject(code)
24 | } else {
25 | resolve(code)
26 | }
27 | })
28 | })
29 | }
30 |
31 | await (async () => {
32 | // If dependencies are not vendored-in, install them at runtime.
33 | try {
34 | await fs.accessSync(
35 | url.fileURLToPath(new URL('./node_modules', import.meta.url)),
36 | fs.constants.R_OK
37 | )
38 | } catch {
39 | try {
40 | await spawnPromisified('npm', ['ci'], {
41 | cwd: url.fileURLToPath(new URL('.', import.meta.url)),
42 | quiet: true
43 | })
44 | } catch {
45 | process.exit(1)
46 | }
47 | } finally {
48 | // Compile TypeScript.
49 | try {
50 | await spawnPromisified('npm', ['run', 'build'], {
51 | cwd: url.fileURLToPath(new URL('.', import.meta.url)),
52 | quiet: true
53 | })
54 | } catch {
55 | process.exit(1)
56 | }
57 | // Run the main script.
58 | const action = await import('./dist/index.js')
59 | await action.default()
60 | }
61 | })()
--------------------------------------------------------------------------------
/.github/actions/find/bootstrap.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | //@ts-check
3 |
4 | import fs from 'node:fs'
5 | import * as url from 'node:url'
6 | import { spawn } from 'node:child_process'
7 |
8 | function spawnPromisified(command, args, { quiet = false, ...options } = {}) {
9 | return new Promise((resolve, reject) => {
10 | const proc = spawn(command, args, options)
11 | proc.stdout.setEncoding('utf8')
12 | proc.stdout.on('data', (data) => {
13 | if (!quiet) {
14 | console.log(data)
15 | }
16 | })
17 | proc.stderr.setEncoding('utf8')
18 | proc.stderr.on('data', (data) => {
19 | console.error(data)
20 | })
21 | proc.on('close', (code) => {
22 | if (code !== 0) {
23 | reject(code)
24 | } else {
25 | resolve(code)
26 | }
27 | })
28 | })
29 | }
30 |
31 | await (async () => {
32 | // If dependencies are not vendored-in, install them at runtime.
33 | try {
34 | await fs.accessSync(
35 | url.fileURLToPath(new URL('./node_modules', import.meta.url)),
36 | fs.constants.R_OK
37 | )
38 | } catch {
39 | try {
40 | await spawnPromisified('npm', ['ci'], {
41 | cwd: url.fileURLToPath(new URL('.', import.meta.url)),
42 | quiet: true
43 | })
44 | } catch {
45 | process.exit(1)
46 | }
47 | } finally {
48 | // Compile TypeScript.
49 | try {
50 | await spawnPromisified('npm', ['run', 'build'], {
51 | cwd: url.fileURLToPath(new URL('.', import.meta.url)),
52 | quiet: true
53 | })
54 | } catch {
55 | process.exit(1)
56 | }
57 | // Run the main script.
58 | const action = await import('./dist/index.js')
59 | await action.default()
60 | }
61 | })()
--------------------------------------------------------------------------------
/.github/actions/fix/bootstrap.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | //@ts-check
3 |
4 | import fs from 'node:fs'
5 | import * as url from 'node:url'
6 | import { spawn } from 'node:child_process'
7 |
8 | function spawnPromisified(command, args, { quiet = false, ...options } = {}) {
9 | return new Promise((resolve, reject) => {
10 | const proc = spawn(command, args, options)
11 | proc.stdout.setEncoding('utf8')
12 | proc.stdout.on('data', (data) => {
13 | if (!quiet) {
14 | console.log(data)
15 | }
16 | })
17 | proc.stderr.setEncoding('utf8')
18 | proc.stderr.on('data', (data) => {
19 | console.error(data)
20 | })
21 | proc.on('close', (code) => {
22 | if (code !== 0) {
23 | reject(code)
24 | } else {
25 | resolve(code)
26 | }
27 | })
28 | })
29 | }
30 |
31 | await (async () => {
32 | // If dependencies are not vendored-in, install them at runtime.
33 | try {
34 | await fs.accessSync(
35 | url.fileURLToPath(new URL('./node_modules', import.meta.url)),
36 | fs.constants.R_OK
37 | )
38 | } catch {
39 | try {
40 | await spawnPromisified('npm', ['ci'], {
41 | cwd: url.fileURLToPath(new URL('.', import.meta.url)),
42 | quiet: true
43 | })
44 | } catch {
45 | process.exit(1)
46 | }
47 | } finally {
48 | // Compile TypeScript.
49 | try {
50 | await spawnPromisified('npm', ['run', 'build'], {
51 | cwd: url.fileURLToPath(new URL('.', import.meta.url)),
52 | quiet: true
53 | })
54 | } catch {
55 | process.exit(1)
56 | }
57 | // Run the main script.
58 | const action = await import('./dist/index.js')
59 | await action.default()
60 | }
61 | })()
--------------------------------------------------------------------------------
/.github/actions/fix/src/getLinkedPR.ts:
--------------------------------------------------------------------------------
1 | import type { Octokit } from "@octokit/core";
2 | import { Issue } from "./Issue.js";
3 |
4 | export async function getLinkedPR(
5 | octokit: Octokit,
6 | { owner, repository, issueNumber }: Issue
7 | ) {
8 | // Check whether issues can be assigned to Copilot
9 | const response = await octokit.graphql<{
10 | repository?: {
11 | issue?: {
12 | timelineItems?: {
13 | nodes: (
14 | | { source: { id: string; url: string; title: string } }
15 | | { subject: { id: string; url: string; title: string } }
16 | )[];
17 | };
18 | };
19 | };
20 | }>(
21 | `query($owner: String!, $repository: String!, $issueNumber: Int!) {
22 | repository(owner: $owner, name: $repository) {
23 | issue(number: $issueNumber) {
24 | timelineItems(first: 100, itemTypes: [CONNECTED_EVENT, CROSS_REFERENCED_EVENT]) {
25 | nodes {
26 | ... on CrossReferencedEvent { source { ... on PullRequest { id url title } } }
27 | ... on ConnectedEvent { subject { ... on PullRequest { id url title } } }
28 | }
29 | }
30 | }
31 | }
32 | }`,
33 | { owner, repository, issueNumber }
34 | );
35 | const timelineNodes = response?.repository?.issue?.timelineItems?.nodes || [];
36 | const pullRequest: { id: string; url: string; title: string } | undefined =
37 | timelineNodes
38 | .map((node) => {
39 | if ("source" in node && node.source?.url) return node.source;
40 | if ("subject" in node && node.subject?.url) return node.subject;
41 | return undefined;
42 | })
43 | .find((pr) => !!pr);
44 | return pullRequest;
45 | }
46 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | [fork]: https://github.com/github/accessibility-scanner/fork
4 | [pr]: https://github.com/github/accessibility-scanner/compare
5 |
6 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
7 |
8 | Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE).
9 |
10 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
11 |
12 | ## How you can contribute
13 |
14 | We are not accepting pull requests at this time. However, we welcome and encourage you to:
15 |
16 | - **Test the a11y scanner** and share your experiences
17 | - **Create issues** to report bugs, unexpected behavior, or documentation improvements
18 | - **Share feedback** about your use cases and workflows
19 |
20 | ### Creating issues
21 |
22 | When creating an issue, please provide:
23 |
24 | - A clear and descriptive title
25 | - Detailed steps to reproduce (for bugs)
26 | - Expected vs. actual behavior
27 | - Your environment details (OS, browser, GitHub Actions runner, etc.)
28 | - Any relevant logs or screenshots
29 |
30 | ### Feature requests
31 |
32 | While we welcome feature requests, please note that we cannot guarantee that any feature request will be implemented or prioritized. We review all suggestions and consider them as part of our ongoing development process.
33 |
34 | ## Resources
35 |
36 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
37 | - [GitHub Support](https://support.github.com)
38 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Please see the documentation for all configuration options:
2 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
3 |
4 | version: 2
5 | updates:
6 | - package-ecosystem: "npm"
7 | directories:
8 | - "/"
9 | - "/.github/actions/auth"
10 | - "/.github/actions/find"
11 | - "/.github/actions/file"
12 | - "/.github/actions/fix"
13 | groups:
14 | # Open PRs for each major version and security update
15 | # Open an aggregate PR for all minor and patch version updates
16 | npm-minor-and-patch:
17 | applies-to: version-updates
18 | patterns:
19 | - "*"
20 | update-types:
21 | - "minor"
22 | - "patch"
23 | schedule:
24 | interval: "weekly"
25 | - package-ecosystem: "github-actions"
26 | directories:
27 | - "/"
28 | - "/.github/actions/gh-cache/cache"
29 | - "/.github/actions/gh-cache/delete"
30 | - "/.github/actions/gh-cache/restore"
31 | - "/.github/actions/gh-cache/save"
32 | groups:
33 | # Open an aggregate PR for all GitHub Actions updates
34 | github-actions:
35 | patterns:
36 | - "*"
37 | schedule:
38 | interval: "weekly"
39 | - package-ecosystem: "bundler"
40 | directory: "/sites/site-with-errors"
41 | groups:
42 | # Open PRs for each major version and security update
43 | # Open an aggregate PR for all minor and patch version updates
44 | bundler-minor-and-patch:
45 | applies-to: version-updates
46 | patterns:
47 | - "*"
48 | update-types:
49 | - "minor"
50 | - "patch"
51 | schedule:
52 | interval: "weekly"
53 |
--------------------------------------------------------------------------------
/.github/actions/file/src/updateFilingsWithNewFindings.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Finding,
3 | ResolvedFiling,
4 | NewFiling,
5 | RepeatedFiling,
6 | Filing,
7 | } from "./types.d.js";
8 |
9 | function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string {
10 | return filing.issue.url;
11 | }
12 |
13 | function getFindingKey(finding: Finding): string {
14 | return `${finding.url};${finding.ruleId};${finding.html}`;
15 | }
16 |
17 | export function updateFilingsWithNewFindings(
18 | filings: (ResolvedFiling | RepeatedFiling)[],
19 | findings: Finding[]
20 | ): Filing[] {
21 | const filingKeys: {
22 | [key: string]: ResolvedFiling | RepeatedFiling;
23 | } = {};
24 | const findingKeys: { [key: string]: string } = {};
25 | const newFilings: NewFiling[] = [];
26 |
27 | // Create maps for filing and finding data from previous runs, for quick lookups
28 | for (const filing of filings) {
29 | // Reset findings to empty array; we'll repopulate it as we find matches
30 | filingKeys[getFilingKey(filing)] = {
31 | issue: filing.issue,
32 | findings: [],
33 | };
34 | for (const finding of filing.findings) {
35 | findingKeys[getFindingKey(finding)] = getFilingKey(filing);
36 | }
37 | }
38 |
39 | for (const finding of findings) {
40 | const filingKey = findingKeys[getFindingKey(finding)];
41 | if (filingKey) {
42 | // This finding already has an associated filing; add it to that filing's findings
43 | (filingKeys[filingKey] as RepeatedFiling).findings.push(finding);
44 | } else {
45 | // This finding is new; create a new entry with no associated issue yet
46 | newFilings.push({ findings: [finding] });
47 | }
48 | }
49 |
50 | const updatedFilings = Object.values(filingKeys);
51 | return [...updatedFilings, ...newFilings];
52 | }
53 |
--------------------------------------------------------------------------------
/.github/actions/file/src/Issue.ts:
--------------------------------------------------------------------------------
1 | import type { Issue as IssueInput } from "./types.d.js";
2 |
3 | export class Issue implements IssueInput {
4 | #url!: string;
5 | #parsedUrl!: {
6 | owner: string;
7 | repository: string;
8 | issueNumber: number;
9 | };
10 | nodeId: string;
11 | id: number;
12 | title: string;
13 | state?: "open" | "reopened" | "closed";
14 |
15 | constructor({ url, nodeId, id, title, state }: IssueInput) {
16 | this.url = url;
17 | this.nodeId = nodeId;
18 | this.id = id;
19 | this.title = title;
20 | this.state = state;
21 | }
22 |
23 | set url(newUrl: string) {
24 | this.#url = newUrl;
25 | this.#parsedUrl = this.#parseUrl();
26 | }
27 |
28 | get url(): string {
29 | return this.#url;
30 | }
31 |
32 | get owner(): string {
33 | return this.#parsedUrl.owner;
34 | }
35 |
36 | get repository(): string {
37 | return this.#parsedUrl.repository;
38 | }
39 |
40 | get issueNumber(): number {
41 | return this.#parsedUrl.issueNumber;
42 | }
43 |
44 | /**
45 | * Extracts owner, repository, and issue number from the Issue instance’s GitHub issue URL.
46 | * @returns An object with `owner`, `repository`, and `issueNumber` keys.
47 | * @throws The provided URL is unparseable due to its unexpected format.
48 | */
49 | #parseUrl(): {
50 | owner: string;
51 | repository: string;
52 | issueNumber: number;
53 | } {
54 | const { owner, repository, issueNumber } =
55 | /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec(
56 | this.#url
57 | )?.groups || {};
58 | if (!owner || !repository || !issueNumber) {
59 | throw new Error(`Could not parse issue URL: ${this.#url}`);
60 | }
61 | return { owner, repository, issueNumber: Number(issueNumber) };
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | Thanks for helping make GitHub safe for everyone.
2 |
3 | # Security
4 |
5 | GitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub).
6 |
7 | Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation.
8 |
9 | ## Reporting Security Issues
10 |
11 | If you believe you have found a security vulnerability in any GitHub-owned repository, please report it to us through coordinated disclosure.
12 |
13 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
14 |
15 | Instead, please send an email to opensource-security[@]github.com.
16 |
17 | Please include as much of the information listed below as you can to help us better understand and resolve the issue:
18 |
19 | * The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)
20 | * Full paths of source file(s) related to the manifestation of the issue
21 | * The location of the affected source code (tag/branch/commit or direct URL)
22 | * Any special configuration required to reproduce the issue
23 | * Step-by-step instructions to reproduce the issue
24 | * Proof-of-concept or exploit code (if possible)
25 | * Impact of the issue, including how an attacker might exploit the issue
26 |
27 | This information will help us triage your report more quickly.
28 |
29 | ## Policy
30 |
31 | See [GitHub's Safe Harbor Policy](https://docs.github.com/en/site-policy/security-policies/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms)
32 |
--------------------------------------------------------------------------------
/.github/actions/find/src/AuthContext.ts:
--------------------------------------------------------------------------------
1 | import type playwright from "playwright";
2 | import type { Cookie, LocalStorage, AuthContextInput } from "./types.js";
3 |
4 | export class AuthContext implements AuthContextInput {
5 | readonly username?: string;
6 | readonly password?: string;
7 | readonly cookies?: Cookie[];
8 | readonly localStorage?: LocalStorage;
9 |
10 | constructor({ username, password, cookies, localStorage }: AuthContextInput) {
11 | this.username = username;
12 | this.password = password;
13 | this.cookies = cookies;
14 | this.localStorage = localStorage;
15 | }
16 |
17 | toPlaywrightBrowserContextOptions(): playwright.BrowserContextOptions {
18 | const playwrightBrowserContextOptions: playwright.BrowserContextOptions =
19 | {};
20 | if (this.username && this.password) {
21 | playwrightBrowserContextOptions.httpCredentials = {
22 | username: this.username,
23 | password: this.password,
24 | };
25 | }
26 | if (this.cookies || this.localStorage) {
27 | playwrightBrowserContextOptions.storageState = {
28 | // Add default values for fields Playwright requires which aren’t actually required by the Cookie API.
29 | cookies:
30 | this.cookies?.map((cookie) => ({
31 | expires: -1,
32 | httpOnly: false,
33 | secure: false,
34 | sameSite: "Lax",
35 | ...cookie,
36 | })) ?? [],
37 | // Transform the localStorage object into the shape Playwright expects.
38 | origins:
39 | Object.entries(this.localStorage ?? {}).map(([origin, kv]) => ({
40 | origin,
41 | localStorage: Object.entries(kv).map(([name, value]) => ({
42 | name,
43 | value,
44 | })),
45 | })) ?? [],
46 | };
47 | }
48 | return playwrightBrowserContextOptions;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/sites/site-with-errors/config.ru:
--------------------------------------------------------------------------------
1 | require 'rack'
2 | require 'rack/auth/basic'
3 | require 'rack/static'
4 |
5 | # Basic authentication middleware
6 | use Rack::Auth::Basic, "Protected Area" do |username, password|
7 | expected_username = ENV['TEST_USERNAME']
8 | expected_password = ENV['TEST_PASSWORD']
9 |
10 | # Check if environment variables are set
11 | if expected_username.nil? || expected_password.nil?
12 | puts "Warning: TEST_USERNAME and/or TEST_PASSWORD environment variables not set"
13 | false
14 | else
15 | username == expected_username && password == expected_password
16 | end
17 | end
18 |
19 | # Serve static files from _site directory
20 | use Rack::Static,
21 | urls: %w[/],
22 | root: File.expand_path('_site', __dir__),
23 | index: 'index.html'
24 |
25 | # Fallback for requests that don't match static files
26 | run lambda { |env|
27 | path = env['PATH_INFO']
28 |
29 | # Try to serve the requested file
30 | file_path = File.join(File.expand_path('_site', __dir__), path)
31 |
32 | # If it's a directory, try to serve index.html
33 | if File.directory?(file_path)
34 | index_path = File.join(file_path, 'index.html')
35 | if File.exist?(index_path)
36 | [200, {'Content-Type' => 'text/html'}, [File.read(index_path)]]
37 | else
38 | [404, {'Content-Type' => 'text/html'}, [File.read(File.join(File.expand_path('_site', __dir__), '404.html'))]]
39 | end
40 | elsif File.exist?(file_path)
41 | content_type = case File.extname(file_path)
42 | when '.html' then 'text/html'
43 | when '.css' then 'text/css'
44 | when '.js' then 'application/javascript'
45 | when '.xml' then 'application/xml'
46 | else 'text/plain'
47 | end
48 | [200, {'Content-Type' => content_type}, [File.read(file_path)]]
49 | else
50 | # Serve 404 page
51 | [404, {'Content-Type' => 'text/html'}, [File.read(File.join(File.expand_path('_site', __dir__), '404.html'))]]
52 | end
53 | }
--------------------------------------------------------------------------------
/sites/site-with-errors/_config.yml:
--------------------------------------------------------------------------------
1 | # Welcome to Jekyll!
2 | #
3 | # This config file is meant for settings that affect your whole blog, values
4 | # which you are expected to set up once and rarely edit after that. If you find
5 | # yourself editing this file very often, consider using Jekyll's data files
6 | # feature for the data you need to update frequently.
7 | #
8 | # For technical reasons, this file is *NOT* reloaded automatically when you use
9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process.
10 | #
11 | # If you need help with YAML syntax, here are some quick references for you:
12 | # https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
13 | # https://learnxinyminutes.com/docs/yaml/
14 | #
15 | # Site settings
16 | # These are used to personalize your new site. If you look in the HTML files,
17 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
18 | # You can create any custom variable you would like, and they will be accessible
19 | # in the templates via {{ site.myvariable }}.
20 |
21 | title: Accessibility Scanner Demo
22 | email: noreply@github.com
23 | description: >- # this means to ignore newlines until "baseurl:"
24 | Write an awesome description for your new site here. You can edit this
25 | line in _config.yml. It will appear in your document head meta (for
26 | Google search results) and in your feed.xml site description.
27 | baseurl: "" # the subpath of your site, e.g. /blog
28 | url: "" # the base hostname & protocol for your site, e.g. http://example.com
29 | twitter_username: github
30 | github_username: github
31 |
32 | # Build settings
33 | theme: minima
34 | plugins:
35 | - jekyll-feed
36 |
37 | # Exclude from processing.
38 | # The following items will not be processed, by default.
39 | # Any item listed under the `exclude:` key here will be automatically added to
40 | # the internal "default list".
41 | #
42 | # Excluded items can be processed by explicitly listing the directories or
43 | # their entries' file path in the `include:` list.
44 | #
45 | # exclude:
46 | # - .sass-cache/
47 | # - .jekyll-cache/
48 | # - gemfiles/
49 | # - Gemfile
50 | # - Gemfile.lock
51 | # - node_modules/
52 | # - vendor/bundle/
53 | # - vendor/cache/
54 | # - vendor/gems/
55 | # - vendor/ruby/
--------------------------------------------------------------------------------
/.github/actions/file/src/openIssue.ts:
--------------------------------------------------------------------------------
1 | import type { Octokit } from '@octokit/core';
2 | import type { Finding } from './types.d.js';
3 | import * as url from 'node:url'
4 | const URL = url.URL;
5 |
6 | /** Max length for GitHub issue titles */
7 | const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256;
8 |
9 | /**
10 | * Truncates text to a maximum length, adding an ellipsis if truncated.
11 | * @param text Original text
12 | * @param maxLength Maximum length of the returned text (including ellipsis)
13 | * @returns Either the original text or a truncated version with an ellipsis
14 | */
15 | function truncateWithEllipsis(text: string, maxLength: number): string {
16 | return text.length > maxLength ? text.slice(0, maxLength - 1) + '…' : text;
17 | }
18 |
19 | export async function openIssue(octokit: Octokit, repoWithOwner: string, finding: Finding) {
20 | const owner = repoWithOwner.split('/')[0];
21 | const repo = repoWithOwner.split('/')[1];
22 |
23 | const labels = [`${finding.scannerType} rule: ${finding.ruleId}`, `${finding.scannerType}-scanning-issue`];
24 | const title = truncateWithEllipsis(
25 | `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`,
26 | GITHUB_ISSUE_TITLE_MAX_LENGTH
27 | );
28 | const solutionLong = finding.solutionLong
29 | ?.split("\n")
30 | .map((line) =>
31 | !line.trim().startsWith("Fix any") &&
32 | !line.trim().startsWith("Fix all") &&
33 | line.trim() !== ""
34 | ? `- ${line}`
35 | : line
36 | )
37 | .join("\n");
38 | const acceptanceCriteria = `## Acceptance Criteria
39 | - [ ] The specific axe violation reported in this issue is no longer reproducible.
40 | - [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization.
41 | - [ ] A test SHOULD be added to ensure this specific axe violation does not regress.
42 | - [ ] This PR MUST NOT introduce any new accessibility issues or regressions.
43 | `;
44 | const body = `## What
45 | An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}.
46 |
47 | To fix this, ${finding.solutionShort}.
48 | ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''}
49 |
50 | ${acceptanceCriteria}
51 | `;
52 |
53 | return octokit.request(`POST /repos/${owner}/${repo}/issues`, {
54 | owner,
55 | repo,
56 | title,
57 | body,
58 | labels
59 | });
60 | }
61 |
--------------------------------------------------------------------------------
/.github/actions/gh-cache/cache/action.yml:
--------------------------------------------------------------------------------
1 | name: "cache"
2 | description: "Cache/uncache strings using an orphaned 'gh-cache' branch."
3 |
4 | inputs:
5 | key:
6 | description: "Identifier for the string to cache/uncache"
7 | required: true
8 | token:
9 | description: "Token with fine-grained permissions 'contents: write'"
10 | required: true
11 | value:
12 | description: "String to cache"
13 | required: false
14 | fail_on_cache_miss:
15 | description: "Fail the workflow if cached item is not found."
16 | required: false
17 | default: "false"
18 |
19 | outputs:
20 | value:
21 | description: "Cached string"
22 | value: ${{ steps.output_cached_value.outputs.value }}
23 |
24 | runs:
25 | using: "composite"
26 | steps:
27 | - name: Make sub-actions referenceable
28 | working-directory: ${{ github.action_path }}
29 | shell: bash
30 | run: |
31 | RUNNER_WORK_DIR="$(realpath "${GITHUB_WORKSPACE}/../..")"
32 | ACTION_DIR="${RUNNER_WORK_DIR}/_actions/github/accessibility-scanner/current"
33 | mkdir -p "${ACTION_DIR}/.github/actions"
34 | if [ "$(realpath "../../../../.github/actions")" != "$(realpath "${ACTION_DIR}/.github/actions")" ]; then
35 | cp -a "../../../../.github/actions/." "${ACTION_DIR}/.github/actions/"
36 | fi
37 | - name: Restore cached value
38 | uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/restore
39 | with:
40 | path: ${{ inputs.key }}
41 | token: ${{ inputs.token }}
42 | fail_on_cache_miss: ${{ inputs.fail_on_cache_miss }}
43 |
44 | - if: ${{ inputs.value }}
45 | name: Update cached value
46 | shell: bash
47 | run: |
48 | mkdir -p "$(dirname "${{ inputs.key }}")"
49 | echo '${{ inputs.value }}' > "${{ inputs.key }}"
50 | echo "Updated contents of '${{ inputs.key }}'"
51 |
52 | - if: ${{ inputs.value }}
53 | name: Save cached value
54 | uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/save
55 | with:
56 | path: ${{ inputs.key }}
57 | token: ${{ inputs.token }}
58 |
59 | - name: Output cached value
60 | id: output_cached_value
61 | shell: bash
62 | run: |
63 | echo "value<> $GITHUB_OUTPUT
64 | if [ -f "${{ inputs.key }}" ]; then
65 | cat "${{ inputs.key }}" >> $GITHUB_OUTPUT
66 | echo "Outputted 'value=$(cat "${{ inputs.key }}")'"
67 | else
68 | echo "Skipped outputting 'value'"
69 | fi
70 | echo "EOF" >> $GITHUB_OUTPUT
71 |
72 | branding:
73 | icon: "archive"
74 | color: "blue"
75 |
--------------------------------------------------------------------------------
/.github/actions/fix/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { Issue as IssueInput, Fixing } from "./types.d.js";
2 | import process from "node:process";
3 | import core from "@actions/core";
4 | import { Octokit } from "@octokit/core";
5 | import { throttling } from "@octokit/plugin-throttling";
6 | import { assignIssue } from "./assignIssue.js";
7 | import { getLinkedPR } from "./getLinkedPR.js";
8 | import { retry } from "./retry.js";
9 | import { Issue } from "./Issue.js";
10 | const OctokitWithThrottling = Octokit.plugin(throttling);
11 |
12 | export default async function () {
13 | core.info("Started 'fix' action");
14 | const issues: IssueInput[] = JSON.parse(
15 | core.getInput("issues", { required: true }) || "[]"
16 | );
17 | const repoWithOwner = core.getInput("repository", { required: true });
18 | const token = core.getInput("token", { required: true });
19 | core.debug(`Input: 'issues: ${JSON.stringify(issues)}'`);
20 | core.debug(`Input: 'repository: ${repoWithOwner}'`);
21 |
22 | const octokit = new OctokitWithThrottling({
23 | auth: token,
24 | throttle: {
25 | onRateLimit: (retryAfter, options, octokit, retryCount) => {
26 | octokit.log.warn(
27 | `Request quota exhausted for request ${options.method} ${options.url}`
28 | );
29 | if (retryCount < 3) {
30 | octokit.log.info(`Retrying after ${retryAfter} seconds!`);
31 | return true;
32 | }
33 | },
34 | onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => {
35 | octokit.log.warn(
36 | `Secondary rate limit hit for request ${options.method} ${options.url}`
37 | );
38 | if (retryCount < 3) {
39 | octokit.log.info(`Retrying after ${retryAfter} seconds!`);
40 | return true;
41 | }
42 | },
43 | },
44 | });
45 | const fixings: Fixing[] = issues.map((issue) => ({ issue })) as Fixing[];
46 |
47 | for (const fixing of fixings) {
48 | try {
49 | const issue = new Issue(fixing.issue);
50 | await assignIssue(octokit, issue);
51 | core.info(
52 | `Assigned ${issue.owner}/${issue.repository}#${issue.issueNumber} to Copilot!`
53 | );
54 | const pullRequest = await retry(() => getLinkedPR(octokit, issue));
55 | if (pullRequest) {
56 | fixing.pullRequest = pullRequest;
57 | core.info(
58 | `Found linked PR for ${issue.owner}/${issue.repository}#${issue.issueNumber}: ${pullRequest.url}`
59 | );
60 | } else {
61 | core.info(
62 | `No linked PR was found for ${issue.owner}/${issue.repository}#${issue.issueNumber}`
63 | );
64 | }
65 | } catch (error) {
66 | core.setFailed(
67 | `Failed to assign ${fixing.issue.url} to Copilot: ${error}`
68 | );
69 | process.exit(1);
70 | }
71 | }
72 |
73 | core.setOutput("fixings", JSON.stringify(fixings));
74 | core.debug(`Output: 'fixings: ${JSON.stringify(fixings)}'`);
75 | core.info("Finished 'fix' action");
76 | }
77 |
--------------------------------------------------------------------------------
/.github/actions/gh-cache/restore/action.yml:
--------------------------------------------------------------------------------
1 | name: "restore"
2 | description: "Checkout a file or directory from an orphaned 'gh-cache' branch"
3 |
4 | inputs:
5 | path:
6 | description: "Relative path to a file or directory to restore."
7 | required: true
8 | token:
9 | description: "Token with fine-grained permissions 'contents: read'"
10 | required: true
11 | fail_on_cache_miss:
12 | description: "Fail the workflow if cached item is not found."
13 | required: false
14 | default: 'false'
15 |
16 | runs:
17 | using: "composite"
18 | steps:
19 | - name: Validate path
20 | shell: bash
21 | run: |
22 | # Check for empty
23 | if [[ -z "${{ inputs.path }}" ]]; then
24 | echo "Invalid 'path' input (empty)"
25 | exit 1
26 | fi
27 | # Check for absolute paths
28 | if [[ "${{ inputs.path }}" == /* ]]; then
29 | echo "Invalid 'path' input (absolute path): ${{ inputs.path }}"
30 | exit 1
31 | fi
32 | # Check for directory traversal
33 | if [[
34 | "${{ inputs.path }}" == "~"* ||
35 | "${{ inputs.path }}" =~ (^|/)\.\.(/|$)
36 | ]]; then
37 | echo "Invalid 'path' input (directory traversal): ${{ inputs.path }}"
38 | exit 1
39 | fi
40 | # Check for disallowed characters (to ensure portability)
41 | if [[ ! "${{ inputs.path }}" =~ ^[A-Za-z0-9._/-]+$ ]]; then
42 | echo "Invalid 'path' input (disallowed characters): ${{ inputs.path }}"
43 | exit 1
44 | fi
45 |
46 | - name: Checkout repository to temporary directory
47 | uses: actions/checkout@v6
48 | with:
49 | token: ${{ inputs.token }}
50 | fetch-depth: 0
51 | path: .gh-cache-${{ github.run_id }}
52 | show-progress: 'false'
53 |
54 | - name: Switch to 'gh-cache' branch if it exists
55 | shell: bash
56 | working-directory: .gh-cache-${{ github.run_id }}
57 | run: |
58 | if git ls-remote --exit-code --heads origin gh-cache >/dev/null; then
59 | git fetch origin gh-cache:gh-cache >/dev/null 2>&1
60 | git checkout gh-cache >/dev/null 2>&1
61 | else
62 | echo "Branch 'gh-cache' does not exist"
63 | if [[ "${{ inputs.fail_on_cache_miss }}" == "true" ]]; then
64 | echo "Exiting with error because 'fail_on_cache_miss' is 'true'"
65 | exit 1
66 | fi
67 | fi
68 |
69 | - name: Restore cached item
70 | shell: bash
71 | run: |
72 | src=".gh-cache-${{ github.run_id }}/${{ inputs.path }}"
73 | dest="${{ inputs.path }}"
74 | if [ -e "$src" ]; then
75 | mkdir -p "$(dirname "$dest")"
76 | cp -Rf "$src" "$dest"
77 | echo "Restored '${{ inputs.path }}' from cache"
78 | else
79 | echo "'${{ inputs.path }}' not found in cache"
80 | if [[ "${{ inputs.fail_on_cache_miss }}" == "true" ]]; then
81 | echo "Exiting with error because 'fail_on_cache_miss' is 'true'"
82 | exit 1
83 | fi
84 | fi
85 |
86 | - name: Clean up temporary directory
87 | shell: bash
88 | run: |
89 | rm -rf .gh-cache-${{ github.run_id }}
90 |
91 | branding:
92 | icon: "download"
93 | color: "blue"
94 |
--------------------------------------------------------------------------------
/.github/actions/fix/src/assignIssue.ts:
--------------------------------------------------------------------------------
1 | import type { Octokit } from "@octokit/core";
2 | import { Issue } from "./Issue.js";
3 |
4 | // https://docs.github.com/en/enterprise-cloud@latest/copilot/how-tos/use-copilot-agents/coding-agent/assign-copilot-to-an-issue#assigning-an-existing-issue
5 | export async function assignIssue(
6 | octokit: Octokit,
7 | { owner, repository, issueNumber, nodeId }: Issue
8 | ) {
9 | // Check whether issues can be assigned to Copilot
10 | const suggestedActorsResponse = await octokit.graphql<{
11 | repository: {
12 | suggestedActors: {
13 | nodes: { login: string; id: string }[];
14 | };
15 | };
16 | }>(
17 | `query ($owner: String!, $repository: String!) {
18 | repository(owner: $owner, name: $repository) {
19 | suggestedActors(capabilities: [CAN_BE_ASSIGNED], first: 1) {
20 | nodes {
21 | login
22 | __typename
23 | ... on Bot { id }
24 | ... on User { id }
25 | }
26 | }
27 | }
28 | }`,
29 | { owner, repository }
30 | );
31 | if (
32 | suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.login !==
33 | "copilot-swe-agent"
34 | ) {
35 | return;
36 | }
37 | // Get GraphQL identifier for issue (unless already provided)
38 | let issueId = nodeId;
39 | if (!issueId) {
40 | console.debug(
41 | `Fetching identifier for issue ${owner}/${repository}#${issueNumber}`
42 | );
43 | const issueResponse = await octokit.graphql<{
44 | repository: {
45 | issue: { id: string };
46 | };
47 | }>(
48 | `query($owner: String!, $repository: String!, $issueNumber: Int!) {
49 | repository(owner: $owner, name: $repository) {
50 | issue(number: $issueNumber) { id }
51 | }
52 | }`,
53 | { owner, repository, issueNumber }
54 | );
55 | issueId = issueResponse?.repository?.issue?.id;
56 | console.debug(
57 | `Fetched identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`
58 | );
59 | } else {
60 | console.debug(
61 | `Using provided identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`
62 | );
63 | }
64 | if (!issueId) {
65 | console.warn(
66 | `Couldn’t get identifier for issue ${owner}/${repository}#${issueNumber}. Skipping assignment to Copilot.`
67 | );
68 | return;
69 | }
70 | // Assign issue to Copilot
71 | await octokit.graphql<{
72 | replaceActorsForAssignable: {
73 | assignable: {
74 | id: string;
75 | title: string;
76 | assignees: {
77 | nodes: { login: string }[];
78 | };
79 | };
80 | };
81 | }>(
82 | `mutation($issueId: ID!, $assigneeId: ID!) {
83 | replaceActorsForAssignable(input: {assignableId: $issueId, actorIds: [$assigneeId]}) {
84 | assignable {
85 | ... on Issue {
86 | id
87 | title
88 | assignees(first: 10) {
89 | nodes {
90 | login
91 | }
92 | }
93 | }
94 | }
95 | }
96 | }`,
97 | {
98 | issueId,
99 | assigneeId:
100 | suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.id,
101 | }
102 | );
103 | }
104 |
--------------------------------------------------------------------------------
/.github/actions/gh-cache/delete/action.yml:
--------------------------------------------------------------------------------
1 | name: "delete"
2 | description: "Delete a file or directory from the orphaned 'gh-cache' branch"
3 |
4 | inputs:
5 | path:
6 | description: "Relative path to a file or directory to delete"
7 | required: true
8 | token:
9 | description: "Token with fine-grained permissions 'contents: write'"
10 | required: true
11 |
12 | runs:
13 | using: "composite"
14 | steps:
15 | - name: Validate path
16 | shell: bash
17 | run: |
18 | # Check for empty
19 | if [[ -z "${{ inputs.path }}" ]]; then
20 | echo "Invalid 'path' input (empty)"
21 | exit 1
22 | fi
23 | # Check for absolute paths
24 | if [[ "${{ inputs.path }}" == /* ]]; then
25 | echo "Invalid 'path' input (absolute path): ${{ inputs.path }}"
26 | exit 1
27 | fi
28 | # Check for directory traversal
29 | if [[
30 | "${{ inputs.path }}" == "~"* ||
31 | "${{ inputs.path }}" =~ (^|/)\.\.(/|$)
32 | ]]; then
33 | echo "Invalid 'path' input (directory traversal): ${{ inputs.path }}"
34 | exit 1
35 | fi
36 | # Check for disallowed characters (to ensure portability)
37 | if [[ ! "${{ inputs.path }}" =~ ^[A-Za-z0-9._/-]+$ ]]; then
38 | echo "Invalid 'path' input (disallowed characters): ${{ inputs.path }}"
39 | exit 1
40 | fi
41 |
42 | - name: Checkout repository to temporary directory
43 | uses: actions/checkout@v6
44 | with:
45 | token: ${{ inputs.token }}
46 | fetch-depth: 0
47 | path: .gh-cache-${{ github.run_id }}
48 | show-progress: 'false'
49 |
50 | - name: Switch to gh-cache branch (or create orphan)
51 | shell: bash
52 | working-directory: .gh-cache-${{ github.run_id }}
53 | run: |
54 | if git ls-remote --exit-code --heads origin gh-cache >/dev/null; then
55 | git checkout gh-cache >/dev/null 2>&1
56 | echo "Checked out existing 'gh-cache' branch"
57 | else
58 | git checkout --orphan gh-cache >/dev/null 2>&1
59 | git rm -rfq . # Clear files from the initial checkout, to avoid adding them to the orphaned branch
60 | echo "Created new orphaned 'gh-cache' branch"
61 | fi
62 |
63 | - name: Copy file to repo
64 | shell: bash
65 | run: |
66 | if [ -f "${{ inputs.path }}" ]; then
67 | rm -Rf "${{ inputs.path }}"
68 | rm -Rf ".gh-cache-${{ github.run_id }}/${{ inputs.path }}"
69 | echo "Deleted '${{ inputs.path }}' from 'gh-cache' branch"
70 | fi
71 |
72 | - name: Commit and push
73 | shell: bash
74 | working-directory: .gh-cache-${{ github.run_id }}
75 | run: |
76 | git add "${{ inputs.path }}" || true
77 | git config user.name "github-actions[bot]"
78 | git config user.email "github-actions[bot]@users.noreply.github.com"
79 | git commit -m "$(printf 'Delete artifact: %s' "${{ inputs.path }}")" \
80 | && echo "Committed '${{ inputs.path }}' to 'gh-cache' branch" \
81 | || echo "No changes to commit"
82 | git push origin gh-cache
83 |
84 | - name: Clean up temporary directory
85 | shell: bash
86 | run: |
87 | rm -rf .gh-cache-${{ github.run_id }}
88 |
89 | branding:
90 | icon: "trash"
91 | color: "red"
92 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at . All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct/][version]
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 | [version]: https://www.contributor-covenant.org/version/1/4/code-of-conduct/
75 |
--------------------------------------------------------------------------------
/.github/actions/gh-cache/save/action.yml:
--------------------------------------------------------------------------------
1 | name: "save"
2 | description: "Commit a file or directory to an orphaned 'gh-cache' branch"
3 |
4 | inputs:
5 | path:
6 | description: "Relative path to a file or directory to save"
7 | required: true
8 | token:
9 | description: "Token with fine-grained permissions 'contents: write'"
10 | required: true
11 |
12 | runs:
13 | using: "composite"
14 | steps:
15 | - name: Validate path
16 | shell: bash
17 | run: |
18 | # Check for empty
19 | if [[ -z "${{ inputs.path }}" ]]; then
20 | echo "Invalid 'path' input (empty)"
21 | exit 1
22 | fi
23 | # Check for absolute paths
24 | if [[ "${{ inputs.path }}" == /* ]]; then
25 | echo "Invalid 'path' input (absolute path): ${{ inputs.path }}"
26 | exit 1
27 | fi
28 | # Check for directory traversal
29 | if [[
30 | "${{ inputs.path }}" == "~"* ||
31 | "${{ inputs.path }}" =~ (^|/)\.\.(/|$)
32 | ]]; then
33 | echo "Invalid 'path' input (directory traversal): ${{ inputs.path }}"
34 | exit 1
35 | fi
36 | # Check for disallowed characters (to ensure portability)
37 | if [[ ! "${{ inputs.path }}" =~ ^[A-Za-z0-9._/-]+$ ]]; then
38 | echo "Invalid 'path' input (disallowed characters): ${{ inputs.path }}"
39 | exit 1
40 | fi
41 |
42 | - name: Checkout repository to temporary directory
43 | uses: actions/checkout@v6
44 | with:
45 | token: ${{ inputs.token }}
46 | fetch-depth: 0
47 | path: .gh-cache-${{ github.run_id }}
48 | show-progress: "false"
49 |
50 | - name: Switch to gh-cache branch (or create orphan)
51 | shell: bash
52 | working-directory: .gh-cache-${{ github.run_id }}
53 | run: |
54 | if git ls-remote --exit-code --heads origin gh-cache >/dev/null; then
55 | git checkout gh-cache >/dev/null 2>&1
56 | echo "Checked out existing 'gh-cache' branch"
57 | else
58 | git checkout --orphan gh-cache >/dev/null 2>&1
59 | git rm -rfq . # Clear files from the initial checkout, to avoid adding them to the orphaned branch
60 | echo "Created new orphaned 'gh-cache' branch"
61 | fi
62 |
63 | - name: Copy file to repo
64 | shell: bash
65 | run: |
66 | if [ -f "${{ inputs.path }}" ]; then
67 | mkdir -p ".gh-cache-${{ github.run_id }}/$(dirname "${{ inputs.path }}")"
68 | cp -Rf "${{ inputs.path }}" ".gh-cache-${{ github.run_id }}/${{ inputs.path }}"
69 | echo "Copied '${{ inputs.path }}' to 'gh-cache' branch"
70 | fi
71 |
72 | - name: Commit and push
73 | shell: bash
74 | working-directory: .gh-cache-${{ github.run_id }}
75 | run: |
76 | if [ -f "${{ inputs.path }}" ]; then
77 | git add "${{ inputs.path }}"
78 | git config user.name "github-actions[bot]"
79 | git config user.email "github-actions[bot]@users.noreply.github.com"
80 | git commit -m "$(printf 'Save artifact: %s' "${{ inputs.path }}")" \
81 | && echo "Committed '${{ inputs.path }}' to 'gh-cache' branch" \
82 | || echo "No changes to commit"
83 | if git ls-remote --exit-code --heads origin gh-cache >/dev/null; then
84 | git fetch origin gh-cache
85 | git rebase origin/gh-cache
86 | fi
87 | git push origin gh-cache
88 | else
89 | echo "'${{ inputs.path }}' does not exist"
90 | echo "Skipping commit and push"
91 | fi
92 |
93 | - name: Clean up temporary directory
94 | shell: bash
95 | run: |
96 | rm -rf .gh-cache-${{ github.run_id }}
97 |
98 | branding:
99 | icon: "upload"
100 | color: "blue"
101 |
--------------------------------------------------------------------------------
/.github/actions/file/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { Finding, ResolvedFiling, RepeatedFiling } from "./types.d.js";
2 | import process from "node:process";
3 | import core from "@actions/core";
4 | import { Octokit } from "@octokit/core";
5 | import { throttling } from "@octokit/plugin-throttling";
6 | import { Issue } from "./Issue.js";
7 | import { closeIssue } from "./closeIssue.js";
8 | import { isNewFiling } from "./isNewFiling.js";
9 | import { isRepeatedFiling } from "./isRepeatedFiling.js";
10 | import { isResolvedFiling } from "./isResolvedFiling.js";
11 | import { openIssue } from "./openIssue.js";
12 | import { reopenIssue } from "./reopenIssue.js";
13 | import { updateFilingsWithNewFindings } from "./updateFilingsWithNewFindings.js";
14 | const OctokitWithThrottling = Octokit.plugin(throttling);
15 |
16 | export default async function () {
17 | core.info("Started 'file' action");
18 | const findings: Finding[] = JSON.parse(
19 | core.getInput("findings", { required: true })
20 | );
21 | const repoWithOwner = core.getInput("repository", { required: true });
22 | const token = core.getInput("token", { required: true });
23 | const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse(
24 | core.getInput("cached_filings", { required: false }) || "[]"
25 | );
26 | core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`);
27 | core.debug(`Input: 'repository: ${repoWithOwner}'`);
28 | core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`);
29 |
30 | const octokit = new OctokitWithThrottling({
31 | auth: token,
32 | throttle: {
33 | onRateLimit: (retryAfter, options, octokit, retryCount) => {
34 | octokit.log.warn(
35 | `Request quota exhausted for request ${options.method} ${options.url}`
36 | );
37 | if (retryCount < 3) {
38 | octokit.log.info(`Retrying after ${retryAfter} seconds!`);
39 | return true;
40 | }
41 | },
42 | onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => {
43 | octokit.log.warn(
44 | `Secondary rate limit hit for request ${options.method} ${options.url}`
45 | );
46 | if (retryCount < 3) {
47 | octokit.log.info(`Retrying after ${retryAfter} seconds!`);
48 | return true;
49 | }
50 | },
51 | },
52 | });
53 | const filings = updateFilingsWithNewFindings(cachedFilings, findings);
54 |
55 | for (const filing of filings) {
56 | let response;
57 | try {
58 | if (isResolvedFiling(filing)) {
59 | // Close the filing’s issue (if necessary)
60 | response = await closeIssue(octokit, new Issue(filing.issue));
61 | filing.issue.state = "closed";
62 | } else if (isNewFiling(filing)) {
63 | // Open a new issue for the filing
64 | response = await openIssue(octokit, repoWithOwner, filing.findings[0]);
65 | (filing as any).issue = { state: "open" } as Issue;
66 | } else if (isRepeatedFiling(filing)) {
67 | // Reopen the filing’s issue (if necessary)
68 | response = await reopenIssue(octokit, new Issue(filing.issue));
69 | filing.issue.state = "reopened";
70 | }
71 | if (response?.data && filing.issue) {
72 | // Update the filing with the latest issue data
73 | filing.issue.id = response.data.id;
74 | filing.issue.nodeId = response.data.node_id;
75 | filing.issue.url = response.data.html_url;
76 | filing.issue.title = response.data.title;
77 | core.info(
78 | `Set issue ${response.data.title} (${repoWithOwner}#${response.data.number}) state to ${filing.issue.state}`
79 | );
80 | }
81 | } catch (error) {
82 | core.setFailed(`Failed on filing: ${filing}\n${error}`);
83 | process.exit(1);
84 | }
85 | }
86 |
87 | core.setOutput("filings", JSON.stringify(filings));
88 | core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`);
89 | core.info("Finished 'file' action");
90 | }
91 |
--------------------------------------------------------------------------------
/.github/actions/auth/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { AuthContextOutput } from "./types.d.js";
2 | import crypto from "node:crypto";
3 | import process from "node:process";
4 | import * as url from "node:url";
5 | import core from "@actions/core";
6 | import playwright from "playwright";
7 |
8 | export default async function () {
9 | core.info("Starting 'auth' action");
10 |
11 | let browser: playwright.Browser | undefined;
12 | let context: playwright.BrowserContext | undefined;
13 | let page: playwright.Page | undefined;
14 | try {
15 | // Get inputs
16 | const loginUrl = core.getInput("login_url", { required: true });
17 | const username = core.getInput("username", { required: true });
18 | const password = core.getInput("password", { required: true });
19 | core.setSecret(password);
20 |
21 | // Determine storage path for authenticated session state
22 | // Playwright will create missing directories, if needed
23 | const actionDirectory = `${url.fileURLToPath(new URL(import.meta.url))}/..`;
24 | const sessionStatePath = `${
25 | process.env.RUNNER_TEMP ?? actionDirectory
26 | }/.auth/${crypto.randomUUID()}/sessionState.json`;
27 |
28 | // Launch a headless browser
29 | browser = await playwright.chromium.launch({
30 | headless: true,
31 | executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined,
32 | });
33 | context = await browser.newContext({
34 | // Try HTTP Basic authentication
35 | httpCredentials: {
36 | username,
37 | password,
38 | },
39 | });
40 | page = await context.newPage();
41 |
42 | // Navigate to login page
43 | core.info("Navigating to login page");
44 | await page.goto(loginUrl);
45 |
46 | // Check for a login form.
47 | // If no login form is found, then either HTTP Basic auth succeeded, or the page does not require authentication.
48 | core.info("Checking for login form");
49 | const [usernameField, passwordField] = await Promise.all([
50 | page.getByLabel(/user ?name/i).first(),
51 | page.getByLabel(/password/i).first(),
52 | ]);
53 | const [usernameFieldExists, passwordFieldExists] = await Promise.all([
54 | usernameField.count(),
55 | passwordField.count(),
56 | ]);
57 | if (usernameFieldExists && passwordFieldExists) {
58 | // Try form authentication
59 | core.info("Filling username");
60 | await usernameField.fill(username);
61 | core.info("Filling password");
62 | await passwordField.fill(password);
63 | core.info("Logging in");
64 | await page
65 | .getByLabel(/password/i)
66 | .locator("xpath=ancestor::form")
67 | .evaluate((form) => (form as HTMLFormElement).submit());
68 | } else {
69 | core.info("No login form detected");
70 | // This occurs if HTTP Basic auth succeeded, or if the page does not require authentication.
71 | }
72 |
73 | // Output authenticated session state
74 | const { cookies, origins } = await context.storageState();
75 | const authContextOutput: AuthContextOutput = {
76 | username,
77 | password,
78 | cookies,
79 | localStorage: origins.reduce((acc, { origin, localStorage }) => {
80 | acc[origin] = localStorage.reduce((acc, { name, value }) => {
81 | acc[name] = value;
82 | return acc;
83 | }, {} as Record);
84 | return acc;
85 | }, {} as Record>),
86 | };
87 | core.setOutput("auth_context", JSON.stringify(authContextOutput));
88 | core.debug("Output: 'auth_context'");
89 | } catch (error) {
90 | if (page) {
91 | core.info(`Errored at page URL: ${page.url()}`);
92 | }
93 | core.setFailed(`${error}`);
94 | process.exit(1);
95 | } finally {
96 | // Clean up
97 | await context?.close();
98 | await browser?.close();
99 | }
100 |
101 | core.info("Finished 'auth' action");
102 | }
103 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | pull_request:
4 | types:
5 | - opened
6 | - reopened
7 | - synchronize
8 | - ready_for_review
9 | workflow_dispatch:
10 |
11 | permissions:
12 | contents: write
13 | issues: write
14 | pull-requests: write
15 |
16 | concurrency:
17 | group: ${{ github.workflow }}-${{ github.ref }}
18 | cancel-in-progress: false # Allow previous workflows’ clean-up steps to complete
19 |
20 | env:
21 | TESTING_REPOSITORY: github/accessibility-scanner-testing
22 |
23 | jobs:
24 | test:
25 | name: Test
26 | runs-on: ubuntu-latest
27 | # Run if triggered manually, or for a non-draft PR
28 | if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
29 | strategy:
30 | matrix:
31 | site: ["sites/site-with-errors"]
32 | steps:
33 | - name: Checkout
34 | uses: actions/checkout@v6
35 |
36 | - name: Setup Ruby
37 | uses: ruby/setup-ruby@ac793fdd38cc468a4dd57246fa9d0e868aba9085
38 | with:
39 | ruby-version: "3.4"
40 | bundler-cache: true
41 | working-directory: ${{ matrix.site }}
42 |
43 | - name: Build Jekyll site (${{ matrix.site }})
44 | shell: bash
45 | working-directory: ${{ matrix.site }}
46 | env:
47 | JEKYLL_ENV: production
48 | run: bundle exec jekyll build
49 |
50 | - name: Start Puma (${{ matrix.site }})
51 | shell: bash
52 | working-directory: ${{ matrix.site }}
53 | env:
54 | TEST_USERNAME: ${{ secrets.TEST_USERNAME }}
55 | TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
56 | run: |
57 | set -euo pipefail
58 | bundle exec puma -b tcp://127.0.0.1:4000 &
59 | echo "Starting Puma on port 4000"
60 | curl -fsS --retry 25 --retry-delay 1 --retry-all-errors -u "${TEST_USERNAME}:${TEST_PASSWORD}" "http://127.0.0.1:4000/" > /dev/null
61 | echo "Puma has started"
62 |
63 | - name: Generate cache key
64 | id: cache_key
65 | shell: bash
66 | run: |
67 | echo "cache_key=$(printf 'cached_results-%s-%s.json' "${{ matrix.site }}" "${{ github.ref_name }}" | tr -cs 'A-Za-z0-9._-' '_')" >> $GITHUB_OUTPUT
68 |
69 | - name: Scan site (${{ matrix.site }})
70 | uses: ./
71 | with:
72 | urls: |
73 | http://127.0.0.1:4000/
74 | http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html
75 | http://127.0.0.1:4000/about/
76 | http://127.0.0.1:4000/404.html
77 | login_url: http://127.0.0.1:4000/
78 | username: ${{ secrets.TEST_USERNAME }}
79 | password: ${{ secrets.TEST_PASSWORD }}
80 | repository: ${{ env.TESTING_REPOSITORY }}
81 | token: ${{ secrets.GH_TOKEN }}
82 | cache_key: ${{ steps.cache_key.outputs.cache_key }}
83 |
84 | - name: Retrieve cached results
85 | uses: ./.github/actions/gh-cache/restore
86 | with:
87 | path: ${{ steps.cache_key.outputs.cache_key }}
88 | token: ${{ secrets.GITHUB_TOKEN }}
89 |
90 | - name: Validate scan results (${{ matrix.site }})
91 | run: |
92 | npm ci
93 | npm run test
94 | env:
95 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
96 | CACHE_PATH: ${{ steps.cache_key.outputs.cache_key }}
97 |
98 | - name: Clean up issues and pull requests
99 | if: ${{ always() }}
100 | shell: bash
101 | run: |
102 | set -euo pipefail
103 | if [[ ! -f "${{ steps.cache_key.outputs.cache_key }}" ]]; then
104 | echo "Skipping 'Clean up issues and pull requests' (no cached results)."
105 | exit 0
106 | fi
107 | jq -r '
108 | (if type=="string" then fromjson else . end)
109 | | .[] | .issue.url, .pullRequest.url
110 | | select(. != null)
111 | ' "${{ steps.cache_key.outputs.cache_key }}" \
112 | | while read -r URL; do
113 | if [[ "$URL" == *"/pull/"* ]]; then
114 | echo "Closing pull request: $URL"
115 | gh pr close "$URL" || echo "Failed to close pull request: $URL"
116 | branch="$(gh pr view "$URL" --json headRefName -q .headRefName || true)"
117 | if [[ -n "$branch" ]]; then
118 | echo "Deleting branch: $branch"
119 | gh api -X DELETE "repos/${{ env.TESTING_REPOSITORY }}/git/refs/heads/$branch" || echo "Failed to delete branch: $branch"
120 | fi
121 | elif [[ "$URL" == *"/issues/"* ]]; then
122 | echo "Closing issue: $URL"
123 | gh issue close "$URL" || echo "Failed to close issue: $URL"
124 | else
125 | echo "Skipping unrecognized url: $URL"
126 | fi
127 | done
128 | env:
129 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
130 |
131 | - name: Clean up cached results
132 | if: ${{ always() }}
133 | uses: ./.github/actions/gh-cache/delete
134 | with:
135 | path: ${{ steps.cache_key.outputs.cache_key }}
136 | token: ${{ secrets.GITHUB_TOKEN }}
137 |
--------------------------------------------------------------------------------
/sites/site-with-errors/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | addressable (2.8.7)
5 | public_suffix (>= 2.0.2, < 7.0)
6 | base64 (0.3.0)
7 | bigdecimal (3.2.2)
8 | colorator (1.1.0)
9 | concurrent-ruby (1.3.5)
10 | csv (3.3.5)
11 | em-websocket (0.5.3)
12 | eventmachine (>= 0.12.9)
13 | http_parser.rb (~> 0)
14 | eventmachine (1.2.7)
15 | ffi (1.17.2)
16 | ffi (1.17.2-aarch64-linux-gnu)
17 | ffi (1.17.2-aarch64-linux-musl)
18 | ffi (1.17.2-arm-linux-gnu)
19 | ffi (1.17.2-arm-linux-musl)
20 | ffi (1.17.2-arm64-darwin)
21 | ffi (1.17.2-x86-linux-gnu)
22 | ffi (1.17.2-x86-linux-musl)
23 | ffi (1.17.2-x86_64-darwin)
24 | ffi (1.17.2-x86_64-linux-gnu)
25 | ffi (1.17.2-x86_64-linux-musl)
26 | forwardable-extended (2.6.0)
27 | google-protobuf (4.32.0)
28 | bigdecimal
29 | rake (>= 13)
30 | google-protobuf (4.32.0-aarch64-linux-gnu)
31 | bigdecimal
32 | rake (>= 13)
33 | google-protobuf (4.32.0-aarch64-linux-musl)
34 | bigdecimal
35 | rake (>= 13)
36 | google-protobuf (4.32.0-arm64-darwin)
37 | bigdecimal
38 | rake (>= 13)
39 | google-protobuf (4.32.0-x86-linux-gnu)
40 | bigdecimal
41 | rake (>= 13)
42 | google-protobuf (4.32.0-x86-linux-musl)
43 | bigdecimal
44 | rake (>= 13)
45 | google-protobuf (4.32.0-x86_64-darwin)
46 | bigdecimal
47 | rake (>= 13)
48 | google-protobuf (4.32.0-x86_64-linux-gnu)
49 | bigdecimal
50 | rake (>= 13)
51 | google-protobuf (4.32.0-x86_64-linux-musl)
52 | bigdecimal
53 | rake (>= 13)
54 | http_parser.rb (0.8.0)
55 | i18n (1.14.7)
56 | concurrent-ruby (~> 1.0)
57 | jekyll (4.4.1)
58 | addressable (~> 2.4)
59 | base64 (~> 0.2)
60 | colorator (~> 1.0)
61 | csv (~> 3.0)
62 | em-websocket (~> 0.5)
63 | i18n (~> 1.0)
64 | jekyll-sass-converter (>= 2.0, < 4.0)
65 | jekyll-watch (~> 2.0)
66 | json (~> 2.6)
67 | kramdown (~> 2.3, >= 2.3.1)
68 | kramdown-parser-gfm (~> 1.0)
69 | liquid (~> 4.0)
70 | mercenary (~> 0.3, >= 0.3.6)
71 | pathutil (~> 0.9)
72 | rouge (>= 3.0, < 5.0)
73 | safe_yaml (~> 1.0)
74 | terminal-table (>= 1.8, < 4.0)
75 | webrick (~> 1.7)
76 | jekyll-feed (0.17.0)
77 | jekyll (>= 3.7, < 5.0)
78 | jekyll-sass-converter (3.1.0)
79 | sass-embedded (~> 1.75)
80 | jekyll-seo-tag (2.8.0)
81 | jekyll (>= 3.8, < 5.0)
82 | jekyll-watch (2.2.1)
83 | listen (~> 3.0)
84 | json (2.13.2)
85 | kramdown (2.5.1)
86 | rexml (>= 3.3.9)
87 | kramdown-parser-gfm (1.1.0)
88 | kramdown (~> 2.0)
89 | liquid (4.0.4)
90 | listen (3.9.0)
91 | rb-fsevent (~> 0.10, >= 0.10.3)
92 | rb-inotify (~> 0.9, >= 0.9.10)
93 | mercenary (0.4.0)
94 | minima (2.5.2)
95 | jekyll (>= 3.5, < 5.0)
96 | jekyll-feed (~> 0.9)
97 | jekyll-seo-tag (~> 2.1)
98 | nio4r (2.7.4)
99 | pathutil (0.16.2)
100 | forwardable-extended (~> 2.6)
101 | public_suffix (6.0.2)
102 | puma (7.1.0)
103 | nio4r (~> 2.0)
104 | rack (3.2.4)
105 | rake (13.3.0)
106 | rb-fsevent (0.11.2)
107 | rb-inotify (0.11.1)
108 | ffi (~> 1.0)
109 | rexml (3.4.2)
110 | rouge (4.6.0)
111 | safe_yaml (1.0.5)
112 | sass-embedded (1.90.0)
113 | google-protobuf (~> 4.31)
114 | rake (>= 13)
115 | sass-embedded (1.90.0-aarch64-linux-android)
116 | google-protobuf (~> 4.31)
117 | sass-embedded (1.90.0-aarch64-linux-gnu)
118 | google-protobuf (~> 4.31)
119 | sass-embedded (1.90.0-aarch64-linux-musl)
120 | google-protobuf (~> 4.31)
121 | sass-embedded (1.90.0-arm-linux-androideabi)
122 | google-protobuf (~> 4.31)
123 | sass-embedded (1.90.0-arm-linux-gnueabihf)
124 | google-protobuf (~> 4.31)
125 | sass-embedded (1.90.0-arm-linux-musleabihf)
126 | google-protobuf (~> 4.31)
127 | sass-embedded (1.90.0-arm64-darwin)
128 | google-protobuf (~> 4.31)
129 | sass-embedded (1.90.0-riscv64-linux-android)
130 | google-protobuf (~> 4.31)
131 | sass-embedded (1.90.0-riscv64-linux-gnu)
132 | google-protobuf (~> 4.31)
133 | sass-embedded (1.90.0-riscv64-linux-musl)
134 | google-protobuf (~> 4.31)
135 | sass-embedded (1.90.0-x86_64-darwin)
136 | google-protobuf (~> 4.31)
137 | sass-embedded (1.90.0-x86_64-linux-android)
138 | google-protobuf (~> 4.31)
139 | sass-embedded (1.90.0-x86_64-linux-gnu)
140 | google-protobuf (~> 4.31)
141 | sass-embedded (1.90.0-x86_64-linux-musl)
142 | google-protobuf (~> 4.31)
143 | terminal-table (3.0.2)
144 | unicode-display_width (>= 1.1.1, < 3)
145 | unicode-display_width (2.6.0)
146 | webrick (1.9.1)
147 |
148 | PLATFORMS
149 | aarch64-linux-android
150 | aarch64-linux-gnu
151 | aarch64-linux-musl
152 | arm-linux-androideabi
153 | arm-linux-gnu
154 | arm-linux-gnueabihf
155 | arm-linux-musl
156 | arm-linux-musleabihf
157 | arm64-darwin
158 | riscv64-linux-android
159 | riscv64-linux-gnu
160 | riscv64-linux-musl
161 | ruby
162 | x86-linux-gnu
163 | x86-linux-musl
164 | x86_64-darwin
165 | x86_64-linux-android
166 | x86_64-linux-gnu
167 | x86_64-linux-musl
168 |
169 | DEPENDENCIES
170 | http_parser.rb (~> 0.6.0)
171 | jekyll (~> 4.4.1)
172 | jekyll-feed (~> 0.12)
173 | minima (~> 2.5)
174 | puma (~> 7.1)
175 | rack (~> 3.2)
176 | tzinfo (>= 1, < 3)
177 | tzinfo-data
178 | wdm (~> 0.1)
179 |
180 | BUNDLED WITH
181 | 2.7.1
182 |
--------------------------------------------------------------------------------
/.github/actions/auth/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "auth",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "auth",
9 | "version": "1.0.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@actions/core": "^2.0.1",
13 | "playwright": "^1.57.0"
14 | },
15 | "devDependencies": {
16 | "@types/node": "^25.0.2",
17 | "typescript": "^5.9.3"
18 | }
19 | },
20 | "node_modules/@actions/core": {
21 | "version": "2.0.1",
22 | "resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.1.tgz",
23 | "integrity": "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==",
24 | "license": "MIT",
25 | "dependencies": {
26 | "@actions/exec": "^2.0.0",
27 | "@actions/http-client": "^3.0.0"
28 | }
29 | },
30 | "node_modules/@actions/exec": {
31 | "version": "2.0.0",
32 | "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-2.0.0.tgz",
33 | "integrity": "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==",
34 | "license": "MIT",
35 | "dependencies": {
36 | "@actions/io": "^2.0.0"
37 | }
38 | },
39 | "node_modules/@actions/http-client": {
40 | "version": "3.0.0",
41 | "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.0.tgz",
42 | "integrity": "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==",
43 | "license": "MIT",
44 | "dependencies": {
45 | "tunnel": "^0.0.6",
46 | "undici": "^5.28.5"
47 | }
48 | },
49 | "node_modules/@actions/io": {
50 | "version": "2.0.0",
51 | "resolved": "https://registry.npmjs.org/@actions/io/-/io-2.0.0.tgz",
52 | "integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==",
53 | "license": "MIT"
54 | },
55 | "node_modules/@fastify/busboy": {
56 | "version": "2.1.1",
57 | "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
58 | "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
59 | "license": "MIT",
60 | "engines": {
61 | "node": ">=14"
62 | }
63 | },
64 | "node_modules/@types/node": {
65 | "version": "25.0.2",
66 | "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
67 | "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
68 | "dev": true,
69 | "license": "MIT",
70 | "dependencies": {
71 | "undici-types": "~7.16.0"
72 | }
73 | },
74 | "node_modules/fsevents": {
75 | "version": "2.3.2",
76 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
77 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
78 | "hasInstallScript": true,
79 | "license": "MIT",
80 | "optional": true,
81 | "os": [
82 | "darwin"
83 | ],
84 | "engines": {
85 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
86 | }
87 | },
88 | "node_modules/playwright": {
89 | "version": "1.57.0",
90 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
91 | "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
92 | "license": "Apache-2.0",
93 | "dependencies": {
94 | "playwright-core": "1.57.0"
95 | },
96 | "bin": {
97 | "playwright": "cli.js"
98 | },
99 | "engines": {
100 | "node": ">=18"
101 | },
102 | "optionalDependencies": {
103 | "fsevents": "2.3.2"
104 | }
105 | },
106 | "node_modules/playwright-core": {
107 | "version": "1.57.0",
108 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
109 | "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
110 | "license": "Apache-2.0",
111 | "bin": {
112 | "playwright-core": "cli.js"
113 | },
114 | "engines": {
115 | "node": ">=18"
116 | }
117 | },
118 | "node_modules/tunnel": {
119 | "version": "0.0.6",
120 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
121 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
122 | "license": "MIT",
123 | "engines": {
124 | "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
125 | }
126 | },
127 | "node_modules/typescript": {
128 | "version": "5.9.3",
129 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
130 | "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
131 | "dev": true,
132 | "license": "Apache-2.0",
133 | "bin": {
134 | "tsc": "bin/tsc",
135 | "tsserver": "bin/tsserver"
136 | },
137 | "engines": {
138 | "node": ">=14.17"
139 | }
140 | },
141 | "node_modules/undici": {
142 | "version": "5.29.0",
143 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
144 | "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
145 | "license": "MIT",
146 | "dependencies": {
147 | "@fastify/busboy": "^2.0.0"
148 | },
149 | "engines": {
150 | "node": ">=14.0"
151 | }
152 | },
153 | "node_modules/undici-types": {
154 | "version": "7.16.0",
155 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
156 | "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
157 | "dev": true,
158 | "license": "MIT"
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: "accessibility-scanner"
2 | description: "Finds potential accessibility gaps, files GitHub issues to track them, and attempts to fix them with Copilot."
3 |
4 | inputs:
5 | urls:
6 | description: "Newline-delimited list of URLs to check for accessibility issues"
7 | required: true
8 | multiline: true
9 | repository:
10 | description: "Repository (with owner) to file issues in"
11 | required: true
12 | token:
13 | description: "Personal access token (PAT) with fine-grained permissions 'contents: write', 'issues: write', and 'pull_requests: write'"
14 | required: true
15 | cache_key:
16 | description: "Key for caching results across runs"
17 | required: true
18 | login_url:
19 | description: "If scanned pages require authentication, the URL of the login page"
20 | required: false
21 | username:
22 | description: "If scanned pages require authentication, the username to use for login"
23 | required: false
24 | password:
25 | description: "If scanned pages require authentication, the password to use for login"
26 | required: false
27 | auth_context:
28 | description: "If scanned pages require authentication, a stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session"
29 | required: false
30 | skip_copilot_assignment:
31 | description: "Whether to skip assigning filed issues to Copilot"
32 | required: false
33 | default: "false"
34 |
35 | outputs:
36 | results:
37 | description: "List of issues and pull requests filed (and their associated finding(s)), as stringified JSON"
38 | value: ${{ steps.results.outputs.results }}
39 |
40 | runs:
41 | using: "composite"
42 | steps:
43 | - name: Make sub-actions referenceable
44 | working-directory: ${{ github.action_path }}
45 | shell: bash
46 | run: |
47 | RUNNER_WORK_DIR="$(realpath "${GITHUB_WORKSPACE}/../..")"
48 | ACTION_DIR="${RUNNER_WORK_DIR}/_actions/github/accessibility-scanner/current"
49 | mkdir -p "${ACTION_DIR}/.github/actions"
50 | if [ "$(realpath ".github/actions")" != "$(realpath "${ACTION_DIR}/.github/actions")" ]; then
51 | cp -a ".github/actions/." "${ACTION_DIR}/.github/actions/"
52 | fi
53 | - name: Restore cached results
54 | id: restore
55 | uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/cache
56 | with:
57 | key: ${{ inputs.cache_key }}
58 | token: ${{ inputs.token }}
59 | - if: ${{ steps.restore.outputs.value }}
60 | name: Normalize cache format
61 | id: normalize_cache
62 | shell: bash
63 | run: |
64 | # Migrate cache format from v1 to v2:
65 | # If cached data is a list of Finding objects, each with 'issueUrl' keys (i.e. v1),
66 | # convert to a list of (partial) Result objects, each with 'findings' and 'issue' keys (i.e. v2).
67 | # Otherwise, re-output as-is.
68 | printf '%s' "value=$(printf '%s' '${{ steps.restore.outputs.value }}' | jq -c 'if (type == "array" and length > 0 and (.[0] | has("issueUrl"))) then map({findings: [del(.issueUrl)], issue: {url: .issueUrl}}) else . end' )" >> $GITHUB_OUTPUT
69 | - if: ${{ inputs.login_url && inputs.username && inputs.password && !inputs.auth_context }}
70 | name: Authenticate
71 | id: auth
72 | uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/auth
73 | with:
74 | login_url: ${{ inputs.login_url }}
75 | username: ${{ inputs.username }}
76 | password: ${{ inputs.password }}
77 | - name: Find
78 | id: find
79 | uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/find
80 | with:
81 | urls: ${{ inputs.urls }}
82 | auth_context: ${{ inputs.auth_context || steps.auth.outputs.auth_context }}
83 | - name: File
84 | id: file
85 | uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/file
86 | with:
87 | findings: ${{ steps.find.outputs.findings }}
88 | repository: ${{ inputs.repository }}
89 | token: ${{ inputs.token }}
90 | cached_filings: ${{ steps.normalize_cache.outputs.value }}
91 | - if: ${{ steps.file.outputs.filings }}
92 | name: Get issues from filings
93 | id: get_issues_from_filings
94 | shell: bash
95 | run: |
96 | # Extract open issues from Filing objects and output as a single-line JSON array
97 | issues=$(jq -c '[.[] | select(.issue.state == "open") | .issue]' <<< '${{ steps.file.outputs.filings }}')
98 | echo "issues=$issues" >> "$GITHUB_OUTPUT"
99 | - if: ${{ inputs.skip_copilot_assignment != 'true' }}
100 | name: Fix
101 | id: fix
102 | uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/fix
103 | with:
104 | issues: ${{ steps.get_issues_from_filings.outputs.issues }}
105 | repository: ${{ inputs.repository }}
106 | token: ${{ inputs.token }}
107 | - name: Set results output
108 | id: results
109 | uses: actions/github-script@v8
110 | with:
111 | script: |
112 | const filings = ${{ steps.file.outputs.filings || '""' }} || [];
113 | const fixings = ${{ steps.fix.outputs.fixings || '""' }} || [];
114 | const fixingsByIssueUrl = fixings.reduce((acc, fixing) => {
115 | if (fixing.issue && fixing.issue.url) {
116 | acc[fixing.issue.url] = fixing;
117 | }
118 | return acc;
119 | }, {});
120 | const results = filings;
121 | for (const result of results) {
122 | if (result.issue && result.issue.url && fixingsByIssueUrl[result.issue.url]) {
123 | result.pullRequest = fixingsByIssueUrl[result.issue.url].pullRequest;
124 | }
125 | }
126 | core.setOutput('results', JSON.stringify(results));
127 | core.debug(`Results: ${JSON.stringify(results)}`);
128 | - name: Save cached results
129 | uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/cache
130 | with:
131 | key: ${{ inputs.cache_key }}
132 | value: ${{ steps.results.outputs.results }}
133 | token: ${{ inputs.token }}
134 |
135 | branding:
136 | icon: "compass"
137 | color: "blue"
138 |
--------------------------------------------------------------------------------
/.github/actions/find/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "find",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "find",
9 | "version": "1.0.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@actions/core": "^2.0.1",
13 | "@axe-core/playwright": "^4.11.0",
14 | "playwright": "^1.57.0"
15 | },
16 | "devDependencies": {
17 | "@types/node": "^25.0.2",
18 | "typescript": "^5.9.3"
19 | }
20 | },
21 | "node_modules/@actions/core": {
22 | "version": "2.0.1",
23 | "resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.1.tgz",
24 | "integrity": "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==",
25 | "license": "MIT",
26 | "dependencies": {
27 | "@actions/exec": "^2.0.0",
28 | "@actions/http-client": "^3.0.0"
29 | }
30 | },
31 | "node_modules/@actions/exec": {
32 | "version": "2.0.0",
33 | "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-2.0.0.tgz",
34 | "integrity": "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==",
35 | "license": "MIT",
36 | "dependencies": {
37 | "@actions/io": "^2.0.0"
38 | }
39 | },
40 | "node_modules/@actions/http-client": {
41 | "version": "3.0.0",
42 | "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.0.tgz",
43 | "integrity": "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==",
44 | "license": "MIT",
45 | "dependencies": {
46 | "tunnel": "^0.0.6",
47 | "undici": "^5.28.5"
48 | }
49 | },
50 | "node_modules/@actions/io": {
51 | "version": "2.0.0",
52 | "resolved": "https://registry.npmjs.org/@actions/io/-/io-2.0.0.tgz",
53 | "integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==",
54 | "license": "MIT"
55 | },
56 | "node_modules/@axe-core/playwright": {
57 | "version": "4.11.0",
58 | "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.0.tgz",
59 | "integrity": "sha512-70vBT/Ylqpm65RQz2iCG2o0JJCEG/WCNyefTr2xcOcr1CoSee60gNQYUMZZ7YukoKkFLv26I/jjlsvwwp532oQ==",
60 | "license": "MPL-2.0",
61 | "dependencies": {
62 | "axe-core": "~4.11.0"
63 | },
64 | "peerDependencies": {
65 | "playwright-core": ">= 1.0.0"
66 | }
67 | },
68 | "node_modules/@fastify/busboy": {
69 | "version": "2.1.1",
70 | "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
71 | "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
72 | "license": "MIT",
73 | "engines": {
74 | "node": ">=14"
75 | }
76 | },
77 | "node_modules/@types/node": {
78 | "version": "25.0.2",
79 | "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
80 | "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
81 | "dev": true,
82 | "license": "MIT",
83 | "dependencies": {
84 | "undici-types": "~7.16.0"
85 | }
86 | },
87 | "node_modules/axe-core": {
88 | "version": "4.11.0",
89 | "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz",
90 | "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==",
91 | "license": "MPL-2.0",
92 | "engines": {
93 | "node": ">=4"
94 | }
95 | },
96 | "node_modules/fsevents": {
97 | "version": "2.3.2",
98 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
99 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
100 | "hasInstallScript": true,
101 | "license": "MIT",
102 | "optional": true,
103 | "os": [
104 | "darwin"
105 | ],
106 | "engines": {
107 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
108 | }
109 | },
110 | "node_modules/playwright": {
111 | "version": "1.57.0",
112 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
113 | "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
114 | "license": "Apache-2.0",
115 | "dependencies": {
116 | "playwright-core": "1.57.0"
117 | },
118 | "bin": {
119 | "playwright": "cli.js"
120 | },
121 | "engines": {
122 | "node": ">=18"
123 | },
124 | "optionalDependencies": {
125 | "fsevents": "2.3.2"
126 | }
127 | },
128 | "node_modules/playwright-core": {
129 | "version": "1.57.0",
130 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
131 | "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
132 | "license": "Apache-2.0",
133 | "peer": true,
134 | "bin": {
135 | "playwright-core": "cli.js"
136 | },
137 | "engines": {
138 | "node": ">=18"
139 | }
140 | },
141 | "node_modules/tunnel": {
142 | "version": "0.0.6",
143 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
144 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
145 | "license": "MIT",
146 | "engines": {
147 | "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
148 | }
149 | },
150 | "node_modules/typescript": {
151 | "version": "5.9.3",
152 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
153 | "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
154 | "dev": true,
155 | "license": "Apache-2.0",
156 | "bin": {
157 | "tsc": "bin/tsc",
158 | "tsserver": "bin/tsserver"
159 | },
160 | "engines": {
161 | "node": ">=14.17"
162 | }
163 | },
164 | "node_modules/undici": {
165 | "version": "5.29.0",
166 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
167 | "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
168 | "license": "MIT",
169 | "dependencies": {
170 | "@fastify/busboy": "^2.0.0"
171 | },
172 | "engines": {
173 | "node": ">=14.0"
174 | }
175 | },
176 | "node_modules/undici-types": {
177 | "version": "7.16.0",
178 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
179 | "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
180 | "dev": true,
181 | "license": "MIT"
182 | }
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/tests/site-with-errors.test.ts:
--------------------------------------------------------------------------------
1 | import type { Endpoints } from "@octokit/types"
2 | import type { Result } from "./types.d.js";
3 | import fs from "node:fs";
4 | import { describe, it, expect, beforeAll } from "vitest";
5 | import { Octokit } from "@octokit/core";
6 | import { throttling } from "@octokit/plugin-throttling";
7 | const OctokitWithThrottling = Octokit.plugin(throttling);
8 |
9 | describe("site-with-errors", () => {
10 | let results: Result[];
11 |
12 | beforeAll(() => {
13 | expect(process.env.CACHE_PATH).toBeDefined();
14 | expect(fs.existsSync(process.env.CACHE_PATH!)).toBe(true);
15 | results = JSON.parse(fs.readFileSync(process.env.CACHE_PATH!, "utf-8"));
16 | });
17 |
18 | it("cache has expected results", () => {
19 | const actual = results.map(({ issue: { url: issueUrl }, pullRequest: { url: pullRequestUrl }, findings }) => {
20 | const { problemUrl, solutionLong, ...finding } = findings[0];
21 | // Check volatile fields for existence only
22 | expect(issueUrl).toBeDefined();
23 | expect(pullRequestUrl).toBeDefined();
24 | expect(problemUrl).toBeDefined();
25 | expect(solutionLong).toBeDefined();
26 | // Check `problemUrl`, ignoring axe version
27 | expect(problemUrl.startsWith("https://dequeuniversity.com/rules/axe/")).toBe(true);
28 | expect(problemUrl.endsWith(`/${finding.ruleId}?application=playwright`)).toBe(true);
29 | return finding;
30 | });
31 | const expected = [
32 | {
33 | scannerType: "axe",
34 | url: "http://127.0.0.1:4000/",
35 | html: 'Jul 30, 2025',
36 | problemShort: "elements must meet minimum color contrast ratio thresholds",
37 | ruleId: "color-contrast",
38 | solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds"
39 | }, {
40 | scannerType: "axe",
41 | url: "http://127.0.0.1:4000/",
42 | html: '',
43 | problemShort: "page should contain a level-one heading",
44 | ruleId: "page-has-heading-one",
45 | solutionShort: "ensure that the page, or at least one of its frames contains a level-one heading"
46 | }, {
47 | scannerType: "axe",
48 | url: "http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html",
49 | html: ``,
51 | problemShort: "elements must meet minimum color contrast ratio thresholds",
52 | ruleId: "color-contrast",
53 | solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds",
54 | }, {
55 | scannerType: "axe",
56 | url: "http://127.0.0.1:4000/about/",
57 | html: 'jekyllrb.com',
58 | problemShort: "elements must meet minimum color contrast ratio thresholds",
59 | ruleId: "color-contrast",
60 | solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds",
61 | }, {
62 | scannerType: "axe",
63 | url: "http://127.0.0.1:4000/404.html",
64 | html: 'Accessibility Scanner Demo',
65 | problemShort: "elements must meet minimum color contrast ratio thresholds",
66 | ruleId: "color-contrast",
67 | solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds"
68 | }, {
69 | scannerType: "axe",
70 | url: "http://127.0.0.1:4000/404.html",
71 | html: '',
72 | problemShort: "headings should not be empty",
73 | ruleId: "empty-heading",
74 | solutionShort: "ensure headings have discernible text",
75 | },
76 | ];
77 | // Check that:
78 | // - every expected object exists (no more and no fewer), and
79 | // - each object has all fields, and
80 | // - field values match expectations exactly
81 | // A specific order is _not_ enforced.
82 | expect(actual).toHaveLength(expected.length);
83 | expect(actual).toEqual(expect.arrayContaining(expected));
84 | });
85 |
86 | it("GITHUB_TOKEN environment variable is set", () => {
87 | expect(process.env.GITHUB_TOKEN).toBeDefined();
88 | });
89 |
90 | describe.runIf(!!process.env.GITHUB_TOKEN)("—", () => {
91 | let octokit: Octokit;
92 | let issues: Endpoints["GET /repos/{owner}/{repo}/issues/{issue_number}"]["response"]["data"][];
93 | let pullRequests: Endpoints["GET /repos/{owner}/{repo}/pulls/{pull_number}"]["response"]["data"][];
94 |
95 | beforeAll(async () => {
96 | octokit = new OctokitWithThrottling({
97 | auth: process.env.GITHUB_TOKEN,
98 | throttle: {
99 | onRateLimit: (retryAfter, options, octokit, retryCount) => {
100 | octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
101 | if (retryCount < 3) {
102 | octokit.log.info(`Retrying after ${retryAfter} seconds!`);
103 | return true;
104 | }
105 | },
106 | onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => {
107 | octokit.log.warn(`Secondary rate limit hit for request ${options.method} ${options.url}`);
108 | if (retryCount < 3) {
109 | octokit.log.info(`Retrying after ${retryAfter} seconds!`);
110 | return true;
111 | }
112 | },
113 | }
114 | });
115 | // Fetch issues referenced in the cache file
116 | issues = await Promise.all(results.map(async ({ issue: { url: issueUrl } }) => {
117 | expect(issueUrl).toBeDefined();
118 | const { owner, repo, issueNumber } =
119 | /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)/.exec(issueUrl!)!.groups!;
120 | const {data: issue} = await octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}", {
121 | owner,
122 | repo,
123 | issue_number: parseInt(issueNumber, 10)
124 | });
125 | expect(issue).toBeDefined();
126 | return issue;
127 | }));
128 | // Fetch pull requests referenced in the findings file
129 | pullRequests = await Promise.all(results.map(async ({ pullRequest: { url: pullRequestUrl } }) => {
130 | expect(pullRequestUrl).toBeDefined();
131 | const { owner, repo, pullNumber } =
132 | /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/pull\/(?\d+)/.exec(pullRequestUrl!)!.groups!;
133 | const {data: pullRequest} = await octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", {
134 | owner,
135 | repo,
136 | pull_number: parseInt(pullNumber, 10)
137 | });
138 | expect(pullRequest).toBeDefined();
139 | return pullRequest;
140 | }));
141 | });
142 |
143 | it("issues exist and have expected title, state, and assignee", async () => {
144 | const actualTitles = issues.map(({ title }) => (title));
145 | const expectedTitles = [
146 | "Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /",
147 | "Accessibility issue: Page should contain a level-one heading on /",
148 | "Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /404.html",
149 | "Accessibility issue: Headings should not be empty on /404.html",
150 | "Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /about/",
151 | "Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /jekyll/update/2025/07/30/welcome-to-jekyll.html",
152 | ];
153 | expect(actualTitles).toHaveLength(expectedTitles.length);
154 | expect(actualTitles).toEqual(expect.arrayContaining(expectedTitles));
155 | for (const issue of issues) {
156 | expect(issue.state).toBe("open");
157 | expect(issue.assignees).toBeDefined();
158 | expect(issue.assignees!.some(a => a.login === "Copilot")).toBe(true);
159 | }
160 | });
161 |
162 | it("pull requests exist and have expected author, state, and assignee", async () => {
163 | for (const pullRequest of pullRequests) {
164 | expect(pullRequest.user.login).toBe("Copilot");
165 | expect(pullRequest.state).toBe("open");
166 | expect(pullRequest.assignees).toBeDefined();
167 | expect(pullRequest.assignees!.some(a => a.login === "Copilot")).toBe(true);
168 | }
169 | });
170 | });
171 | });
172 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AI-powered Accessibility Scanner
2 |
3 | The AI-powered Accessibility Scanner (a11y scanner) is a GitHub Action that detects accessibility barriers across your digital products, creates trackable issues, and leverages GitHub Copilot for AI-powered fixes.
4 |
5 | The a11y scanner helps teams:
6 |
7 | - 🔍 Scan websites, files, repositories, and dynamic content for accessibility issues
8 | - 📝 Create actionable GitHub issues that can be assigned to GitHub Copilot
9 | - 🤖 Propose fixes with GitHub Copilot, with humans reviewing before merging
10 |
11 | > ⚠️ **Note:** The a11y scanner is currently in public preview. Feature development work is still ongoing. It can help identify accessibility gaps but cannot guarantee fully accessible code suggestions. Always review before merging!
12 |
13 | 🎥 **[Watch the demo video](https://youtu.be/CvRJcEzCSQM)** to see the a11y scanner in action.
14 |
15 | ---
16 |
17 | ## Requirements
18 |
19 | To use the a11y scanner, you'll need:
20 |
21 | - **GitHub Actions** enabled in your repository
22 | - **GitHub Issues** enabled in your repository
23 | - **Available GitHub Actions minutes** for your account
24 | - **Admin access** to add repository secrets
25 | - **GitHub Copilot** (optional) - The a11y scanner works without GitHub Copilot and will still create issues for accessibility findings. However, without GitHub Copilot, you won't be able to automatically assign issues to GitHub Copilot for AI-powered fix suggestions and PR creation.
26 |
27 | ## Getting started
28 |
29 | ### 1. Add a workflow file
30 |
31 | Create a workflow file in `.github/workflows/` (e.g., `a11y-scan.yml`) in your repository:
32 |
33 | ```yaml
34 | name: Accessibility Scanner
35 | on: workflow_dispatch # This configures the workflow to run manually, instead of (e.g.) automatically in every PR. Check out https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#on for more options.
36 |
37 | jobs:
38 | accessibility_scanner:
39 | runs-on: ubuntu-latest
40 | steps:
41 | - uses: github/accessibility-scanner@v2
42 | with:
43 | urls: | # Provide a newline-delimited list of URLs to scan; more information below.
44 | REPLACE_THIS
45 | repository: REPLACE_THIS/REPLACE_THIS # Provide a repository name-with-owner (in the format "primer/primer-docs"). This is where issues will be filed and where Copilot will open PRs; more information below.
46 | token: ${{ secrets.GH_TOKEN }} # This token must have write access to the repo above (contents, issues, and PRs); more information below. Note: GitHub Actions' GITHUB_TOKEN cannot be used here.
47 | cache_key: REPLACE_THIS # Provide a filename that will be used when caching results. We recommend including the name or domain of the site being scanned.
48 | # login_url: # Optional: URL of the login page if authentication is required
49 | # username: # Optional: Username for authentication
50 | # password: ${{ secrets.PASSWORD }} # Optional: Password for authentication (use secrets!)
51 | # auth_context: # Optional: Stringified JSON object for complex authentication
52 | # skip_copilot_assignment: false # Optional: Set to true to skip assigning issues to GitHub Copilot (or if you don't have GitHub Copilot)
53 | ```
54 |
55 | > 👉 Update all `REPLACE_THIS` placeholders with your actual values. See [Action Inputs](#action-inputs) for details.
56 |
57 | **Required permissions:**
58 |
59 | - Write access to add or update workflows
60 | - Admin access to add repository secrets
61 |
62 | 📚 Learn more
63 | - [Quickstart for GitHub Actions](https://docs.github.com/en/actions/get-started/quickstart)
64 | - [Understanding GitHub Actions](https://docs.github.com/en/actions/get-started/understand-github-actions)
65 | - [Writing workflows](https://docs.github.com/en/actions/how-tos/write-workflows)
66 | - [Managing GitHub Actions settings](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository)
67 | - [GitHub Actions billing](https://docs.github.com/en/billing/concepts/product-billing/github-actions)
68 |
69 | ---
70 |
71 | ### 2. Create a token and add a secret
72 |
73 | The a11y scanner requires a Personal Access Token (PAT) as a repository secret:
74 |
75 | **The `GH_TOKEN` is a fine-grained PAT with:**
76 |
77 | - `actions: write`
78 | - `contents: write`
79 | - `issues: write`
80 | - `pull-requests: write`
81 | - `metadata: read`
82 | - **Scope:** Your target repository (where issues and PRs will be created) and the repository containing your workflow
83 |
84 | > 👉 GitHub Actions' default [GITHUB_TOKEN](https://docs.github.com/en/actions/tutorials/authenticate-with-github_token) cannot be used here.
85 |
86 | 📚 Learn more
87 | - [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)
88 | - [Creating repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository)
89 |
90 | ---
91 |
92 | ### 3. Run your first scan
93 |
94 | 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.
95 |
96 | 📚 Learn more
97 | - [View workflow run history](https://docs.github.com/en/actions/how-tos/monitor-workflows/view-workflow-run-history)
98 | - [Running a workflow manually](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow#running-a-workflow)
99 | - [Re-run workflows and jobs](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/re-run-workflows-and-jobs)
100 |
101 | ---
102 |
103 | ## Action inputs
104 |
105 | | Input | Required | Description | Example |
106 | |-------|----------|-------------|---------|
107 | | `urls` | Yes | Newline-delimited list of URLs to scan | `https://primer.style`
`https://primer.style/octicons` |
108 | | `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` |
109 | | `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` |
110 | | `cache_key` | Yes | Key for caching results across runs
Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` |
111 | | `login_url` | No | If scanned pages require authentication, the URL of the login page | `https://github.com/login` |
112 | | `username` | No | If scanned pages require authentication, the username to use for login | `some-user` |
113 | | `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` |
114 | | `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":[...]}` |
115 | | `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` |
116 |
117 | ---
118 |
119 | ## Authentication
120 |
121 | If access to a page requires logging-in first, and logging-in requires only a username and password, then provide the `login_url`, `username`, and `password` inputs.
122 |
123 | If your login flow is more complex—if it requires two-factor authentication, single sign-on, passkeys, etc.—and you have a custom action that [authenticates with Playwright](https://playwright.dev/docs/auth) and persists authenticated session state to a file, then provide the `auth_context` input. (If `auth_context` is provided, `login_url`, `username`, and `password` will be ignored.)
124 |
125 | > [!IMPORTANT]
126 | > 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).
127 |
128 | ---
129 |
130 | ## Configuring GitHub Copilot
131 |
132 | The a11y scanner leverages GitHub Copilot coding agent, which can be configured with custom instructions:
133 |
134 | - **Repository-wide:** `.github/copilot-instructions.md`
135 | - **Directory/file-scoped:** `.github/instructions/*.instructions.md`
136 |
137 | 📚 Learn more
138 | - [Adding repository custom instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions)
139 | - [Optimizing GitHub Copilot for accessibility](https://accessibility.github.com/documentation/guide/copilot-instructions)
140 | - [GitHub Copilot .instructions.md support](https://github.blog/changelog/2025-07-23-github-copilot-coding-agent-now-supports-instructions-md-custom-instructions/)
141 | - [GitHub Copilot agents.md support](https://github.blog/changelog/2025-08-28-copilot-coding-agent-now-supports-agents-md-custom-instructions)
142 |
143 | ---
144 |
145 | ## Feedback
146 |
147 | 💬 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.
148 |
149 | ## License
150 |
151 | 📄 This project is licensed under the terms of the MIT open source license. Please refer to the [LICENSE](./LICENSE) file for the full terms.
152 |
153 | ## Maintainers
154 |
155 | 🔧 Please refer to the [CODEOWNERS](./.github/CODEOWNERS) file for more information.
156 |
157 | ## Support
158 |
159 | ❓ Please refer to the [SUPPORT](./SUPPORT.md) file for more information.
160 |
161 | ## Acknowledgement
162 |
163 | ✨ Thank you to our beta testers for their help with making this project!
164 |
--------------------------------------------------------------------------------
/.github/actions/fix/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fix",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "fix",
9 | "version": "1.0.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@actions/core": "^2.0.1",
13 | "@octokit/core": "^7.0.6",
14 | "@octokit/plugin-throttling": "^11.0.3"
15 | },
16 | "devDependencies": {
17 | "@types/node": "^25.0.2",
18 | "typescript": "^5.9.3"
19 | }
20 | },
21 | "node_modules/@actions/core": {
22 | "version": "2.0.1",
23 | "resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.1.tgz",
24 | "integrity": "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==",
25 | "license": "MIT",
26 | "dependencies": {
27 | "@actions/exec": "^2.0.0",
28 | "@actions/http-client": "^3.0.0"
29 | }
30 | },
31 | "node_modules/@actions/exec": {
32 | "version": "2.0.0",
33 | "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-2.0.0.tgz",
34 | "integrity": "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==",
35 | "license": "MIT",
36 | "dependencies": {
37 | "@actions/io": "^2.0.0"
38 | }
39 | },
40 | "node_modules/@actions/http-client": {
41 | "version": "3.0.0",
42 | "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.0.tgz",
43 | "integrity": "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==",
44 | "license": "MIT",
45 | "dependencies": {
46 | "tunnel": "^0.0.6",
47 | "undici": "^5.28.5"
48 | }
49 | },
50 | "node_modules/@actions/io": {
51 | "version": "2.0.0",
52 | "resolved": "https://registry.npmjs.org/@actions/io/-/io-2.0.0.tgz",
53 | "integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==",
54 | "license": "MIT"
55 | },
56 | "node_modules/@fastify/busboy": {
57 | "version": "2.1.1",
58 | "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
59 | "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
60 | "license": "MIT",
61 | "engines": {
62 | "node": ">=14"
63 | }
64 | },
65 | "node_modules/@octokit/auth-token": {
66 | "version": "6.0.0",
67 | "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
68 | "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
69 | "license": "MIT",
70 | "engines": {
71 | "node": ">= 20"
72 | }
73 | },
74 | "node_modules/@octokit/core": {
75 | "version": "7.0.6",
76 | "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
77 | "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
78 | "license": "MIT",
79 | "peer": true,
80 | "dependencies": {
81 | "@octokit/auth-token": "^6.0.0",
82 | "@octokit/graphql": "^9.0.3",
83 | "@octokit/request": "^10.0.6",
84 | "@octokit/request-error": "^7.0.2",
85 | "@octokit/types": "^16.0.0",
86 | "before-after-hook": "^4.0.0",
87 | "universal-user-agent": "^7.0.0"
88 | },
89 | "engines": {
90 | "node": ">= 20"
91 | }
92 | },
93 | "node_modules/@octokit/endpoint": {
94 | "version": "11.0.2",
95 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz",
96 | "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==",
97 | "license": "MIT",
98 | "dependencies": {
99 | "@octokit/types": "^16.0.0",
100 | "universal-user-agent": "^7.0.2"
101 | },
102 | "engines": {
103 | "node": ">= 20"
104 | }
105 | },
106 | "node_modules/@octokit/graphql": {
107 | "version": "9.0.3",
108 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
109 | "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
110 | "license": "MIT",
111 | "dependencies": {
112 | "@octokit/request": "^10.0.6",
113 | "@octokit/types": "^16.0.0",
114 | "universal-user-agent": "^7.0.0"
115 | },
116 | "engines": {
117 | "node": ">= 20"
118 | }
119 | },
120 | "node_modules/@octokit/openapi-types": {
121 | "version": "27.0.0",
122 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
123 | "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
124 | "license": "MIT"
125 | },
126 | "node_modules/@octokit/plugin-throttling": {
127 | "version": "11.0.3",
128 | "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.3.tgz",
129 | "integrity": "sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg==",
130 | "license": "MIT",
131 | "dependencies": {
132 | "@octokit/types": "^16.0.0",
133 | "bottleneck": "^2.15.3"
134 | },
135 | "engines": {
136 | "node": ">= 20"
137 | },
138 | "peerDependencies": {
139 | "@octokit/core": "^7.0.0"
140 | }
141 | },
142 | "node_modules/@octokit/request": {
143 | "version": "10.0.6",
144 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.6.tgz",
145 | "integrity": "sha512-FO+UgZCUu+pPnZAR+iKdUt64kPE7QW7ciqpldaMXaNzixz5Jld8dJ31LAUewk0cfSRkNSRKyqG438ba9c/qDlQ==",
146 | "license": "MIT",
147 | "dependencies": {
148 | "@octokit/endpoint": "^11.0.2",
149 | "@octokit/request-error": "^7.0.2",
150 | "@octokit/types": "^16.0.0",
151 | "fast-content-type-parse": "^3.0.0",
152 | "universal-user-agent": "^7.0.2"
153 | },
154 | "engines": {
155 | "node": ">= 20"
156 | }
157 | },
158 | "node_modules/@octokit/request-error": {
159 | "version": "7.0.2",
160 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.2.tgz",
161 | "integrity": "sha512-U8piOROoQQUyExw5c6dTkU3GKxts5/ERRThIauNL7yaRoeXW0q/5bgHWT7JfWBw1UyrbK8ERId2wVkcB32n0uQ==",
162 | "license": "MIT",
163 | "dependencies": {
164 | "@octokit/types": "^16.0.0"
165 | },
166 | "engines": {
167 | "node": ">= 20"
168 | }
169 | },
170 | "node_modules/@octokit/types": {
171 | "version": "16.0.0",
172 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
173 | "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
174 | "license": "MIT",
175 | "dependencies": {
176 | "@octokit/openapi-types": "^27.0.0"
177 | }
178 | },
179 | "node_modules/@types/node": {
180 | "version": "25.0.2",
181 | "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
182 | "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
183 | "dev": true,
184 | "license": "MIT",
185 | "dependencies": {
186 | "undici-types": "~7.16.0"
187 | }
188 | },
189 | "node_modules/before-after-hook": {
190 | "version": "4.0.0",
191 | "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
192 | "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
193 | "license": "Apache-2.0"
194 | },
195 | "node_modules/bottleneck": {
196 | "version": "2.19.5",
197 | "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
198 | "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==",
199 | "license": "MIT"
200 | },
201 | "node_modules/fast-content-type-parse": {
202 | "version": "3.0.0",
203 | "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
204 | "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
205 | "funding": [
206 | {
207 | "type": "github",
208 | "url": "https://github.com/sponsors/fastify"
209 | },
210 | {
211 | "type": "opencollective",
212 | "url": "https://opencollective.com/fastify"
213 | }
214 | ],
215 | "license": "MIT"
216 | },
217 | "node_modules/tunnel": {
218 | "version": "0.0.6",
219 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
220 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
221 | "license": "MIT",
222 | "engines": {
223 | "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
224 | }
225 | },
226 | "node_modules/typescript": {
227 | "version": "5.9.3",
228 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
229 | "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
230 | "dev": true,
231 | "license": "Apache-2.0",
232 | "bin": {
233 | "tsc": "bin/tsc",
234 | "tsserver": "bin/tsserver"
235 | },
236 | "engines": {
237 | "node": ">=14.17"
238 | }
239 | },
240 | "node_modules/undici": {
241 | "version": "5.29.0",
242 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
243 | "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
244 | "license": "MIT",
245 | "dependencies": {
246 | "@fastify/busboy": "^2.0.0"
247 | },
248 | "engines": {
249 | "node": ">=14.0"
250 | }
251 | },
252 | "node_modules/undici-types": {
253 | "version": "7.16.0",
254 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
255 | "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
256 | "dev": true,
257 | "license": "MIT"
258 | },
259 | "node_modules/universal-user-agent": {
260 | "version": "7.0.3",
261 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
262 | "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
263 | "license": "ISC"
264 | }
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/.github/actions/file/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "file",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "file",
9 | "version": "1.0.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@actions/core": "^2.0.1",
13 | "@octokit/core": "^7.0.6",
14 | "@octokit/plugin-throttling": "^11.0.3"
15 | },
16 | "devDependencies": {
17 | "@types/node": "^25.0.2",
18 | "typescript": "^5.9.3"
19 | }
20 | },
21 | "node_modules/@actions/core": {
22 | "version": "2.0.1",
23 | "resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.1.tgz",
24 | "integrity": "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==",
25 | "license": "MIT",
26 | "dependencies": {
27 | "@actions/exec": "^2.0.0",
28 | "@actions/http-client": "^3.0.0"
29 | }
30 | },
31 | "node_modules/@actions/exec": {
32 | "version": "2.0.0",
33 | "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-2.0.0.tgz",
34 | "integrity": "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==",
35 | "license": "MIT",
36 | "dependencies": {
37 | "@actions/io": "^2.0.0"
38 | }
39 | },
40 | "node_modules/@actions/http-client": {
41 | "version": "3.0.0",
42 | "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.0.tgz",
43 | "integrity": "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==",
44 | "license": "MIT",
45 | "dependencies": {
46 | "tunnel": "^0.0.6",
47 | "undici": "^5.28.5"
48 | }
49 | },
50 | "node_modules/@actions/io": {
51 | "version": "2.0.0",
52 | "resolved": "https://registry.npmjs.org/@actions/io/-/io-2.0.0.tgz",
53 | "integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==",
54 | "license": "MIT"
55 | },
56 | "node_modules/@fastify/busboy": {
57 | "version": "2.1.1",
58 | "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
59 | "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
60 | "license": "MIT",
61 | "engines": {
62 | "node": ">=14"
63 | }
64 | },
65 | "node_modules/@octokit/auth-token": {
66 | "version": "6.0.0",
67 | "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
68 | "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
69 | "license": "MIT",
70 | "engines": {
71 | "node": ">= 20"
72 | }
73 | },
74 | "node_modules/@octokit/core": {
75 | "version": "7.0.6",
76 | "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
77 | "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
78 | "license": "MIT",
79 | "peer": true,
80 | "dependencies": {
81 | "@octokit/auth-token": "^6.0.0",
82 | "@octokit/graphql": "^9.0.3",
83 | "@octokit/request": "^10.0.6",
84 | "@octokit/request-error": "^7.0.2",
85 | "@octokit/types": "^16.0.0",
86 | "before-after-hook": "^4.0.0",
87 | "universal-user-agent": "^7.0.0"
88 | },
89 | "engines": {
90 | "node": ">= 20"
91 | }
92 | },
93 | "node_modules/@octokit/endpoint": {
94 | "version": "11.0.2",
95 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz",
96 | "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==",
97 | "license": "MIT",
98 | "dependencies": {
99 | "@octokit/types": "^16.0.0",
100 | "universal-user-agent": "^7.0.2"
101 | },
102 | "engines": {
103 | "node": ">= 20"
104 | }
105 | },
106 | "node_modules/@octokit/graphql": {
107 | "version": "9.0.3",
108 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
109 | "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
110 | "license": "MIT",
111 | "dependencies": {
112 | "@octokit/request": "^10.0.6",
113 | "@octokit/types": "^16.0.0",
114 | "universal-user-agent": "^7.0.0"
115 | },
116 | "engines": {
117 | "node": ">= 20"
118 | }
119 | },
120 | "node_modules/@octokit/openapi-types": {
121 | "version": "27.0.0",
122 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
123 | "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
124 | "license": "MIT"
125 | },
126 | "node_modules/@octokit/plugin-throttling": {
127 | "version": "11.0.3",
128 | "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.3.tgz",
129 | "integrity": "sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg==",
130 | "license": "MIT",
131 | "dependencies": {
132 | "@octokit/types": "^16.0.0",
133 | "bottleneck": "^2.15.3"
134 | },
135 | "engines": {
136 | "node": ">= 20"
137 | },
138 | "peerDependencies": {
139 | "@octokit/core": "^7.0.0"
140 | }
141 | },
142 | "node_modules/@octokit/request": {
143 | "version": "10.0.6",
144 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.6.tgz",
145 | "integrity": "sha512-FO+UgZCUu+pPnZAR+iKdUt64kPE7QW7ciqpldaMXaNzixz5Jld8dJ31LAUewk0cfSRkNSRKyqG438ba9c/qDlQ==",
146 | "license": "MIT",
147 | "dependencies": {
148 | "@octokit/endpoint": "^11.0.2",
149 | "@octokit/request-error": "^7.0.2",
150 | "@octokit/types": "^16.0.0",
151 | "fast-content-type-parse": "^3.0.0",
152 | "universal-user-agent": "^7.0.2"
153 | },
154 | "engines": {
155 | "node": ">= 20"
156 | }
157 | },
158 | "node_modules/@octokit/request-error": {
159 | "version": "7.0.2",
160 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.2.tgz",
161 | "integrity": "sha512-U8piOROoQQUyExw5c6dTkU3GKxts5/ERRThIauNL7yaRoeXW0q/5bgHWT7JfWBw1UyrbK8ERId2wVkcB32n0uQ==",
162 | "license": "MIT",
163 | "dependencies": {
164 | "@octokit/types": "^16.0.0"
165 | },
166 | "engines": {
167 | "node": ">= 20"
168 | }
169 | },
170 | "node_modules/@octokit/types": {
171 | "version": "16.0.0",
172 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
173 | "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
174 | "license": "MIT",
175 | "dependencies": {
176 | "@octokit/openapi-types": "^27.0.0"
177 | }
178 | },
179 | "node_modules/@types/node": {
180 | "version": "25.0.2",
181 | "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
182 | "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
183 | "dev": true,
184 | "license": "MIT",
185 | "dependencies": {
186 | "undici-types": "~7.16.0"
187 | }
188 | },
189 | "node_modules/before-after-hook": {
190 | "version": "4.0.0",
191 | "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
192 | "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
193 | "license": "Apache-2.0"
194 | },
195 | "node_modules/bottleneck": {
196 | "version": "2.19.5",
197 | "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
198 | "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==",
199 | "license": "MIT"
200 | },
201 | "node_modules/fast-content-type-parse": {
202 | "version": "3.0.0",
203 | "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
204 | "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
205 | "funding": [
206 | {
207 | "type": "github",
208 | "url": "https://github.com/sponsors/fastify"
209 | },
210 | {
211 | "type": "opencollective",
212 | "url": "https://opencollective.com/fastify"
213 | }
214 | ],
215 | "license": "MIT"
216 | },
217 | "node_modules/tunnel": {
218 | "version": "0.0.6",
219 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
220 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
221 | "license": "MIT",
222 | "engines": {
223 | "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
224 | }
225 | },
226 | "node_modules/typescript": {
227 | "version": "5.9.3",
228 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
229 | "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
230 | "dev": true,
231 | "license": "Apache-2.0",
232 | "bin": {
233 | "tsc": "bin/tsc",
234 | "tsserver": "bin/tsserver"
235 | },
236 | "engines": {
237 | "node": ">=14.17"
238 | }
239 | },
240 | "node_modules/undici": {
241 | "version": "5.29.0",
242 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
243 | "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
244 | "license": "MIT",
245 | "dependencies": {
246 | "@fastify/busboy": "^2.0.0"
247 | },
248 | "engines": {
249 | "node": ">=14.0"
250 | }
251 | },
252 | "node_modules/undici-types": {
253 | "version": "7.16.0",
254 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
255 | "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
256 | "dev": true,
257 | "license": "MIT"
258 | },
259 | "node_modules/universal-user-agent": {
260 | "version": "7.0.3",
261 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
262 | "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
263 | "license": "ISC"
264 | }
265 | }
266 | }
267 |
--------------------------------------------------------------------------------