From fd9ac0aef3da73d832efbe7f50f1ea65993a6983 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 05:09:19 +0000 Subject: [PATCH 01/46] chore(deps): Bump ruby/setup-ruby Bumps the github-actions group with 1 update in the / directory: [ruby/setup-ruby](https://github.com/ruby/setup-ruby). Updates `ruby/setup-ruby` from 1.307.0 to 1.308.0 - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/6aaa311d81eba98ae12eaffbcb63296ace0efcde...97ecb7b512899eb71ab1bf2310a624c6f1589ac6) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.308.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d11d609..b9443ba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v6 - name: Setup Ruby - uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde + uses: ruby/setup-ruby@97ecb7b512899eb71ab1bf2310a624c6f1589ac6 with: ruby-version: '3.4' bundler-cache: true From 11599273bc8808262f47835e49bf2852d082bb65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 05:10:38 +0000 Subject: [PATCH 02/46] chore(deps-dev): Bump the npm-minor-and-patch group across 5 directories with 3 updates Bumps the npm-minor-and-patch group with 3 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [eslint](https://github.com/eslint/eslint) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint). Bumps the npm-minor-and-patch group with 1 update in the /.github/actions/auth directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Bumps the npm-minor-and-patch group with 1 update in the /.github/actions/file directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Bumps the npm-minor-and-patch group with 1 update in the /.github/actions/find directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Bumps the npm-minor-and-patch group with 1 update in the /.github/actions/fix directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 25.7.0 to 25.9.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `eslint` from 10.3.0 to 10.4.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.3.0...v10.4.0) Updates `typescript-eslint` from 8.59.3 to 8.59.4 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.4/packages/typescript-eslint) Updates `@types/node` from 25.7.0 to 25.9.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.7.0 to 25.9.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.7.0 to 25.9.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.7.0 to 25.9.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.9.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: eslint dependency-version: 10.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: typescript-eslint dependency-version: 8.59.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.9.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.9.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.9.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.9.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch ... Signed-off-by: dependabot[bot] --- .github/actions/auth/package-lock.json | 16 +-- .github/actions/auth/package.json | 2 +- .github/actions/file/package-lock.json | 16 +-- .github/actions/file/package.json | 2 +- .github/actions/find/package-lock.json | 16 +-- .github/actions/find/package.json | 2 +- .github/actions/fix/package-lock.json | 16 +-- .github/actions/fix/package.json | 2 +- package-lock.json | 156 ++++++++++++------------- package.json | 6 +- 10 files changed, 117 insertions(+), 117 deletions(-) diff --git a/.github/actions/auth/package-lock.json b/.github/actions/auth/package-lock.json index 1c43de4..eec4329 100644 --- a/.github/actions/auth/package-lock.json +++ b/.github/actions/auth/package-lock.json @@ -13,7 +13,7 @@ "playwright": "^1.60.0" }, "devDependencies": { - "@types/node": "^25.7.0", + "@types/node": "^25.9.0", "typescript": "^6.0.3" } }, @@ -53,13 +53,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", - "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "version": "25.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", + "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.21.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/fsevents": { @@ -139,9 +139,9 @@ } }, "node_modules/undici-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", - "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" } diff --git a/.github/actions/auth/package.json b/.github/actions/auth/package.json index 0e18285..3ab837d 100644 --- a/.github/actions/auth/package.json +++ b/.github/actions/auth/package.json @@ -17,7 +17,7 @@ "playwright": "^1.60.0" }, "devDependencies": { - "@types/node": "^25.7.0", + "@types/node": "^25.9.0", "typescript": "^6.0.3" } } \ No newline at end of file diff --git a/.github/actions/file/package-lock.json b/.github/actions/file/package-lock.json index f65b3c2..9c10330 100644 --- a/.github/actions/file/package-lock.json +++ b/.github/actions/file/package-lock.json @@ -14,7 +14,7 @@ "@octokit/plugin-throttling": "^11.0.3" }, "devDependencies": { - "@types/node": "^25.7.0", + "@types/node": "^25.9.0", "typescript": "^6.0.3" } }, @@ -167,13 +167,13 @@ } }, "node_modules/@types/node": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", - "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "version": "25.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", + "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.21.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/before-after-hook": { @@ -237,9 +237,9 @@ } }, "node_modules/undici-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", - "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, diff --git a/.github/actions/file/package.json b/.github/actions/file/package.json index 55022f9..a5d7cbd 100644 --- a/.github/actions/file/package.json +++ b/.github/actions/file/package.json @@ -18,7 +18,7 @@ "@octokit/plugin-throttling": "^11.0.3" }, "devDependencies": { - "@types/node": "^25.7.0", + "@types/node": "^25.9.0", "typescript": "^6.0.3" } } diff --git a/.github/actions/find/package-lock.json b/.github/actions/find/package-lock.json index 60a9751..410fb74 100644 --- a/.github/actions/find/package-lock.json +++ b/.github/actions/find/package-lock.json @@ -15,7 +15,7 @@ "playwright": "^1.60.0" }, "devDependencies": { - "@types/node": "^25.7.0", + "@types/node": "^25.9.0", "typescript": "^6.0.3" } }, @@ -483,13 +483,13 @@ } }, "node_modules/@types/node": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", - "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "version": "25.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", + "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.21.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/axe-core": { @@ -619,9 +619,9 @@ } }, "node_modules/undici-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", - "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" } diff --git a/.github/actions/find/package.json b/.github/actions/find/package.json index 92f464b..7f3fd7a 100644 --- a/.github/actions/find/package.json +++ b/.github/actions/find/package.json @@ -19,7 +19,7 @@ "playwright": "^1.60.0" }, "devDependencies": { - "@types/node": "^25.7.0", + "@types/node": "^25.9.0", "typescript": "^6.0.3" } } diff --git a/.github/actions/fix/package-lock.json b/.github/actions/fix/package-lock.json index 5979030..8b4ead6 100644 --- a/.github/actions/fix/package-lock.json +++ b/.github/actions/fix/package-lock.json @@ -14,7 +14,7 @@ "@octokit/plugin-throttling": "^11.0.3" }, "devDependencies": { - "@types/node": "^25.7.0", + "@types/node": "^25.9.0", "typescript": "^6.0.3" } }, @@ -167,13 +167,13 @@ } }, "node_modules/@types/node": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", - "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "version": "25.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", + "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.21.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/before-after-hook": { @@ -237,9 +237,9 @@ } }, "node_modules/undici-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", - "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, diff --git a/.github/actions/fix/package.json b/.github/actions/fix/package.json index 769e9f2..c6dc791 100644 --- a/.github/actions/fix/package.json +++ b/.github/actions/fix/package.json @@ -18,7 +18,7 @@ "@octokit/plugin-throttling": "^11.0.3" }, "devDependencies": { - "@types/node": "^25.7.0", + "@types/node": "^25.9.0", "typescript": "^6.0.3" } } diff --git a/package-lock.json b/package-lock.json index 0c6ee53..320e290 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,11 @@ "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", - "@types/node": "^25.7.0", - "eslint": "^10.3.0", + "@types/node": "^25.9.0", + "eslint": "^10.4.0", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.3", - "typescript-eslint": "^8.59.3", + "typescript-eslint": "^8.59.4", "vitest": "^4.1.6" } }, @@ -153,9 +153,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -741,27 +741,27 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", - "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "version": "25.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", + "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.21.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", - "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.3", - "@typescript-eslint/type-utils": "8.59.3", - "@typescript-eslint/utils": "8.59.3", - "@typescript-eslint/visitor-keys": "8.59.3", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -774,7 +774,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.59.3", + "@typescript-eslint/parser": "^8.59.4", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -790,16 +790,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", - "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.59.3", - "@typescript-eslint/types": "8.59.3", - "@typescript-eslint/typescript-estree": "8.59.3", - "@typescript-eslint/visitor-keys": "8.59.3", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3" }, "engines": { @@ -815,14 +815,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", - "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.3", - "@typescript-eslint/types": "^8.59.3", + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", "debug": "^4.4.3" }, "engines": { @@ -837,14 +837,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", - "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.3", - "@typescript-eslint/visitor-keys": "8.59.3" + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -855,9 +855,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", - "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", "dev": true, "license": "MIT", "engines": { @@ -872,15 +872,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", - "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.3", - "@typescript-eslint/typescript-estree": "8.59.3", - "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -897,9 +897,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", - "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", "dev": true, "license": "MIT", "engines": { @@ -911,16 +911,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", - "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.3", - "@typescript-eslint/tsconfig-utils": "8.59.3", - "@typescript-eslint/types": "8.59.3", - "@typescript-eslint/visitor-keys": "8.59.3", + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -939,16 +939,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", - "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.3", - "@typescript-eslint/types": "8.59.3", - "@typescript-eslint/typescript-estree": "8.59.3" + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -963,13 +963,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", - "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/types": "8.59.4", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1268,16 +1268,16 @@ } }, "node_modules/eslint": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", - "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", + "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", @@ -2399,16 +2399,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", - "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz", + "integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.3", - "@typescript-eslint/parser": "8.59.3", - "@typescript-eslint/typescript-estree": "8.59.3", - "@typescript-eslint/utils": "8.59.3" + "@typescript-eslint/eslint-plugin": "8.59.4", + "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2433,9 +2433,9 @@ } }, "node_modules/undici-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", - "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 593f92e..786e52b 100644 --- a/package.json +++ b/package.json @@ -26,11 +26,11 @@ "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", - "@types/node": "^25.7.0", - "eslint": "^10.3.0", + "@types/node": "^25.9.0", + "eslint": "^10.4.0", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.3", - "typescript-eslint": "^8.59.3", + "typescript-eslint": "^8.59.4", "vitest": "^4.1.6" } } From 87c3dadc89f92e8e68f828d00404b7905c7b1f0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 01:43:15 +0000 Subject: [PATCH 03/46] chore(deps): Bump ruby/setup-ruby Bumps the github-actions group with 1 update in the / directory: [ruby/setup-ruby](https://github.com/ruby/setup-ruby). Updates `ruby/setup-ruby` from 1.308.0 to 1.310.0 - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/97ecb7b512899eb71ab1bf2310a624c6f1589ac6...afeafc3d1ab54a631816aba4c914a0081c12ff2f) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.310.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9443ba..7a83241 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v6 - name: Setup Ruby - uses: ruby/setup-ruby@97ecb7b512899eb71ab1bf2310a624c6f1589ac6 + uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f with: ruby-version: '3.4' bundler-cache: true From 93a50679a85b9baeac8493e2db9639fb7cbeb4bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 06:18:50 +0000 Subject: [PATCH 04/46] chore(deps): Bump puma Bumps the bundler-minor-and-patch group in /sites/site-with-errors with 1 update: [puma](https://github.com/puma/puma). Updates `puma` from 8.0.1 to 8.0.2 - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/main/History.md) - [Commits](https://github.com/puma/puma/compare/v8.0.1...v8.0.2) --- updated-dependencies: - dependency-name: puma dependency-version: 8.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: bundler-minor-and-patch ... Signed-off-by: dependabot[bot] --- sites/site-with-errors/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/site-with-errors/Gemfile.lock b/sites/site-with-errors/Gemfile.lock index 4d6b3b8..16698ae 100644 --- a/sites/site-with-errors/Gemfile.lock +++ b/sites/site-with-errors/Gemfile.lock @@ -99,7 +99,7 @@ GEM pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (7.0.5) - puma (8.0.1) + puma (8.0.2) nio4r (~> 2.0) rack (3.2.6) rake (13.3.0) From 80093261e500b51782db094bff3c800832ecaf6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:05:15 +0000 Subject: [PATCH 05/46] chore(deps): Bump ruby/setup-ruby Bumps the github-actions group with 1 update in the / directory: [ruby/setup-ruby](https://github.com/ruby/setup-ruby). Updates `ruby/setup-ruby` from 1.310.0 to 1.311.0 - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/afeafc3d1ab54a631816aba4c914a0081c12ff2f...a99ac844649df2596b52b4db087cfa0881b172af) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.311.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a83241..eff181b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v6 - name: Setup Ruby - uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f + uses: ruby/setup-ruby@a99ac844649df2596b52b4db087cfa0881b172af with: ruby-version: '3.4' bundler-cache: true From b96847db29b8f82a8b11c9399e7c45c0599f1be0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:05:50 +0000 Subject: [PATCH 06/46] chore(deps): Bump ruby/setup-ruby Bumps the github-actions group with 1 update in the / directory: [ruby/setup-ruby](https://github.com/ruby/setup-ruby). Updates `ruby/setup-ruby` from 1.311.0 to 1.313.0 - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/a99ac844649df2596b52b4db087cfa0881b172af...89f90524b88a01fe6e0b732220432cc6142926af) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.313.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eff181b..b350b09 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v6 - name: Setup Ruby - uses: ruby/setup-ruby@a99ac844649df2596b52b4db087cfa0881b172af + uses: ruby/setup-ruby@89f90524b88a01fe6e0b732220432cc6142926af with: ruby-version: '3.4' bundler-cache: true From cace3822de90df9d76df9cd532591be560b242a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:02:11 +0000 Subject: [PATCH 07/46] chore(deps-dev): Bump vite from 8.0.12 to 8.0.16 Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.12 to 8.0.16. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 8.0.16 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 176 +++++++++++++++++++++++----------------------- 1 file changed, 88 insertions(+), 88 deletions(-) diff --git a/package-lock.json b/package-lock.json index 320e290..c8cb629 100644 --- a/package-lock.json +++ b/package-lock.json @@ -269,14 +269,14 @@ "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.2" }, "funding": { "type": "github", @@ -410,9 +410,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.129.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", - "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -420,9 +420,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", - "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -437,9 +437,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", - "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -454,9 +454,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", - "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -471,9 +471,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", - "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -488,9 +488,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", - "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -505,9 +505,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", - "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], @@ -522,9 +522,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", - "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], @@ -539,9 +539,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", - "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], @@ -556,9 +556,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", - "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], @@ -573,9 +573,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", - "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], @@ -590,9 +590,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", - "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], @@ -607,9 +607,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", - "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -624,9 +624,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", - "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ "wasm32" ], @@ -643,9 +643,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", - "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ "arm64" ], @@ -660,9 +660,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", - "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -677,9 +677,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", - "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -2130,9 +2130,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -2150,7 +2150,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2195,14 +2195,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", - "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.129.0", - "@rolldown/pluginutils": "1.0.0" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -2211,21 +2211,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0", - "@rolldown/binding-darwin-arm64": "1.0.0", - "@rolldown/binding-darwin-x64": "1.0.0", - "@rolldown/binding-freebsd-x64": "1.0.0", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", - "@rolldown/binding-linux-arm64-gnu": "1.0.0", - "@rolldown/binding-linux-arm64-musl": "1.0.0", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0", - "@rolldown/binding-linux-s390x-gnu": "1.0.0", - "@rolldown/binding-linux-x64-gnu": "1.0.0", - "@rolldown/binding-linux-x64-musl": "1.0.0", - "@rolldown/binding-openharmony-arm64": "1.0.0", - "@rolldown/binding-wasm32-wasi": "1.0.0", - "@rolldown/binding-win32-arm64-msvc": "1.0.0", - "@rolldown/binding-win32-x64-msvc": "1.0.0" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/semver": { @@ -2313,9 +2313,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -2457,17 +2457,17 @@ } }, "node_modules/vite": { - "version": "8.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", - "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "rolldown": "1.0.0", - "tinyglobby": "^0.2.16" + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" From b526fe6f79923c7e4923da53c6d09ff4667ac6ce Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Tue, 16 Jun 2026 15:34:35 -0500 Subject: [PATCH 08/46] Add dry_run input to file action --- .github/actions/file/action.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index 836e125..7d76af7 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -24,6 +24,10 @@ inputs: description: "In the 'file' step, also open grouped issues which link to all issues with the same root cause" required: false default: "false" + dry_run: + description: "When true, log the issues that would be filed without opening, closing, or reopening any issues." + required: false + default: "false" outputs: filings_file: From 045b92bb902c38186be824e104c2aef53ae88067 Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Tue, 16 Jun 2026 15:36:56 -0500 Subject: [PATCH 09/46] Gate side effects on dry_run in composite action --- action.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/action.yml b/action.yml index b86a45e..1b852a5 100644 --- a/action.yml +++ b/action.yml @@ -54,6 +54,10 @@ inputs: scans: description: 'Stringified JSON array of scans to perform. If not provided, only Axe will be performed' required: false + dry_run: + description: 'When true, scan and log the issues that would be filed without opening, closing, reopening, or assigning any issues, and without writing to the cache.' + required: false + default: 'false' outputs: results: @@ -129,6 +133,7 @@ runs: cached_filings_file: ${{ steps.normalize_cache.outputs.cached_filings_file }} screenshot_repository: ${{ github.repository }} open_grouped_issues: ${{ inputs.open_grouped_issues }} + dry_run: ${{ inputs.dry_run }} - if: ${{ steps.file.outputs.filings_file }} name: Get issues from filings id: get_issues_from_filings @@ -137,7 +142,7 @@ runs: # Extract open issues from Filing objects and write to a file jq -c '[.[] | select(.issue.state == "open") | .issue]' "${{ steps.file.outputs.filings_file }}" > "$RUNNER_TEMP/issues.json" echo "issues_file=$RUNNER_TEMP/issues.json" >> "$GITHUB_OUTPUT" - - if: ${{ inputs.skip_copilot_assignment != 'true' }} + - if: ${{ inputs.skip_copilot_assignment != 'true' && inputs.dry_run != 'true' }} name: Fix id: fix uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/fix @@ -185,19 +190,20 @@ runs: # Set results_file output echo "results_file=$RESULTS_FILE" >> "$GITHUB_OUTPUT" - - if: ${{ inputs.include_screenshots == 'true' }} + - if: ${{ inputs.include_screenshots == 'true' && inputs.dry_run != 'true' }} name: Save screenshots uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/save with: path: .screenshots token: ${{ inputs.token }} - name: Copy results to cache path + if: ${{ inputs.dry_run != 'true' }} shell: bash run: | mkdir -p "$(dirname '${{ inputs.cache_key }}')" cp "$GITHUB_WORKSPACE/scanner-results.json" "${{ inputs.cache_key }}" - - name: Save cached results + if: ${{ inputs.dry_run != 'true' }} uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/save with: path: ${{ inputs.cache_key }} From 399155688e6a766e907b3315bf53992e645c6ac7 Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Tue, 16 Jun 2026 15:36:56 -0500 Subject: [PATCH 10/46] Make file action log intended actions in dry run --- .github/actions/file/src/index.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 86d14ec..dbb9cc1 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -29,12 +29,14 @@ export default async function () { ? JSON.parse(fs.readFileSync(cachedFilingsFile, 'utf8')) : [] const shouldOpenGroupedIssues = core.getBooleanInput('open_grouped_issues') + const dryRun = core.getBooleanInput('dry_run') core.debug(`Input: 'findings_file: ${findingsFile}'`) core.debug(`Input: 'repository: ${repoWithOwner}'`) core.debug(`Input: 'base_url: ${baseUrl ?? '(default)'}'`) core.debug(`Input: 'screenshot_repository: ${screenshotRepo}'`) core.debug(`Input: 'cached_filings_file: ${cachedFilingsFile}'`) core.debug(`Input: 'open_grouped_issues: ${shouldOpenGroupedIssues}'`) + core.debug(`Input: 'dry_run: ${dryRun}'`) const octokit = new OctokitWithThrottling({ auth: token, @@ -61,10 +63,26 @@ export default async function () { // Track new issues for grouping const newIssuesByProblemShort: Record = {} const trackingIssueUrls: Record = {} + const dryRunCounts = {open: 0, reopen: 0, close: 0} for (const filing of filings) { let response: OctokitResponse | undefined try { + if (dryRun) { + if (isResolvedFiling(filing)) { + dryRunCounts.close++ + core.info(`[dry run] Would CLOSE issue: ${filing.issue.url}`) + } else if (isNewFiling(filing)) { + dryRunCounts.open++ + core.info( + `[dry run] Would OPEN a new issue for: ${filing.findings[0].problemShort} (${filing.findings[0].url})`, + ) + } else if (isRepeatedFiling(filing)) { + dryRunCounts.reopen++ + core.info(`[dry run] Would REOPEN issue: ${filing.issue.url}`) + } + continue + } if (isResolvedFiling(filing)) { // Close the filing’s issue (if necessary) response = await closeIssue(octokit, new Issue(filing.issue)) @@ -114,7 +132,7 @@ export default async function () { // Open tracking issues for groups with >1 new issue and link back from each // new issue - if (shouldOpenGroupedIssues) { + if (shouldOpenGroupedIssues && !dryRun) { for (const [problemShort, issues] of Object.entries(newIssuesByProblemShort)) { if (issues.length > 1) { const capitalizedProblemShort = problemShort[0].toUpperCase() + problemShort.slice(1) @@ -138,6 +156,12 @@ export default async function () { } } + if (dryRun) { + core.info( + `[dry run] ${filings.length} findings: ${dryRunCounts.open} would open, ${dryRunCounts.reopen} would reopen, ${dryRunCounts.close} would close.`, + ) + } + const filingsPath = path.join(process.env.RUNNER_TEMP || '/tmp', `filings-${crypto.randomUUID()}.json`) fs.writeFileSync(filingsPath, JSON.stringify(filings)) core.setOutput('filings_file', filingsPath) From 16383c2d3b765b4a229d5beb65301570106cf22f Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Tue, 16 Jun 2026 15:36:57 -0500 Subject: [PATCH 11/46] Add dryRun tests --- .github/actions/file/tests/dryRun.test.ts | 158 ++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 .github/actions/file/tests/dryRun.test.ts diff --git a/.github/actions/file/tests/dryRun.test.ts b/.github/actions/file/tests/dryRun.test.ts new file mode 100644 index 0000000..e5ea657 --- /dev/null +++ b/.github/actions/file/tests/dryRun.test.ts @@ -0,0 +1,158 @@ +import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest' + +// --- Mock the issue-mutating helpers so we can assert they are NEVER called in dry run --- +const openIssue = vi.fn() +const reopenIssue = vi.fn() +const closeIssue = vi.fn() +vi.mock('../src/openIssue.js', () => ({openIssue: (...args: unknown[]) => openIssue(...args)})) +vi.mock('../src/reopenIssue.js', () => ({reopenIssue: (...args: unknown[]) => reopenIssue(...args)})) +vi.mock('../src/closeIssue.js', () => ({closeIssue: (...args: unknown[]) => closeIssue(...args)})) + +// --- Mock @actions/core: control inputs, capture logs/outputs --- +const inputs: Record = {} +const infoLines: string[] = [] +const outputs: Record = {} +vi.mock('@actions/core', () => ({ + getInput: (name: string) => inputs[name] ?? '', + getBooleanInput: (name: string) => (inputs[name] ?? 'false') === 'true', + setOutput: (name: string, value: string) => { + outputs[name] = value + }, + info: (msg: string) => { + infoLines.push(msg) + }, + debug: () => {}, + warning: () => {}, + setFailed: () => {}, +})) + +// --- Mock fs: feed findings/cached filings in, swallow the output write --- +const files: Record = {} +vi.mock('node:fs', () => ({ + default: { + readFileSync: (p: string) => files[p], + writeFileSync: (p: string, data: string) => { + files[p] = data + }, + }, +})) + +// --- Stub Octokit so constructing it in index.ts doesn't do anything real --- +const octokitRequest = vi.fn() +vi.mock('@octokit/core', () => ({ + Octokit: { + plugin: () => + class { + request = octokitRequest + }, + }, +})) +vi.mock('@octokit/plugin-throttling', () => ({throttling: {}})) + +import runFileAction from '../src/index.ts' + +const finding = { + scannerType: 'axe', + ruleId: 'color-contrast', + url: 'https://example.com/page', + html: 'Low contrast', + problemShort: 'elements must meet minimum color contrast ratio thresholds', + problemUrl: 'https://dequeuniversity.com/rules/axe/4.10/color-contrast', + solutionShort: 'ensure sufficient contrast', +} + +// A second finding with no matching cached filing -> NEW (open) +const newFinding = {...finding, ruleId: 'heading-order', html: '

Skipped

'} + +// A cached filing whose finding matches `finding` -> REPEATED (reopen) +const repeatedCached = { + issue: {id: 1, nodeId: 'N1', url: 'https://github.com/org/repo/issues/1', title: 'repeat'}, + findings: [finding], +} + +// A cached filing with NO matching finding this run -> RESOLVED (close) +const resolvedCached = { + issue: {id: 2, nodeId: 'N2', url: 'https://github.com/org/repo/issues/2', title: 'resolved'}, + findings: [{...finding, ruleId: 'landmark-one-main', html: '
old
'}], +} + +function setup() { + // findings file: includes `finding` (matches repeatedCached) and `newFinding` (brand new) + files['/tmp/findings.json'] = JSON.stringify([finding, newFinding]) + // cached filings: one repeated, one resolved (its finding is absent from findings file) + files['/tmp/cached.json'] = JSON.stringify([repeatedCached, resolvedCached]) + inputs.findings_file = '/tmp/findings.json' + inputs.cached_filings_file = '/tmp/cached.json' + inputs.repository = 'org/repo' + inputs.token = 'fake-token' +} + +describe('file action — dry_run', () => { + beforeEach(() => { + vi.clearAllMocks() + infoLines.length = 0 + for (const k of Object.keys(inputs)) delete inputs[k] + for (const k of Object.keys(outputs)) delete outputs[k] + }) + afterEach(() => { + vi.restoreAllMocks() + }) + + it('does not open, reopen, or close any issues when dry_run is true', async () => { + setup() + inputs.dry_run = 'true' + + await runFileAction() + + expect(openIssue).not.toHaveBeenCalled() + expect(reopenIssue).not.toHaveBeenCalled() + expect(closeIssue).not.toHaveBeenCalled() + expect(octokitRequest).not.toHaveBeenCalled() + }) + + it('logs the intended action for each filing type', async () => { + setup() + inputs.dry_run = 'true' + + await runFileAction() + + const log = infoLines.join('\n') + expect(log).toMatch(/\[dry run] Would OPEN a new issue for: .*heading-order|Skipped|elements must meet/) + expect(log).toContain('[dry run] Would REOPEN issue: https://github.com/org/repo/issues/1') + expect(log).toContain('[dry run] Would CLOSE issue: https://github.com/org/repo/issues/2') + }) + + it('logs a summary line with counts', async () => { + setup() + inputs.dry_run = 'true' + + await runFileAction() + + expect(infoLines.join('\n')).toMatch(/\[dry run] \d+ findings: 1 would open, 1 would reopen, 1 would close\./) + }) + + it('still writes the filings_file output in dry run', async () => { + setup() + inputs.dry_run = 'true' + + await runFileAction() + + expect(outputs.filings_file).toBeDefined() + }) + + it('does call the mutating helpers when dry_run is false (regression guard)', async () => { + setup() + inputs.dry_run = 'false' + // helpers return a minimal Octokit-style response so index.ts can read response.data + const resp = {data: {id: 9, node_id: 'N', number: 9, html_url: 'https://github.com/org/repo/issues/9', title: 't'}} + openIssue.mockResolvedValue(resp) + reopenIssue.mockResolvedValue(resp) + closeIssue.mockResolvedValue(resp) + + await runFileAction() + + expect(openIssue).toHaveBeenCalled() + expect(reopenIssue).toHaveBeenCalled() + expect(closeIssue).toHaveBeenCalled() + }) +}) From f58dfc0215edede0e0217911d74b5d2717495849 Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Tue, 16 Jun 2026 15:36:57 -0500 Subject: [PATCH 12/46] Document dry_run option --- FAQ.md | 11 +++++++++++ README.md | 2 ++ 2 files changed, 13 insertions(+) diff --git a/FAQ.md b/FAQ.md index 2fc4016..2b26ab9 100644 --- a/FAQ.md +++ b/FAQ.md @@ -60,6 +60,17 @@ Just keep in mind that resetting the cache means the Action will "forget" what it's already seen, so it may reopen issues that were previously tracked or closed. +### How can I preview what the scanner would do without filing issues? + +Set the `dry_run` input to `true`. The scanner will run a normal scan and log the +issues it _would_ open, reopen, or close — but it won't create, close, reopen, or +assign any issues, and it won't write to the `gh-cache` branch. + +This is handy for trying out a new configuration or seeing how many issues a scan +would file, without making any changes to your repository. Because dry runs don't +update the cache, your next real run behaves exactly as if the dry run never +happened. + ### Does this work with private repositories? Yes! The Action works with both public and private repositories. Since it runs diff --git a/README.md b/README.md index 88b68c1..a20261e 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ jobs: # skip_copilot_assignment: false # Optional: Set to true to skip assigning issues to GitHub Copilot (or if you don't have GitHub Copilot) # include_screenshots: false # Optional: Set to true to capture screenshots and include links to them in filed issues # open_grouped_issues: false # Optional: Set to true to open an issue grouping individual issues per violation + # dry_run: false # Optional: Set to true to scan and log what would be filed without creating/closing issues or writing the cache # reduced_motion: no-preference # Optional: Playwright reduced motion configuration option # color_scheme: light # Optional: Playwright color scheme configuration option # scans: '["axe","reflow-scan"]' # Optional: An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. @@ -131,6 +132,7 @@ Trigger the workflow manually or automatically based on your configuration. The | `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | | `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | | `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | +| `dry_run` | No | When `true`, scan and log the issues that _would_ be filed without opening, closing, reopening, or assigning any issues — and without writing to the `gh-cache` branch. Useful for safely previewing results. Default: `false` | `true` | | `url_configs` | No | A stringified JSON array of URL config objects. Each object must have a `url` field and may have an optional `excludeSelectors` field (array of CSS selectors to exclude from the Axe scan for that URL). When provided, takes precedence over the `urls` input. | `'[{"url":"https://example.com","excludeSelectors":["iframe","#widget"]}]'` | --- From 2099da385c78b4dcb79ffcaeb1f01ee43559b4d1 Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Wed, 17 Jun 2026 15:39:09 -0500 Subject: [PATCH 13/46] Use console.table for dry-run summary, tighten OPEN assertion --- .github/actions/file/src/index.ts | 10 +++++++--- .github/actions/file/tests/dryRun.test.ts | 11 ++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index dbb9cc1..506fdff 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -157,9 +157,13 @@ export default async function () { } if (dryRun) { - core.info( - `[dry run] ${filings.length} findings: ${dryRunCounts.open} would open, ${dryRunCounts.reopen} would reopen, ${dryRunCounts.close} would close.`, - ) + core.info('[dry run] Summary of actions that would be taken:') + console.table({ + open: dryRunCounts.open, + reopen: dryRunCounts.reopen, + close: dryRunCounts.close, + total: dryRunCounts.open + dryRunCounts.reopen + dryRunCounts.close, + }) } const filingsPath = path.join(process.env.RUNNER_TEMP || '/tmp', `filings-${crypto.randomUUID()}.json`) diff --git a/.github/actions/file/tests/dryRun.test.ts b/.github/actions/file/tests/dryRun.test.ts index e5ea657..53f231f 100644 --- a/.github/actions/file/tests/dryRun.test.ts +++ b/.github/actions/file/tests/dryRun.test.ts @@ -93,6 +93,7 @@ describe('file action — dry_run', () => { infoLines.length = 0 for (const k of Object.keys(inputs)) delete inputs[k] for (const k of Object.keys(outputs)) delete outputs[k] + vi.spyOn(console, 'table').mockImplementation(() => {}) }) afterEach(() => { vi.restoreAllMocks() @@ -117,18 +118,22 @@ describe('file action — dry_run', () => { await runFileAction() const log = infoLines.join('\n') - expect(log).toMatch(/\[dry run] Would OPEN a new issue for: .*heading-order|Skipped|elements must meet/) + expect(log).toContain( + '[dry run] Would OPEN a new issue for: elements must meet minimum color contrast ratio thresholds (https://example.com/page)', + ) expect(log).toContain('[dry run] Would REOPEN issue: https://github.com/org/repo/issues/1') expect(log).toContain('[dry run] Would CLOSE issue: https://github.com/org/repo/issues/2') }) - it('logs a summary line with counts', async () => { + it('logs a summary table with counts', async () => { setup() inputs.dry_run = 'true' await runFileAction() - expect(infoLines.join('\n')).toMatch(/\[dry run] \d+ findings: 1 would open, 1 would reopen, 1 would close\./) + expect(vi.mocked(console.table)).toHaveBeenCalledWith( + expect.objectContaining({open: 1, reopen: 1, close: 1, total: 3}), + ) }) it('still writes the filings_file output in dry run', async () => { From ec1334f65cb903713cce9d60ee544a84d1c4a599 Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Wed, 17 Jun 2026 16:06:34 -0500 Subject: [PATCH 14/46] Update reflow-scan text to improve clarity and reference WCAG 2.2 --- .github/scanner-plugins/reflow-scan/index.ts | 15 ++++++++------- tests/site-with-errors.test.ts | 7 ++++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/scanner-plugins/reflow-scan/index.ts b/.github/scanner-plugins/reflow-scan/index.ts index fa4fa5d..4d158a9 100644 --- a/.github/scanner-plugins/reflow-scan/index.ts +++ b/.github/scanner-plugins/reflow-scan/index.ts @@ -1,9 +1,9 @@ -export default async function reflowScan({ page, addFinding } = {}) { +export default async function reflowScan({page, addFinding} = {}) { const originalViewport = page.viewportSize() const url = page.url() - // Check for horizontal scrolling at 320x256 viewport + // Check for horizontal scrolling at 320 viewport try { - await page.setViewportSize({ width: 320, height: 256 }) + await page.setViewportSize({width: 320, height: 256}) const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth) const clientWidth = await page.evaluate(() => document.documentElement.clientWidth) @@ -12,10 +12,11 @@ export default async function reflowScan({ page, addFinding } = {}) { await addFinding({ scannerType: 'reflow-scan', url, - problemShort: 'page requires horizontal scrolling at 320x256 viewport', - problemUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/reflow.html', - solutionShort: 'ensure content is responsive and does not require horizontal scrolling at small viewport sizes', - solutionLong: `The page has a scroll width of ${scrollWidth}px but a client width of only ${clientWidth}px at a 320x256 viewport, requiring horizontal scrolling. This violates WCAG 2.1 Level AA Success Criterion 1.4.10 (Reflow).`, + problemShort: 'needs review: page presents a horizontal scrollbar at a 320px wide viewport', + problemUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/reflow.html', + solutionShort: + 'verify if sections of content can be viewed within the 320px wide viewport without needing to scroll in two-dimensions to read the content of an individual section', + solutionLong: `The page has a scroll width of ${scrollWidth}px but a client width of only ${clientWidth}px at a 320px wide viewport, resulting in a horizontal scrollbar. Ensure that multi-line text does not require scrolling in two-dimensions to read, as this would be a violation of the WCAG 1.4.10 Reflow Success Criterion.`, }) } } catch (e) { diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index 06ba78d..ab72fea 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -112,8 +112,9 @@ describe('site-with-errors', () => { { scannerType: 'reflow-scan', url: 'http://127.0.0.1:4000/404.html', - problemShort: 'page requires horizontal scrolling at 320x256 viewport', - solutionShort: 'ensure content is responsive and does not require horizontal scrolling at small viewport sizes', + problemShort: 'needs review: page presents a horizontal scrollbar at a 320px wide viewport', + solutionShort: + 'verify if sections of content can be viewed within the 320px wide viewport without needing to scroll in two-dimensions to read the content of an individual section', }, ] // Check that: @@ -161,7 +162,7 @@ describe('site-with-errors', () => { 'Accessibility issue: Headings should not be empty on /404.html', 'Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /about/', 'Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /jekyll/update/2025/07/30/welcome-to-jekyll.html', - 'Accessibility issue: Page requires horizontal scrolling at 320x256 viewport on /404.html', + 'Accessibility issue: Needs review: page presents a horizontal scrollbar at a 320px wide viewport on /404.html', ] expect(actualTitles).toHaveLength(expectedTitles.length) expect(actualTitles).toEqual(expect.arrayContaining(expectedTitles)) From 0459d86e6fcb575042bbdfb48e431bebedfbcf10 Mon Sep 17 00:00:00 2001 From: Taarik <147209483+taarikashenafi@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:41:20 -0500 Subject: [PATCH 15/46] Update FAQ.md Co-authored-by: Joyce Zhu --- FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FAQ.md b/FAQ.md index 2b26ab9..08a425e 100644 --- a/FAQ.md +++ b/FAQ.md @@ -63,7 +63,7 @@ closed. ### How can I preview what the scanner would do without filing issues? Set the `dry_run` input to `true`. The scanner will run a normal scan and log the -issues it _would_ open, reopen, or close — but it won't create, close, reopen, or +issues it _would_ open, reopen, or close — but it won't actually mutate any data or write to the `gh-cache` branch assign any issues, and it won't write to the `gh-cache` branch. This is handy for trying out a new configuration or seeing how many issues a scan From c5538c28186615834e6a9030501db01c4914eb40 Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Thu, 18 Jun 2026 16:21:07 -0500 Subject: [PATCH 16/46] Refactor dry-run to else block; update in-memory issue state for accurate preview --- .github/actions/file/src/index.ts | 81 ++++++++++++----------- .github/actions/file/tests/dryRun.test.ts | 32 +++++++++ 2 files changed, 74 insertions(+), 39 deletions(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 506fdff..fd16b68 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -71,58 +71,61 @@ export default async function () { if (dryRun) { if (isResolvedFiling(filing)) { dryRunCounts.close++ + filing.issue.state = 'closed' core.info(`[dry run] Would CLOSE issue: ${filing.issue.url}`) } else if (isNewFiling(filing)) { dryRunCounts.open++ + ;(filing as Filing).issue = {state: 'open'} as Issue core.info( `[dry run] Would OPEN a new issue for: ${filing.findings[0].problemShort} (${filing.findings[0].url})`, ) } else if (isRepeatedFiling(filing)) { dryRunCounts.reopen++ + filing.issue.state = 'reopened' core.info(`[dry run] Would REOPEN issue: ${filing.issue.url}`) } - continue - } - if (isResolvedFiling(filing)) { - // Close the filing’s issue (if necessary) - response = await closeIssue(octokit, new Issue(filing.issue)) - filing.issue.state = 'closed' - } else if (isNewFiling(filing)) { - // Open a new issue for the filing - response = await openIssue(octokit, repoWithOwner, filing.findings[0], screenshotRepo) - ;(filing as Filing).issue = {state: 'open'} as Issue + } else { + if (isResolvedFiling(filing)) { + // Close the filing's issue (if necessary) + response = await closeIssue(octokit, new Issue(filing.issue)) + filing.issue.state = 'closed' + } else if (isNewFiling(filing)) { + // Open a new issue for the filing + response = await openIssue(octokit, repoWithOwner, filing.findings[0], screenshotRepo) + ;(filing as Filing).issue = {state: 'open'} as Issue - // Track for grouping - if (shouldOpenGroupedIssues) { - const problemShort: string = filing.findings[0].problemShort - if (!newIssuesByProblemShort[problemShort]) { - newIssuesByProblemShort[problemShort] = [] + // Track for grouping + if (shouldOpenGroupedIssues) { + const problemShort: string = filing.findings[0].problemShort + if (!newIssuesByProblemShort[problemShort]) { + newIssuesByProblemShort[problemShort] = [] + } + newIssuesByProblemShort[problemShort].push({ + url: response.data.html_url, + id: response.data.number, + }) } - newIssuesByProblemShort[problemShort].push({ - url: response.data.html_url, - id: response.data.number, - }) + } else if (isRepeatedFiling(filing)) { + // Reopen the filing's issue (if necessary) and update the body with the latest finding + response = await reopenIssue( + octokit, + new Issue(filing.issue), + filing.findings[0], + repoWithOwner, + screenshotRepo, + ) + filing.issue.state = 'reopened' + } + if (response?.data && filing.issue) { + // Update the filing with the latest issue data + filing.issue.id = response.data.id + filing.issue.nodeId = response.data.node_id + filing.issue.url = response.data.html_url + filing.issue.title = response.data.title + core.info( + `Set issue ${response.data.title} (${repoWithOwner}#${response.data.number}) state to ${filing.issue.state}`, + ) } - } else if (isRepeatedFiling(filing)) { - // Reopen the filing's issue (if necessary) and update the body with the latest finding - response = await reopenIssue( - octokit, - new Issue(filing.issue), - filing.findings[0], - repoWithOwner, - screenshotRepo, - ) - filing.issue.state = 'reopened' - } - if (response?.data && filing.issue) { - // Update the filing with the latest issue data - filing.issue.id = response.data.id - filing.issue.nodeId = response.data.node_id - filing.issue.url = response.data.html_url - filing.issue.title = response.data.title - core.info( - `Set issue ${response.data.title} (${repoWithOwner}#${response.data.number}) state to ${filing.issue.state}`, - ) } } catch (error) { core.setFailed(`Failed on filing: ${JSON.stringify(filing, null, 2)}\n${error}`) diff --git a/.github/actions/file/tests/dryRun.test.ts b/.github/actions/file/tests/dryRun.test.ts index 53f231f..ce8470f 100644 --- a/.github/actions/file/tests/dryRun.test.ts +++ b/.github/actions/file/tests/dryRun.test.ts @@ -145,6 +145,38 @@ describe('file action — dry_run', () => { expect(outputs.filings_file).toBeDefined() }) + it('updates in-memory issue state for an accurate preview without mutating remotely', async () => { + setup() + inputs.dry_run = 'true' + + await runFileAction() + + // The path written is `${RUNNER_TEMP||'/tmp'}/filings-.json`; grab it from the output. + const writtenPath = outputs.filings_file + const writtenFilings = JSON.parse(files[writtenPath]) + + // Resolved cached filing (issues/2) -> would be CLOSED + const resolved = writtenFilings.find( + (f: {issue?: {url?: string}}) => f.issue?.url === 'https://github.com/org/repo/issues/2', + ) + expect(resolved?.issue.state).toBe('closed') + + // Repeated cached filing (issues/1) -> would be REOPENED + const repeated = writtenFilings.find( + (f: {issue?: {url?: string}}) => f.issue?.url === 'https://github.com/org/repo/issues/1', + ) + expect(repeated?.issue.state).toBe('reopened') + + // New filing -> issue object created with state 'open' + const opened = writtenFilings.find((f: {issue?: {state?: string}}) => f.issue?.state === 'open') + expect(opened).toBeDefined() + + // And confirm we still didn't actually mutate anything remotely + expect(openIssue).not.toHaveBeenCalled() + expect(reopenIssue).not.toHaveBeenCalled() + expect(closeIssue).not.toHaveBeenCalled() + }) + it('does call the mutating helpers when dry_run is false (regression guard)', async () => { setup() inputs.dry_run = 'false' From 571b42cfae801edc5b9cf6bab75c8090a6e36f23 Mon Sep 17 00:00:00 2001 From: Taarik <147209483+taarikashenafi@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:37:01 -0500 Subject: [PATCH 17/46] Apply suggestions from code review Co-authored-by: Joyce Zhu --- .github/scanner-plugins/reflow-scan/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scanner-plugins/reflow-scan/index.ts b/.github/scanner-plugins/reflow-scan/index.ts index 4d158a9..7f6a5e3 100644 --- a/.github/scanner-plugins/reflow-scan/index.ts +++ b/.github/scanner-plugins/reflow-scan/index.ts @@ -15,8 +15,8 @@ export default async function reflowScan({page, addFinding} = {}) { problemShort: 'needs review: page presents a horizontal scrollbar at a 320px wide viewport', problemUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/reflow.html', solutionShort: - 'verify if sections of content can be viewed within the 320px wide viewport without needing to scroll in two-dimensions to read the content of an individual section', - solutionLong: `The page has a scroll width of ${scrollWidth}px but a client width of only ${clientWidth}px at a 320px wide viewport, resulting in a horizontal scrollbar. Ensure that multi-line text does not require scrolling in two-dimensions to read, as this would be a violation of the WCAG 1.4.10 Reflow Success Criterion.`, + 'verify if sections of content can be viewed within the 320px wide viewport without needing to scroll in two dimensions to read the content of an individual section', + solutionLong: `The page has a scroll width of ${scrollWidth}px but a client width of only ${clientWidth}px at a 320px wide viewport, resulting in a horizontal scrollbar. Ensure that multi-line text does not require scrolling in two-dimensions to read, as this would be a violation of WCAG Success Criterion 1.4.10 (Reflow).`, }) } } catch (e) { From 0aa982f036219533b1595c22047ec34d5f32ecba Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Thu, 18 Jun 2026 16:54:25 -0500 Subject: [PATCH 18/46] Fix solutionShort hyphen in test to match plugin --- tests/site-with-errors.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index ab72fea..8c7981b 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -114,7 +114,7 @@ describe('site-with-errors', () => { url: 'http://127.0.0.1:4000/404.html', problemShort: 'needs review: page presents a horizontal scrollbar at a 320px wide viewport', solutionShort: - 'verify if sections of content can be viewed within the 320px wide viewport without needing to scroll in two-dimensions to read the content of an individual section', + 'verify if sections of content can be viewed within the 320px wide viewport without needing to scroll in two dimensions to read the content of an individual section', }, ] // Check that: From cc8e4e25f38440ad2ee5e60bf9e975f6103f3ffd Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 14:08:54 -0700 Subject: [PATCH 19/46] Classify Axe findings by conformance tier --- .github/actions/find/src/findForUrl.ts | 11 +++++- .github/actions/find/src/types.d.ts | 3 ++ .github/actions/find/tests/findForUrl.test.ts | 36 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index d9f1ea8..ea4f1e1 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -1,4 +1,4 @@ -import type {ColorSchemePreference, Finding, ReducedMotionPreference, UrlConfig} from './types.d.js' +import type {ColorSchemePreference, Finding, FindingCategory, ReducedMotionPreference, UrlConfig} from './types.d.js' import {AxeBuilder} from '@axe-core/playwright' import playwright from 'playwright' import {AuthContext} from './AuthContext.js' @@ -87,6 +87,7 @@ async function runAxeScan({ for (const violation of rawFindings.violations) { await addFinding({ scannerType: 'axe', + category: categorizeAxeViolation(violation.tags), url, html: violation.nodes[0].html.replace(/'/g, '''), problemShort: violation.help.toLowerCase().replace(/'/g, '''), @@ -98,3 +99,11 @@ async function runAxeScan({ } } } + +// Maps an Axe violation's tags to a conformance tier. Experimental is checked +// first because some experimental rules also carry a wcag* tag. +function categorizeAxeViolation(tags: string[]): FindingCategory { + if (tags.includes('experimental')) return 'experimental' + if (tags.includes('best-practice')) return 'best-practice' + return 'wcag' +} diff --git a/.github/actions/find/src/types.d.ts b/.github/actions/find/src/types.d.ts index dcbc860..a2f4a53 100644 --- a/.github/actions/find/src/types.d.ts +++ b/.github/actions/find/src/types.d.ts @@ -1,5 +1,8 @@ +export type FindingCategory = 'wcag' | 'best-practice' | 'experimental' + export type Finding = { scannerType: string + category?: FindingCategory url: string html?: string problemShort: string diff --git a/.github/actions/find/tests/findForUrl.test.ts b/.github/actions/find/tests/findForUrl.test.ts index 85299c5..5424488 100644 --- a/.github/actions/find/tests/findForUrl.test.ts +++ b/.github/actions/find/tests/findForUrl.test.ts @@ -117,4 +117,40 @@ describe('findForUrl', () => { expect(loadedPlugins[1].default).toHaveBeenCalledTimes(0) }) }) + + describe('axe finding categorization', () => { + function axeViolation(tags: string[]) { + return { + id: 'some-rule', + help: 'Help', + helpUrl: 'https://example.com', + description: 'Description', + tags, + nodes: [{html: '
', failureSummary: 'summary'}], + } + } + + async function categoryFor(tags: string[]) { + clearAll() + actionInput = JSON.stringify(['axe']) + vi.mocked(AxeBuilder.prototype.analyze).mockResolvedValueOnce({ + violations: [axeViolation(tags)], + } as unknown as axe.AxeResults) + + const findings = await findForUrl('test.com') + return findings[0].category + } + + it('categorizes a violation with only wcag tags as wcag', async () => { + expect(await categoryFor(['wcag2a', 'wcag111'])).toBe('wcag') + }) + + it('categorizes a violation with a best-practice tag as best-practice', async () => { + expect(await categoryFor(['cat.semantics', 'best-practice'])).toBe('best-practice') + }) + + it('categorizes a violation with an experimental tag as experimental, even alongside wcag tags', async () => { + expect(await categoryFor(['wcag2a', 'experimental'])).toBe('experimental') + }) + }) }) From a753177c4ca4a950a75a6da7bf7cb9b209eb7a53 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 14:09:15 -0700 Subject: [PATCH 20/46] Surface finding category in issue body and label --- .github/actions/file/src/generateIssueBody.ts | 9 +++++++- .github/actions/file/src/openIssue.ts | 4 ++++ .github/actions/file/src/types.d.ts | 3 +++ .../file/tests/generateIssueBody.test.ts | 23 +++++++++++++++++++ .github/actions/file/tests/openIssue.test.ts | 22 ++++++++++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 18e25d3..8ca8a67 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -18,13 +18,20 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str ` } + const categoryNotice = + finding.category && finding.category !== 'wcag' + ? `**Note:** This is ${ + finding.category === 'experimental' ? 'an experimental check' : 'a best-practice recommendation' + }, not a hard WCAG failure.\n\n` + : '' + const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific violation reported in this issue is no longer reproducible. - [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization. - [ ] A test SHOULD be added to ensure this specific violation does not regress. - [ ] This PR MUST NOT introduce any new accessibility issues or regressions.` - const body = `## What + const body = `${categoryNotice}## What An accessibility scan ${finding.html ? `flagged the element \`${finding.html}\`` : `found an issue on ${finding.url}`} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. ${screenshotSection ?? ''} diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 937f06c..03161c2 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -26,6 +26,10 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding if (finding.ruleId) { labels.push(`${finding.scannerType} rule: ${finding.ruleId}`) } + // Flag non-WCAG findings so they can be filtered or triaged separately + if (finding.category && finding.category !== 'wcag') { + labels.push(finding.category) + } const title = truncateWithEllipsis( `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index ee91bc6..ba36ecc 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -1,5 +1,8 @@ +export type FindingCategory = 'wcag' | 'best-practice' | 'experimental' + export type Finding = { scannerType: string + category?: FindingCategory ruleId?: string url: string html?: string diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 167ee5f..048441b 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -76,4 +76,27 @@ describe('generateIssueBody', () => { expect(body).toContain(`found an issue on ${findingWithEmptyOptionalFields.url}`) expect(body).not.toContain('flagged the element') }) + + it('omits the category notice for WCAG findings', () => { + expect(generateIssueBody(baseFinding, 'github/accessibility-scanner')).not.toContain('**Note:**') + expect(generateIssueBody({...baseFinding, category: 'wcag'}, 'github/accessibility-scanner')).not.toContain( + '**Note:**', + ) + }) + + it('includes a best-practice notice for best-practice findings', () => { + const body = generateIssueBody({...baseFinding, category: 'best-practice'}, 'github/accessibility-scanner') + + expect(body).toContain('**Note:**') + expect(body).toContain('best-practice recommendation') + expect(body).toContain('not a hard WCAG failure') + }) + + it('includes an experimental notice for experimental findings', () => { + const body = generateIssueBody({...baseFinding, category: 'experimental'}, 'github/accessibility-scanner') + + expect(body).toContain('**Note:**') + expect(body).toContain('an experimental check') + expect(body).toContain('not a hard WCAG failure') + }) }) diff --git a/.github/actions/file/tests/openIssue.test.ts b/.github/actions/file/tests/openIssue.test.ts index 77a184c..e9cb46f 100644 --- a/.github/actions/file/tests/openIssue.test.ts +++ b/.github/actions/file/tests/openIssue.test.ts @@ -65,6 +65,28 @@ describe('openIssue', () => { ) }) + it('adds a category label for non-WCAG findings', async () => { + const octokit = mockOctokit() + await openIssue(octokit, 'org/repo', {...baseFinding, category: 'best-practice'}) + + expect(octokit.request).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + labels: ['axe-scanning-issue', 'axe rule: color-contrast', 'best-practice'], + }), + ) + }) + + it('does not add a category label for WCAG findings', async () => { + const octokit = mockOctokit() + await openIssue(octokit, 'org/repo', {...baseFinding, category: 'wcag'}) + + const labels = octokit.request.mock.calls[0][1].labels + expect(labels).not.toContain('wcag') + expect(labels).not.toContain('best-practice') + expect(labels).not.toContain('experimental') + }) + it('truncates long titles with ellipsis', async () => { const octokit = mockOctokit() const longFinding = { From 7973e5f22f9df1e6525bbbcf7bc13479f4db0702 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 14:09:29 -0700 Subject: [PATCH 21/46] Add switches to skip filing best-practice and experimental issues --- .github/actions/file/action.yml | 8 ++++++++ .github/actions/file/src/index.ts | 33 ++++++++++++++++++++++++++++++- action.yml | 10 ++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index 7d76af7..6d22925 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -28,6 +28,14 @@ inputs: description: "When true, log the issues that would be filed without opening, closing, or reopening any issues." required: false default: "false" + file_best_practice_issues: + description: "File issues for best-practice findings (accessibility recommendations that are not hard WCAG failures). Disabling only suppresses new issues; existing ones are left untouched." + required: false + default: "true" + file_experimental_issues: + description: "File issues for experimental findings (checks that are not yet stable). Disabling only suppresses new issues; existing ones are left untouched." + required: false + default: "true" outputs: filings_file: diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index fd16b68..6270063 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -16,6 +16,13 @@ import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' import {OctokitResponse} from '@octokit/types' const OctokitWithThrottling = Octokit.plugin(throttling) +// core.getBooleanInput throws when an input is missing, so default unset switches. +function getBooleanInputWithDefault(name: string, defaultValue: boolean): boolean { + const raw = core.getInput(name) + if (!raw) return defaultValue + return raw.toLowerCase() === 'true' +} + export default async function () { core.info("Started 'file' action") const findingsFile = core.getInput('findings_file', {required: true}) @@ -30,6 +37,8 @@ export default async function () { : [] const shouldOpenGroupedIssues = core.getBooleanInput('open_grouped_issues') const dryRun = core.getBooleanInput('dry_run') + const fileBestPracticeIssues = getBooleanInputWithDefault('file_best_practice_issues', true) + const fileExperimentalIssues = getBooleanInputWithDefault('file_experimental_issues', true) core.debug(`Input: 'findings_file: ${findingsFile}'`) core.debug(`Input: 'repository: ${repoWithOwner}'`) core.debug(`Input: 'base_url: ${baseUrl ?? '(default)'}'`) @@ -37,6 +46,8 @@ export default async function () { core.debug(`Input: 'cached_filings_file: ${cachedFilingsFile}'`) core.debug(`Input: 'open_grouped_issues: ${shouldOpenGroupedIssues}'`) core.debug(`Input: 'dry_run: ${dryRun}'`) + core.debug(`Input: 'file_best_practice_issues: ${fileBestPracticeIssues}'`) + core.debug(`Input: 'file_experimental_issues: ${fileExperimentalIssues}'`) const octokit = new OctokitWithThrottling({ auth: token, @@ -60,6 +71,10 @@ export default async function () { }) const filings = updateFilingsWithNewFindings(cachedFilings, findings) + // Suppressed new filings are kept out of the cache so they aren't seen as + // resolved (and auto-closed) on the next run. + const suppressedFilings = new Set() + // Track new issues for grouping const newIssuesByProblemShort: Record = {} const trackingIssueUrls: Record = {} @@ -68,6 +83,21 @@ export default async function () { for (const filing of filings) { let response: OctokitResponse | undefined try { + // Category switches gate only NEW issues; existing ones are reconciled normally. + if (isNewFiling(filing)) { + const category = filing.findings[0].category ?? 'wcag' + if ( + (category === 'best-practice' && !fileBestPracticeIssues) || + (category === 'experimental' && !fileExperimentalIssues) + ) { + core.info( + `Skipping new ${category} issue (filing disabled for this category): ${filing.findings[0].problemShort}`, + ) + suppressedFilings.add(filing) + continue + } + } + if (dryRun) { if (isResolvedFiling(filing)) { dryRunCounts.close++ @@ -170,7 +200,8 @@ export default async function () { } const filingsPath = path.join(process.env.RUNNER_TEMP || '/tmp', `filings-${crypto.randomUUID()}.json`) - fs.writeFileSync(filingsPath, JSON.stringify(filings)) + const outputFilings = suppressedFilings.size > 0 ? filings.filter(f => !suppressedFilings.has(f)) : filings + fs.writeFileSync(filingsPath, JSON.stringify(outputFilings)) core.setOutput('filings_file', filingsPath) core.debug(`Output: 'filings_file: ${filingsPath}'`) diff --git a/action.yml b/action.yml index 1b852a5..9c3052b 100644 --- a/action.yml +++ b/action.yml @@ -45,6 +45,14 @@ inputs: description: "In the 'file' step, also open grouped issues which link to all issues with the same problem" required: false default: 'false' + file_best_practice_issues: + description: 'File issues for best-practice findings (accessibility recommendations that are not hard WCAG failures). Disabling suppresses new issues while existing ones are left untouched.' + required: false + default: 'true' + file_experimental_issues: + description: 'File issues for experimental findings (checks that are not yet stable). Disabling suppresses new issues while existing ones are left untouched.' + required: false + default: 'true' reduced_motion: description: 'Playwright reducedMotion setting: https://playwright.dev/docs/api/class-browser#browser-new-page-option-reduced-motion' required: false @@ -134,6 +142,8 @@ runs: screenshot_repository: ${{ github.repository }} open_grouped_issues: ${{ inputs.open_grouped_issues }} dry_run: ${{ inputs.dry_run }} + file_best_practice_issues: ${{ inputs.file_best_practice_issues }} + file_experimental_issues: ${{ inputs.file_experimental_issues }} - if: ${{ steps.file.outputs.filings_file }} name: Get issues from filings id: get_issues_from_filings From eedc7725b252a31f3ed26ed70ce2e9ba24be27df Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 14:53:16 -0700 Subject: [PATCH 22/46] Set finding category in integration test expectations --- tests/site-with-errors.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index 8c7981b..cf21688 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -58,6 +58,7 @@ describe('site-with-errors', () => { const expected = [ { scannerType: 'axe', + category: 'wcag', url: 'http://127.0.0.1:4000/', html: '', problemShort: 'elements must meet minimum color contrast ratio thresholds', @@ -67,6 +68,7 @@ describe('site-with-errors', () => { }, { scannerType: 'axe', + category: 'best-practice', url: 'http://127.0.0.1:4000/', html: '', problemShort: 'page should contain a level-one heading', @@ -75,6 +77,7 @@ describe('site-with-errors', () => { }, { scannerType: 'axe', + category: 'wcag', url: 'http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html', html: ``, @@ -85,6 +88,7 @@ describe('site-with-errors', () => { }, { scannerType: 'axe', + category: 'wcag', url: 'http://127.0.0.1:4000/about/', html: 'jekyllrb.com', problemShort: 'elements must meet minimum color contrast ratio thresholds', @@ -94,6 +98,7 @@ describe('site-with-errors', () => { }, { scannerType: 'axe', + category: 'wcag', url: 'http://127.0.0.1:4000/404.html', html: '
  • Accessibility Scanner Demo
  • ', problemShort: 'elements must meet minimum color contrast ratio thresholds', @@ -103,6 +108,7 @@ describe('site-with-errors', () => { }, { scannerType: 'axe', + category: 'best-practice', url: 'http://127.0.0.1:4000/404.html', html: '

    ', problemShort: 'headings should not be empty', From a95cc310dcbe673adf403b37d9879de5ece15ae0 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 14:53:16 -0700 Subject: [PATCH 23/46] Address Copilot feedback on input validation and acceptance criteria --- .github/actions/file/src/generateIssueBody.ts | 7 ++++++- .github/actions/file/src/index.ts | 8 ++++++-- .github/actions/file/tests/generateIssueBody.test.ts | 5 +++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 8ca8a67..eb75ab6 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -25,9 +25,14 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str }, not a hard WCAG failure.\n\n` : '' + const standardsLine = + finding.category && finding.category !== 'wcag' + ? '- [ ] The fix MUST meet the accessibility standards specified by the repository or organization (WCAG 2.1 if applicable).' + : '- [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization.' + const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific violation reported in this issue is no longer reproducible. -- [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization. +${standardsLine} - [ ] A test SHOULD be added to ensure this specific violation does not regress. - [ ] This PR MUST NOT introduce any new accessibility issues or regressions.` diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 6270063..1f4d7d9 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -16,11 +16,15 @@ import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' import {OctokitResponse} from '@octokit/types' const OctokitWithThrottling = Octokit.plugin(throttling) -// core.getBooleanInput throws when an input is missing, so default unset switches. +// core.getBooleanInput throws when an input is unset, so this defaults unset +// switches while still rejecting values that aren't a valid boolean. function getBooleanInputWithDefault(name: string, defaultValue: boolean): boolean { const raw = core.getInput(name) if (!raw) return defaultValue - return raw.toLowerCase() === 'true' + const normalized = raw.trim().toLowerCase() + if (normalized === 'true') return true + if (normalized === 'false') return false + throw new TypeError(`Invalid boolean input '${name}': '${raw}'. Expected 'true' or 'false'.`) } export default async function () { diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 048441b..7a6ec42 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -26,6 +26,7 @@ describe('generateIssueBody', () => { expect(body).toContain('## What') expect(body).toContain('## Acceptance Criteria') expect(body).toContain('The specific violation reported in this issue is no longer reproducible.') + expect(body).toContain('The fix MUST meet WCAG 2.1 guidelines OR') expect(body).not.toContain('Specifically:') }) @@ -90,6 +91,8 @@ describe('generateIssueBody', () => { expect(body).toContain('**Note:**') expect(body).toContain('best-practice recommendation') expect(body).toContain('not a hard WCAG failure') + expect(body).toContain('WCAG 2.1 if applicable') + expect(body).not.toContain('The fix MUST meet WCAG 2.1 guidelines OR') }) it('includes an experimental notice for experimental findings', () => { @@ -98,5 +101,7 @@ describe('generateIssueBody', () => { expect(body).toContain('**Note:**') expect(body).toContain('an experimental check') expect(body).toContain('not a hard WCAG failure') + expect(body).toContain('WCAG 2.1 if applicable') + expect(body).not.toContain('The fix MUST meet WCAG 2.1 guidelines OR') }) }) From 3b62b40f9a86c4e299e9b7a995a39a8af76d19b8 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 15:04:51 -0700 Subject: [PATCH 24/46] Trimming verbose comments --- .github/actions/file/src/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 1f4d7d9..1d25ced 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -16,7 +16,7 @@ import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' import {OctokitResponse} from '@octokit/types' const OctokitWithThrottling = Octokit.plugin(throttling) -// core.getBooleanInput throws when an input is unset, so this defaults unset +// Throws when an input is unset, so this defaults unset // switches while still rejecting values that aren't a valid boolean. function getBooleanInputWithDefault(name: string, defaultValue: boolean): boolean { const raw = core.getInput(name) @@ -75,8 +75,7 @@ export default async function () { }) const filings = updateFilingsWithNewFindings(cachedFilings, findings) - // Suppressed new filings are kept out of the cache so they aren't seen as - // resolved (and auto-closed) on the next run. + // Suppressed new filings are kept out of the cache const suppressedFilings = new Set() // Track new issues for grouping @@ -87,7 +86,7 @@ export default async function () { for (const filing of filings) { let response: OctokitResponse | undefined try { - // Category switches gate only NEW issues; existing ones are reconciled normally. + // Category switches gate only new issues if (isNewFiling(filing)) { const category = filing.findings[0].category ?? 'wcag' if ( From 06e558d86b6ea03d52306a6b3722c2a8397eb199 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 17:02:42 -0700 Subject: [PATCH 25/46] Skip reopening issues labeled wontfix --- .github/actions/file/src/index.ts | 20 +-- .github/actions/file/src/isWontfixIssue.ts | 17 +++ .github/actions/file/tests/dryRun.test.ts | 2 + .../actions/file/tests/isWontfixIssue.test.ts | 60 +++++++++ .../actions/file/tests/wontfixReopen.test.ts | 120 ++++++++++++++++++ 5 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 .github/actions/file/src/isWontfixIssue.ts create mode 100644 .github/actions/file/tests/isWontfixIssue.test.ts create mode 100644 .github/actions/file/tests/wontfixReopen.test.ts diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index fd16b68..15714db 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -10,6 +10,7 @@ import {closeIssue} from './closeIssue.js' import {isNewFiling} from './isNewFiling.js' import {isRepeatedFiling} from './isRepeatedFiling.js' import {isResolvedFiling} from './isResolvedFiling.js' +import {isWontfixIssue, WONTFIX_LABEL} from './isWontfixIssue.js' import {openIssue} from './openIssue.js' import {reopenIssue} from './reopenIssue.js' import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' @@ -106,15 +107,16 @@ export default async function () { }) } } else if (isRepeatedFiling(filing)) { - // Reopen the filing's issue (if necessary) and update the body with the latest finding - response = await reopenIssue( - octokit, - new Issue(filing.issue), - filing.findings[0], - repoWithOwner, - screenshotRepo, - ) - filing.issue.state = 'reopened' + const issue = new Issue(filing.issue) + if (await isWontfixIssue(octokit, issue)) { + // The developer intentionally closed this issue and labeled it + // wontfix, so leave it closed instead of reopening it. + core.info(`Skipping reopen of issue labeled '${WONTFIX_LABEL}': ${filing.issue.url}`) + } else { + // Reopen the filing's issue (if necessary) and update the body with the latest finding + response = await reopenIssue(octokit, issue, filing.findings[0], repoWithOwner, screenshotRepo) + filing.issue.state = 'reopened' + } } if (response?.data && filing.issue) { // Update the filing with the latest issue data diff --git a/.github/actions/file/src/isWontfixIssue.ts b/.github/actions/file/src/isWontfixIssue.ts new file mode 100644 index 0000000..bb1f770 --- /dev/null +++ b/.github/actions/file/src/isWontfixIssue.ts @@ -0,0 +1,17 @@ +import type {Octokit} from '@octokit/core' +import type {Issue} from './Issue.js' + +/** Issues with this label are intentionally closed and should not be reopened. */ +export const WONTFIX_LABEL = 'wontfix' + +type IssueLabel = string | {name?: string} + +export async function isWontfixIssue(octokit: Octokit, {owner, repository, issueNumber}: Issue): Promise { + const response = await octokit.request(`GET /repos/${owner}/${repository}/issues/${issueNumber}`, { + owner, + repository, + issue_number: issueNumber, + }) + const labels = ((response.data as {labels?: IssueLabel[]}).labels ?? []) as IssueLabel[] + return labels.some(label => (typeof label === 'string' ? label : label.name) === WONTFIX_LABEL) +} diff --git a/.github/actions/file/tests/dryRun.test.ts b/.github/actions/file/tests/dryRun.test.ts index ce8470f..f5fdf2b 100644 --- a/.github/actions/file/tests/dryRun.test.ts +++ b/.github/actions/file/tests/dryRun.test.ts @@ -185,6 +185,8 @@ describe('file action — dry_run', () => { openIssue.mockResolvedValue(resp) reopenIssue.mockResolvedValue(resp) closeIssue.mockResolvedValue(resp) + // the wontfix-label check issues a GET before reopening; return no labels so the reopen proceeds + octokitRequest.mockResolvedValue({data: {labels: []}}) await runFileAction() diff --git a/.github/actions/file/tests/isWontfixIssue.test.ts b/.github/actions/file/tests/isWontfixIssue.test.ts new file mode 100644 index 0000000..d943aae --- /dev/null +++ b/.github/actions/file/tests/isWontfixIssue.test.ts @@ -0,0 +1,60 @@ +import {describe, it, expect, vi, beforeEach} from 'vitest' + +import {isWontfixIssue, WONTFIX_LABEL} from '../src/isWontfixIssue.ts' +import {Issue} from '../src/Issue.ts' + +const testIssue = new Issue({ + id: 42, + nodeId: 'MDU6SXNzdWU0Mg==', + url: 'https://github.com/org/filing-repo/issues/7', + title: 'Accessibility issue: test', + state: 'closed', +}) + +function mockOctokit(labels: unknown) { + return { + request: vi.fn().mockResolvedValue({data: {labels}}), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any +} + +describe('isWontfixIssue', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns true when the issue has the wontfix label (object form)', async () => { + const octokit = mockOctokit([{name: 'bug'}, {name: WONTFIX_LABEL}]) + + expect(await isWontfixIssue(octokit, testIssue)).toBe(true) + }) + + it('returns true when the issue has the wontfix label (string form)', async () => { + const octokit = mockOctokit(['bug', WONTFIX_LABEL]) + + expect(await isWontfixIssue(octokit, testIssue)).toBe(true) + }) + + it('returns false when the issue has no wontfix label', async () => { + const octokit = mockOctokit([{name: 'bug'}]) + + expect(await isWontfixIssue(octokit, testIssue)).toBe(false) + }) + + it('returns false when the issue has no labels', async () => { + const octokit = mockOctokit(undefined) + + expect(await isWontfixIssue(octokit, testIssue)).toBe(false) + }) + + it('requests the issue at the correct URL', async () => { + const octokit = mockOctokit([]) + + await isWontfixIssue(octokit, testIssue) + + expect(octokit.request).toHaveBeenCalledWith( + 'GET /repos/org/filing-repo/issues/7', + expect.objectContaining({issue_number: 7}), + ) + }) +}) diff --git a/.github/actions/file/tests/wontfixReopen.test.ts b/.github/actions/file/tests/wontfixReopen.test.ts new file mode 100644 index 0000000..065e5ba --- /dev/null +++ b/.github/actions/file/tests/wontfixReopen.test.ts @@ -0,0 +1,120 @@ +import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest' + +const openIssue = vi.fn() +const reopenIssue = vi.fn() +const closeIssue = vi.fn() +vi.mock('../src/openIssue.js', () => ({openIssue: (...args: unknown[]) => openIssue(...args)})) +vi.mock('../src/reopenIssue.js', () => ({reopenIssue: (...args: unknown[]) => reopenIssue(...args)})) +vi.mock('../src/closeIssue.js', () => ({closeIssue: (...args: unknown[]) => closeIssue(...args)})) + +const inputs: Record = {} +const infoLines: string[] = [] +const outputs: Record = {} +vi.mock('@actions/core', () => ({ + getInput: (name: string) => inputs[name] ?? '', + getBooleanInput: (name: string) => (inputs[name] ?? 'false') === 'true', + setOutput: (name: string, value: string) => { + outputs[name] = value + }, + info: (msg: string) => { + infoLines.push(msg) + }, + debug: () => {}, + warning: () => {}, + setFailed: () => {}, +})) + +// Feed findings/cached filings in +const files: Record = {} +vi.mock('node:fs', () => ({ + default: { + readFileSync: (p: string) => files[p], + writeFileSync: (p: string, data: string) => { + files[p] = data + }, + }, +})) + +// Stub Octokit: `request` serves the GET that isWontfixIssue makes +const octokitRequest = vi.fn() +vi.mock('@octokit/core', () => ({ + Octokit: { + plugin: () => + class { + request = octokitRequest + }, + }, +})) +vi.mock('@octokit/plugin-throttling', () => ({throttling: {}})) + +import runFileAction from '../src/index.ts' + +const wontfixFinding = { + scannerType: 'axe', + ruleId: 'color-contrast', + url: 'https://example.com/page', + html: 'Low contrast', + problemShort: 'elements must meet minimum color contrast ratio thresholds', + problemUrl: 'https://dequeuniversity.com/rules/axe/4.10/color-contrast', + solutionShort: 'ensure sufficient contrast', +} +const normalFinding = {...wontfixFinding, ruleId: 'heading-order', html: '

    Skipped

    '} + +// Both cached filings' findings reappear this run, so both are repeated +const wontfixCached = { + issue: {id: 1, nodeId: 'N1', url: 'https://github.com/org/repo/issues/1', title: 'wontfix'}, + findings: [wontfixFinding], +} +const normalCached = { + issue: {id: 3, nodeId: 'N3', url: 'https://github.com/org/repo/issues/3', title: 'normal'}, + findings: [normalFinding], +} + +function setup() { + files['/tmp/findings.json'] = JSON.stringify([wontfixFinding, normalFinding]) + files['/tmp/cached.json'] = JSON.stringify([wontfixCached, normalCached]) + inputs.findings_file = '/tmp/findings.json' + inputs.cached_filings_file = '/tmp/cached.json' + inputs.repository = 'org/repo' + inputs.token = 'fake-token' + // GET issue: issue 1 is labeled wontfix, issue 3 is not + octokitRequest.mockImplementation((route: string) => + route.includes('/issues/1') + ? Promise.resolve({data: {labels: [{name: 'wontfix'}]}}) + : Promise.resolve({data: {labels: []}}), + ) +} + +describe('file action — wontfix label', () => { + beforeEach(() => { + vi.clearAllMocks() + infoLines.length = 0 + for (const k of Object.keys(inputs)) delete inputs[k] + for (const k of Object.keys(outputs)) delete outputs[k] + }) + afterEach(() => { + vi.restoreAllMocks() + }) + + it('reopens the unlabeled issue but not the one labeled wontfix', async () => { + setup() + + await runFileAction() + + expect(reopenIssue).toHaveBeenCalledTimes(1) + const reopenedIssue = reopenIssue.mock.calls[0][1] as {url: string} + expect(reopenedIssue.url).toBe('https://github.com/org/repo/issues/3') + expect(openIssue).not.toHaveBeenCalled() + expect(closeIssue).not.toHaveBeenCalled() + }) + + it('logs that it skipped the wontfix issue', async () => { + setup() + + await runFileAction() + + expect(infoLines.join('\n')).toContain( + "Skipping reopen of issue labeled 'wontfix': https://github.com/org/repo/issues/1", + ) + }) +}) From fb48e69612ce678cf14be591944c9c63fa8876bd Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 17:02:42 -0700 Subject: [PATCH 26/46] Document wontfix label in README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index a20261e..27c4d89 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,17 @@ If your login flow is more complex—if it requires two-factor authentication, s --- +## Keeping an issue closed with `wontfix` + +When the scanner files an issue for an accessibility finding and that same finding turns up again on a later run, it reopens the issue (if you'd closed it) so the barrier doesn't get lost. Sometimes, though, you may want a closed issue to _stay_ closed—for example, if you've decided not to act on a particular finding, or you're tracking the work somewhere else. + +To stop the scanner from reopening a closed issue, add the **`wontfix`** label to it. On its next run, the scanner sees the label and skips reopening the issue, leaving it closed. + +> [!NOTE] +> The `wontfix` label only affects _reopening_. If you remove the label later, the scanner resumes its normal behavior and will reopen the issue on the next run if the finding is still present. + +--- + ## Configuring GitHub Copilot The a11y scanner leverages GitHub Copilot coding agent, which can be configured with custom instructions: From 52d65a74f40c95208e28697c5273069f9133f459 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 17:10:10 -0700 Subject: [PATCH 27/46] Trim verbose comments --- .github/actions/file/src/index.ts | 5 ++--- README.md | 3 --- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 15714db..9c446af 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -109,11 +109,10 @@ export default async function () { } else if (isRepeatedFiling(filing)) { const issue = new Issue(filing.issue) if (await isWontfixIssue(octokit, issue)) { - // The developer intentionally closed this issue and labeled it - // wontfix, so leave it closed instead of reopening it. + // The developer intentionally closed this issue and labeled it 'wontfix', so leave it closed core.info(`Skipping reopen of issue labeled '${WONTFIX_LABEL}': ${filing.issue.url}`) } else { - // Reopen the filing's issue (if necessary) and update the body with the latest finding + // Reopen the filing's issue and update the body with the latest finding response = await reopenIssue(octokit, issue, filing.findings[0], repoWithOwner, screenshotRepo) filing.issue.state = 'reopened' } diff --git a/README.md b/README.md index 27c4d89..a734c37 100644 --- a/README.md +++ b/README.md @@ -154,9 +154,6 @@ When the scanner files an issue for an accessibility finding and that same findi To stop the scanner from reopening a closed issue, add the **`wontfix`** label to it. On its next run, the scanner sees the label and skips reopening the issue, leaving it closed. -> [!NOTE] -> The `wontfix` label only affects _reopening_. If you remove the label later, the scanner resumes its normal behavior and will reopen the issue on the next run if the finding is still present. - --- ## Configuring GitHub Copilot From 2feeb9d4a4cbd48629e4626970c2bb963c36b352 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 17:14:58 -0700 Subject: [PATCH 28/46] Proceed with reopen when wontfix label check fails --- .github/actions/file/src/index.ts | 9 ++++++++- .../actions/file/tests/wontfixReopen.test.ts | 18 +++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 9c446af..fc1b8d3 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -108,7 +108,14 @@ export default async function () { } } else if (isRepeatedFiling(filing)) { const issue = new Issue(filing.issue) - if (await isWontfixIssue(octokit, issue)) { + let isWontfix = false + try { + isWontfix = await isWontfixIssue(octokit, issue) + } catch (error) { + // A failed label check shouldn't abort the run, so reopen as usual + core.warning(`Could not check labels for ${filing.issue.url}; proceeding with reopen: ${error}`) + } + if (isWontfix) { // The developer intentionally closed this issue and labeled it 'wontfix', so leave it closed core.info(`Skipping reopen of issue labeled '${WONTFIX_LABEL}': ${filing.issue.url}`) } else { diff --git a/.github/actions/file/tests/wontfixReopen.test.ts b/.github/actions/file/tests/wontfixReopen.test.ts index 065e5ba..ee26871 100644 --- a/.github/actions/file/tests/wontfixReopen.test.ts +++ b/.github/actions/file/tests/wontfixReopen.test.ts @@ -9,6 +9,7 @@ vi.mock('../src/closeIssue.js', () => ({closeIssue: (...args: unknown[]) => clos const inputs: Record = {} const infoLines: string[] = [] +const warnLines: string[] = [] const outputs: Record = {} vi.mock('@actions/core', () => ({ getInput: (name: string) => inputs[name] ?? '', @@ -20,7 +21,9 @@ vi.mock('@actions/core', () => ({ infoLines.push(msg) }, debug: () => {}, - warning: () => {}, + warning: (msg: string) => { + warnLines.push(msg) + }, setFailed: () => {}, })) @@ -89,6 +92,7 @@ describe('file action — wontfix label', () => { beforeEach(() => { vi.clearAllMocks() infoLines.length = 0 + warnLines.length = 0 for (const k of Object.keys(inputs)) delete inputs[k] for (const k of Object.keys(outputs)) delete outputs[k] }) @@ -117,4 +121,16 @@ describe('file action — wontfix label', () => { "Skipping reopen of issue labeled 'wontfix': https://github.com/org/repo/issues/1", ) }) + + it('reopens as usual (and warns) when the label check fails', async () => { + setup() + // The label-check GET fails for every issue (e.g. transient API error) + octokitRequest.mockRejectedValue(new Error('boom')) + + await runFileAction() + + // Both repeated filings should still be reopened rather than aborting the run + expect(reopenIssue).toHaveBeenCalledTimes(2) + expect(warnLines.join('\n')).toContain('Could not check labels for') + }) }) From 9d668aaa908fc92c2569fff4397881ba3f7465b8 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 19 Jun 2026 17:20:15 -0700 Subject: [PATCH 29/46] Document best-practice and experimental issue inputs in README --- README.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a20261e..fa8cf0e 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ jobs: # skip_copilot_assignment: false # Optional: Set to true to skip assigning issues to GitHub Copilot (or if you don't have GitHub Copilot) # include_screenshots: false # Optional: Set to true to capture screenshots and include links to them in filed issues # open_grouped_issues: false # Optional: Set to true to open an issue grouping individual issues per violation + # file_best_practice_issues: true # Optional: Set to false to stop filing new issues for best-practice findings (recommendations that are not hard WCAG failures) + # file_experimental_issues: true # Optional: Set to false to stop filing new issues for experimental findings (checks that are not yet stable) # dry_run: false # Optional: Set to true to scan and log what would be filed without creating/closing issues or writing the cache # reduced_motion: no-preference # Optional: Playwright reduced motion configuration option # color_scheme: light # Optional: Playwright color scheme configuration option @@ -115,25 +117,27 @@ Trigger the workflow manually or automatically based on your configuration. The ## Action inputs -| Input | Required | Description | Example | -| ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `urls` | No\* | Newline-delimited list of URLs to scan. Required unless `url_configs` is provided. | `https://primer.style`
    `https://primer.style/octicons` | -| `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` | -| `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` | -| `cache_key` | Yes | Key for caching results across runs
    Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` | -| `base_url` | No | GitHub API base URL used by Octokit. Set this for GitHub Enterprise Server (format: `https://HOSTNAME/api/v3`). Defaults to `https://api.github.com` | `https://ghe.example.com/api/v3` | -| `login_url` | No | If scanned pages require authentication, the URL of the login page | `https://github.com/login` | -| `username` | No | If scanned pages require authentication, the username to use for login | `some-user` | -| `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` | -| `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":[...]}` | -| `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` | -| `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | -| `open_grouped_issues` | No | Whether to create a tracking issue which groups filed issues together by violation type. Default: `false` | `true` | -| `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | -| `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | -| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | -| `dry_run` | No | When `true`, scan and log the issues that _would_ be filed without opening, closing, reopening, or assigning any issues — and without writing to the `gh-cache` branch. Useful for safely previewing results. Default: `false` | `true` | -| `url_configs` | No | A stringified JSON array of URL config objects. Each object must have a `url` field and may have an optional `excludeSelectors` field (array of CSS selectors to exclude from the Axe scan for that URL). When provided, takes precedence over the `urls` input. | `'[{"url":"https://example.com","excludeSelectors":["iframe","#widget"]}]'` | +| Input | Required | Description | Example | +| --------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `urls` | No\* | Newline-delimited list of URLs to scan. Required unless `url_configs` is provided. | `https://primer.style`
    `https://primer.style/octicons` | +| `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` | +| `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` | +| `cache_key` | Yes | Key for caching results across runs
    Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` | +| `base_url` | No | GitHub API base URL used by Octokit. Set this for GitHub Enterprise Server (format: `https://HOSTNAME/api/v3`). Defaults to `https://api.github.com` | `https://ghe.example.com/api/v3` | +| `login_url` | No | If scanned pages require authentication, the URL of the login page | `https://github.com/login` | +| `username` | No | If scanned pages require authentication, the username to use for login | `some-user` | +| `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` | +| `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":[...]}` | +| `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` | +| `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | +| `open_grouped_issues` | No | Whether to create a tracking issue which groups filed issues together by violation type. Default: `false` | `true` | +| `file_best_practice_issues` | No | Whether to file issues for best-practice findings (accessibility recommendations that are not hard WCAG failures). Set to `false` to suppress new best-practice issues; existing ones are left untouched. Default: `true` | `false` | +| `file_experimental_issues` | No | Whether to file issues for experimental findings (checks that are not yet stable). Set to `false` to suppress new experimental issues; existing ones are left untouched. Default: `true` | `false` | +| `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | +| `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | +| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | +| `dry_run` | No | When `true`, scan and log the issues that _would_ be filed without opening, closing, reopening, or assigning any issues — and without writing to the `gh-cache` branch. Useful for safely previewing results. Default: `false` | `true` | +| `url_configs` | No | A stringified JSON array of URL config objects. Each object must have a `url` field and may have an optional `excludeSelectors` field (array of CSS selectors to exclude from the Axe scan for that URL). When provided, takes precedence over the `urls` input. | `'[{"url":"https://example.com","excludeSelectors":["iframe","#widget"]}]'` | --- From 9b4a9bdf19fbf58ffc37245fdde6d9e6d80e780b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:04:29 +0000 Subject: [PATCH 30/46] chore(deps): Bump the github-actions group across 4 directories with 2 updates Bumps the github-actions group with 2 updates in the / directory: [actions/checkout](https://github.com/actions/checkout) and [ruby/setup-ruby](https://github.com/ruby/setup-ruby). Bumps the github-actions group with 1 update in the /.github/actions/gh-cache/delete directory: [actions/checkout](https://github.com/actions/checkout). Bumps the github-actions group with 1 update in the /.github/actions/gh-cache/restore directory: [actions/checkout](https://github.com/actions/checkout). Bumps the github-actions group with 1 update in the /.github/actions/gh-cache/save directory: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) Updates `ruby/setup-ruby` from 1.313.0 to 1.314.0 - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/89f90524b88a01fe6e0b732220432cc6142926af...9eb537ca036ebaed86729dcb9309076e4c5c3b74) Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: ruby/setup-ruby dependency-version: 1.314.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/actions/gh-cache/delete/action.yml | 2 +- .github/actions/gh-cache/restore/action.yml | 2 +- .github/actions/gh-cache/save/action.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/gh-cache/delete/action.yml b/.github/actions/gh-cache/delete/action.yml index 1f0242a..e0a130b 100644 --- a/.github/actions/gh-cache/delete/action.yml +++ b/.github/actions/gh-cache/delete/action.yml @@ -40,7 +40,7 @@ runs: fi - name: Checkout repository to temporary directory - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: token: ${{ inputs.token }} fetch-depth: 0 diff --git a/.github/actions/gh-cache/restore/action.yml b/.github/actions/gh-cache/restore/action.yml index 26d0ecc..acb340d 100644 --- a/.github/actions/gh-cache/restore/action.yml +++ b/.github/actions/gh-cache/restore/action.yml @@ -44,7 +44,7 @@ runs: fi - name: Checkout repository to temporary directory - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: token: ${{ inputs.token }} fetch-depth: 0 diff --git a/.github/actions/gh-cache/save/action.yml b/.github/actions/gh-cache/save/action.yml index ee5df6f..d6d4893 100644 --- a/.github/actions/gh-cache/save/action.yml +++ b/.github/actions/gh-cache/save/action.yml @@ -40,7 +40,7 @@ runs: fi - name: Checkout repository to temporary directory - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: token: ${{ inputs.token }} fetch-depth: 0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7e44c7d..663d3e7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup Node uses: actions/setup-node@v6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b350b09..2244973 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,10 +31,10 @@ jobs: site: ['sites/site-with-errors'] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup Ruby - uses: ruby/setup-ruby@89f90524b88a01fe6e0b732220432cc6142926af + uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 with: ruby-version: '3.4' bundler-cache: true From a71d89afe5e030d6f8651f6f9c3bdaa6fd1efada Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:06:27 +0000 Subject: [PATCH 31/46] chore(deps-dev): Bump @types/node from 25.9.0 to 26.0.0 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.0 to 26.0.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 26.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index c8cb629..6c1d420 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", - "@types/node": "^25.9.0", + "@types/node": "^26.0.0", "eslint": "^10.4.0", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.3", @@ -741,13 +741,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", - "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -2433,9 +2433,9 @@ } }, "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 786e52b..f7cb117 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", - "@types/node": "^25.9.0", + "@types/node": "^26.0.0", "eslint": "^10.4.0", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.3", From 428089035b04952365b2226c3e5504e2c2d67734 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:03:30 +0000 Subject: [PATCH 32/46] chore(deps-dev): Bump undici from 6.24.1 to 6.27.0 Bumps [undici](https://github.com/nodejs/undici) from 6.24.1 to 6.27.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v6.24.1...v6.27.0) --- updated-dependencies: - dependency-name: undici dependency-version: 6.27.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c1d420..ac6f3f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2423,9 +2423,9 @@ } }, "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", "dev": true, "license": "MIT", "engines": { From 35ebacb888989cb7dfef419a63bb0155ca692c1a Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Mon, 22 Jun 2026 16:06:54 -0500 Subject: [PATCH 33/46] Add group_by option to consolidate scanner issues --- .github/actions/file/README.md | 27 +++++- .github/actions/file/action.yml | 32 ++++--- .github/actions/file/src/generateIssueBody.ts | 18 +++- .github/actions/file/src/index.ts | 30 ++++--- .github/actions/file/src/openIssue.ts | 15 ++-- .github/actions/file/src/reopenIssue.ts | 6 +- .github/actions/file/src/types.d.ts | 2 + .../file/src/updateFilingsWithNewFindings.ts | 45 +++++++--- .github/actions/file/tests/dryRun.test.ts | 37 +++++++- .../file/tests/generateIssueBody.test.ts | 15 ++++ .github/actions/file/tests/openIssue.test.ts | 24 ++++-- .../actions/file/tests/reopenIssue.test.ts | 14 +-- .../updateFilingsWithNewFindings.test.ts | 86 +++++++++++++++++++ README.md | 40 +++++---- action.yml | 5 ++ 15 files changed, 313 insertions(+), 83 deletions(-) create mode 100644 .github/actions/file/tests/updateFilingsWithNewFindings.test.ts diff --git a/.github/actions/file/README.md b/.github/actions/file/README.md index 7680cce..fd956e4 100644 --- a/.github/actions/file/README.md +++ b/.github/actions/file/README.md @@ -11,6 +11,7 @@ Files GitHub issues to track potential accessibility gaps. **Required** Path to a JSON file containing the list of potential accessibility gaps. The path can be absolute or relative to the working directory (which defaults to `GITHUB_WORKSPACE`). For example: `findings.json`. The file should contain a JSON array of finding objects. For example: + ```json [] ``` @@ -28,15 +29,31 @@ The file should contain a JSON array of finding objects. For example: **Optional** Path to a JSON file containing cached filings from previous runs. The path can be absolute or relative to the working directory (which defaults to `GITHUB_WORKSPACE`). Without this, duplicate issues may be filed. For example: `cached-filings.json`. The file should contain a JSON array of filing objects. For example: + ```json [ { "findings": [], - "issue": {"id":1,"nodeId":"SXNzdWU6MQ==","url":"https://github.com/github/docs/issues/123","title":"Accessibility issue: 1"} + "issue": { + "id": 1, + "nodeId": "SXNzdWU6MQ==", + "url": "https://github.com/github/docs/issues/123", + "title": "Accessibility issue: 1" + } } ] ``` +#### `group_by` + +**Optional** How to consolidate findings into issues. One of: + +- `finding` (default): one issue per individual violation — current behavior, unchanged. +- `rule`: one issue per rule (`ruleId`/`scannerType`), aggregating every occurrence across all scanned URLs. +- `rule+url`: one issue per rule per scanned URL. + +When grouping, each additional occurrence is appended to the single "umbrella" issue body as a checklist item under an **Occurrences** section rather than spawning a new issue. This is the preferred mechanism for consolidating issues over `open_grouped_issues`. + ### Outputs #### `filings_file` @@ -44,11 +61,17 @@ The file should contain a JSON array of filing objects. For example: Absolute path to a JSON file containing the list of issues filed (and their associated finding(s)). The action writes this file to a temporary directory and returns the absolute path. For example: `$RUNNER_TEMP/filings-.json`. The file will contain a JSON array of filing objects. For example: + ```json [ { "findings": [], - "issue": {"id":1,"nodeId":"SXNzdWU6MQ==","url":"https://github.com/github/docs/issues/123","title":"Accessibility issue: 1"} + "issue": { + "id": 1, + "nodeId": "SXNzdWU6MQ==", + "url": "https://github.com/github/docs/issues/123", + "title": "Accessibility issue: 1" + } } ] ``` diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index 7d76af7..714bf09 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -1,21 +1,21 @@ -name: "File" -description: "Files GitHub issues to track potential accessibility gaps." +name: 'File' +description: 'Files GitHub issues to track potential accessibility gaps.' inputs: findings_file: - description: "Path to a JSON file containing the list of potential accessibility gaps" + description: 'Path to a JSON file containing the list of potential accessibility gaps' required: true repository: - description: "Repository (with owner) to file issues in" + description: 'Repository (with owner) to file issues in' required: true token: description: "Token with fine-grained permission 'issues: write'" required: true base_url: - description: "Optional base URL to pass into Octokit for the GitHub API (for example, `https://YOUR_HOSTNAME/api/v3` for GitHub Enterprise Server)" + description: 'Optional base URL to pass into Octokit for the GitHub API (for example, `https://YOUR_HOSTNAME/api/v3` for GitHub Enterprise Server)' required: false cached_filings_file: - description: "Path to a JSON file containing cached filings from previous runs. Without this, duplicate issues may be filed." + description: 'Path to a JSON file containing cached filings from previous runs. Without this, duplicate issues may be filed.' required: false screenshot_repository: description: "Repository (with owner) where screenshots are stored on the gh-cache branch. Defaults to the 'repository' input if not set. Required if issues are open in a different repo to construct proper screenshot URLs." @@ -23,20 +23,24 @@ inputs: open_grouped_issues: description: "In the 'file' step, also open grouped issues which link to all issues with the same root cause" required: false - default: "false" + default: 'false' + group_by: + description: "How to group findings into issues: 'finding' (one issue per violation, default), 'rule' (one issue per rule), or 'rule+url' (one issue per rule per scanned URL)." + required: false + default: 'finding' dry_run: - description: "When true, log the issues that would be filed without opening, closing, or reopening any issues." + description: 'When true, log the issues that would be filed without opening, closing, or reopening any issues.' required: false - default: "false" + default: 'false' outputs: filings_file: - description: "Path to a JSON file containing the list of issues filed (and their associated finding(s))" + description: 'Path to a JSON file containing the list of issues filed (and their associated finding(s))' runs: - using: "node24" - main: "bootstrap.js" + using: 'node24' + main: 'bootstrap.js' branding: - icon: "compass" - color: "blue" + icon: 'compass' + color: 'blue' diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 18e25d3..0f6cc30 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -1,6 +1,9 @@ import type {Finding} from './types.d.js' -export function generateIssueBody(finding: Finding, screenshotRepo: string): string { +export function generateIssueBody(findingOrFindings: Finding | Finding[], screenshotRepo: string): string { + const findings = Array.isArray(findingOrFindings) ? findingOrFindings : [findingOrFindings] + const finding = findings[0] + const solutionLong = finding.solutionLong ?.split('\n') .map((line: string) => @@ -18,6 +21,17 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str ` } + // When this issue groups multiple findings, list each occurrence as a checklist item. + let occurrencesSection = '' + if (findings.length > 1) { + const items = findings.map(f => `- [ ] ${f.html ? `\`${f.html}\` on ${f.url}` : f.url}`).join('\n') + occurrencesSection = ` +## Occurrences (${findings.length}) + +${items} +` + } + const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific violation reported in this issue is no longer reproducible. - [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization. @@ -30,7 +44,7 @@ An accessibility scan ${finding.html ? `flagged the element \`${finding.html}\`` ${screenshotSection ?? ''} To fix this, ${finding.solutionShort}. ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} - +${occurrencesSection} ${acceptanceCriteria} ` diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index fd16b68..6d95dfd 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -1,4 +1,12 @@ -import type {Finding, ResolvedFiling, RepeatedFiling, FindingGroupIssue, Filing, IssueResponse} from './types.d.js' +import type { + Finding, + ResolvedFiling, + RepeatedFiling, + FindingGroupIssue, + Filing, + IssueResponse, + GroupBy, +} from './types.d.js' import fs from 'node:fs' import path from 'node:path' import process from 'node:process' @@ -29,6 +37,13 @@ export default async function () { ? JSON.parse(fs.readFileSync(cachedFilingsFile, 'utf8')) : [] const shouldOpenGroupedIssues = core.getBooleanInput('open_grouped_issues') + const groupByInput = core.getInput('group_by') || 'finding' + const validGroupByValues: GroupBy[] = ['finding', 'rule', 'rule+url'] + if (!validGroupByValues.includes(groupByInput as GroupBy)) { + core.setFailed(`Invalid 'group_by' value: '${groupByInput}'. Must be one of: ${validGroupByValues.join(', ')}.`) + return + } + const groupBy = groupByInput as GroupBy const dryRun = core.getBooleanInput('dry_run') core.debug(`Input: 'findings_file: ${findingsFile}'`) core.debug(`Input: 'repository: ${repoWithOwner}'`) @@ -36,6 +51,7 @@ export default async function () { core.debug(`Input: 'screenshot_repository: ${screenshotRepo}'`) core.debug(`Input: 'cached_filings_file: ${cachedFilingsFile}'`) core.debug(`Input: 'open_grouped_issues: ${shouldOpenGroupedIssues}'`) + core.debug(`Input: 'group_by: ${groupBy}'`) core.debug(`Input: 'dry_run: ${dryRun}'`) const octokit = new OctokitWithThrottling({ @@ -58,7 +74,7 @@ export default async function () { }, }, }) - const filings = updateFilingsWithNewFindings(cachedFilings, findings) + const filings = updateFilingsWithNewFindings(cachedFilings, findings, groupBy) // Track new issues for grouping const newIssuesByProblemShort: Record = {} @@ -91,7 +107,7 @@ export default async function () { filing.issue.state = 'closed' } else if (isNewFiling(filing)) { // Open a new issue for the filing - response = await openIssue(octokit, repoWithOwner, filing.findings[0], screenshotRepo) + response = await openIssue(octokit, repoWithOwner, filing.findings, screenshotRepo) ;(filing as Filing).issue = {state: 'open'} as Issue // Track for grouping @@ -107,13 +123,7 @@ export default async function () { } } else if (isRepeatedFiling(filing)) { // Reopen the filing's issue (if necessary) and update the body with the latest finding - response = await reopenIssue( - octokit, - new Issue(filing.issue), - filing.findings[0], - repoWithOwner, - screenshotRepo, - ) + response = await reopenIssue(octokit, new Issue(filing.issue), filing.findings, repoWithOwner, screenshotRepo) filing.issue.state = 'reopened' } if (response?.data && filing.issue) { diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 937f06c..d25f23d 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -17,22 +17,25 @@ function truncateWithEllipsis(text: string, maxLength: number): string { return text.length > maxLength ? text.slice(0, maxLength - 1) + '…' : text } -export async function openIssue(octokit: Octokit, repoWithOwner: string, finding: Finding, screenshotRepo?: string) { +export async function openIssue(octokit: Octokit, repoWithOwner: string, findings: Finding[], screenshotRepo?: string) { const owner = repoWithOwner.split('/')[0] const repo = repoWithOwner.split('/')[1] + const primary = findings[0] - const labels = [`${finding.scannerType}-scanning-issue`] + const labels = [`${primary.scannerType}-scanning-issue`] // Only include a ruleId label when it's defined - if (finding.ruleId) { - labels.push(`${finding.scannerType} rule: ${finding.ruleId}`) + if (primary.ruleId) { + labels.push(`${primary.scannerType} rule: ${primary.ruleId}`) } + const count = findings.length + const titleSuffix = count > 1 ? ` (${count} occurrences)` : ` on ${new URL(primary.url).pathname}` const title = truncateWithEllipsis( - `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, + `Accessibility issue: ${primary.problemShort[0].toUpperCase() + primary.problemShort.slice(1)}${titleSuffix}`, GITHUB_ISSUE_TITLE_MAX_LENGTH, ) - const body = generateIssueBody(finding, screenshotRepo ?? repoWithOwner) + const body = generateIssueBody(findings, screenshotRepo ?? repoWithOwner) return octokit.request(`POST /repos/${owner}/${repo}/issues`, { owner, diff --git a/.github/actions/file/src/reopenIssue.ts b/.github/actions/file/src/reopenIssue.ts index 329c695..b37c8ca 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -6,13 +6,13 @@ import {generateIssueBody} from './generateIssueBody.js' export async function reopenIssue( octokit: Octokit, {owner, repository, issueNumber}: Issue, - finding?: Finding, + findings?: Finding[], repoWithOwner?: string, screenshotRepo?: string, ) { let body: string | undefined - if (finding && repoWithOwner) { - body = generateIssueBody(finding, screenshotRepo ?? repoWithOwner) + if (findings && findings.length > 0 && repoWithOwner) { + body = generateIssueBody(findings, screenshotRepo ?? repoWithOwner) } return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index ee91bc6..c7bf804 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -1,3 +1,5 @@ +export type GroupBy = 'finding' | 'rule' | 'rule+url' + export type Finding = { scannerType: string ruleId?: string diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index eee1c6a..6e258f8 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -1,25 +1,42 @@ -import type {Finding, ResolvedFiling, NewFiling, RepeatedFiling, Filing} from './types.d.js' +import type {Finding, ResolvedFiling, NewFiling, RepeatedFiling, Filing, GroupBy} from './types.d.js' function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string { return filing.issue.url } -function getFindingKey(finding: Finding): string { - if (finding.ruleId && finding.html) { - return `${finding.url};${finding.ruleId};${finding.html}` +/** + * Computes the dedup key for a finding based on the grouping mode. + * - 'finding' (default): one filing per individual violation (URL + rule + element). + * - 'rule': one filing per rule, aggregating every occurrence across all URLs. + * - 'rule+url': one filing per rule per scanned URL. + */ +function getFindingKey(finding: Finding, groupBy: GroupBy): string { + const rule = finding.ruleId ?? `${finding.scannerType};${finding.problemUrl}` + + switch (groupBy) { + case 'rule': + return rule + case 'rule+url': + return `${finding.url};${rule}` + case 'finding': + default: + if (finding.ruleId && finding.html) { + return `${finding.url};${finding.ruleId};${finding.html}` + } + return `${finding.url};${finding.scannerType};${finding.problemUrl}` } - return `${finding.url};${finding.scannerType};${finding.problemUrl}` } export function updateFilingsWithNewFindings( filings: (ResolvedFiling | RepeatedFiling)[], findings: Finding[], + groupBy: GroupBy = 'finding', ): Filing[] { const filingKeys: { [key: string]: ResolvedFiling | RepeatedFiling } = {} const findingKeys: {[key: string]: string} = {} - const newFilings: NewFiling[] = [] + const newFilingKeys: {[key: string]: NewFiling} = {} // Create maps for filing and finding data from previous runs, for quick lookups for (const filing of filings) { @@ -29,21 +46,25 @@ export function updateFilingsWithNewFindings( findings: [], } for (const finding of filing.findings) { - findingKeys[getFindingKey(finding)] = getFilingKey(filing) + findingKeys[getFindingKey(finding, groupBy)] = getFilingKey(filing) } } for (const finding of findings) { - const filingKey = findingKeys[getFindingKey(finding)] + const key = getFindingKey(finding, groupBy) + const filingKey = findingKeys[key] if (filingKey) { - // This finding already has an associated filing; add it to that filing's findings + // This finding already maps to an existing issue; append it to that filing ;(filingKeys[filingKey] as RepeatedFiling).findings.push(finding) + } else if (newFilingKeys[key]) { + // A new filing for this group already exists this run; append to it + newFilingKeys[key].findings.push(finding) } else { - // This finding is new; create a new entry with no associated issue yet - newFilings.push({findings: [finding]}) + // First occurrence of this group with no existing issue; start a new filing + newFilingKeys[key] = {findings: [finding]} } } const updatedFilings = Object.values(filingKeys) - return [...updatedFilings, ...newFilings] + return [...updatedFilings, ...Object.values(newFilingKeys)] } diff --git a/.github/actions/file/tests/dryRun.test.ts b/.github/actions/file/tests/dryRun.test.ts index ce8470f..c8567d2 100644 --- a/.github/actions/file/tests/dryRun.test.ts +++ b/.github/actions/file/tests/dryRun.test.ts @@ -12,6 +12,7 @@ vi.mock('../src/closeIssue.js', () => ({closeIssue: (...args: unknown[]) => clos const inputs: Record = {} const infoLines: string[] = [] const outputs: Record = {} +const failedMessages: string[] = [] vi.mock('@actions/core', () => ({ getInput: (name: string) => inputs[name] ?? '', getBooleanInput: (name: string) => (inputs[name] ?? 'false') === 'true', @@ -23,7 +24,9 @@ vi.mock('@actions/core', () => ({ }, debug: () => {}, warning: () => {}, - setFailed: () => {}, + setFailed: (msg: string) => { + failedMessages.push(msg) + }, })) // --- Mock fs: feed findings/cached filings in, swallow the output write --- @@ -91,6 +94,7 @@ describe('file action — dry_run', () => { beforeEach(() => { vi.clearAllMocks() infoLines.length = 0 + failedMessages.length = 0 for (const k of Object.keys(inputs)) delete inputs[k] for (const k of Object.keys(outputs)) delete outputs[k] vi.spyOn(console, 'table').mockImplementation(() => {}) @@ -192,4 +196,35 @@ describe('file action — dry_run', () => { expect(reopenIssue).toHaveBeenCalled() expect(closeIssue).toHaveBeenCalled() }) + + it("group_by 'rule' collapses multiple same-rule findings into a single OPEN", async () => { + // Three brand-new color-contrast findings across two URLs, no cached filings. + const ccA1 = {...finding, url: 'https://example.com/a', html: '1'} + const ccA2 = {...finding, url: 'https://example.com/a', html: '2'} + const ccB1 = {...finding, url: 'https://example.com/b', html: '3'} + files['/tmp/findings.json'] = JSON.stringify([ccA1, ccA2, ccB1]) + files['/tmp/cached.json'] = JSON.stringify([]) + inputs.findings_file = '/tmp/findings.json' + inputs.cached_filings_file = '/tmp/cached.json' + inputs.repository = 'org/repo' + inputs.token = 'fake-token' + inputs.dry_run = 'true' + inputs.group_by = 'rule' + + await runFileAction() + + expect(vi.mocked(console.table)).toHaveBeenCalledWith(expect.objectContaining({open: 1})) + }) + + it('fails fast on an invalid group_by value', async () => { + setup() + inputs.group_by = 'bogus' + + await runFileAction() + + expect(failedMessages.join('\n')).toContain("Invalid 'group_by' value: 'bogus'") + expect(openIssue).not.toHaveBeenCalled() + expect(reopenIssue).not.toHaveBeenCalled() + expect(closeIssue).not.toHaveBeenCalled() + }) }) diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 167ee5f..d3ad876 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -76,4 +76,19 @@ describe('generateIssueBody', () => { expect(body).toContain(`found an issue on ${findingWithEmptyOptionalFields.url}`) expect(body).not.toContain('flagged the element') }) + + it('omits the Occurrences section for a single finding', () => { + const body = generateIssueBody(baseFinding, 'github/accessibility-scanner') + + expect(body).not.toContain('## Occurrences') + }) + + it('renders an Occurrences checklist when given multiple findings', () => { + const second = {...baseFinding, url: 'https://example.com/other', html: 'Link'} + const body = generateIssueBody([baseFinding, second], 'github/accessibility-scanner') + + expect(body).toContain('## Occurrences (2)') + expect(body).toContain(`- [ ] \`${baseFinding.html}\` on ${baseFinding.url}`) + expect(body).toContain(`- [ ] \`${second.html}\` on ${second.url}`) + }) }) diff --git a/.github/actions/file/tests/openIssue.test.ts b/.github/actions/file/tests/openIssue.test.ts index 77a184c..0aaa526 100644 --- a/.github/actions/file/tests/openIssue.test.ts +++ b/.github/actions/file/tests/openIssue.test.ts @@ -28,21 +28,21 @@ function mockOctokit() { describe('openIssue', () => { it('passes screenshotRepo to generateIssueBody when provided', async () => { const octokit = mockOctokit() - await openIssue(octokit, 'org/filing-repo', baseFinding, 'org/workflow-repo') + await openIssue(octokit, 'org/filing-repo', [baseFinding], 'org/workflow-repo') - expect(generateIssueBody).toHaveBeenCalledWith(baseFinding, 'org/workflow-repo') + expect(generateIssueBody).toHaveBeenCalledWith([baseFinding], 'org/workflow-repo') }) it('falls back to repoWithOwner when screenshotRepo is not provided', async () => { const octokit = mockOctokit() - await openIssue(octokit, 'org/filing-repo', baseFinding) + await openIssue(octokit, 'org/filing-repo', [baseFinding]) - expect(generateIssueBody).toHaveBeenCalledWith(baseFinding, 'org/filing-repo') + expect(generateIssueBody).toHaveBeenCalledWith([baseFinding], 'org/filing-repo') }) it('posts to the correct filing repo, not the screenshot repo', async () => { const octokit = mockOctokit() - await openIssue(octokit, 'org/filing-repo', baseFinding, 'org/workflow-repo') + await openIssue(octokit, 'org/filing-repo', [baseFinding], 'org/workflow-repo') expect(octokit.request).toHaveBeenCalledWith( 'POST /repos/org/filing-repo/issues', @@ -55,7 +55,7 @@ describe('openIssue', () => { it('includes the correct labels based on the finding', async () => { const octokit = mockOctokit() - await openIssue(octokit, 'org/repo', baseFinding) + await openIssue(octokit, 'org/repo', [baseFinding]) expect(octokit.request).toHaveBeenCalledWith( expect.any(String), @@ -71,10 +71,20 @@ describe('openIssue', () => { ...baseFinding, problemShort: 'a'.repeat(300), } - await openIssue(octokit, 'org/repo', longFinding) + await openIssue(octokit, 'org/repo', [longFinding]) const callArgs = octokit.request.mock.calls[0][1] expect(callArgs.title.length).toBeLessThanOrEqual(256) expect(callArgs.title).toMatch(/…$/) }) + + it('includes an occurrence count in the title when grouping multiple findings', async () => { + const octokit = mockOctokit() + const second = {...baseFinding, url: 'https://example.com/other', html: 'Another'} + await openIssue(octokit, 'org/repo', [baseFinding, second]) + + const callArgs = octokit.request.mock.calls[0][1] + expect(callArgs.title).toContain('(2 occurrences)') + expect(generateIssueBody).toHaveBeenCalledWith([baseFinding, second], 'org/repo') + }) }) diff --git a/.github/actions/file/tests/reopenIssue.test.ts b/.github/actions/file/tests/reopenIssue.test.ts index f5b34ef..a739a3f 100644 --- a/.github/actions/file/tests/reopenIssue.test.ts +++ b/.github/actions/file/tests/reopenIssue.test.ts @@ -41,16 +41,16 @@ describe('reopenIssue', () => { it('passes screenshotRepo to generateIssueBody when provided', async () => { const octokit = mockOctokit() - await reopenIssue(octokit, testIssue, baseFinding, 'org/filing-repo', 'org/workflow-repo') + await reopenIssue(octokit, testIssue, [baseFinding], 'org/filing-repo', 'org/workflow-repo') - expect(generateIssueBody).toHaveBeenCalledWith(baseFinding, 'org/workflow-repo') + expect(generateIssueBody).toHaveBeenCalledWith([baseFinding], 'org/workflow-repo') }) it('falls back to repoWithOwner when screenshotRepo is not provided', async () => { const octokit = mockOctokit() - await reopenIssue(octokit, testIssue, baseFinding, 'org/filing-repo') + await reopenIssue(octokit, testIssue, [baseFinding], 'org/filing-repo') - expect(generateIssueBody).toHaveBeenCalledWith(baseFinding, 'org/filing-repo') + expect(generateIssueBody).toHaveBeenCalledWith([baseFinding], 'org/filing-repo') }) it('does not generate a body when finding is not provided', async () => { @@ -66,14 +66,14 @@ describe('reopenIssue', () => { it('does not generate a body when repoWithOwner is not provided', async () => { const octokit = mockOctokit() - await reopenIssue(octokit, testIssue, baseFinding) + await reopenIssue(octokit, testIssue, [baseFinding]) expect(generateIssueBody).not.toHaveBeenCalled() }) it('sends PATCH to the correct issue URL with state open', async () => { const octokit = mockOctokit() - await reopenIssue(octokit, testIssue, baseFinding, 'org/filing-repo', 'org/workflow-repo') + await reopenIssue(octokit, testIssue, [baseFinding], 'org/filing-repo', 'org/workflow-repo') expect(octokit.request).toHaveBeenCalledWith( 'PATCH /repos/org/filing-repo/issues/7', @@ -86,7 +86,7 @@ describe('reopenIssue', () => { it('includes generated body when finding and repoWithOwner are provided', async () => { const octokit = mockOctokit() - await reopenIssue(octokit, testIssue, baseFinding, 'org/filing-repo', 'org/workflow-repo') + await reopenIssue(octokit, testIssue, [baseFinding], 'org/filing-repo', 'org/workflow-repo') expect(octokit.request).toHaveBeenCalledWith( expect.any(String), diff --git a/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts b/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts new file mode 100644 index 0000000..336ba50 --- /dev/null +++ b/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts @@ -0,0 +1,86 @@ +import {describe, it, expect} from 'vitest' +import {updateFilingsWithNewFindings} from '../src/updateFilingsWithNewFindings.ts' + +const cc = (url: string, html: string) => ({ + scannerType: 'axe', + ruleId: 'color-contrast', + url, + html, + problemShort: 'elements must meet minimum color contrast ratio thresholds', + problemUrl: 'https://dequeuniversity.com/rules/axe/4.10/color-contrast', + solutionShort: 'ensure sufficient contrast', +}) + +describe('updateFilingsWithNewFindings — group_by', () => { + const findings = [ + cc('https://example.com/a', '1'), + cc('https://example.com/a', '2'), + cc('https://example.com/b', '3'), + ] + + it("defaults to 'finding': one filing per individual violation", () => { + const result = updateFilingsWithNewFindings([], findings) + expect(result).toHaveLength(3) + for (const filing of result) expect(filing.findings).toHaveLength(1) + }) + + it("'rule': collapses all occurrences of a rule into a single filing", () => { + const result = updateFilingsWithNewFindings([], findings, 'rule') + expect(result).toHaveLength(1) + expect(result[0].findings).toHaveLength(3) + }) + + it("'rule+url': one filing per rule per URL", () => { + const result = updateFilingsWithNewFindings([], findings, 'rule+url') + expect(result).toHaveLength(2) + const counts = result.map(f => f.findings.length).sort() + expect(counts).toEqual([1, 2]) // 2 on /a, 1 on /b + }) + + it("'rule': appends new occurrences to an existing cached filing instead of opening a new issue", () => { + const cached = [ + { + issue: { + id: 1, + nodeId: 'N1', + url: 'https://github.com/org/repo/issues/1', + title: 'color-contrast', + }, + findings: [cc('https://example.com/a', '1')], + }, + ] + const result = updateFilingsWithNewFindings(cached, findings, 'rule') + // No brand-new filing; all three findings attach to the cached issue. + expect(result).toHaveLength(1) + expect(result[0].issue?.url).toBe('https://github.com/org/repo/issues/1') + expect(result[0].findings).toHaveLength(3) + }) + + it("keeps distinct rules separate under 'rule'", () => { + const mixed = [ + cc('https://example.com/a', '1'), + {...cc('https://example.com/a', '

    x

    '), ruleId: 'heading-order'}, + ] + const result = updateFilingsWithNewFindings([], mixed, 'rule') + expect(result).toHaveLength(2) + }) + + it("'finding' (default) preserves the original 1:1 behavior with cached filings", () => { + const cached = [ + { + issue: { + id: 1, + nodeId: 'N1', + url: 'https://github.com/org/repo/issues/1', + title: 'color-contrast', + }, + findings: [cc('https://example.com/a', '1')], + }, + ] + const result = updateFilingsWithNewFindings(cached, findings) + // One repeated filing (issues/1) plus two brand-new filings. + expect(result).toHaveLength(3) + const repeated = result.find(f => f.issue?.url === 'https://github.com/org/repo/issues/1') + expect(repeated?.findings).toHaveLength(1) + }) +}) diff --git a/README.md b/README.md index a20261e..5330f79 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ jobs: # skip_copilot_assignment: false # Optional: Set to true to skip assigning issues to GitHub Copilot (or if you don't have GitHub Copilot) # include_screenshots: false # Optional: Set to true to capture screenshots and include links to them in filed issues # open_grouped_issues: false # Optional: Set to true to open an issue grouping individual issues per violation + # group_by: finding # Optional: 'finding' (default, one issue per violation), 'rule' (one per rule), or 'rule+url' (one per rule per URL) # dry_run: false # Optional: Set to true to scan and log what would be filed without creating/closing issues or writing the cache # reduced_motion: no-preference # Optional: Playwright reduced motion configuration option # color_scheme: light # Optional: Playwright color scheme configuration option @@ -115,25 +116,26 @@ Trigger the workflow manually or automatically based on your configuration. The ## Action inputs -| Input | Required | Description | Example | -| ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `urls` | No\* | Newline-delimited list of URLs to scan. Required unless `url_configs` is provided. | `https://primer.style`
    `https://primer.style/octicons` | -| `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` | -| `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` | -| `cache_key` | Yes | Key for caching results across runs
    Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` | -| `base_url` | No | GitHub API base URL used by Octokit. Set this for GitHub Enterprise Server (format: `https://HOSTNAME/api/v3`). Defaults to `https://api.github.com` | `https://ghe.example.com/api/v3` | -| `login_url` | No | If scanned pages require authentication, the URL of the login page | `https://github.com/login` | -| `username` | No | If scanned pages require authentication, the username to use for login | `some-user` | -| `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` | -| `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":[...]}` | -| `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` | -| `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | -| `open_grouped_issues` | No | Whether to create a tracking issue which groups filed issues together by violation type. Default: `false` | `true` | -| `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | -| `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | -| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | -| `dry_run` | No | When `true`, scan and log the issues that _would_ be filed without opening, closing, reopening, or assigning any issues — and without writing to the `gh-cache` branch. Useful for safely previewing results. Default: `false` | `true` | -| `url_configs` | No | A stringified JSON array of URL config objects. Each object must have a `url` field and may have an optional `excludeSelectors` field (array of CSS selectors to exclude from the Axe scan for that URL). When provided, takes precedence over the `urls` input. | `'[{"url":"https://example.com","excludeSelectors":["iframe","#widget"]}]'` | +| Input | Required | Description | Example | +| ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `urls` | No\* | Newline-delimited list of URLs to scan. Required unless `url_configs` is provided. | `https://primer.style`
    `https://primer.style/octicons` | +| `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` | +| `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` | +| `cache_key` | Yes | Key for caching results across runs
    Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` | +| `base_url` | No | GitHub API base URL used by Octokit. Set this for GitHub Enterprise Server (format: `https://HOSTNAME/api/v3`). Defaults to `https://api.github.com` | `https://ghe.example.com/api/v3` | +| `login_url` | No | If scanned pages require authentication, the URL of the login page | `https://github.com/login` | +| `username` | No | If scanned pages require authentication, the username to use for login | `some-user` | +| `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` | +| `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":[...]}` | +| `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` | +| `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | +| `open_grouped_issues` | No | Whether to create a tracking issue which groups filed issues together by violation type. Default: `false` | `true` | +| `group_by` | No | How to consolidate findings into issues: `finding` (default, one issue per individual violation), `rule` (one issue per rule, aggregating every occurrence across all scanned URLs), or `rule+url` (one issue per rule per scanned URL). Preferred over `open_grouped_issues` for consolidation. Default: `finding` | `rule` | +| `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | +| `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | +| `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | +| `dry_run` | No | When `true`, scan and log the issues that _would_ be filed without opening, closing, reopening, or assigning any issues — and without writing to the `gh-cache` branch. Useful for safely previewing results. Default: `false` | `true` | +| `url_configs` | No | A stringified JSON array of URL config objects. Each object must have a `url` field and may have an optional `excludeSelectors` field (array of CSS selectors to exclude from the Axe scan for that URL). When provided, takes precedence over the `urls` input. | `'[{"url":"https://example.com","excludeSelectors":["iframe","#widget"]}]'` | --- diff --git a/action.yml b/action.yml index 1b852a5..1ace330 100644 --- a/action.yml +++ b/action.yml @@ -45,6 +45,10 @@ inputs: description: "In the 'file' step, also open grouped issues which link to all issues with the same problem" required: false default: 'false' + group_by: + description: "How to group findings into issues: 'finding' (one issue per violation, default), 'rule' (one issue per rule), or 'rule+url' (one issue per rule per scanned URL)." + required: false + default: 'finding' reduced_motion: description: 'Playwright reducedMotion setting: https://playwright.dev/docs/api/class-browser#browser-new-page-option-reduced-motion' required: false @@ -133,6 +137,7 @@ runs: cached_filings_file: ${{ steps.normalize_cache.outputs.cached_filings_file }} screenshot_repository: ${{ github.repository }} open_grouped_issues: ${{ inputs.open_grouped_issues }} + group_by: ${{ inputs.group_by }} dry_run: ${{ inputs.dry_run }} - if: ${{ steps.file.outputs.filings_file }} name: Get issues from filings From c1cf4d01fcf5a1ef464bcc12fea4db2441bbfee3 Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Mon, 22 Jun 2026 16:20:25 -0500 Subject: [PATCH 34/46] Remove comments --- .github/actions/file/src/generateIssueBody.ts | 1 - .../actions/file/src/updateFilingsWithNewFindings.ts | 10 +--------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 0f6cc30..91e10e6 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -21,7 +21,6 @@ export function generateIssueBody(findingOrFindings: Finding | Finding[], screen ` } - // When this issue groups multiple findings, list each occurrence as a checklist item. let occurrencesSection = '' if (findings.length > 1) { const items = findings.map(f => `- [ ] ${f.html ? `\`${f.html}\` on ${f.url}` : f.url}`).join('\n') diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index 6e258f8..42a712e 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -4,12 +4,6 @@ function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string { return filing.issue.url } -/** - * Computes the dedup key for a finding based on the grouping mode. - * - 'finding' (default): one filing per individual violation (URL + rule + element). - * - 'rule': one filing per rule, aggregating every occurrence across all URLs. - * - 'rule+url': one filing per rule per scanned URL. - */ function getFindingKey(finding: Finding, groupBy: GroupBy): string { const rule = finding.ruleId ?? `${finding.scannerType};${finding.problemUrl}` @@ -54,13 +48,11 @@ export function updateFilingsWithNewFindings( const key = getFindingKey(finding, groupBy) const filingKey = findingKeys[key] if (filingKey) { - // This finding already maps to an existing issue; append it to that filing + // This finding already has an associated filing; add it to that filing's findings ;(filingKeys[filingKey] as RepeatedFiling).findings.push(finding) } else if (newFilingKeys[key]) { - // A new filing for this group already exists this run; append to it newFilingKeys[key].findings.push(finding) } else { - // First occurrence of this group with no existing issue; start a new filing newFilingKeys[key] = {findings: [finding]} } } From 84f022fabf45768e1aa48f16863348e7334f164c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:27:37 +0000 Subject: [PATCH 35/46] chore(deps): Bump concurrent-ruby in /sites/site-with-errors Bumps [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) from 1.3.5 to 1.3.7. - [Release notes](https://github.com/ruby-concurrency/concurrent-ruby/releases) - [Changelog](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/CHANGELOG.md) - [Commits](https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.3.5...v1.3.7) --- updated-dependencies: - dependency-name: concurrent-ruby dependency-version: 1.3.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- sites/site-with-errors/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/site-with-errors/Gemfile.lock b/sites/site-with-errors/Gemfile.lock index 16698ae..57c5380 100644 --- a/sites/site-with-errors/Gemfile.lock +++ b/sites/site-with-errors/Gemfile.lock @@ -6,7 +6,7 @@ GEM base64 (0.3.0) bigdecimal (3.2.2) colorator (1.1.0) - concurrent-ruby (1.3.5) + concurrent-ruby (1.3.7) csv (3.3.5) em-websocket (0.5.3) eventmachine (>= 0.12.9) From edc9c3fb7978a271a1a85945324b13b351b1d1b6 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Mon, 22 Jun 2026 16:50:12 -0700 Subject: [PATCH 36/46] Match axe findings by rule and report all failing elements --- .github/actions/file/src/generateIssueBody.ts | 24 ++++++- .github/actions/file/src/types.d.ts | 6 ++ .../file/src/updateFilingsWithNewFindings.ts | 7 +++ .../file/tests/generateIssueBody.test.ts | 19 ++++++ .../updateFilingsWithNewFindings.test.ts | 63 +++++++++++++++++++ .github/actions/find/src/findForUrl.ts | 7 +++ .github/actions/find/src/types.d.ts | 6 ++ .github/actions/find/tests/findForUrl.test.ts | 28 +++++++++ tests/site-with-errors.test.ts | 5 +- tests/types.d.ts | 6 ++ 10 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 .github/actions/file/tests/updateFilingsWithNewFindings.test.ts diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 18e25d3..297cbe6 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -25,7 +25,7 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str - [ ] This PR MUST NOT introduce any new accessibility issues or regressions.` const body = `## What -An accessibility scan ${finding.html ? `flagged the element \`${finding.html}\`` : `found an issue on ${finding.url}`} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. +${describeWhat(finding)} ${screenshotSection ?? ''} To fix this, ${finding.solutionShort}. @@ -36,3 +36,25 @@ ${acceptanceCriteria} return body } + +function describeWhat(finding: Finding): string { + const reason = `because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}.` + + // Axe findings carry every element that failed the rule. List them all so the + // issue reflects the full scope of the violation, not just one example node. + if (finding.nodes && finding.nodes.length > 0) { + const count = finding.nodes.length + const subject = count === 1 ? 'an element' : `${count} elements` + const elementList = finding.nodes + .map(node => `- \`${node.html}\`${node.target ? ` (selector: \`${node.target}\`)` : ''}`) + .join('\n') + const heading = count === 1 ? 'The following element needs' : 'The following elements need' + return `An accessibility scan flagged ${subject} on ${finding.url} ${reason}\n\n${heading} attention:\n\n${elementList}` + } + + if (finding.html) { + return `An accessibility scan flagged the element \`${finding.html}\` ${reason}` + } + + return `An accessibility scan found an issue on ${finding.url} ${reason}` +} diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index ee91bc6..ca7b71a 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -1,8 +1,14 @@ +export type FindingNode = { + html: string + target?: string +} + export type Finding = { scannerType: string ruleId?: string url: string html?: string + nodes?: FindingNode[] problemShort: string problemUrl: string solutionShort: string diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index eee1c6a..27244fc 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -5,6 +5,13 @@ function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string { } function getFindingKey(finding: Finding): string { + // Axe reports every element failing a rule under a single rule-level finding, + // so the rule itself is the stable identity. Keying on one element's exact + // markup was fragile: any DOM shift produced a new key and re-filed an issue + // that was already tracked. Axe findings therefore key on the rule, not HTML. + if (finding.scannerType === 'axe' && finding.ruleId) { + return `${finding.url};axe;${finding.ruleId}` + } if (finding.ruleId && finding.html) { return `${finding.url};${finding.ruleId};${finding.html}` } diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 167ee5f..976aa6d 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -76,4 +76,23 @@ describe('generateIssueBody', () => { expect(body).toContain(`found an issue on ${findingWithEmptyOptionalFields.url}`) expect(body).not.toContain('flagged the element') }) + + it('lists every node when the finding carries multiple elements', () => { + const body = generateIssueBody( + { + ...baseFinding, + html: 'first', + nodes: [ + {html: 'first', target: 'span.first'}, + {html: 'link', target: 'a.link'}, + ], + }, + 'github/accessibility-scanner', + ) + + expect(body).toContain('flagged 2 elements') + expect(body).toContain('- `first` (selector: `span.first`)') + expect(body).toContain('- `link` (selector: `a.link`)') + expect(body).not.toContain('flagged the element') + }) }) diff --git a/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts b/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts new file mode 100644 index 0000000..6750273 --- /dev/null +++ b/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts @@ -0,0 +1,63 @@ +import {describe, it, expect} from 'vitest' +import {updateFilingsWithNewFindings} from '../src/updateFilingsWithNewFindings.ts' +import type {Finding, RepeatedFiling} from '../src/types.d.ts' + +const cachedFinding: Finding = { + scannerType: 'axe', + ruleId: 'color-contrast', + url: 'https://example.com/', + html: '', + nodes: [{html: '', target: 'span.post-meta'}], + problemShort: 'elements must meet minimum color contrast ratio thresholds', + problemUrl: 'https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=playwright', + solutionShort: 'ensure the contrast meets WCAG thresholds', +} + +const cachedFiling: RepeatedFiling = { + issue: { + id: 1, + nodeId: 'node-1', + url: 'https://github.com/org/repo/issues/1', + title: 'Accessibility issue: color contrast on /', + }, + findings: [cachedFinding], +} + +describe('updateFilingsWithNewFindings', () => { + it('re-matches an axe finding to its existing issue after the element HTML shifts', () => { + // The same rule fails on the same page, but a layout change altered the + // element's surrounding markup. The finding should still map to issue #1 + // rather than being treated as a brand new violation. + const shiftedFinding: Finding = { + ...cachedFinding, + html: '', + nodes: [ + {html: '', target: 'div > span.post-meta'}, + ], + } + + const result = updateFilingsWithNewFindings([cachedFiling], [shiftedFinding]) + + expect(result).toHaveLength(1) + const filing = result[0] as RepeatedFiling + expect(filing.issue.url).toBe('https://github.com/org/repo/issues/1') + expect(filing.findings).toHaveLength(1) + expect(filing.findings[0].html).toContain('new container') + }) + + it('files a new issue when a different rule fails on the same page', () => { + const differentRule: Finding = { + ...cachedFinding, + ruleId: 'image-alt', + html: '', + nodes: [{html: '', target: 'img'}], + } + + const result = updateFilingsWithNewFindings([cachedFiling], [differentRule]) + + expect(result).toHaveLength(2) + const newFilings = result.filter(filing => filing.issue === undefined) + expect(newFilings).toHaveLength(1) + expect(newFilings[0].findings[0].ruleId).toBe('image-alt') + }) +}) diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index d9f1ea8..d40e44d 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -85,10 +85,17 @@ async function runAxeScan({ if (rawFindings) { for (const violation of rawFindings.violations) { + // Axe groups every element that fails a rule into one violation. Capture + // all of them so a single issue can report the rule's full scope on the + // page, and so matching keys on the rule rather than one element's markup. await addFinding({ scannerType: 'axe', url, html: violation.nodes[0].html.replace(/'/g, '''), + nodes: violation.nodes.map(node => ({ + html: node.html.replace(/'/g, '''), + target: node.target.map(part => (Array.isArray(part) ? part.join(' ') : part)).join(' '), + })), problemShort: violation.help.toLowerCase().replace(/'/g, '''), problemUrl: violation.helpUrl.replace(/'/g, '''), ruleId: violation.id, diff --git a/.github/actions/find/src/types.d.ts b/.github/actions/find/src/types.d.ts index dcbc860..d615ea5 100644 --- a/.github/actions/find/src/types.d.ts +++ b/.github/actions/find/src/types.d.ts @@ -1,7 +1,13 @@ +export type FindingNode = { + html: string + target?: string +} + export type Finding = { scannerType: string url: string html?: string + nodes?: FindingNode[] problemShort: string problemUrl: string solutionShort: string diff --git a/.github/actions/find/tests/findForUrl.test.ts b/.github/actions/find/tests/findForUrl.test.ts index 85299c5..9249a69 100644 --- a/.github/actions/find/tests/findForUrl.test.ts +++ b/.github/actions/find/tests/findForUrl.test.ts @@ -117,4 +117,32 @@ describe('findForUrl', () => { expect(loadedPlugins[1].default).toHaveBeenCalledTimes(0) }) }) + + it('captures every failing element of an axe violation as nodes', async () => { + actionInput = '' + clearAll() + + const violation = { + id: 'color-contrast', + help: 'Elements must meet minimum color contrast ratio thresholds', + helpUrl: 'https://dequeuniversity.com/rules/axe/4.10/color-contrast', + description: 'Ensure contrast meets WCAG thresholds', + nodes: [ + {html: 'one', target: ['span.one'], failureSummary: 'Fix any of the following:'}, + {html: 'two', target: ['div', 'span.two'], failureSummary: 'Fix any of the following:'}, + ], + } + vi.mocked(AxeBuilder.prototype.analyze).mockResolvedValueOnce({ + violations: [violation], + } as unknown as axe.AxeResults) + + const findings = await findForUrl('test.com') + + expect(findings).toHaveLength(1) + expect(findings[0].html).toBe('one') + expect(findings[0].nodes).toEqual([ + {html: 'one', target: 'span.one'}, + {html: 'two', target: 'div span.two'}, + ]) + }) }) diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index 8c7981b..6994862 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -39,13 +39,16 @@ describe('site-with-errors', () => { it('cache has expected results', () => { const actual = results.map(({issue: {url: issueUrl}, findings}) => { - const {problemUrl, solutionLong, screenshotId, ...finding} = findings[0] + const {problemUrl, solutionLong, screenshotId, nodes, ...finding} = findings[0] // Check volatile fields for existence only expect(issueUrl).toBeDefined() expect(problemUrl).toBeDefined() // Axe-specific assertions if (finding.scannerType === 'axe') { expect(solutionLong).toBeDefined() + expect(nodes).toBeDefined() + expect(nodes!.length).toBeGreaterThan(0) + expect(nodes![0].html).toBe(finding.html) expect(problemUrl.startsWith('https://dequeuniversity.com/rules/axe/')).toBe(true) expect(problemUrl.endsWith(`/${finding.ruleId}?application=playwright`)).toBe(true) } diff --git a/tests/types.d.ts b/tests/types.d.ts index b12077a..ea72c54 100644 --- a/tests/types.d.ts +++ b/tests/types.d.ts @@ -1,8 +1,14 @@ +export type FindingNode = { + html: string + target?: string +} + export type Finding = { scannerType: string ruleId?: string url: string html?: string + nodes?: FindingNode[] problemShort: string problemUrl: string solutionShort: string From d5afdd2ffff3ba2c5a05d6c8fb758f44863588db Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Mon, 22 Jun 2026 17:01:18 -0700 Subject: [PATCH 37/46] Trim verbose comments --- .github/actions/file/src/generateIssueBody.ts | 3 +-- .github/actions/file/src/updateFilingsWithNewFindings.ts | 6 ++---- .../actions/file/tests/updateFilingsWithNewFindings.test.ts | 4 +--- .github/actions/find/src/findForUrl.ts | 4 +--- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 297cbe6..b20ca00 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -40,8 +40,7 @@ ${acceptanceCriteria} function describeWhat(finding: Finding): string { const reason = `because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}.` - // Axe findings carry every element that failed the rule. List them all so the - // issue reflects the full scope of the violation, not just one example node. + // Axe carries every failing element; list them all, not just the first. if (finding.nodes && finding.nodes.length > 0) { const count = finding.nodes.length const subject = count === 1 ? 'an element' : `${count} elements` diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index 27244fc..2ab0a53 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -5,10 +5,8 @@ function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string { } function getFindingKey(finding: Finding): string { - // Axe reports every element failing a rule under a single rule-level finding, - // so the rule itself is the stable identity. Keying on one element's exact - // markup was fragile: any DOM shift produced a new key and re-filed an issue - // that was already tracked. Axe findings therefore key on the rule, not HTML. + // Axe groups every failing element under one rule, so key on the rule, not the + // element's HTML, which shifts with DOM changes and re-files tracked issues. if (finding.scannerType === 'axe' && finding.ruleId) { return `${finding.url};axe;${finding.ruleId}` } diff --git a/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts b/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts index 6750273..a1e447e 100644 --- a/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts +++ b/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts @@ -25,9 +25,7 @@ const cachedFiling: RepeatedFiling = { describe('updateFilingsWithNewFindings', () => { it('re-matches an axe finding to its existing issue after the element HTML shifts', () => { - // The same rule fails on the same page, but a layout change altered the - // element's surrounding markup. The finding should still map to issue #1 - // rather than being treated as a brand new violation. + // Same rule and page, but the element's markup shifted; should still map to issue #1. const shiftedFinding: Finding = { ...cachedFinding, html: '', diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index d40e44d..db31c6d 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -85,9 +85,7 @@ async function runAxeScan({ if (rawFindings) { for (const violation of rawFindings.violations) { - // Axe groups every element that fails a rule into one violation. Capture - // all of them so a single issue can report the rule's full scope on the - // page, and so matching keys on the rule rather than one element's markup. + // Capture every failing element, not just the first, so one issue covers the rule. await addFinding({ scannerType: 'axe', url, From b61d801e89af74b2b463887076fcad972c26fca8 Mon Sep 17 00:00:00 2001 From: Taarik <147209483+taarikashenafi@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:38:54 -0500 Subject: [PATCH 38/46] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Joyce Zhu --- .github/actions/file/src/generateIssueBody.ts | 2 +- .github/actions/file/src/updateFilingsWithNewFindings.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 91e10e6..27746d0 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -25,7 +25,7 @@ export function generateIssueBody(findingOrFindings: Finding | Finding[], screen if (findings.length > 1) { const items = findings.map(f => `- [ ] ${f.html ? `\`${f.html}\` on ${f.url}` : f.url}`).join('\n') occurrencesSection = ` -## Occurrences (${findings.length}) +## ${findings.length} Other Occurrences: ${items} ` diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index 42a712e..910c3c3 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -5,7 +5,7 @@ function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string { } function getFindingKey(finding: Finding, groupBy: GroupBy): string { - const rule = finding.ruleId ?? `${finding.scannerType};${finding.problemUrl}` + const rule = finding.ruleId ? `${finding.scannerType};${finding.ruleId}` : `${finding.scannerType};${finding.problemUrl}` switch (groupBy) { case 'rule': From 8997171e6374db454020ed40d0cdbdfacd6b37a1 Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Tue, 23 Jun 2026 17:53:32 -0500 Subject: [PATCH 39/46] Address review feedback: scannerType-safe keys, GroupBy source of truth, naming, docs --- .github/actions/file/src/generateIssueBody.ts | 4 +-- .github/actions/file/src/groupBy.ts | 7 +++++ .github/actions/file/src/index.ts | 18 +++-------- .github/actions/file/src/reopenIssue.ts | 2 +- .github/actions/file/src/types.d.ts | 2 -- .../file/src/updateFilingsWithNewFindings.ts | 7 +++-- .../file/tests/generateIssueBody.test.ts | 4 +-- .../updateFilingsWithNewFindings.test.ts | 31 ++++++++++++++----- README.md | 2 +- 9 files changed, 46 insertions(+), 31 deletions(-) create mode 100644 .github/actions/file/src/groupBy.ts diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 27746d0..5629936 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -1,7 +1,7 @@ import type {Finding} from './types.d.js' -export function generateIssueBody(findingOrFindings: Finding | Finding[], screenshotRepo: string): string { - const findings = Array.isArray(findingOrFindings) ? findingOrFindings : [findingOrFindings] +export function generateIssueBody(occurrences: Finding | Finding[], screenshotRepo: string): string { + const findings = Array.isArray(occurrences) ? occurrences : [occurrences] const finding = findings[0] const solutionLong = finding.solutionLong diff --git a/.github/actions/file/src/groupBy.ts b/.github/actions/file/src/groupBy.ts new file mode 100644 index 0000000..35d2c7b --- /dev/null +++ b/.github/actions/file/src/groupBy.ts @@ -0,0 +1,7 @@ +export const GROUP_BY_VALUES = ['finding', 'rule', 'rule+url'] as const + +export type GroupBy = (typeof GROUP_BY_VALUES)[number] + +export function isGroupBy(value: string): value is GroupBy { + return (GROUP_BY_VALUES as readonly string[]).includes(value) +} diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 6d95dfd..c72a381 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -1,12 +1,4 @@ -import type { - Finding, - ResolvedFiling, - RepeatedFiling, - FindingGroupIssue, - Filing, - IssueResponse, - GroupBy, -} from './types.d.js' +import type {Finding, ResolvedFiling, RepeatedFiling, FindingGroupIssue, Filing, IssueResponse} from './types.d.js' import fs from 'node:fs' import path from 'node:path' import process from 'node:process' @@ -21,6 +13,7 @@ import {isResolvedFiling} from './isResolvedFiling.js' import {openIssue} from './openIssue.js' import {reopenIssue} from './reopenIssue.js' import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' +import {GROUP_BY_VALUES, isGroupBy} from './groupBy.js' import {OctokitResponse} from '@octokit/types' const OctokitWithThrottling = Octokit.plugin(throttling) @@ -38,12 +31,11 @@ export default async function () { : [] const shouldOpenGroupedIssues = core.getBooleanInput('open_grouped_issues') const groupByInput = core.getInput('group_by') || 'finding' - const validGroupByValues: GroupBy[] = ['finding', 'rule', 'rule+url'] - if (!validGroupByValues.includes(groupByInput as GroupBy)) { - core.setFailed(`Invalid 'group_by' value: '${groupByInput}'. Must be one of: ${validGroupByValues.join(', ')}.`) + if (!isGroupBy(groupByInput)) { + core.setFailed(`Invalid 'group_by' value: '${groupByInput}'. Must be one of: ${GROUP_BY_VALUES.join(', ')}.`) return } - const groupBy = groupByInput as GroupBy + const groupBy = groupByInput const dryRun = core.getBooleanInput('dry_run') core.debug(`Input: 'findings_file: ${findingsFile}'`) core.debug(`Input: 'repository: ${repoWithOwner}'`) diff --git a/.github/actions/file/src/reopenIssue.ts b/.github/actions/file/src/reopenIssue.ts index b37c8ca..10705f3 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -11,7 +11,7 @@ export async function reopenIssue( screenshotRepo?: string, ) { let body: string | undefined - if (findings && findings.length > 0 && repoWithOwner) { + if (findings?.length && repoWithOwner) { body = generateIssueBody(findings, screenshotRepo ?? repoWithOwner) } diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index c7bf804..ee91bc6 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -1,5 +1,3 @@ -export type GroupBy = 'finding' | 'rule' | 'rule+url' - export type Finding = { scannerType: string ruleId?: string diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index 910c3c3..9c00be5 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -1,11 +1,14 @@ -import type {Finding, ResolvedFiling, NewFiling, RepeatedFiling, Filing, GroupBy} from './types.d.js' +import type {Finding, ResolvedFiling, NewFiling, RepeatedFiling, Filing} from './types.d.js' +import type {GroupBy} from './groupBy.js' function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string { return filing.issue.url } function getFindingKey(finding: Finding, groupBy: GroupBy): string { - const rule = finding.ruleId ? `${finding.scannerType};${finding.ruleId}` : `${finding.scannerType};${finding.problemUrl}` + const rule = finding.ruleId + ? `${finding.scannerType};${finding.ruleId}` + : `${finding.scannerType};${finding.problemUrl}` switch (groupBy) { case 'rule': diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index d3ad876..1bf0d4c 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -80,14 +80,14 @@ describe('generateIssueBody', () => { it('omits the Occurrences section for a single finding', () => { const body = generateIssueBody(baseFinding, 'github/accessibility-scanner') - expect(body).not.toContain('## Occurrences') + expect(body).not.toContain('Other Occurrences') }) it('renders an Occurrences checklist when given multiple findings', () => { const second = {...baseFinding, url: 'https://example.com/other', html: 'Link'} const body = generateIssueBody([baseFinding, second], 'github/accessibility-scanner') - expect(body).toContain('## Occurrences (2)') + expect(body).toContain('## 2 Other Occurrences:') expect(body).toContain(`- [ ] \`${baseFinding.html}\` on ${baseFinding.url}`) expect(body).toContain(`- [ ] \`${second.html}\` on ${second.url}`) }) diff --git a/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts b/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts index 336ba50..008e186 100644 --- a/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts +++ b/.github/actions/file/tests/updateFilingsWithNewFindings.test.ts @@ -1,7 +1,7 @@ import {describe, it, expect} from 'vitest' import {updateFilingsWithNewFindings} from '../src/updateFilingsWithNewFindings.ts' -const cc = (url: string, html: string) => ({ +const colorContrastFinding = (url: string, html: string) => ({ scannerType: 'axe', ruleId: 'color-contrast', url, @@ -13,9 +13,9 @@ const cc = (url: string, html: string) => ({ describe('updateFilingsWithNewFindings — group_by', () => { const findings = [ - cc('https://example.com/a', '1'), - cc('https://example.com/a', '2'), - cc('https://example.com/b', '3'), + colorContrastFinding('https://example.com/a', '1'), + colorContrastFinding('https://example.com/a', '2'), + colorContrastFinding('https://example.com/b', '3'), ] it("defaults to 'finding': one filing per individual violation", () => { @@ -46,7 +46,7 @@ describe('updateFilingsWithNewFindings — group_by', () => { url: 'https://github.com/org/repo/issues/1', title: 'color-contrast', }, - findings: [cc('https://example.com/a', '1')], + findings: [colorContrastFinding('https://example.com/a', '1')], }, ] const result = updateFilingsWithNewFindings(cached, findings, 'rule') @@ -58,13 +58,28 @@ describe('updateFilingsWithNewFindings — group_by', () => { it("keeps distinct rules separate under 'rule'", () => { const mixed = [ - cc('https://example.com/a', '1'), - {...cc('https://example.com/a', '

    x

    '), ruleId: 'heading-order'}, + colorContrastFinding('https://example.com/a', '1'), + {...colorContrastFinding('https://example.com/a', '

    x

    '), ruleId: 'heading-order'}, ] const result = updateFilingsWithNewFindings([], mixed, 'rule') expect(result).toHaveLength(2) }) + it("'rule': does not merge findings from different scanners that share a ruleId", () => { + const a = { + ...colorContrastFinding('https://example.com/a', '1'), + scannerType: 'axe', + ruleId: 'duplicate-id', + } + const b = { + ...colorContrastFinding('https://example.com/a', '2'), + scannerType: 'reflow', + ruleId: 'duplicate-id', + } + const result = updateFilingsWithNewFindings([], [a, b], 'rule') + expect(result).toHaveLength(2) + }) + it("'finding' (default) preserves the original 1:1 behavior with cached filings", () => { const cached = [ { @@ -74,7 +89,7 @@ describe('updateFilingsWithNewFindings — group_by', () => { url: 'https://github.com/org/repo/issues/1', title: 'color-contrast', }, - findings: [cc('https://example.com/a', '1')], + findings: [colorContrastFinding('https://example.com/a', '1')], }, ] const result = updateFilingsWithNewFindings(cached, findings) diff --git a/README.md b/README.md index 5330f79..d4ff986 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Trigger the workflow manually or automatically based on your configuration. The | `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` | | `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | | `open_grouped_issues` | No | Whether to create a tracking issue which groups filed issues together by violation type. Default: `false` | `true` | -| `group_by` | No | How to consolidate findings into issues: `finding` (default, one issue per individual violation), `rule` (one issue per rule, aggregating every occurrence across all scanned URLs), or `rule+url` (one issue per rule per scanned URL). Preferred over `open_grouped_issues` for consolidation. Default: `finding` | `rule` | +| `group_by` | No | How to consolidate findings into issues: `finding` (one issue per individual violation), `rule` (one issue per rule, aggregating every occurrence across all scanned URLs), or `rule+url` (one issue per rule per scanned URL). Preferred over `open_grouped_issues` for consolidation. Default: `finding` | `rule` | | `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | | `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | | `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | From 37b9db96725c1c54c76277ba5584fe7d269f2afa Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Wed, 24 Jun 2026 00:44:55 -0700 Subject: [PATCH 40/46] Batch wontfix lookups into a single set check --- .github/actions/file/src/index.ts | 22 +++-- .github/actions/file/src/isWontfixIssue.ts | 17 ---- .github/actions/file/src/shouldReopenIssue.ts | 40 +++++++++ .../actions/file/tests/isWontfixIssue.test.ts | 60 ------------- .../file/tests/shouldReopenIssue.test.ts | 90 +++++++++++++++++++ .../actions/file/tests/wontfixReopen.test.ts | 13 ++- 6 files changed, 149 insertions(+), 93 deletions(-) delete mode 100644 .github/actions/file/src/isWontfixIssue.ts create mode 100644 .github/actions/file/src/shouldReopenIssue.ts delete mode 100644 .github/actions/file/tests/isWontfixIssue.test.ts create mode 100644 .github/actions/file/tests/shouldReopenIssue.test.ts diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index fc1b8d3..832a701 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -10,7 +10,7 @@ import {closeIssue} from './closeIssue.js' import {isNewFiling} from './isNewFiling.js' import {isRepeatedFiling} from './isRepeatedFiling.js' import {isResolvedFiling} from './isResolvedFiling.js' -import {isWontfixIssue, WONTFIX_LABEL} from './isWontfixIssue.js' +import {getWontfixIssueNumbers, shouldReopenIssue, WONTFIX_LABEL} from './shouldReopenIssue.js' import {openIssue} from './openIssue.js' import {reopenIssue} from './reopenIssue.js' import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' @@ -61,6 +61,17 @@ export default async function () { }) const filings = updateFilingsWithNewFindings(cachedFilings, findings) + // Fetch closed wontfix issues once up front; a failed fetch reopens as usual + let wontfixIssueNumbers = new Set() + if (!dryRun) { + try { + const [owner, repository] = repoWithOwner.split('/') + wontfixIssueNumbers = await getWontfixIssueNumbers(octokit, {owner, repository}) + } catch (error) { + core.warning(`Could not fetch '${WONTFIX_LABEL}' issues; proceeding with reopen as usual: ${error}`) + } + } + // Track new issues for grouping const newIssuesByProblemShort: Record = {} const trackingIssueUrls: Record = {} @@ -108,14 +119,7 @@ export default async function () { } } else if (isRepeatedFiling(filing)) { const issue = new Issue(filing.issue) - let isWontfix = false - try { - isWontfix = await isWontfixIssue(octokit, issue) - } catch (error) { - // A failed label check shouldn't abort the run, so reopen as usual - core.warning(`Could not check labels for ${filing.issue.url}; proceeding with reopen: ${error}`) - } - if (isWontfix) { + if (!shouldReopenIssue(issue, wontfixIssueNumbers)) { // The developer intentionally closed this issue and labeled it 'wontfix', so leave it closed core.info(`Skipping reopen of issue labeled '${WONTFIX_LABEL}': ${filing.issue.url}`) } else { diff --git a/.github/actions/file/src/isWontfixIssue.ts b/.github/actions/file/src/isWontfixIssue.ts deleted file mode 100644 index bb1f770..0000000 --- a/.github/actions/file/src/isWontfixIssue.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type {Octokit} from '@octokit/core' -import type {Issue} from './Issue.js' - -/** Issues with this label are intentionally closed and should not be reopened. */ -export const WONTFIX_LABEL = 'wontfix' - -type IssueLabel = string | {name?: string} - -export async function isWontfixIssue(octokit: Octokit, {owner, repository, issueNumber}: Issue): Promise { - const response = await octokit.request(`GET /repos/${owner}/${repository}/issues/${issueNumber}`, { - owner, - repository, - issue_number: issueNumber, - }) - const labels = ((response.data as {labels?: IssueLabel[]}).labels ?? []) as IssueLabel[] - return labels.some(label => (typeof label === 'string' ? label : label.name) === WONTFIX_LABEL) -} diff --git a/.github/actions/file/src/shouldReopenIssue.ts b/.github/actions/file/src/shouldReopenIssue.ts new file mode 100644 index 0000000..d431ed9 --- /dev/null +++ b/.github/actions/file/src/shouldReopenIssue.ts @@ -0,0 +1,40 @@ +import type {Octokit} from '@octokit/core' +import type {Issue} from './Issue.js' + +/** Issues with this label are intentionally closed and should not be reopened. */ +export const WONTFIX_LABEL = 'wontfix' + +// Fetch every closed wontfix issue once so the per-filing check is a set lookup +export async function getWontfixIssueNumbers( + octokit: Octokit, + {owner, repository}: {owner: string; repository: string}, +): Promise> { + const wontfixIssueNumbers = new Set() + const perPage = 100 + for (let page = 1; ; page++) { + const response = await octokit.request(`GET /repos/${owner}/${repository}/issues`, { + owner, + repo: repository, + state: 'closed', + labels: WONTFIX_LABEL, + per_page: perPage, + page, + }) + const issues = (response.data as Array<{number: number; pull_request?: unknown}>) ?? [] + for (const issue of issues) { + // The issues endpoint also returns pull requests; skip them + if (!issue.pull_request) { + wontfixIssueNumbers.add(issue.number) + } + } + if (issues.length < perPage) { + break + } + } + return wontfixIssueNumbers +} + +// The single place to decide whether a repeated filing's issue should reopen +export function shouldReopenIssue(issue: Issue, wontfixIssueNumbers: Set): boolean { + return !wontfixIssueNumbers.has(issue.issueNumber) +} diff --git a/.github/actions/file/tests/isWontfixIssue.test.ts b/.github/actions/file/tests/isWontfixIssue.test.ts deleted file mode 100644 index d943aae..0000000 --- a/.github/actions/file/tests/isWontfixIssue.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {describe, it, expect, vi, beforeEach} from 'vitest' - -import {isWontfixIssue, WONTFIX_LABEL} from '../src/isWontfixIssue.ts' -import {Issue} from '../src/Issue.ts' - -const testIssue = new Issue({ - id: 42, - nodeId: 'MDU6SXNzdWU0Mg==', - url: 'https://github.com/org/filing-repo/issues/7', - title: 'Accessibility issue: test', - state: 'closed', -}) - -function mockOctokit(labels: unknown) { - return { - request: vi.fn().mockResolvedValue({data: {labels}}), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any -} - -describe('isWontfixIssue', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('returns true when the issue has the wontfix label (object form)', async () => { - const octokit = mockOctokit([{name: 'bug'}, {name: WONTFIX_LABEL}]) - - expect(await isWontfixIssue(octokit, testIssue)).toBe(true) - }) - - it('returns true when the issue has the wontfix label (string form)', async () => { - const octokit = mockOctokit(['bug', WONTFIX_LABEL]) - - expect(await isWontfixIssue(octokit, testIssue)).toBe(true) - }) - - it('returns false when the issue has no wontfix label', async () => { - const octokit = mockOctokit([{name: 'bug'}]) - - expect(await isWontfixIssue(octokit, testIssue)).toBe(false) - }) - - it('returns false when the issue has no labels', async () => { - const octokit = mockOctokit(undefined) - - expect(await isWontfixIssue(octokit, testIssue)).toBe(false) - }) - - it('requests the issue at the correct URL', async () => { - const octokit = mockOctokit([]) - - await isWontfixIssue(octokit, testIssue) - - expect(octokit.request).toHaveBeenCalledWith( - 'GET /repos/org/filing-repo/issues/7', - expect.objectContaining({issue_number: 7}), - ) - }) -}) diff --git a/.github/actions/file/tests/shouldReopenIssue.test.ts b/.github/actions/file/tests/shouldReopenIssue.test.ts new file mode 100644 index 0000000..7eedc13 --- /dev/null +++ b/.github/actions/file/tests/shouldReopenIssue.test.ts @@ -0,0 +1,90 @@ +import {describe, it, expect, vi, beforeEach} from 'vitest' + +import {getWontfixIssueNumbers, shouldReopenIssue, WONTFIX_LABEL} from '../src/shouldReopenIssue.ts' +import {Issue} from '../src/Issue.ts' + +function issueAt(issueNumber: number): Issue { + return new Issue({ + id: issueNumber, + nodeId: `node-${issueNumber}`, + url: `https://github.com/org/filing-repo/issues/${issueNumber}`, + title: `Accessibility issue ${issueNumber}`, + state: 'closed', + }) +} + +// `pages` is consumed one response per request call, in order. +function mockOctokit(pages: Array>) { + const request = vi.fn() + for (const page of pages) { + request.mockResolvedValueOnce({data: page}) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {request} as any +} + +describe('getWontfixIssueNumbers', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns the numbers of closed wontfix issues as a set', async () => { + const octokit = mockOctokit([[{number: 1}, {number: 5}, {number: 9}]]) + + const result = await getWontfixIssueNumbers(octokit, {owner: 'org', repository: 'repo'}) + + expect(result).toEqual(new Set([1, 5, 9])) + }) + + it('requests closed issues filtered by the wontfix label', async () => { + const octokit = mockOctokit([[]]) + + await getWontfixIssueNumbers(octokit, {owner: 'org', repository: 'repo'}) + + expect(octokit.request).toHaveBeenCalledWith( + 'GET /repos/org/repo/issues', + expect.objectContaining({state: 'closed', labels: WONTFIX_LABEL}), + ) + }) + + it('returns an empty set when no issues are labeled wontfix', async () => { + const octokit = mockOctokit([[]]) + + const result = await getWontfixIssueNumbers(octokit, {owner: 'org', repository: 'repo'}) + + expect(result.size).toBe(0) + }) + + it('paginates until a short page is returned', async () => { + const firstPage = Array.from({length: 100}, (_, i) => ({number: i + 1})) + const octokit = mockOctokit([firstPage, [{number: 101}]]) + + const result = await getWontfixIssueNumbers(octokit, {owner: 'org', repository: 'repo'}) + + expect(octokit.request).toHaveBeenCalledTimes(2) + expect(result.has(1)).toBe(true) + expect(result.has(101)).toBe(true) + }) + + it('ignores pull requests returned by the issues endpoint', async () => { + const octokit = mockOctokit([[{number: 2}, {number: 3, pull_request: {url: 'https://example.com/pull/3'}}]]) + + const result = await getWontfixIssueNumbers(octokit, {owner: 'org', repository: 'repo'}) + + expect(result).toEqual(new Set([2])) + }) +}) + +describe('shouldReopenIssue', () => { + it('returns false when the issue is in the wontfix set', () => { + expect(shouldReopenIssue(issueAt(7), new Set([7]))).toBe(false) + }) + + it('returns true when the issue is not in the wontfix set', () => { + expect(shouldReopenIssue(issueAt(7), new Set([1, 2, 3]))).toBe(true) + }) + + it('returns true when the wontfix set is empty', () => { + expect(shouldReopenIssue(issueAt(7), new Set())).toBe(true) + }) +}) diff --git a/.github/actions/file/tests/wontfixReopen.test.ts b/.github/actions/file/tests/wontfixReopen.test.ts index ee26871..b938d2b 100644 --- a/.github/actions/file/tests/wontfixReopen.test.ts +++ b/.github/actions/file/tests/wontfixReopen.test.ts @@ -38,7 +38,8 @@ vi.mock('node:fs', () => ({ }, })) -// Stub Octokit: `request` serves the GET that isWontfixIssue makes +// Stub Octokit: `request` serves the list of closed `wontfix` issues that +// getWontfixIssueNumbers fetches once up front. const octokitRequest = vi.fn() vi.mock('@octokit/core', () => ({ Octokit: { @@ -80,11 +81,9 @@ function setup() { inputs.cached_filings_file = '/tmp/cached.json' inputs.repository = 'org/repo' inputs.token = 'fake-token' - // GET issue: issue 1 is labeled wontfix, issue 3 is not + // Single up-front fetch: only issue 1 is closed and labeled wontfix octokitRequest.mockImplementation((route: string) => - route.includes('/issues/1') - ? Promise.resolve({data: {labels: [{name: 'wontfix'}]}}) - : Promise.resolve({data: {labels: []}}), + route.includes('GET /repos/org/repo/issues') ? Promise.resolve({data: [{number: 1}]}) : Promise.resolve({data: {}}), ) } @@ -124,13 +123,13 @@ describe('file action — wontfix label', () => { it('reopens as usual (and warns) when the label check fails', async () => { setup() - // The label-check GET fails for every issue (e.g. transient API error) + // The up-front wontfix fetch fails (e.g. transient API error) octokitRequest.mockRejectedValue(new Error('boom')) await runFileAction() // Both repeated filings should still be reopened rather than aborting the run expect(reopenIssue).toHaveBeenCalledTimes(2) - expect(warnLines.join('\n')).toContain('Could not check labels for') + expect(warnLines.join('\n')).toContain("Could not fetch 'wontfix' issues") }) }) From 63eef9aa25e2edffb1de5edb6823430d5fb70640 Mon Sep 17 00:00:00 2001 From: Taarik <147209483+taarikashenafi@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:24:36 -0500 Subject: [PATCH 41/46] Update README.md Co-authored-by: Joyce Zhu --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d4ff986..38ea1e8 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Trigger the workflow manually or automatically based on your configuration. The | `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` | | `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | | `open_grouped_issues` | No | Whether to create a tracking issue which groups filed issues together by violation type. Default: `false` | `true` | -| `group_by` | No | How to consolidate findings into issues: `finding` (one issue per individual violation), `rule` (one issue per rule, aggregating every occurrence across all scanned URLs), or `rule+url` (one issue per rule per scanned URL). Preferred over `open_grouped_issues` for consolidation. Default: `finding` | `rule` | +| `group_by` | No | How to consolidate findings when filing issues: `finding` (one issue per individual violation), `rule` (one issue per rule, aggregating every occurrence across all scanned URLs), or `rule+url` (one issue per rule per scanned URL). Preferred over `open_grouped_issues` for consolidation. Default: `finding` | | `reduced_motion` | No | Playwright `reducedMotion` setting for scan contexts. Allowed values: `reduce`, `no-preference` | `reduce` | | `color_scheme` | No | Playwright `colorScheme` setting for scan contexts. Allowed values: `light`, `dark`, `no-preference` | `dark` | | `scans` | No | An array of scans (or plugins) to be performed. If not provided, only Axe will be performed. | `'["axe", "reflow-scan", ...other plugins]'` | From 33d72ad6ce22a2f4b70edb7f669439a818301941 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Wed, 24 Jun 2026 12:00:53 -0700 Subject: [PATCH 42/46] Update README.md Co-authored-by: Joyce Zhu --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a734c37..2814517 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ If your login flow is more complex—if it requires two-factor authentication, s ## Keeping an issue closed with `wontfix` -When the scanner files an issue for an accessibility finding and that same finding turns up again on a later run, it reopens the issue (if you'd closed it) so the barrier doesn't get lost. Sometimes, though, you may want a closed issue to _stay_ closed—for example, if you've decided not to act on a particular finding, or you're tracking the work somewhere else. +When the scanner files an issue for an accessibility finding and that same finding turns up again on a later run, it reopens closed issues so the problem doesn't get lost. Sometimes, though, you may want a closed issue to _stay_ closed -- for example, if you've decided not to act on a particular finding, or if you're already tracking the work outside of GitHub issues. To stop the scanner from reopening a closed issue, add the **`wontfix`** label to it. On its next run, the scanner sees the label and skips reopening the issue, leaving it closed. From f898bda75b9d3ad498c0e5551a5a4f4a9f5c7323 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Wed, 24 Jun 2026 13:18:57 -0700 Subject: [PATCH 43/46] Wording changes Co-authored-by: Joyce Zhu --- .github/actions/file/src/generateIssueBody.ts | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index eb75ab6..a7e3dd3 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -22,7 +22,7 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str finding.category && finding.category !== 'wcag' ? `**Note:** This is ${ finding.category === 'experimental' ? 'an experimental check' : 'a best-practice recommendation' - }, not a hard WCAG failure.\n\n` + }, not a definite WCAG failure.\n\n` : '' const standardsLine = diff --git a/README.md b/README.md index fa8cf0e..f8a5ff4 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Trigger the workflow manually or automatically based on your configuration. The | Input | Required | Description | Example | | --------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `urls` | No\* | Newline-delimited list of URLs to scan. Required unless `url_configs` is provided. | `https://primer.style`
    `https://primer.style/octicons` | +| `urls` | No | Newline-delimited list of URLs to scan. Required unless `url_configs` is provided. | `https://primer.style`
    `https://primer.style/octicons` | | `repository` | Yes | Repository (with owner) for issues and PRs | `primer/primer-docs` | | `token` | Yes | PAT with write permissions (see above) | `${{ secrets.GH_TOKEN }}` | | `cache_key` | Yes | Key for caching results across runs
    Allowed: `A-Za-z0-9._/-` | `cached_results-primer.style-main.json` | From 10248572a137781df5a30359bd2d00ba7039ca21 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Wed, 24 Jun 2026 14:14:50 -0700 Subject: [PATCH 44/46] Use GFM note alert, WCAG 2.2, and core.getBooleanInput --- .github/actions/file/src/generateIssueBody.ts | 6 ++--- .github/actions/file/src/index.ts | 11 +++------- .../file/tests/generateIssueBody.test.ts | 22 +++++++++---------- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index a7e3dd3..f0167ff 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -20,15 +20,15 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str const categoryNotice = finding.category && finding.category !== 'wcag' - ? `**Note:** This is ${ + ? `> [!NOTE]\n> This is ${ finding.category === 'experimental' ? 'an experimental check' : 'a best-practice recommendation' }, not a definite WCAG failure.\n\n` : '' const standardsLine = finding.category && finding.category !== 'wcag' - ? '- [ ] The fix MUST meet the accessibility standards specified by the repository or organization (WCAG 2.1 if applicable).' - : '- [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization.' + ? '- [ ] The fix MUST meet the accessibility standards specified by the repository or organization (WCAG 2.2 if applicable).' + : '- [ ] The fix MUST meet WCAG 2.2 guidelines OR the accessibility standards specified by the repository or organization.' const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific violation reported in this issue is no longer reproducible. diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 1d25ced..f3ad2b9 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -16,15 +16,10 @@ import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' import {OctokitResponse} from '@octokit/types' const OctokitWithThrottling = Octokit.plugin(throttling) -// Throws when an input is unset, so this defaults unset -// switches while still rejecting values that aren't a valid boolean. +// core.getBooleanInput throws on unset inputs, so apply the default first. function getBooleanInputWithDefault(name: string, defaultValue: boolean): boolean { - const raw = core.getInput(name) - if (!raw) return defaultValue - const normalized = raw.trim().toLowerCase() - if (normalized === 'true') return true - if (normalized === 'false') return false - throw new TypeError(`Invalid boolean input '${name}': '${raw}'. Expected 'true' or 'false'.`) + if (!core.getInput(name)) return defaultValue + return core.getBooleanInput(name) } export default async function () { diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 7a6ec42..7fbabe5 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -26,7 +26,7 @@ describe('generateIssueBody', () => { expect(body).toContain('## What') expect(body).toContain('## Acceptance Criteria') expect(body).toContain('The specific violation reported in this issue is no longer reproducible.') - expect(body).toContain('The fix MUST meet WCAG 2.1 guidelines OR') + expect(body).toContain('The fix MUST meet WCAG 2.2 guidelines OR') expect(body).not.toContain('Specifically:') }) @@ -79,29 +79,29 @@ describe('generateIssueBody', () => { }) it('omits the category notice for WCAG findings', () => { - expect(generateIssueBody(baseFinding, 'github/accessibility-scanner')).not.toContain('**Note:**') + expect(generateIssueBody(baseFinding, 'github/accessibility-scanner')).not.toContain('> [!NOTE]') expect(generateIssueBody({...baseFinding, category: 'wcag'}, 'github/accessibility-scanner')).not.toContain( - '**Note:**', + '> [!NOTE]', ) }) it('includes a best-practice notice for best-practice findings', () => { const body = generateIssueBody({...baseFinding, category: 'best-practice'}, 'github/accessibility-scanner') - expect(body).toContain('**Note:**') + expect(body).toContain('> [!NOTE]') expect(body).toContain('best-practice recommendation') - expect(body).toContain('not a hard WCAG failure') - expect(body).toContain('WCAG 2.1 if applicable') - expect(body).not.toContain('The fix MUST meet WCAG 2.1 guidelines OR') + expect(body).toContain('not a definite WCAG failure') + expect(body).toContain('WCAG 2.2 if applicable') + expect(body).not.toContain('The fix MUST meet WCAG 2.2 guidelines OR') }) it('includes an experimental notice for experimental findings', () => { const body = generateIssueBody({...baseFinding, category: 'experimental'}, 'github/accessibility-scanner') - expect(body).toContain('**Note:**') + expect(body).toContain('> [!NOTE]') expect(body).toContain('an experimental check') - expect(body).toContain('not a hard WCAG failure') - expect(body).toContain('WCAG 2.1 if applicable') - expect(body).not.toContain('The fix MUST meet WCAG 2.1 guidelines OR') + expect(body).toContain('not a definite WCAG failure') + expect(body).toContain('WCAG 2.2 if applicable') + expect(body).not.toContain('The fix MUST meet WCAG 2.2 guidelines OR') }) }) From f9a77a46d896dd3f1d3ac5ddc09afee4e1a6b5ff Mon Sep 17 00:00:00 2001 From: taarikashenafi Date: Thu, 25 Jun 2026 18:26:21 -0500 Subject: [PATCH 45/46] Fix openIssue array signature and category tests after merge --- .github/actions/file/src/openIssue.ts | 4 ++-- .github/actions/file/tests/openIssue.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index ed74a8f..67128d0 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -28,8 +28,8 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding labels.push(`${primary.scannerType} rule: ${primary.ruleId}`) } // Flag non-WCAG findings so they can be filtered or triaged separately - if (finding.category && finding.category !== 'wcag') { - labels.push(finding.category) + if (primary.category && primary.category !== 'wcag') { + labels.push(primary.category) } const count = findings.length diff --git a/.github/actions/file/tests/openIssue.test.ts b/.github/actions/file/tests/openIssue.test.ts index a376666..890f843 100644 --- a/.github/actions/file/tests/openIssue.test.ts +++ b/.github/actions/file/tests/openIssue.test.ts @@ -67,7 +67,7 @@ describe('openIssue', () => { it('adds a category label for non-WCAG findings', async () => { const octokit = mockOctokit() - await openIssue(octokit, 'org/repo', {...baseFinding, category: 'best-practice'}) + await openIssue(octokit, 'org/repo', [{...baseFinding, category: 'best-practice'}]) expect(octokit.request).toHaveBeenCalledWith( expect.any(String), @@ -79,7 +79,7 @@ describe('openIssue', () => { it('does not add a category label for WCAG findings', async () => { const octokit = mockOctokit() - await openIssue(octokit, 'org/repo', {...baseFinding, category: 'wcag'}) + await openIssue(octokit, 'org/repo', [{...baseFinding, category: 'wcag'}]) const labels = octokit.request.mock.calls[0][1].labels expect(labels).not.toContain('wcag') From 03e48182f3275c6b54d13115ddc814a319d38d27 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Fri, 26 Jun 2026 17:19:24 -0700 Subject: [PATCH 46/46] Rename describeWhat to describeFinding --- .github/actions/file/src/generateIssueBody.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 047d2c8..d1e3d80 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -37,7 +37,7 @@ ${standardsLine} - [ ] This PR MUST NOT introduce any new accessibility issues or regressions.` const body = `${categoryNotice}## What -${describeWhat(finding)} +${describeFinding(finding)} ${screenshotSection ?? ''} To fix this, ${finding.solutionShort}. @@ -49,7 +49,7 @@ ${acceptanceCriteria} return body } -function describeWhat(finding: Finding): string { +function describeFinding(finding: Finding): string { const reason = `because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}.` // Axe carries every failing element; list them all, not just the first.