From d4dde6f820729436d42d2423d0ac880dbbc83897 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:45:19 +0000 Subject: [PATCH 01/54] 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.275.0 to 1.281.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/d354de180d0c9e813cfddfcbdc079945d4be589b...675dd7ba1b06c8786a1480d89c384f5620a42647) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.281.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 9fa8a72..ba8f613 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@d354de180d0c9e813cfddfcbdc079945d4be589b + uses: ruby/setup-ruby@675dd7ba1b06c8786a1480d89c384f5620a42647 with: ruby-version: "3.4" bundler-cache: true From ff2a69ec2effe3636eb1387875050a5a7099189f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:43:16 +0000 Subject: [PATCH 02/54] 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 7.1.0 to 7.2.0 - [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/v7.1.0...v7.2.0) --- updated-dependencies: - dependency-name: puma dependency-version: 7.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: bundler-minor-and-patch ... Signed-off-by: dependabot[bot] --- sites/site-with-errors/Gemfile | 2 +- sites/site-with-errors/Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sites/site-with-errors/Gemfile b/sites/site-with-errors/Gemfile index 289d1ab..40ff7b5 100644 --- a/sites/site-with-errors/Gemfile +++ b/sites/site-with-errors/Gemfile @@ -34,4 +34,4 @@ gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] # Web server gem "rack", "~> 3.2" -gem "puma", "~> 7.1" \ No newline at end of file +gem "puma", "~> 7.2" \ No newline at end of file diff --git a/sites/site-with-errors/Gemfile.lock b/sites/site-with-errors/Gemfile.lock index 0a89ba5..d63b76e 100644 --- a/sites/site-with-errors/Gemfile.lock +++ b/sites/site-with-errors/Gemfile.lock @@ -95,11 +95,11 @@ GEM jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - nio4r (2.7.4) + nio4r (2.7.5) pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (6.0.2) - puma (7.1.0) + puma (7.2.0) nio4r (~> 2.0) rack (3.2.4) rake (13.3.0) @@ -171,7 +171,7 @@ DEPENDENCIES jekyll (~> 4.4.1) jekyll-feed (~> 0.12) minima (~> 2.5) - puma (~> 7.1) + puma (~> 7.2) rack (~> 3.2) tzinfo (>= 1, < 3) tzinfo-data From adcc4e969dd6c77724e79cd88d08cb32cd26e039 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:01:26 +0000 Subject: [PATCH 03/54] chore(deps): Bump the npm-minor-and-patch group across 5 directories with 3 updates Bumps the npm-minor-and-patch group with 2 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). Bumps the npm-minor-and-patch group with 2 updates in the /.github/actions/auth directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [playwright](https://github.com/microsoft/playwright). 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 2 updates in the /.github/actions/find directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [playwright](https://github.com/microsoft/playwright). 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.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `vitest` from 4.0.16 to 4.0.18 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.18/packages/vitest) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `playwright` from 1.57.0 to 1.58.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.57.0...v1.58.1) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `playwright` from 1.57.0 to 1.58.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.57.0...v1.58.1) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `playwright` from 1.57.0 to 1.58.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.57.0...v1.58.1) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `playwright` from 1.57.0 to 1.58.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.57.0...v1.58.1) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 25.0.3 to 25.2.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.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: vitest dependency-version: 4.0.18 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: playwright dependency-version: 1.58.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: playwright dependency-version: 1.58.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: playwright dependency-version: 1.58.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: playwright dependency-version: 1.58.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@types/node" dependency-version: 25.2.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 | 24 +- .github/actions/auth/package.json | 4 +- .github/actions/file/package-lock.json | 9 +- .github/actions/file/package.json | 2 +- .github/actions/find/package-lock.json | 25 +- .github/actions/find/package.json | 4 +- .github/actions/fix/package-lock.json | 9 +- .github/actions/fix/package.json | 2 +- package-lock.json | 335 ++++++++++++++----------- package.json | 4 +- 10 files changed, 228 insertions(+), 190 deletions(-) diff --git a/.github/actions/auth/package-lock.json b/.github/actions/auth/package-lock.json index 48a1790..d311f11 100644 --- a/.github/actions/auth/package-lock.json +++ b/.github/actions/auth/package-lock.json @@ -10,10 +10,10 @@ "license": "MIT", "dependencies": { "@actions/core": "^2.0.1", - "playwright": "^1.57.0" + "playwright": "^1.58.1" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } }, @@ -62,9 +62,9 @@ } }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "dev": true, "license": "MIT", "dependencies": { @@ -86,12 +86,12 @@ } }, "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz", + "integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0" + "playwright-core": "1.58.1" }, "bin": { "playwright": "cli.js" @@ -104,9 +104,9 @@ } }, "node_modules/playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz", + "integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" diff --git a/.github/actions/auth/package.json b/.github/actions/auth/package.json index 4e79bfe..bcf6f10 100644 --- a/.github/actions/auth/package.json +++ b/.github/actions/auth/package.json @@ -14,10 +14,10 @@ "type": "module", "dependencies": { "@actions/core": "^2.0.1", - "playwright": "^1.57.0" + "playwright": "^1.58.1" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } } \ No newline at end of file diff --git a/.github/actions/file/package-lock.json b/.github/actions/file/package-lock.json index 511b3cd..0aed60d 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.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } }, @@ -76,7 +76,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -177,9 +176,9 @@ } }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/.github/actions/file/package.json b/.github/actions/file/package.json index 754b3ea..d18a810 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.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } } diff --git a/.github/actions/find/package-lock.json b/.github/actions/find/package-lock.json index af1a912..0da5db9 100644 --- a/.github/actions/find/package-lock.json +++ b/.github/actions/find/package-lock.json @@ -11,10 +11,10 @@ "dependencies": { "@actions/core": "^2.0.1", "@axe-core/playwright": "^4.11.0", - "playwright": "^1.57.0" + "playwright": "^1.58.1" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } }, @@ -75,9 +75,9 @@ } }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "dev": true, "license": "MIT", "dependencies": { @@ -108,12 +108,12 @@ } }, "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz", + "integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0" + "playwright-core": "1.58.1" }, "bin": { "playwright": "cli.js" @@ -126,11 +126,10 @@ } }, "node_modules/playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz", + "integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==", "license": "Apache-2.0", - "peer": true, "bin": { "playwright-core": "cli.js" }, diff --git a/.github/actions/find/package.json b/.github/actions/find/package.json index 05c85fa..878981a 100644 --- a/.github/actions/find/package.json +++ b/.github/actions/find/package.json @@ -15,10 +15,10 @@ "dependencies": { "@actions/core": "^2.0.1", "@axe-core/playwright": "^4.11.0", - "playwright": "^1.57.0" + "playwright": "^1.58.1" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } } \ No newline at end of file diff --git a/.github/actions/fix/package-lock.json b/.github/actions/fix/package-lock.json index 5775460..eb77729 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.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } }, @@ -76,7 +76,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -177,9 +176,9 @@ } }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/.github/actions/fix/package.json b/.github/actions/fix/package.json index 9c11f05..9d1acba 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.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } } diff --git a/package-lock.json b/package-lock.json index 423256e..985db5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,8 @@ "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", - "@types/node": "^25.0.3", - "vitest": "^4.0.16" + "@types/node": "^25.2.0", + "vitest": "^4.0.18" } }, "node_modules/@actions/core": { @@ -531,7 +531,6 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -639,9 +638,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "cpu": [ "arm" ], @@ -653,9 +652,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "cpu": [ "arm64" ], @@ -667,9 +666,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "cpu": [ "arm64" ], @@ -681,9 +680,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "cpu": [ "x64" ], @@ -695,9 +694,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "cpu": [ "arm64" ], @@ -709,9 +708,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "cpu": [ "x64" ], @@ -723,9 +722,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "cpu": [ "arm" ], @@ -737,9 +736,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "cpu": [ "arm" ], @@ -751,9 +750,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "cpu": [ "arm64" ], @@ -765,9 +764,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "cpu": [ "arm64" ], @@ -779,9 +778,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", "cpu": [ "loong64" ], @@ -793,9 +806,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", "cpu": [ "ppc64" ], @@ -807,9 +834,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "cpu": [ "riscv64" ], @@ -821,9 +848,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "cpu": [ "riscv64" ], @@ -835,9 +862,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "cpu": [ "s390x" ], @@ -849,9 +876,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", "cpu": [ "x64" ], @@ -863,9 +890,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "cpu": [ "x64" ], @@ -876,10 +903,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", "cpu": [ "arm64" ], @@ -891,9 +932,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "cpu": [ "arm64" ], @@ -905,9 +946,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "cpu": [ "ia32" ], @@ -919,9 +960,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", "cpu": [ "x64" ], @@ -933,9 +974,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "cpu": [ "x64" ], @@ -979,27 +1020,26 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, "node_modules/@vitest/expect": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", - "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.16", - "@vitest/utils": "4.0.16", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" }, @@ -1008,13 +1048,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", - "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.16", + "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1035,9 +1075,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", - "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, "license": "MIT", "dependencies": { @@ -1048,13 +1088,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", - "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.16", + "@vitest/utils": "4.0.18", "pathe": "^2.0.3" }, "funding": { @@ -1062,13 +1102,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", - "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.16", + "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1077,9 +1117,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", - "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", "dev": true, "license": "MIT", "funding": { @@ -1087,13 +1127,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", - "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.16", + "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" }, "funding": { @@ -1125,9 +1165,9 @@ "license": "MIT" }, "node_modules/chai": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", - "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { @@ -1313,7 +1353,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1351,9 +1390,9 @@ } }, "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", "dependencies": { @@ -1367,28 +1406,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" } }, @@ -1505,12 +1547,11 @@ "license": "ISC" }, "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -1581,19 +1622,19 @@ } }, "node_modules/vitest": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", - "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.16", - "@vitest/mocker": "4.0.16", - "@vitest/pretty-format": "4.0.16", - "@vitest/runner": "4.0.16", - "@vitest/snapshot": "4.0.16", - "@vitest/spy": "4.0.16", - "@vitest/utils": "4.0.16", + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", @@ -1621,10 +1662,10 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.16", - "@vitest/browser-preview": "4.0.16", - "@vitest/browser-webdriverio": "4.0.16", - "@vitest/ui": "4.0.16", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, diff --git a/package.json b/package.json index ce59fd6..64d3653 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", - "@types/node": "^25.0.3", - "vitest": "^4.0.16" + "@types/node": "^25.2.0", + "vitest": "^4.0.18" } } From 41d88d58ef2f3ad378bcdd42accce3ce3d5ad1f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 23:10:54 +0000 Subject: [PATCH 04/54] 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.281.0 to 1.288.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/675dd7ba1b06c8786a1480d89c384f5620a42647...09a7688d3b55cf0e976497ff046b70949eeaccfd) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.288.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 ba8f613..60874c8 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@675dd7ba1b06c8786a1480d89c384f5620a42647 + uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd with: ruby-version: "3.4" bundler-cache: true From ba75df3f87cac0fde583c424c131a36abc34b09e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 23:12:20 +0000 Subject: [PATCH 05/54] chore(deps-dev): Bump @actions/core from 2.0.1 to 3.0.0 Bumps [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) from 2.0.1 to 3.0.0. - [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md) - [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core) --- updated-dependencies: - dependency-name: "@actions/core" dependency-version: 3.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 55 ++++++++++++++++++----------------------------- package.json | 2 +- 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 985db5e..16cd5fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-development", "license": "MIT", "devDependencies": { - "@actions/core": "^2.0.1", + "@actions/core": "^3.0.0", "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", @@ -18,41 +18,41 @@ } }, "node_modules/@actions/core": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.1.tgz", - "integrity": "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", + "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", "dev": true, "license": "MIT", "dependencies": { - "@actions/exec": "^2.0.0", - "@actions/http-client": "^3.0.0" + "@actions/exec": "^3.0.0", + "@actions/http-client": "^4.0.0" } }, "node_modules/@actions/exec": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-2.0.0.tgz", - "integrity": "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", + "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==", "dev": true, "license": "MIT", "dependencies": { - "@actions/io": "^2.0.0" + "@actions/io": "^3.0.2" } }, "node_modules/@actions/http-client": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.0.tgz", - "integrity": "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", "dev": true, "license": "MIT", "dependencies": { "tunnel": "^0.0.6", - "undici": "^5.28.5" + "undici": "^6.23.0" } }, "node_modules/@actions/io": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-2.0.0.tgz", - "integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz", + "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", "dev": true, "license": "MIT" }, @@ -498,16 +498,6 @@ "node": ">=18" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1520,16 +1510,13 @@ } }, "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "dev": true, "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=18.17" } }, "node_modules/undici-types": { diff --git a/package.json b/package.json index 64d3653..4a49675 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "homepage": "https://github.com/github/accessibility-scanner#readme", "devDependencies": { - "@actions/core": "^2.0.1", + "@actions/core": "^3.0.0", "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", From 5b279949ff5cbef5c2f488f712416c417280c7be Mon Sep 17 00:00:00 2001 From: Joyce Zhu Date: Fri, 13 Feb 2026 15:52:15 -0500 Subject: [PATCH 06/54] Revert "chore(deps): Bump puma from 7.1.0 to 7.2.0 in /sites/site-with-errors in the bundler-minor-and-patch group" --- sites/site-with-errors/Gemfile | 2 +- sites/site-with-errors/Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sites/site-with-errors/Gemfile b/sites/site-with-errors/Gemfile index 40ff7b5..289d1ab 100644 --- a/sites/site-with-errors/Gemfile +++ b/sites/site-with-errors/Gemfile @@ -34,4 +34,4 @@ gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] # Web server gem "rack", "~> 3.2" -gem "puma", "~> 7.2" \ No newline at end of file +gem "puma", "~> 7.1" \ No newline at end of file diff --git a/sites/site-with-errors/Gemfile.lock b/sites/site-with-errors/Gemfile.lock index d63b76e..0a89ba5 100644 --- a/sites/site-with-errors/Gemfile.lock +++ b/sites/site-with-errors/Gemfile.lock @@ -95,11 +95,11 @@ GEM jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - nio4r (2.7.5) + nio4r (2.7.4) pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (6.0.2) - puma (7.2.0) + puma (7.1.0) nio4r (~> 2.0) rack (3.2.4) rake (13.3.0) @@ -171,7 +171,7 @@ DEPENDENCIES jekyll (~> 4.4.1) jekyll-feed (~> 0.12) minima (~> 2.5) - puma (~> 7.2) + puma (~> 7.1) rack (~> 3.2) tzinfo (>= 1, < 3) tzinfo-data From 05c638da19f58dd506e16eea25b572c530f7f6c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:29:28 +0000 Subject: [PATCH 07/54] chore(deps): Bump rack from 3.2.4 to 3.2.5 in /sites/site-with-errors Bumps [rack](https://github.com/rack/rack) from 3.2.4 to 3.2.5. - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](https://github.com/rack/rack/compare/v3.2.4...v3.2.5) --- updated-dependencies: - dependency-name: rack dependency-version: 3.2.5 dependency-type: direct:production ... 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 0a89ba5..b2b560f 100644 --- a/sites/site-with-errors/Gemfile.lock +++ b/sites/site-with-errors/Gemfile.lock @@ -101,7 +101,7 @@ GEM public_suffix (6.0.2) puma (7.1.0) nio4r (~> 2.0) - rack (3.2.4) + rack (3.2.5) rake (13.3.0) rb-fsevent (0.11.2) rb-inotify (0.11.1) From 06024ab1d7eb76d1acad2dcf4ede48e854e6833e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:38:34 +0000 Subject: [PATCH 08/54] chore(deps): Bump puma Bumps the bundler-minor-and-patch group with 1 update in the /sites/site-with-errors directory: [puma](https://github.com/puma/puma). Updates `puma` from 7.1.0 to 7.2.0 - [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/v7.1.0...v7.2.0) --- updated-dependencies: - dependency-name: puma dependency-version: 7.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: bundler-minor-and-patch ... Signed-off-by: dependabot[bot] --- sites/site-with-errors/Gemfile | 2 +- sites/site-with-errors/Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sites/site-with-errors/Gemfile b/sites/site-with-errors/Gemfile index 289d1ab..40ff7b5 100644 --- a/sites/site-with-errors/Gemfile +++ b/sites/site-with-errors/Gemfile @@ -34,4 +34,4 @@ gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] # Web server gem "rack", "~> 3.2" -gem "puma", "~> 7.1" \ No newline at end of file +gem "puma", "~> 7.2" \ No newline at end of file diff --git a/sites/site-with-errors/Gemfile.lock b/sites/site-with-errors/Gemfile.lock index b2b560f..a391568 100644 --- a/sites/site-with-errors/Gemfile.lock +++ b/sites/site-with-errors/Gemfile.lock @@ -95,11 +95,11 @@ GEM jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - nio4r (2.7.4) + nio4r (2.7.5) pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (6.0.2) - puma (7.1.0) + puma (7.2.0) nio4r (~> 2.0) rack (3.2.5) rake (13.3.0) @@ -171,7 +171,7 @@ DEPENDENCIES jekyll (~> 4.4.1) jekyll-feed (~> 0.12) minima (~> 2.5) - puma (~> 7.1) + puma (~> 7.2) rack (~> 3.2) tzinfo (>= 1, < 3) tzinfo-data From 7a084757a409319a6d1d8fd896f4e307a6e273d0 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:25:41 +0000 Subject: [PATCH 09/54] Extracts issue body generation in prepration for adding screenshots --- .github/actions/file/src/generateIssueBody.ts | 31 +++++++++++++++++++ .github/actions/file/src/openIssue.ts | 27 ++-------------- 2 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 .github/actions/file/src/generateIssueBody.ts diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts new file mode 100644 index 0000000..e82624e --- /dev/null +++ b/.github/actions/file/src/generateIssueBody.ts @@ -0,0 +1,31 @@ +import type { Finding } from "./types.d.js"; + +export function generateIssueBody(finding: Finding, repoWithOwner: string): string { + const solutionLong = finding.solutionLong + ?.split("\n") + .map((line: string) => + !line.trim().startsWith("Fix any") && + !line.trim().startsWith("Fix all") && + line.trim() !== "" + ? `- ${line}` + : line + ) + .join("\n"); + const acceptanceCriteria = `## Acceptance Criteria + - [ ] The specific axe 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 axe violation does not regress. + - [ ] This PR MUST NOT introduce any new accessibility issues or regressions. + `; + const body = `## What + An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. + + To fix this, ${finding.solutionShort}. + ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} + + ${acceptanceCriteria} + `; + + return body; +} + diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 9751772..3220ddf 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -1,5 +1,6 @@ import type { Octokit } from '@octokit/core'; import type { Finding } from './types.d.js'; +import { generateIssueBody } from "./generateIssueBody.js"; import * as url from 'node:url' const URL = url.URL; @@ -23,32 +24,10 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding const labels = [`${finding.scannerType} rule: ${finding.ruleId}`, `${finding.scannerType}-scanning-issue`]; const title = truncateWithEllipsis( `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, - GITHUB_ISSUE_TITLE_MAX_LENGTH + GITHUB_ISSUE_TITLE_MAX_LENGTH, ); - const solutionLong = finding.solutionLong - ?.split("\n") - .map((line) => - !line.trim().startsWith("Fix any") && - !line.trim().startsWith("Fix all") && - line.trim() !== "" - ? `- ${line}` - : line - ) - .join("\n"); - const acceptanceCriteria = `## Acceptance Criteria -- [ ] The specific axe 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 axe violation does not regress. -- [ ] This PR MUST NOT introduce any new accessibility issues or regressions. -`; - const body = `## What -An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. -To fix this, ${finding.solutionShort}. -${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} - -${acceptanceCriteria} -`; + const body = generateIssueBody(finding, repoWithOwner); return octokit.request(`POST /repos/${owner}/${repo}/issues`, { owner, From 0e99f4a2891ea229b687313381375ef9b69fe642 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:35:32 +0000 Subject: [PATCH 10/54] Adds test --- tests/generateIssueBody.test.ts | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/generateIssueBody.test.ts diff --git a/tests/generateIssueBody.test.ts b/tests/generateIssueBody.test.ts new file mode 100644 index 0000000..f5ff211 --- /dev/null +++ b/tests/generateIssueBody.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { generateIssueBody } from "../.github/actions/file/src/generateIssueBody.ts"; + +const baseFinding = { + 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?application=playwright", + solutionShort: "ensure the contrast between foreground and background colors meets WCAG thresholds", +}; + +describe("generateIssueBody", () => { + it("includes acceptance criteria and omits the Specifically section when solutionLong is missing", () => { + const body = generateIssueBody(baseFinding, "github/accessibility-scanner"); + + expect(body).toContain("## What"); + expect(body).toContain("## Acceptance Criteria"); + expect(body).toContain("The specific axe violation reported in this issue is no longer reproducible."); + expect(body).not.toContain("Specifically:"); + }); + + it("formats solutionLong lines into bullets while preserving Fix any/Fix all lines", () => { + const body = generateIssueBody( + { + ...baseFinding, + solutionLong: [ + "Use a darker foreground color.", + "Fix any of the following:", + "Increase font weight.", + "Fix all of the following:", + "Add a non-color visual indicator.", + "", + ].join("\n"), + }, + "github/accessibility-scanner", + ); + + expect(body).toContain("Specifically:"); + expect(body).toContain("- Use a darker foreground color."); + expect(body).toContain("Fix any of the following:"); + expect(body).toContain("- Increase font weight."); + expect(body).toContain("Fix all of the following:"); + expect(body).toContain("- Add a non-color visual indicator."); + + expect(body).not.toContain("- Fix any of the following:"); + expect(body).not.toContain("- Fix all of the following:"); + }); +}); From 7c85a7e5ed1830ef79eb573b392093085a59d61c Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:39:34 +0000 Subject: [PATCH 11/54] Adds tests, updates package.json to include new tests --- {tests => .github/actions/file/tests}/generateIssueBody.test.ts | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename {tests => .github/actions/file/tests}/generateIssueBody.test.ts (95%) diff --git a/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts similarity index 95% rename from tests/generateIssueBody.test.ts rename to .github/actions/file/tests/generateIssueBody.test.ts index f5ff211..b269964 100644 --- a/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { generateIssueBody } from "../.github/actions/file/src/generateIssueBody.ts"; +import { generateIssueBody } from "../src/generateIssueBody.ts"; const baseFinding = { scannerType: "axe", diff --git a/package.json b/package.json index 4a49675..3d6a0c4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0-development", "description": "Finds potential accessibility gaps, files GitHub issues to track them, and attempts to fix them with Copilot", "scripts": { - "test": "vitest run tests/*.test.ts" + "test": "vitest run tests/*.test.ts .github/actions/**/tests/*.test.ts" }, "repository": { "type": "git", From 14a7ae9f45672775bf359af915de987c31979b07 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:27:39 +0000 Subject: [PATCH 12/54] Adds screenshots to issues --- .github/actions/file/src/generateIssueBody.ts | 18 +++++- .github/actions/file/src/types.d.ts | 1 + .github/actions/find/action.yml | 4 ++ .github/actions/find/src/findForUrl.ts | 59 +++++++++++++++++-- .github/actions/find/src/index.ts | 9 ++- .github/actions/find/src/types.d.ts | 1 + .github/actions/gh-cache/delete/action.yml | 10 ++-- .github/actions/gh-cache/restore/action.yml | 3 + .github/actions/gh-cache/save/action.yml | 11 ++-- action.yml | 11 ++++ 10 files changed, 109 insertions(+), 18 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index e82624e..543a35a 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -11,17 +11,29 @@ export function generateIssueBody(finding: Finding, repoWithOwner: string): stri : line ) .join("\n"); - const acceptanceCriteria = `## Acceptance Criteria + + let screenshotSection; + if (finding.screenshotId) { + const screenshotUrl = `https://raw.githubusercontent.com/${repoWithOwner}/gh-cache/.screenshots/${finding.screenshotId}.png`; + screenshotSection = `\n\n + + ![Screenshot of the issue on ${finding.url}](${screenshotUrl}) + `; + } + + const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific axe 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 axe violation does not regress. - [ ] This PR MUST NOT introduce any new accessibility issues or regressions. `; - const body = `## What + + const body = `## What An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. + ${screenshotSection ?? ""} To fix this, ${finding.solutionShort}. - ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} + ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ""} ${acceptanceCriteria} `; diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index bcc52ea..e42f9b3 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -7,6 +7,7 @@ export type Finding = { problemUrl: string; solutionShort: string; solutionLong?: string; + screenshotId?: string; }; export type Issue = { diff --git a/.github/actions/find/action.yml b/.github/actions/find/action.yml index 696d11d..372ae72 100644 --- a/.github/actions/find/action.yml +++ b/.github/actions/find/action.yml @@ -9,6 +9,10 @@ inputs: auth_context: description: "Stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session" required: false + include_screenshots: + description: "Whether to capture screenshots of scanned pages" + required: false + default: "true" outputs: findings: diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 3bcd3fa..a1ebb00 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -2,9 +2,25 @@ import type { Finding } from './types.d.js'; import AxeBuilder from '@axe-core/playwright' import playwright from 'playwright'; import { AuthContext } from './AuthContext.js'; +import fs from "node:fs"; +import path from "node:path"; -export async function findForUrl(url: string, authContext?: AuthContext): Promise { - const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined }); +// Use GITHUB_WORKSPACE to ensure screenshots are saved in the workflow workspace root +// where the artifact upload step can find them +const SCREENSHOT_DIR = path.join( + process.env.GITHUB_WORKSPACE || process.cwd(), + ".screenshots", +); + +export async function findForUrl( + url: string, + authContext?: AuthContext, + includeScreenshots: boolean = true, +): Promise { + const browser = await playwright.chromium.launch({ + headless: true, + executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined, + }); const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {}; const context = await browser.newContext(contextOptions); const page = await context.newPage(); @@ -14,15 +30,46 @@ export async function findForUrl(url: string, authContext?: AuthContext): Promis let findings: Finding[] = []; try { const rawFindings = await new AxeBuilder({ page }).analyze(); - findings = rawFindings.violations.map(violation => ({ - scannerType: 'axe', + + let screenshotId: string | undefined; + + if (includeScreenshots) { + // Ensure screenshot directory exists + if (!fs.existsSync(SCREENSHOT_DIR)) { + fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); + console.log(`Created screenshot directory: ${SCREENSHOT_DIR}`); + } + + try { + const screenshotBuffer = await page.screenshot({ + fullPage: true, + type: "png", + }); + + screenshotId = crypto.randomUUID(); + const filename = `${screenshotId}.png`; + const filepath = path.join(SCREENSHOT_DIR, filename); + + fs.writeFileSync(filepath, screenshotBuffer); + console.log(`Screenshot saved: ${filename}`); + } catch (error) { + console.error("Failed to capture/save screenshot:", error); + screenshotId = undefined; + } + } + + findings = rawFindings.violations.map((violation) => ({ + scannerType: "axe", url, html: violation.nodes[0].html.replace(/'/g, "'"), problemShort: violation.help.toLowerCase().replace(/'/g, "'"), problemUrl: violation.helpUrl.replace(/'/g, "'"), ruleId: violation.id, - solutionShort: violation.description.toLowerCase().replace(/'/g, "'"), - solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'") + solutionShort: violation.description + .toLowerCase() + .replace(/'/g, "'"), + solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'"), + screenshotId, })); } catch (e) { // do something with the error diff --git a/.github/actions/find/src/index.ts b/.github/actions/find/src/index.ts index e596647..ba9c7e8 100644 --- a/.github/actions/find/src/index.ts +++ b/.github/actions/find/src/index.ts @@ -12,10 +12,17 @@ export default async function () { ); const authContext = new AuthContext(authContextInput); + const includeScreenshots = + core.getInput("include_screenshots", { required: false }) !== "false"; + let findings = []; for (const url of urls) { core.info(`Preparing to scan ${url}`); - const findingsForUrl = await findForUrl(url, authContext); + const findingsForUrl = await findForUrl( + url, + authContext, + includeScreenshots, + ); if (findingsForUrl.length === 0) { core.info(`No accessibility gaps were found on ${url}`); continue; diff --git a/.github/actions/find/src/types.d.ts b/.github/actions/find/src/types.d.ts index ee0ea27..c9470f6 100644 --- a/.github/actions/find/src/types.d.ts +++ b/.github/actions/find/src/types.d.ts @@ -5,6 +5,7 @@ export type Finding = { problemUrl: string; solutionShort: string; solutionLong?: string; + screenshotId?: string; }; export type Cookie = { diff --git a/.github/actions/gh-cache/delete/action.yml b/.github/actions/gh-cache/delete/action.yml index 4bea795..26fb7c6 100644 --- a/.github/actions/gh-cache/delete/action.yml +++ b/.github/actions/gh-cache/delete/action.yml @@ -60,20 +60,22 @@ runs: echo "Created new orphaned 'gh-cache' branch" fi - - name: Copy file to repo + - name: Delete file from repo shell: bash run: | - if [ -f "${{ inputs.path }}" ]; then + if [ -e "${{ inputs.path }}" ]; then rm -Rf "${{ inputs.path }}" - rm -Rf ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" echo "Deleted '${{ inputs.path }}' from 'gh-cache' branch" + else + echo "'${{ inputs.path }}' does not exist in 'gh-cache' branch" + echo "Skipping delete" fi - name: Commit and push shell: bash working-directory: .gh-cache-${{ github.run_id }} run: | - git add "${{ inputs.path }}" || true + git add --all -- "${{ inputs.path }}" || true git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git commit -m "$(printf 'Delete artifact: %s' "${{ inputs.path }}")" \ diff --git a/.github/actions/gh-cache/restore/action.yml b/.github/actions/gh-cache/restore/action.yml index 26d0ecc..8ea6950 100644 --- a/.github/actions/gh-cache/restore/action.yml +++ b/.github/actions/gh-cache/restore/action.yml @@ -72,6 +72,9 @@ runs: src=".gh-cache-${{ github.run_id }}/${{ inputs.path }}" dest="${{ inputs.path }}" if [ -e "$src" ]; then + if [ -d "$src" ]; then + rm -rf "$dest" + fi mkdir -p "$(dirname "$dest")" cp -Rf "$src" "$dest" echo "Restored '${{ inputs.path }}' from cache" diff --git a/.github/actions/gh-cache/save/action.yml b/.github/actions/gh-cache/save/action.yml index 9ecc2c9..c0fe5e5 100644 --- a/.github/actions/gh-cache/save/action.yml +++ b/.github/actions/gh-cache/save/action.yml @@ -60,21 +60,24 @@ runs: echo "Created new orphaned 'gh-cache' branch" fi - - name: Copy file to repo + - name: Copy file or directory to repo shell: bash run: | - if [ -f "${{ inputs.path }}" ]; then + if [ -e "${{ inputs.path }}" ]; then mkdir -p ".gh-cache-${{ github.run_id }}/$(dirname "${{ inputs.path }}")" cp -Rf "${{ inputs.path }}" ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" echo "Copied '${{ inputs.path }}' to 'gh-cache' branch" + else + echo "'${{ inputs.path }}' does not exist" + echo "Skipping copy" fi - name: Commit and push shell: bash working-directory: .gh-cache-${{ github.run_id }} run: | - if [ -f "${{ inputs.path }}" ]; then - git add "${{ inputs.path }}" + if [ -e "${{ inputs.path }}" ]; then + git add -- "${{ inputs.path }}" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git commit -m "$(printf 'Save artifact: %s' "${{ inputs.path }}")" \ diff --git a/action.yml b/action.yml index 3e3ee60..a7f8999 100644 --- a/action.yml +++ b/action.yml @@ -31,6 +31,10 @@ inputs: description: "Whether to skip assigning filed issues to Copilot" required: false default: "false" + include_screenshots: + description: "Whether to include screenshots of issues in the issue bodies" + required: false + default: "true" outputs: results: @@ -80,6 +84,7 @@ runs: with: urls: ${{ inputs.urls }} auth_context: ${{ inputs.auth_context || steps.auth.outputs.auth_context }} + include_screenshots: ${{ inputs.include_screenshots }} - name: File id: file uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/file @@ -125,6 +130,12 @@ runs: } core.setOutput('results', JSON.stringify(results)); core.debug(`Results: ${JSON.stringify(results)}`); + - if: ${{ inputs.include_screenshots == 'true' }} + name: Save screenshots + uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/save + with: + path: .screenshots + token: ${{ inputs.token }} - name: Save cached results uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/cache with: From 177a78aa26846be5613e7bceaeacfd44c1b698af Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:31:03 +0000 Subject: [PATCH 13/54] Updates to wait until DOMContentLoaded, moves Axe scan --- .github/actions/find/src/findForUrl.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index a1ebb00..d242f7d 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -25,12 +25,11 @@ export async function findForUrl( const context = await browser.newContext(contextOptions); const page = await context.newPage(); await page.goto(url); + await page.waitForLoadState("domcontentloaded"); console.log(`Scanning ${page.url()}`); let findings: Finding[] = []; try { - const rawFindings = await new AxeBuilder({ page }).analyze(); - let screenshotId: string | undefined; if (includeScreenshots) { @@ -58,6 +57,7 @@ export async function findForUrl( } } + const rawFindings = await new AxeBuilder({ page }).analyze(); findings = rawFindings.violations.map((violation) => ({ scannerType: "axe", url, @@ -72,7 +72,7 @@ export async function findForUrl( screenshotId, })); } catch (e) { - // do something with the error + console.error("Error during accessibility scan:", e); } await context.close(); await browser.close(); From 7b42fcaf853a0da79247178e902d4ad9cfc4aaef Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:35:34 +0000 Subject: [PATCH 14/54] Fixes markdown --- .github/actions/file/src/generateIssueBody.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 543a35a..6d947c9 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -15,10 +15,9 @@ export function generateIssueBody(finding: Finding, repoWithOwner: string): stri let screenshotSection; if (finding.screenshotId) { const screenshotUrl = `https://raw.githubusercontent.com/${repoWithOwner}/gh-cache/.screenshots/${finding.screenshotId}.png`; - screenshotSection = `\n\n - - ![Screenshot of the issue on ${finding.url}](${screenshotUrl}) - `; + screenshotSection = ` +![Screenshot of the issue on ${finding.url}](${screenshotUrl}) +`; } const acceptanceCriteria = `## Acceptance Criteria From ae45dcdcb302078dda521b16af661e10d44a927d Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:56:36 +0000 Subject: [PATCH 15/54] Fixes screenshot location --- .github/actions/gh-cache/save/action.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/actions/gh-cache/save/action.yml b/.github/actions/gh-cache/save/action.yml index c0fe5e5..524a0e5 100644 --- a/.github/actions/gh-cache/save/action.yml +++ b/.github/actions/gh-cache/save/action.yml @@ -65,7 +65,12 @@ runs: run: | if [ -e "${{ inputs.path }}" ]; then mkdir -p ".gh-cache-${{ github.run_id }}/$(dirname "${{ inputs.path }}")" - cp -Rf "${{ inputs.path }}" ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" + if [ -d "${{ inputs.path }}" ]; then + mkdir -p ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" + cp -Rf "${{ inputs.path }}/." ".gh-cache-${{ github.run_id }}/${{ inputs.path }}/" + else + cp -Rf "${{ inputs.path }}" ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" + fi echo "Copied '${{ inputs.path }}' to 'gh-cache' branch" else echo "'${{ inputs.path }}' does not exist" From c7fa1f5cdbec42d4cf9593004bb48eb842741db6 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:17:13 +0000 Subject: [PATCH 16/54] Moves axe builder back --- .github/actions/find/src/findForUrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index d242f7d..3478954 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -30,6 +30,7 @@ export async function findForUrl( let findings: Finding[] = []; try { + const rawFindings = await new AxeBuilder({ page }).analyze(); let screenshotId: string | undefined; if (includeScreenshots) { @@ -57,7 +58,6 @@ export async function findForUrl( } } - const rawFindings = await new AxeBuilder({ page }).analyze(); findings = rawFindings.violations.map((violation) => ({ scannerType: "axe", url, From e7ace49d029a08be77b0b22a40e3db87633e5376 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:27:50 +0000 Subject: [PATCH 17/54] Updates save action --- .github/actions/gh-cache/save/action.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/actions/gh-cache/save/action.yml b/.github/actions/gh-cache/save/action.yml index 524a0e5..ee5df6f 100644 --- a/.github/actions/gh-cache/save/action.yml +++ b/.github/actions/gh-cache/save/action.yml @@ -64,17 +64,24 @@ runs: shell: bash run: | if [ -e "${{ inputs.path }}" ]; then - mkdir -p ".gh-cache-${{ github.run_id }}/$(dirname "${{ inputs.path }}")" if [ -d "${{ inputs.path }}" ]; then + # For directories, copy contents to avoid nesting mkdir -p ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" - cp -Rf "${{ inputs.path }}/." ".gh-cache-${{ github.run_id }}/${{ inputs.path }}/" + # Check if directory has files before copying + if [ "$(ls -A "${{ inputs.path }}" 2>/dev/null)" ]; then + cp -Rf "${{ inputs.path }}"/* ".gh-cache-${{ github.run_id }}/${{ inputs.path }}"/ + echo "Copied directory contents of '${{ inputs.path }}' to 'gh-cache' branch" + else + echo "Directory '${{ inputs.path }}' is empty, nothing to copy" + fi else + # For files, copy file to path + mkdir -p ".gh-cache-${{ github.run_id }}/$(dirname "${{ inputs.path }}")" cp -Rf "${{ inputs.path }}" ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" + echo "Copied file '${{ inputs.path }}' to 'gh-cache' branch" fi - echo "Copied '${{ inputs.path }}' to 'gh-cache' branch" else echo "'${{ inputs.path }}' does not exist" - echo "Skipping copy" fi - name: Commit and push From 706e36300adfd190f89d04120ba7a010a48b749b Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:34:54 +0000 Subject: [PATCH 18/54] Ensures re-opened issues are updated --- .github/actions/file/src/index.ts | 9 ++++++-- .github/actions/file/src/reopenIssue.ts | 29 +++++++++++++++++++------ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 0c1304b..20160b5 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -64,8 +64,13 @@ export default async function () { response = await openIssue(octokit, repoWithOwner, filing.findings[0]); (filing as any).issue = { state: "open" } as Issue; } else if (isRepeatedFiling(filing)) { - // Reopen the filing’s issue (if necessary) - response = await reopenIssue(octokit, new Issue(filing.issue)); + // 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, + ); filing.issue.state = "reopened"; } if (response?.data && filing.issue) { diff --git a/.github/actions/file/src/reopenIssue.ts b/.github/actions/file/src/reopenIssue.ts index e777bfd..042e75f 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -1,11 +1,26 @@ import type { Octokit } from '@octokit/core'; import type { Issue } from './Issue.js'; +import type { Finding } from "./types.d.js"; +import { generateIssueBody } from "./generateIssueBody.js"; -export async function reopenIssue(octokit: Octokit, { owner, repository, issueNumber}: Issue) { - return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { - owner, - repository, - issue_number: issueNumber, - state: 'open' - }); +export async function reopenIssue( + octokit: Octokit, + { owner, repository, issueNumber }: Issue, + finding?: Finding, + repoWithOwner?: string, +) { + const body = + finding && repoWithOwner + ? generateIssueBody(finding, repoWithOwner) + : undefined; + return octokit.request( + `PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, + { + owner, + repository, + issue_number: issueNumber, + state: "open", + ...(body ? { body } : {}), + }, + ); } From dba8913b94d1c2325e0714f34f1c5c13a1ad28e4 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:39:54 +0000 Subject: [PATCH 19/54] Updates action --- action.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/action.yml b/action.yml index a7f8999..365f7c1 100644 --- a/action.yml +++ b/action.yml @@ -85,6 +85,12 @@ runs: urls: ${{ inputs.urls }} auth_context: ${{ inputs.auth_context || steps.auth.outputs.auth_context }} include_screenshots: ${{ inputs.include_screenshots }} + - name: Save Screenshots to gh-cache + if: always() + uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/save + with: + path: .screenshots + token: ${{ inputs.token }} - name: File id: file uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/file From b3430a222eba91311ac7bf39193a70a4e7584650 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:00:21 +0000 Subject: [PATCH 20/54] Changes screenshot to link --- .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 6d947c9..c69800c 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -14,9 +14,9 @@ export function generateIssueBody(finding: Finding, repoWithOwner: string): stri let screenshotSection; if (finding.screenshotId) { - const screenshotUrl = `https://raw.githubusercontent.com/${repoWithOwner}/gh-cache/.screenshots/${finding.screenshotId}.png`; + const screenshotUrl = `https://github.com/${repoWithOwner}/blob/gh-cache/.screenshots/${finding.screenshotId}.png`; screenshotSection = ` -![Screenshot of the issue on ${finding.url}](${screenshotUrl}) +[View screenshot](${screenshotUrl}) `; } From 4618a36f96bd818bb38df2fb45debf11f5f89210 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:23:39 +0000 Subject: [PATCH 21/54] Removes duplicate save workflow --- action.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/action.yml b/action.yml index 365f7c1..a7f8999 100644 --- a/action.yml +++ b/action.yml @@ -85,12 +85,6 @@ runs: urls: ${{ inputs.urls }} auth_context: ${{ inputs.auth_context || steps.auth.outputs.auth_context }} include_screenshots: ${{ inputs.include_screenshots }} - - name: Save Screenshots to gh-cache - if: always() - uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/save - with: - path: .screenshots - token: ${{ inputs.token }} - name: File id: file uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/file From 6d230ded5cc5398c52af5f3284f96376e9653fc4 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:27:49 +0000 Subject: [PATCH 22/54] Attempt to fix matrix gh-cache push --- .github/actions/gh-cache/save/action.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/actions/gh-cache/save/action.yml b/.github/actions/gh-cache/save/action.yml index ee5df6f..ac35a1f 100644 --- a/.github/actions/gh-cache/save/action.yml +++ b/.github/actions/gh-cache/save/action.yml @@ -94,12 +94,17 @@ runs: git config user.email "github-actions[bot]@users.noreply.github.com" git commit -m "$(printf 'Save artifact: %s' "${{ inputs.path }}")" \ && echo "Committed '${{ inputs.path }}' to 'gh-cache' branch" \ - || echo "No changes to commit" - if git ls-remote --exit-code --heads origin gh-cache >/dev/null; then + || { echo "No changes to commit"; exit 0; } + # Push with retry to handle concurrent updates to gh-cache + for attempt in 1 2 3; do + if git push origin gh-cache 2>/dev/null; then + echo "Pushed to 'gh-cache' branch" + break + fi + echo "Push failed (attempt $attempt), rebasing and retrying..." git fetch origin gh-cache git rebase origin/gh-cache - fi - git push origin gh-cache + done else echo "'${{ inputs.path }}' does not exist" echo "Skipping commit and push" From 3420115c2803ac129acdbcee0ba9176d45328ee0 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:50:33 +0000 Subject: [PATCH 23/54] Removes gh-cache updates --- .github/actions/gh-cache/save/action.yml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/actions/gh-cache/save/action.yml b/.github/actions/gh-cache/save/action.yml index ac35a1f..ee5df6f 100644 --- a/.github/actions/gh-cache/save/action.yml +++ b/.github/actions/gh-cache/save/action.yml @@ -94,17 +94,12 @@ runs: git config user.email "github-actions[bot]@users.noreply.github.com" git commit -m "$(printf 'Save artifact: %s' "${{ inputs.path }}")" \ && echo "Committed '${{ inputs.path }}' to 'gh-cache' branch" \ - || { echo "No changes to commit"; exit 0; } - # Push with retry to handle concurrent updates to gh-cache - for attempt in 1 2 3; do - if git push origin gh-cache 2>/dev/null; then - echo "Pushed to 'gh-cache' branch" - break - fi - echo "Push failed (attempt $attempt), rebasing and retrying..." + || echo "No changes to commit" + if git ls-remote --exit-code --heads origin gh-cache >/dev/null; then git fetch origin gh-cache git rebase origin/gh-cache - done + fi + git push origin gh-cache else echo "'${{ inputs.path }}' does not exist" echo "Skipping commit and push" From d462476337e935305f4157c8ef482fd286fac8bd Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:59:23 +0000 Subject: [PATCH 24/54] Adds screenshot repo to ensure we are targeting the correct gh-cache branch --- .github/actions/file/action.yml | 3 +++ .github/actions/file/src/generateIssueBody.ts | 4 ++-- .github/actions/file/src/index.ts | 5 ++++- .github/actions/file/src/openIssue.ts | 4 ++-- .github/actions/file/src/reopenIssue.ts | 3 ++- .../file/tests/generateIssueBody.test.ts | 17 +++++++++++++++++ action.yml | 1 + 7 files changed, 31 insertions(+), 6 deletions(-) diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index 005d3b0..436d2f2 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -14,6 +14,9 @@ inputs: cached_filings: description: "Cached filings from previous runs, as stringified JSON. Without this, duplicate issues may be filed." required: false + screenshot_repo: + description: "Repository (with owner) where screenshots are stored on the gh-cache branch. Defaults to the 'repository' input if not set." + required: false outputs: filings: diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index c69800c..7bb03d9 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -1,6 +1,6 @@ import type { Finding } from "./types.d.js"; -export function generateIssueBody(finding: Finding, repoWithOwner: string): string { +export function generateIssueBody(finding: Finding, screenshotRepo: string): string { const solutionLong = finding.solutionLong ?.split("\n") .map((line: string) => @@ -14,7 +14,7 @@ export function generateIssueBody(finding: Finding, repoWithOwner: string): stri let screenshotSection; if (finding.screenshotId) { - const screenshotUrl = `https://github.com/${repoWithOwner}/blob/gh-cache/.screenshots/${finding.screenshotId}.png`; + const screenshotUrl = `https://github.com/${screenshotRepo}/blob/gh-cache/.screenshots/${finding.screenshotId}.png`; screenshotSection = ` [View screenshot](${screenshotUrl}) `; diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 20160b5..88eaad3 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -20,11 +20,13 @@ export default async function () { ); const repoWithOwner = core.getInput("repository", { required: true }); const token = core.getInput("token", { required: true }); + const screenshotRepo = core.getInput("screenshot_repo", { required: false }) || repoWithOwner; const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( core.getInput("cached_filings", { required: false }) || "[]" ); core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`); core.debug(`Input: 'repository: ${repoWithOwner}'`); + core.debug(`Input: 'screenshot_repo: ${screenshotRepo}'`); core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`); const octokit = new OctokitWithThrottling({ @@ -61,7 +63,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]); + response = await openIssue(octokit, repoWithOwner, filing.findings[0], screenshotRepo); (filing as any).issue = { state: "open" } as Issue; } else if (isRepeatedFiling(filing)) { // Reopen the filing's issue (if necessary) and update the body with the latest finding @@ -70,6 +72,7 @@ export default async function () { new Issue(filing.issue), filing.findings[0], repoWithOwner, + screenshotRepo, ); filing.issue.state = "reopened"; } diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 3220ddf..54e2bc1 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -17,7 +17,7 @@ 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) { +export async function openIssue(octokit: Octokit, repoWithOwner: string, finding: Finding, screenshotRepo?: string) { const owner = repoWithOwner.split('/')[0]; const repo = repoWithOwner.split('/')[1]; @@ -27,7 +27,7 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding GITHUB_ISSUE_TITLE_MAX_LENGTH, ); - const body = generateIssueBody(finding, repoWithOwner); + const body = generateIssueBody(finding, 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 042e75f..70f2ae3 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -8,10 +8,11 @@ export async function reopenIssue( { owner, repository, issueNumber }: Issue, finding?: Finding, repoWithOwner?: string, + screenshotRepo?: string, ) { const body = finding && repoWithOwner - ? generateIssueBody(finding, repoWithOwner) + ? generateIssueBody(finding, screenshotRepo ?? repoWithOwner) : undefined; return octokit.request( `PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index b269964..fe7b8a1 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -47,4 +47,21 @@ describe("generateIssueBody", () => { expect(body).not.toContain("- Fix any of the following:"); expect(body).not.toContain("- Fix all of the following:"); }); + + it("uses the screenshotRepo for the screenshot URL, not the filing repo", () => { + const body = generateIssueBody( + { ...baseFinding, screenshotId: "abc123" }, + "github/my-workflow-repo", + ); + + expect(body).toContain("github/my-workflow-repo/blob/gh-cache/.screenshots/abc123.png"); + expect(body).not.toContain("github/accessibility-scanner"); + }); + + it("omits screenshot section when screenshotId is not present", () => { + const body = generateIssueBody(baseFinding, "github/accessibility-scanner"); + + expect(body).not.toContain("View screenshot"); + expect(body).not.toContain(".screenshots"); + }); }); diff --git a/action.yml b/action.yml index a7f8999..db35874 100644 --- a/action.yml +++ b/action.yml @@ -93,6 +93,7 @@ runs: repository: ${{ inputs.repository }} token: ${{ inputs.token }} cached_filings: ${{ steps.normalize_cache.outputs.value }} + screenshot_repo: ${{ github.repository }} - if: ${{ steps.file.outputs.filings }} name: Get issues from filings id: get_issues_from_filings From c4fbd5b9a820db7c87c86e6501f6b034173e1bf7 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:28:56 +0000 Subject: [PATCH 25/54] Minor tweaks after initial code review --- .github/actions/file/action.yml | 2 +- .github/actions/find/action.yml | 2 +- .github/actions/gh-cache/delete/action.yml | 1 + .github/actions/gh-cache/restore/action.yml | 3 --- action.yml | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index 436d2f2..8337bb2 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -15,7 +15,7 @@ inputs: description: "Cached filings from previous runs, as stringified JSON. Without this, duplicate issues may be filed." required: false screenshot_repo: - description: "Repository (with owner) where screenshots are stored on the gh-cache branch. Defaults to the 'repository' input if not set." + 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." required: false outputs: diff --git a/.github/actions/find/action.yml b/.github/actions/find/action.yml index 372ae72..e238a00 100644 --- a/.github/actions/find/action.yml +++ b/.github/actions/find/action.yml @@ -10,7 +10,7 @@ inputs: description: "Stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session" required: false include_screenshots: - description: "Whether to capture screenshots of scanned pages" + description: "Whether to capture screenshots of scanned pages and include links to them in the issue" required: false default: "true" diff --git a/.github/actions/gh-cache/delete/action.yml b/.github/actions/gh-cache/delete/action.yml index 26fb7c6..1f0242a 100644 --- a/.github/actions/gh-cache/delete/action.yml +++ b/.github/actions/gh-cache/delete/action.yml @@ -65,6 +65,7 @@ runs: run: | if [ -e "${{ inputs.path }}" ]; then rm -Rf "${{ inputs.path }}" + rm -Rf ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" echo "Deleted '${{ inputs.path }}' from 'gh-cache' branch" else echo "'${{ inputs.path }}' does not exist in 'gh-cache' branch" diff --git a/.github/actions/gh-cache/restore/action.yml b/.github/actions/gh-cache/restore/action.yml index 8ea6950..26d0ecc 100644 --- a/.github/actions/gh-cache/restore/action.yml +++ b/.github/actions/gh-cache/restore/action.yml @@ -72,9 +72,6 @@ runs: src=".gh-cache-${{ github.run_id }}/${{ inputs.path }}" dest="${{ inputs.path }}" if [ -e "$src" ]; then - if [ -d "$src" ]; then - rm -rf "$dest" - fi mkdir -p "$(dirname "$dest")" cp -Rf "$src" "$dest" echo "Restored '${{ inputs.path }}' from cache" diff --git a/action.yml b/action.yml index db35874..1ece0f5 100644 --- a/action.yml +++ b/action.yml @@ -32,7 +32,7 @@ inputs: required: false default: "false" include_screenshots: - description: "Whether to include screenshots of issues in the issue bodies" + description: "Whether to capture screenshots and include links to them in the issue" required: false default: "true" From a5b6a29aab63ddc3625cb78e28b0ce25509c0a5d Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:51:31 +0000 Subject: [PATCH 26/54] Adds additional tests --- .github/actions/file/tests/openIssue.test.ts | 84 +++++++++++++++ .../actions/file/tests/reopenIssue.test.ts | 102 ++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 .github/actions/file/tests/openIssue.test.ts create mode 100644 .github/actions/file/tests/reopenIssue.test.ts diff --git a/.github/actions/file/tests/openIssue.test.ts b/.github/actions/file/tests/openIssue.test.ts new file mode 100644 index 0000000..e4b8bab --- /dev/null +++ b/.github/actions/file/tests/openIssue.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, vi } from "vitest"; + +// Mock generateIssueBody so we can inspect what screenshotRepo is passed +vi.mock("../src/generateIssueBody.js", () => ({ + generateIssueBody: vi.fn( + (_finding, screenshotRepo: string) => + `body with screenshotRepo=${screenshotRepo}`, + ), +})); + +import { openIssue } from "../src/openIssue.ts"; +import { generateIssueBody } from "../src/generateIssueBody.ts"; + +const baseFinding = { + 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?application=playwright", + solutionShort: + "ensure the contrast between foreground and background colors meets WCAG thresholds", +}; + +function mockOctokit() { + return { + request: vi.fn().mockResolvedValue({ data: { id: 1, html_url: "https://github.com/org/repo/issues/1" } }), + } as any; +} + +describe("openIssue", () => { + it("passes screenshotRepo to generateIssueBody when provided", async () => { + const octokit = mockOctokit(); + await openIssue(octokit, "org/filing-repo", 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); + + 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"); + + expect(octokit.request).toHaveBeenCalledWith( + "POST /repos/org/filing-repo/issues", + expect.objectContaining({ + owner: "org", + repo: "filing-repo", + }), + ); + }); + + it("includes the correct labels based on the finding", async () => { + const octokit = mockOctokit(); + await openIssue(octokit, "org/repo", baseFinding); + + expect(octokit.request).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + labels: ["axe rule: color-contrast", "axe-scanning-issue"], + }), + ); + }); + + it("truncates long titles with ellipsis", async () => { + const octokit = mockOctokit(); + const longFinding = { + ...baseFinding, + problemShort: "a".repeat(300), + }; + await openIssue(octokit, "org/repo", longFinding); + + const callArgs = octokit.request.mock.calls[0][1]; + expect(callArgs.title.length).toBeLessThanOrEqual(256); + expect(callArgs.title).toMatch(/…$/); + }); +}); diff --git a/.github/actions/file/tests/reopenIssue.test.ts b/.github/actions/file/tests/reopenIssue.test.ts new file mode 100644 index 0000000..6d613a3 --- /dev/null +++ b/.github/actions/file/tests/reopenIssue.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// Mock generateIssueBody so we can inspect what screenshotRepo is passed +vi.mock("../src/generateIssueBody.js", () => ({ + generateIssueBody: vi.fn( + (_finding, screenshotRepo: string) => + `body with screenshotRepo=${screenshotRepo}`, + ), +})); + +import { reopenIssue } from "../src/reopenIssue.ts"; +import { generateIssueBody } from "../src/generateIssueBody.ts"; +import { Issue } from "../src/Issue.ts"; + +const baseFinding = { + 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?application=playwright", + solutionShort: + "ensure the contrast between foreground and background colors meets WCAG thresholds", +}; + +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() { + return { + request: vi.fn().mockResolvedValue({ data: { id: 42, html_url: "https://github.com/org/filing-repo/issues/7" } }), + } as any; +} + +describe("reopenIssue", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("passes screenshotRepo to generateIssueBody when provided", async () => { + const octokit = mockOctokit(); + await reopenIssue(octokit, testIssue, baseFinding, "org/filing-repo", "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"); + + expect(generateIssueBody).toHaveBeenCalledWith(baseFinding, "org/filing-repo"); + }); + + it("does not generate a body when finding is not provided", async () => { + const octokit = mockOctokit(); + await reopenIssue(octokit, testIssue); + + expect(generateIssueBody).not.toHaveBeenCalled(); + expect(octokit.request).toHaveBeenCalledWith( + expect.any(String), + expect.not.objectContaining({ body: expect.anything() }), + ); + }); + + it("does not generate a body when repoWithOwner is not provided", async () => { + const octokit = mockOctokit(); + 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"); + + expect(octokit.request).toHaveBeenCalledWith( + "PATCH /repos/org/filing-repo/issues/7", + expect.objectContaining({ + state: "open", + issue_number: 7, + }), + ); + }); + + 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"); + + expect(octokit.request).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + body: "body with screenshotRepo=org/workflow-repo", + }), + ); + }); +}); From e28ed114f5748b44332a34e6863e0baf966ca9db Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:03:53 +0000 Subject: [PATCH 27/54] Updates site-with-errors test --- tests/site-with-errors.test.ts | 64 +++++++++++++++++++++++++--------- tests/types.d.ts | 1 + 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index f670b4e..9758703 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -17,15 +17,25 @@ describe("site-with-errors", () => { it("cache has expected results", () => { const actual = results.map(({ issue: { url: issueUrl }, pullRequest: { url: pullRequestUrl }, findings }) => { - const { problemUrl, solutionLong, ...finding } = findings[0]; + const { problemUrl, solutionLong, screenshotId, ...finding } = + findings[0]; // Check volatile fields for existence only expect(issueUrl).toBeDefined(); expect(pullRequestUrl).toBeDefined(); expect(problemUrl).toBeDefined(); expect(solutionLong).toBeDefined(); + expect(screenshotId).toBeDefined(); // Check `problemUrl`, ignoring axe version - expect(problemUrl.startsWith("https://dequeuniversity.com/rules/axe/")).toBe(true); - expect(problemUrl.endsWith(`/${finding.ruleId}?application=playwright`)).toBe(true); + expect( + problemUrl.startsWith("https://dequeuniversity.com/rules/axe/"), + ).toBe(true); + expect( + problemUrl.endsWith(`/${finding.ruleId}?application=playwright`), + ).toBe(true); + // Check `screenshotId` is a valid UUID + expect(screenshotId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); return finding; }); const expected = [ @@ -33,45 +43,65 @@ describe("site-with-errors", () => { scannerType: "axe", url: "http://127.0.0.1:4000/", html: '', - problemShort: "elements must meet minimum color contrast ratio thresholds", + problemShort: + "elements must meet minimum color contrast ratio thresholds", ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds" - }, { + solutionShort: + "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + screenshotId: "12345678-1234-1234-1234-123456789012", + }, + { scannerType: "axe", url: "http://127.0.0.1:4000/", html: '', problemShort: "page should contain a level-one heading", ruleId: "page-has-heading-one", - solutionShort: "ensure that the page, or at least one of its frames contains a level-one heading" - }, { + solutionShort: + "ensure that the page, or at least one of its frames contains a level-one heading", + screenshotId: "12345678-1234-1234-1234-123456789012", + }, + { scannerType: "axe", url: "http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html", html: ``, - problemShort: "elements must meet minimum color contrast ratio thresholds", + problemShort: + "elements must meet minimum color contrast ratio thresholds", ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", - }, { + solutionShort: + "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + screenshotId: "12345678-1234-1234-1234-123456789012", + }, + { scannerType: "axe", url: "http://127.0.0.1:4000/about/", html: 'jekyllrb.com', - problemShort: "elements must meet minimum color contrast ratio thresholds", + problemShort: + "elements must meet minimum color contrast ratio thresholds", ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", - }, { + solutionShort: + "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + screenshotId: "12345678-1234-1234-1234-123456789012", + }, + { scannerType: "axe", url: "http://127.0.0.1:4000/404.html", html: '
  • Accessibility Scanner Demo
  • ', - problemShort: "elements must meet minimum color contrast ratio thresholds", + problemShort: + "elements must meet minimum color contrast ratio thresholds", ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds" - }, { + solutionShort: + "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + screenshotId: "12345678-1234-1234-1234-123456789012", + }, + { scannerType: "axe", url: "http://127.0.0.1:4000/404.html", html: '

    ', problemShort: "headings should not be empty", ruleId: "empty-heading", solutionShort: "ensure headings have discernible text", + screenshotId: "12345678-1234-1234-1234-123456789012", }, ]; // Check that: diff --git a/tests/types.d.ts b/tests/types.d.ts index 684ab0e..9316f0d 100644 --- a/tests/types.d.ts +++ b/tests/types.d.ts @@ -7,6 +7,7 @@ export type Finding = { problemUrl: string; solutionShort: string; solutionLong?: string; + screenshotId?: string; }; export type Issue = { From 7cfce7c6c98a3526f309b5b2be21df0e93055aea Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:12:01 +0000 Subject: [PATCH 28/54] Updates test --- tests/site-with-errors.test.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index 9758703..cd7cba3 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -32,10 +32,7 @@ describe("site-with-errors", () => { expect( problemUrl.endsWith(`/${finding.ruleId}?application=playwright`), ).toBe(true); - // Check `screenshotId` is a valid UUID - expect(screenshotId).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, - ); + expect(screenshotId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); return finding; }); const expected = [ @@ -48,7 +45,6 @@ describe("site-with-errors", () => { ruleId: "color-contrast", solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", - screenshotId: "12345678-1234-1234-1234-123456789012", }, { scannerType: "axe", @@ -58,7 +54,6 @@ describe("site-with-errors", () => { ruleId: "page-has-heading-one", solutionShort: "ensure that the page, or at least one of its frames contains a level-one heading", - screenshotId: "12345678-1234-1234-1234-123456789012", }, { scannerType: "axe", @@ -70,7 +65,6 @@ describe("site-with-errors", () => { ruleId: "color-contrast", solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", - screenshotId: "12345678-1234-1234-1234-123456789012", }, { scannerType: "axe", @@ -81,7 +75,6 @@ describe("site-with-errors", () => { ruleId: "color-contrast", solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", - screenshotId: "12345678-1234-1234-1234-123456789012", }, { scannerType: "axe", @@ -92,7 +85,6 @@ describe("site-with-errors", () => { ruleId: "color-contrast", solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", - screenshotId: "12345678-1234-1234-1234-123456789012", }, { scannerType: "axe", @@ -101,7 +93,6 @@ describe("site-with-errors", () => { problemShort: "headings should not be empty", ruleId: "empty-heading", solutionShort: "ensure headings have discernible text", - screenshotId: "12345678-1234-1234-1234-123456789012", }, ]; // Check that: From e2bf89ad76f339fa95889231815c0ad9e962958e Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:15:20 +0000 Subject: [PATCH 29/54] Adds crypto import --- .github/actions/find/src/findForUrl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 3478954..3e3d2cc 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -4,6 +4,7 @@ import playwright from 'playwright'; import { AuthContext } from './AuthContext.js'; import fs from "node:fs"; import path from "node:path"; +import crypto from "node:crypto"; // Use GITHUB_WORKSPACE to ensure screenshots are saved in the workflow workspace root // where the artifact upload step can find them From 4bbb8bc299643fa9d1513a346818cad46966bdbd Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:25:39 +0000 Subject: [PATCH 30/54] Adds eslint & prettier --- .prettierrc.json | 6 + eslint.config.js | 16 + package-lock.json | 1224 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 10 +- 4 files changed, 1249 insertions(+), 7 deletions(-) create mode 100644 .prettierrc.json create mode 100644 eslint.config.js diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..d61ae01 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "all", + "singleQuote": false, + "semi": true, + "tabWidth": 2 +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..6a0c2ec --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,16 @@ +import tseslint from "typescript-eslint"; +import prettierConfig from "eslint-config-prettier"; + +export default tseslint.config(...tseslint.configs.recommended, prettierConfig, { + files: ["**/*.ts"], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + }, +}); diff --git a/package-lock.json b/package-lock.json index 16cd5fe..4dcc13b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,10 @@ "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", "@types/node": "^25.2.0", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.8.1", + "typescript-eslint": "^8.56.0", "vitest": "^4.0.18" } }, @@ -498,6 +502,165 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.1.tgz", + "integrity": "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.1", + "debug": "^4.3.1", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.1.tgz", + "integrity": "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -521,6 +684,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -1002,6 +1166,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1009,16 +1180,288 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.2.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -1130,6 +1573,47 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1140,6 +1624,16 @@ "node": ">=12" } }, + "node_modules/balanced-match": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", @@ -1154,6 +1648,19 @@ "dev": true, "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1164,14 +1671,54 @@ "node": ">=18" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } }, - "node_modules/esbuild": { + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", @@ -1213,6 +1760,178 @@ "@esbuild/win32-x64": "0.27.2" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.0.tgz", + "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.0", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.0", + "eslint-visitor-keys": "^5.0.0", + "espree": "^11.1.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.1.1", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz", + "integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz", + "integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1223,6 +1942,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -1250,6 +1979,27 @@ ], "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1268,6 +2018,57 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1283,6 +2084,130 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1293,6 +2218,29 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1312,6 +2260,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -1323,6 +2278,76 @@ ], "license": "MIT" }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1379,6 +2404,42 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -1424,6 +2485,42 @@ "fsevents": "~2.3.2" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1499,6 +2596,19 @@ "node": ">=14.0.0" } }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -1509,6 +2619,58 @@ "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/undici": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", @@ -1533,12 +2695,23 @@ "dev": true, "license": "ISC" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -1686,6 +2859,22 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -1702,6 +2891,29 @@ "engines": { "node": ">=8" } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 3d6a0c4..200f6c6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,10 @@ "version": "0.0.0-development", "description": "Finds potential accessibility gaps, files GitHub issues to track them, and attempts to fix them with Copilot", "scripts": { - "test": "vitest run tests/*.test.ts .github/actions/**/tests/*.test.ts" + "test": "vitest run tests/*.test.ts .github/actions/**/tests/*.test.ts", + "lint": "eslint '.github/actions/**/src/**/*.ts' 'tests/**/*.ts' '.github/actions/**/tests/**/*.ts'", + "format": "prettier --write '.github/actions/**/src/**/*.ts' 'tests/**/*.ts' '.github/actions/**/tests/**/*.ts'", + "format:check": "prettier --check '.github/actions/**/src/**/*.ts' 'tests/**/*.ts' '.github/actions/**/tests/**/*.ts'" }, "repository": { "type": "git", @@ -15,12 +18,17 @@ "url": "https://github.com/github/accessibility-scanner/issues" }, "homepage": "https://github.com/github/accessibility-scanner#readme", + "type": "module", "devDependencies": { "@actions/core": "^3.0.0", "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", "@types/node": "^25.2.0", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.8.1", + "typescript-eslint": "^8.56.0", "vitest": "^4.0.18" } } From 74498c84b3237c1b9064ba67a1d251438dcfb921 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:28:15 +0000 Subject: [PATCH 31/54] Files that are linted and prettiered --- .github/actions/auth/src/index.ts | 27 ++- .github/actions/file/src/Issue.ts | 2 +- .github/actions/file/src/closeIssue.ts | 26 +-- .github/actions/file/src/generateIssueBody.ts | 27 ++- .github/actions/file/src/index.ts | 12 +- .github/actions/file/src/openIssue.ts | 25 ++- .github/actions/file/src/reopenIssue.ts | 24 ++- .../file/src/updateFilingsWithNewFindings.ts | 2 +- .../file/tests/generateIssueBody.test.ts | 10 +- .github/actions/find/src/findForUrl.ts | 30 ++-- .github/actions/find/src/index.ts | 4 +- .github/actions/fix/src/Issue.ts | 15 +- .github/actions/fix/src/assignIssue.ts | 16 +- .github/actions/fix/src/getLinkedPR.ts | 4 +- .github/actions/fix/src/index.ts | 14 +- .github/actions/fix/src/retry.ts | 2 +- tests/site-with-errors.test.ts | 158 +++++++++++------- 17 files changed, 238 insertions(+), 160 deletions(-) diff --git a/.github/actions/auth/src/index.ts b/.github/actions/auth/src/index.ts index 4e0e4b1..860f7e7 100644 --- a/.github/actions/auth/src/index.ts +++ b/.github/actions/auth/src/index.ts @@ -1,7 +1,5 @@ import type { AuthContextOutput } from "./types.d.js"; -import crypto from "node:crypto"; import process from "node:process"; -import * as url from "node:url"; import core from "@actions/core"; import playwright from "playwright"; @@ -18,13 +16,6 @@ export default async function () { const password = core.getInput("password", { required: true }); core.setSecret(password); - // Determine storage path for authenticated session state - // Playwright will create missing directories, if needed - const actionDirectory = `${url.fileURLToPath(new URL(import.meta.url))}/..`; - const sessionStatePath = `${ - process.env.RUNNER_TEMP ?? actionDirectory - }/.auth/${crypto.randomUUID()}/sessionState.json`; - // Launch a headless browser browser = await playwright.chromium.launch({ headless: true, @@ -76,13 +67,19 @@ export default async function () { username, password, cookies, - localStorage: origins.reduce((acc, { origin, localStorage }) => { - acc[origin] = localStorage.reduce((acc, { name, value }) => { - acc[name] = value; + localStorage: origins.reduce( + (acc, { origin, localStorage }) => { + acc[origin] = localStorage.reduce( + (acc, { name, value }) => { + acc[name] = value; + return acc; + }, + {} as Record, + ); return acc; - }, {} as Record); - return acc; - }, {} as Record>), + }, + {} as Record>, + ), }; core.setOutput("auth_context", JSON.stringify(authContextOutput)); core.debug("Output: 'auth_context'"); diff --git a/.github/actions/file/src/Issue.ts b/.github/actions/file/src/Issue.ts index c494e1b..f622a20 100644 --- a/.github/actions/file/src/Issue.ts +++ b/.github/actions/file/src/Issue.ts @@ -53,7 +53,7 @@ export class Issue implements IssueInput { } { const { owner, repository, issueNumber } = /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec( - this.#url + this.#url, )?.groups || {}; if (!owner || !repository || !issueNumber) { throw new Error(`Could not parse issue URL: ${this.#url}`); diff --git a/.github/actions/file/src/closeIssue.ts b/.github/actions/file/src/closeIssue.ts index a26f203..e645fae 100644 --- a/.github/actions/file/src/closeIssue.ts +++ b/.github/actions/file/src/closeIssue.ts @@ -1,11 +1,17 @@ -import type { Octokit } from '@octokit/core'; -import { Issue } from './Issue.js'; +import type { Octokit } from "@octokit/core"; +import { Issue } from "./Issue.js"; -export async function closeIssue(octokit: Octokit, { owner, repository, issueNumber }: Issue) { - return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { - owner, - repository, - issue_number: issueNumber, - state: 'closed' - }); -} \ No newline at end of file +export async function closeIssue( + octokit: Octokit, + { owner, repository, issueNumber }: Issue, +) { + return octokit.request( + `PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, + { + owner, + repository, + issue_number: issueNumber, + state: "closed", + }, + ); +} diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index e82624e..797f496 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -1,31 +1,30 @@ import type { Finding } from "./types.d.js"; -export function generateIssueBody(finding: Finding, repoWithOwner: string): string { +export function generateIssueBody(finding: Finding): string { const solutionLong = finding.solutionLong - ?.split("\n") - .map((line: string) => - !line.trim().startsWith("Fix any") && - !line.trim().startsWith("Fix all") && - line.trim() !== "" - ? `- ${line}` - : line - ) - .join("\n"); - const acceptanceCriteria = `## Acceptance Criteria + ?.split("\n") + .map((line: string) => + !line.trim().startsWith("Fix any") && + !line.trim().startsWith("Fix all") && + line.trim() !== "" + ? `- ${line}` + : line, + ) + .join("\n"); + const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific axe 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 axe violation does not regress. - [ ] This PR MUST NOT introduce any new accessibility issues or regressions. `; - const body = `## What + const body = `## What An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. To fix this, ${finding.solutionShort}. - ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} + ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ""} ${acceptanceCriteria} `; return body; } - diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 0c1304b..65cbe5d 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -16,12 +16,12 @@ const OctokitWithThrottling = Octokit.plugin(throttling); export default async function () { core.info("Started 'file' action"); const findings: Finding[] = JSON.parse( - core.getInput("findings", { required: true }) + core.getInput("findings", { required: true }), ); const repoWithOwner = core.getInput("repository", { required: true }); const token = core.getInput("token", { required: true }); const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( - core.getInput("cached_filings", { required: false }) || "[]" + core.getInput("cached_filings", { required: false }) || "[]", ); core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`); core.debug(`Input: 'repository: ${repoWithOwner}'`); @@ -32,7 +32,7 @@ export default async function () { throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { octokit.log.warn( - `Request quota exhausted for request ${options.method} ${options.url}` + `Request quota exhausted for request ${options.method} ${options.url}`, ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); @@ -41,7 +41,7 @@ export default async function () { }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { octokit.log.warn( - `Secondary rate limit hit for request ${options.method} ${options.url}` + `Secondary rate limit hit for request ${options.method} ${options.url}`, ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); @@ -62,7 +62,7 @@ export default async function () { } else if (isNewFiling(filing)) { // Open a new issue for the filing response = await openIssue(octokit, repoWithOwner, filing.findings[0]); - (filing as any).issue = { state: "open" } as Issue; + (filing as unknown as { issue: Issue }).issue = { state: "open" } as Issue; } else if (isRepeatedFiling(filing)) { // Reopen the filing’s issue (if necessary) response = await reopenIssue(octokit, new Issue(filing.issue)); @@ -75,7 +75,7 @@ export default async function () { 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}` + `Set issue ${response.data.title} (${repoWithOwner}#${response.data.number}) state to ${filing.issue.state}`, ); } } catch (error) { diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 3220ddf..a468ff4 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -1,7 +1,7 @@ -import type { Octokit } from '@octokit/core'; -import type { Finding } from './types.d.js'; +import type { Octokit } from "@octokit/core"; +import type { Finding } from "./types.d.js"; import { generateIssueBody } from "./generateIssueBody.js"; -import * as url from 'node:url' +import * as url from "node:url"; const URL = url.URL; /** Max length for GitHub issue titles */ @@ -14,14 +14,21 @@ const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256; * @returns Either the original text or a truncated version with an ellipsis */ function truncateWithEllipsis(text: string, maxLength: number): string { - return text.length > maxLength ? text.slice(0, maxLength - 1) + '…' : text; + return text.length > maxLength ? text.slice(0, maxLength - 1) + "…" : text; } -export async function openIssue(octokit: Octokit, repoWithOwner: string, finding: Finding) { - const owner = repoWithOwner.split('/')[0]; - const repo = repoWithOwner.split('/')[1]; +export async function openIssue( + octokit: Octokit, + repoWithOwner: string, + finding: Finding, +) { + const owner = repoWithOwner.split("/")[0]; + const repo = repoWithOwner.split("/")[1]; - const labels = [`${finding.scannerType} rule: ${finding.ruleId}`, `${finding.scannerType}-scanning-issue`]; + const labels = [ + `${finding.scannerType} rule: ${finding.ruleId}`, + `${finding.scannerType}-scanning-issue`, + ]; const title = truncateWithEllipsis( `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, GITHUB_ISSUE_TITLE_MAX_LENGTH, @@ -34,6 +41,6 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding repo, title, body, - labels + labels, }); } diff --git a/.github/actions/file/src/reopenIssue.ts b/.github/actions/file/src/reopenIssue.ts index e777bfd..a65cfff 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -1,11 +1,17 @@ -import type { Octokit } from '@octokit/core'; -import type { Issue } from './Issue.js'; +import type { Octokit } from "@octokit/core"; +import type { Issue } from "./Issue.js"; -export async function reopenIssue(octokit: Octokit, { owner, repository, issueNumber}: Issue) { - return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { - owner, - repository, - issue_number: issueNumber, - state: 'open' - }); +export async function reopenIssue( + octokit: Octokit, + { owner, repository, issueNumber }: Issue, +) { + return octokit.request( + `PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, + { + owner, + repository, + issue_number: issueNumber, + state: "open", + }, + ); } diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index 3a72557..0f93037 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -16,7 +16,7 @@ function getFindingKey(finding: Finding): string { export function updateFilingsWithNewFindings( filings: (ResolvedFiling | RepeatedFiling)[], - findings: Finding[] + findings: Finding[], ): Filing[] { const filingKeys: { [key: string]: ResolvedFiling | RepeatedFiling; diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index b269964..158df86 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -7,8 +7,10 @@ const baseFinding = { 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?application=playwright", - solutionShort: "ensure the contrast between foreground and background colors meets WCAG thresholds", + problemUrl: + "https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=playwright", + solutionShort: + "ensure the contrast between foreground and background colors meets WCAG thresholds", }; describe("generateIssueBody", () => { @@ -17,7 +19,9 @@ describe("generateIssueBody", () => { expect(body).toContain("## What"); expect(body).toContain("## Acceptance Criteria"); - expect(body).toContain("The specific axe violation reported in this issue is no longer reproducible."); + expect(body).toContain( + "The specific axe violation reported in this issue is no longer reproducible.", + ); expect(body).not.toContain("Specifically:"); }); diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 3bcd3fa..7ca7c24 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -1,10 +1,16 @@ -import type { Finding } from './types.d.js'; -import AxeBuilder from '@axe-core/playwright' -import playwright from 'playwright'; -import { AuthContext } from './AuthContext.js'; +import type { Finding } from "./types.d.js"; +import AxeBuilder from "@axe-core/playwright"; +import playwright from "playwright"; +import { AuthContext } from "./AuthContext.js"; -export async function findForUrl(url: string, authContext?: AuthContext): Promise { - const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined }); +export async function findForUrl( + url: string, + authContext?: AuthContext, +): Promise { + const browser = await playwright.chromium.launch({ + headless: true, + executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined, + }); const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {}; const context = await browser.newContext(contextOptions); const page = await context.newPage(); @@ -14,17 +20,19 @@ export async function findForUrl(url: string, authContext?: AuthContext): Promis let findings: Finding[] = []; try { const rawFindings = await new AxeBuilder({ page }).analyze(); - findings = rawFindings.violations.map(violation => ({ - scannerType: 'axe', + findings = rawFindings.violations.map((violation) => ({ + scannerType: "axe", url, html: violation.nodes[0].html.replace(/'/g, "'"), problemShort: violation.help.toLowerCase().replace(/'/g, "'"), problemUrl: violation.helpUrl.replace(/'/g, "'"), ruleId: violation.id, - solutionShort: violation.description.toLowerCase().replace(/'/g, "'"), - solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'") + solutionShort: violation.description + .toLowerCase() + .replace(/'/g, "'"), + solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'"), })); - } catch (e) { + } catch (_e) { // do something with the error } await context.close(); diff --git a/.github/actions/find/src/index.ts b/.github/actions/find/src/index.ts index e596647..8cdf836 100644 --- a/.github/actions/find/src/index.ts +++ b/.github/actions/find/src/index.ts @@ -8,11 +8,11 @@ export default async function () { const urls = core.getMultilineInput("urls", { required: true }); core.debug(`Input: 'urls: ${JSON.stringify(urls)}'`); const authContextInput: AuthContextInput = JSON.parse( - core.getInput("auth_context", { required: false }) || "{}" + core.getInput("auth_context", { required: false }) || "{}", ); const authContext = new AuthContext(authContextInput); - let findings = []; + const findings = []; for (const url of urls) { core.info(`Preparing to scan ${url}`); const findingsForUrl = await findForUrl(url, authContext); diff --git a/.github/actions/fix/src/Issue.ts b/.github/actions/fix/src/Issue.ts index eaecf01..6e79194 100644 --- a/.github/actions/fix/src/Issue.ts +++ b/.github/actions/fix/src/Issue.ts @@ -7,12 +7,19 @@ export class Issue implements IssueInput { * @returns An object with `owner`, `repository`, and `issueNumber` keys. * @throws The provided URL is unparseable due to its unexpected format. */ - static parseIssueUrl(issueUrl: string): { owner: string; repository: string; issueNumber: number } { - const { owner, repository, issueNumber } = /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec(issueUrl)?.groups || {}; + static parseIssueUrl(issueUrl: string): { + owner: string; + repository: string; + issueNumber: number; + } { + const { owner, repository, issueNumber } = + /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec( + issueUrl, + )?.groups || {}; if (!owner || !repository || !issueNumber) { throw new Error(`Could not parse issue URL: ${issueUrl}`); } - return { owner, repository, issueNumber: Number(issueNumber) } + return { owner, repository, issueNumber: Number(issueNumber) }; } url: string; @@ -30,7 +37,7 @@ export class Issue implements IssueInput { return Issue.parseIssueUrl(this.url).issueNumber; } - constructor({url, nodeId}: IssueInput) { + constructor({ url, nodeId }: IssueInput) { this.url = url; this.nodeId = nodeId; } diff --git a/.github/actions/fix/src/assignIssue.ts b/.github/actions/fix/src/assignIssue.ts index ddd28e2..b374325 100644 --- a/.github/actions/fix/src/assignIssue.ts +++ b/.github/actions/fix/src/assignIssue.ts @@ -4,7 +4,7 @@ import { Issue } from "./Issue.js"; // https://docs.github.com/en/enterprise-cloud@latest/copilot/how-tos/use-copilot-agents/coding-agent/assign-copilot-to-an-issue#assigning-an-existing-issue export async function assignIssue( octokit: Octokit, - { owner, repository, issueNumber, nodeId }: Issue + { owner, repository, issueNumber, nodeId }: Issue, ) { // Check whether issues can be assigned to Copilot const suggestedActorsResponse = await octokit.graphql<{ @@ -26,7 +26,7 @@ export async function assignIssue( } } }`, - { owner, repository } + { owner, repository }, ); if ( suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.login !== @@ -38,7 +38,7 @@ export async function assignIssue( let issueId = nodeId; if (!issueId) { console.debug( - `Fetching identifier for issue ${owner}/${repository}#${issueNumber}` + `Fetching identifier for issue ${owner}/${repository}#${issueNumber}`, ); const issueResponse = await octokit.graphql<{ repository: { @@ -50,20 +50,20 @@ export async function assignIssue( issue(number: $issueNumber) { id } } }`, - { owner, repository, issueNumber } + { owner, repository, issueNumber }, ); issueId = issueResponse?.repository?.issue?.id; console.debug( - `Fetched identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}` + `Fetched identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`, ); } else { console.debug( - `Using provided identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}` + `Using provided identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`, ); } if (!issueId) { console.warn( - `Couldn’t get identifier for issue ${owner}/${repository}#${issueNumber}. Skipping assignment to Copilot.` + `Couldn’t get identifier for issue ${owner}/${repository}#${issueNumber}. Skipping assignment to Copilot.`, ); return; } @@ -98,6 +98,6 @@ export async function assignIssue( issueId, assigneeId: suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.id, - } + }, ); } diff --git a/.github/actions/fix/src/getLinkedPR.ts b/.github/actions/fix/src/getLinkedPR.ts index 5a5f6c3..7508d55 100644 --- a/.github/actions/fix/src/getLinkedPR.ts +++ b/.github/actions/fix/src/getLinkedPR.ts @@ -3,7 +3,7 @@ import { Issue } from "./Issue.js"; export async function getLinkedPR( octokit: Octokit, - { owner, repository, issueNumber }: Issue + { owner, repository, issueNumber }: Issue, ) { // Check whether issues can be assigned to Copilot const response = await octokit.graphql<{ @@ -30,7 +30,7 @@ export async function getLinkedPR( } } }`, - { owner, repository, issueNumber } + { owner, repository, issueNumber }, ); const timelineNodes = response?.repository?.issue?.timelineItems?.nodes || []; const pullRequest: { id: string; url: string; title: string } | undefined = diff --git a/.github/actions/fix/src/index.ts b/.github/actions/fix/src/index.ts index 6899f06..f503eda 100644 --- a/.github/actions/fix/src/index.ts +++ b/.github/actions/fix/src/index.ts @@ -12,7 +12,7 @@ const OctokitWithThrottling = Octokit.plugin(throttling); export default async function () { core.info("Started 'fix' action"); const issues: IssueInput[] = JSON.parse( - core.getInput("issues", { required: true }) || "[]" + core.getInput("issues", { required: true }) || "[]", ); const repoWithOwner = core.getInput("repository", { required: true }); const token = core.getInput("token", { required: true }); @@ -24,7 +24,7 @@ export default async function () { throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { octokit.log.warn( - `Request quota exhausted for request ${options.method} ${options.url}` + `Request quota exhausted for request ${options.method} ${options.url}`, ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); @@ -33,7 +33,7 @@ export default async function () { }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { octokit.log.warn( - `Secondary rate limit hit for request ${options.method} ${options.url}` + `Secondary rate limit hit for request ${options.method} ${options.url}`, ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); @@ -49,22 +49,22 @@ export default async function () { const issue = new Issue(fixing.issue); await assignIssue(octokit, issue); core.info( - `Assigned ${issue.owner}/${issue.repository}#${issue.issueNumber} to Copilot!` + `Assigned ${issue.owner}/${issue.repository}#${issue.issueNumber} to Copilot!`, ); const pullRequest = await retry(() => getLinkedPR(octokit, issue)); if (pullRequest) { fixing.pullRequest = pullRequest; core.info( - `Found linked PR for ${issue.owner}/${issue.repository}#${issue.issueNumber}: ${pullRequest.url}` + `Found linked PR for ${issue.owner}/${issue.repository}#${issue.issueNumber}: ${pullRequest.url}`, ); } else { core.info( - `No linked PR was found for ${issue.owner}/${issue.repository}#${issue.issueNumber}` + `No linked PR was found for ${issue.owner}/${issue.repository}#${issue.issueNumber}`, ); } } catch (error) { core.setFailed( - `Failed to assign ${fixing.issue.url} to Copilot: ${error}` + `Failed to assign ${fixing.issue.url} to Copilot: ${error}`, ); process.exit(1); } diff --git a/.github/actions/fix/src/retry.ts b/.github/actions/fix/src/retry.ts index c50ce01..4165133 100644 --- a/.github/actions/fix/src/retry.ts +++ b/.github/actions/fix/src/retry.ts @@ -18,7 +18,7 @@ export async function retry( fn: () => Promise | T | null | undefined, maxAttempts = 6, baseDelay = 2000, - attempt = 1 + attempt = 1, ): Promise { const value = await fn(); if (value != null) return value; diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index f670b4e..8e6b6ea 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -1,4 +1,4 @@ -import type { Endpoints } from "@octokit/types" +import type { Endpoints } from "@octokit/types"; import type { Result } from "./types.d.js"; import fs from "node:fs"; import { describe, it, expect, beforeAll } from "vitest"; @@ -16,56 +16,80 @@ describe("site-with-errors", () => { }); it("cache has expected results", () => { - const actual = results.map(({ issue: { url: issueUrl }, pullRequest: { url: pullRequestUrl }, findings }) => { - const { problemUrl, solutionLong, ...finding } = findings[0]; - // Check volatile fields for existence only - expect(issueUrl).toBeDefined(); - expect(pullRequestUrl).toBeDefined(); - expect(problemUrl).toBeDefined(); - expect(solutionLong).toBeDefined(); - // Check `problemUrl`, ignoring axe version - expect(problemUrl.startsWith("https://dequeuniversity.com/rules/axe/")).toBe(true); - expect(problemUrl.endsWith(`/${finding.ruleId}?application=playwright`)).toBe(true); - return finding; - }); + const actual = results.map( + ({ + issue: { url: issueUrl }, + pullRequest: { url: pullRequestUrl }, + findings, + }) => { + const { problemUrl, solutionLong, ...finding } = findings[0]; + // Check volatile fields for existence only + expect(issueUrl).toBeDefined(); + expect(pullRequestUrl).toBeDefined(); + expect(problemUrl).toBeDefined(); + expect(solutionLong).toBeDefined(); + // Check `problemUrl`, ignoring axe version + expect( + problemUrl.startsWith("https://dequeuniversity.com/rules/axe/"), + ).toBe(true); + expect( + problemUrl.endsWith(`/${finding.ruleId}?application=playwright`), + ).toBe(true); + return finding; + }, + ); const expected = [ { scannerType: "axe", url: "http://127.0.0.1:4000/", html: '', - problemShort: "elements must meet minimum color contrast ratio thresholds", + problemShort: + "elements must meet minimum color contrast ratio thresholds", ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds" - }, { + solutionShort: + "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + }, + { scannerType: "axe", url: "http://127.0.0.1:4000/", html: '', problemShort: "page should contain a level-one heading", ruleId: "page-has-heading-one", - solutionShort: "ensure that the page, or at least one of its frames contains a level-one heading" - }, { + solutionShort: + "ensure that the page, or at least one of its frames contains a level-one heading", + }, + { scannerType: "axe", url: "http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html", html: ``, - problemShort: "elements must meet minimum color contrast ratio thresholds", + problemShort: + "elements must meet minimum color contrast ratio thresholds", ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", - }, { + solutionShort: + "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + }, + { scannerType: "axe", url: "http://127.0.0.1:4000/about/", html: 'jekyllrb.com', - problemShort: "elements must meet minimum color contrast ratio thresholds", + problemShort: + "elements must meet minimum color contrast ratio thresholds", ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", - }, { + solutionShort: + "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + }, + { scannerType: "axe", url: "http://127.0.0.1:4000/404.html", html: '
  • Accessibility Scanner Demo
  • ', - problemShort: "elements must meet minimum color contrast ratio thresholds", + problemShort: + "elements must meet minimum color contrast ratio thresholds", ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds" - }, { + solutionShort: + "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + }, + { scannerType: "axe", url: "http://127.0.0.1:4000/404.html", html: '

    ', @@ -97,51 +121,69 @@ describe("site-with-errors", () => { auth: process.env.GITHUB_TOKEN, throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`); + octokit.log.warn( + `Request quota exhausted for request ${options.method} ${options.url}`, + ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); return true; } }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn(`Secondary rate limit hit for request ${options.method} ${options.url}`); + octokit.log.warn( + `Secondary rate limit hit for request ${options.method} ${options.url}`, + ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); return true; } }, - } + }, }); // Fetch issues referenced in the cache file - issues = await Promise.all(results.map(async ({ issue: { url: issueUrl } }) => { - expect(issueUrl).toBeDefined(); - const { owner, repo, issueNumber } = - /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)/.exec(issueUrl!)!.groups!; - const {data: issue} = await octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}", { - owner, - repo, - issue_number: parseInt(issueNumber, 10) - }); - expect(issue).toBeDefined(); - return issue; - })); + issues = await Promise.all( + results.map(async ({ issue: { url: issueUrl } }) => { + expect(issueUrl).toBeDefined(); + const { owner, repo, issueNumber } = + /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)/.exec( + issueUrl!, + )!.groups!; + const { data: issue } = await octokit.request( + "GET /repos/{owner}/{repo}/issues/{issue_number}", + { + owner, + repo, + issue_number: parseInt(issueNumber, 10), + }, + ); + expect(issue).toBeDefined(); + return issue; + }), + ); // Fetch pull requests referenced in the findings file - pullRequests = await Promise.all(results.map(async ({ pullRequest: { url: pullRequestUrl } }) => { - expect(pullRequestUrl).toBeDefined(); - const { owner, repo, pullNumber } = - /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/pull\/(?\d+)/.exec(pullRequestUrl!)!.groups!; - const {data: pullRequest} = await octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", { - owner, - repo, - pull_number: parseInt(pullNumber, 10) - }); - expect(pullRequest).toBeDefined(); - return pullRequest; - })); + pullRequests = await Promise.all( + results.map(async ({ pullRequest: { url: pullRequestUrl } }) => { + expect(pullRequestUrl).toBeDefined(); + const { owner, repo, pullNumber } = + /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/pull\/(?\d+)/.exec( + pullRequestUrl!, + )!.groups!; + const { data: pullRequest } = await octokit.request( + "GET /repos/{owner}/{repo}/pulls/{pull_number}", + { + owner, + repo, + pull_number: parseInt(pullNumber, 10), + }, + ); + expect(pullRequest).toBeDefined(); + return pullRequest; + }), + ); }); it("issues exist and have expected title, state, and assignee", async () => { - const actualTitles = issues.map(({ title }) => (title)); + const actualTitles = issues.map(({ title }) => title); const expectedTitles = [ "Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /", "Accessibility issue: Page should contain a level-one heading on /", @@ -155,7 +197,7 @@ describe("site-with-errors", () => { for (const issue of issues) { expect(issue.state).toBe("open"); expect(issue.assignees).toBeDefined(); - expect(issue.assignees!.some(a => a.login === "Copilot")).toBe(true); + expect(issue.assignees!.some((a) => a.login === "Copilot")).toBe(true); } }); @@ -164,7 +206,9 @@ describe("site-with-errors", () => { expect(pullRequest.user.login).toBe("Copilot"); expect(pullRequest.state).toBe("open"); expect(pullRequest.assignees).toBeDefined(); - expect(pullRequest.assignees!.some(a => a.login === "Copilot")).toBe(true); + expect(pullRequest.assignees!.some((a) => a.login === "Copilot")).toBe( + true, + ); } }); }); From dab687e9fd0781ac74fb95cf0235dee1855319e7 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:40:16 +0000 Subject: [PATCH 32/54] Sets back to any --- .github/actions/file/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 65cbe5d..776253f 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -62,7 +62,8 @@ export default async function () { } else if (isNewFiling(filing)) { // Open a new issue for the filing response = await openIssue(octokit, repoWithOwner, filing.findings[0]); - (filing as unknown as { issue: Issue }).issue = { state: "open" } as Issue; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (filing as any).issue = { state: "open" } as Issue; } else if (isRepeatedFiling(filing)) { // Reopen the filing’s issue (if necessary) response = await reopenIssue(octokit, new Issue(filing.issue)); From 0a35dcc7dcc8adb4dbfc5879e074231f5ef5dd96 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:42:48 +0000 Subject: [PATCH 33/54] Adds lint to CI --- .github/workflows/lint.yml | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..48ce563 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,41 @@ +name: Lint & Format +on: + pull_request: + types: + - opened + - reopened + - synchronize + - ready_for_review + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint & Format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Format check + run: npm run format:check From 7320f1c5e974dd23baa38a4d5d0fc0fdd1c2cf20 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:45:48 +0000 Subject: [PATCH 34/54] Testing reverting file --- .github/actions/auth/src/index.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/actions/auth/src/index.ts b/.github/actions/auth/src/index.ts index 860f7e7..4e0e4b1 100644 --- a/.github/actions/auth/src/index.ts +++ b/.github/actions/auth/src/index.ts @@ -1,5 +1,7 @@ import type { AuthContextOutput } from "./types.d.js"; +import crypto from "node:crypto"; import process from "node:process"; +import * as url from "node:url"; import core from "@actions/core"; import playwright from "playwright"; @@ -16,6 +18,13 @@ export default async function () { const password = core.getInput("password", { required: true }); core.setSecret(password); + // Determine storage path for authenticated session state + // Playwright will create missing directories, if needed + const actionDirectory = `${url.fileURLToPath(new URL(import.meta.url))}/..`; + const sessionStatePath = `${ + process.env.RUNNER_TEMP ?? actionDirectory + }/.auth/${crypto.randomUUID()}/sessionState.json`; + // Launch a headless browser browser = await playwright.chromium.launch({ headless: true, @@ -67,19 +76,13 @@ export default async function () { username, password, cookies, - localStorage: origins.reduce( - (acc, { origin, localStorage }) => { - acc[origin] = localStorage.reduce( - (acc, { name, value }) => { - acc[name] = value; - return acc; - }, - {} as Record, - ); + localStorage: origins.reduce((acc, { origin, localStorage }) => { + acc[origin] = localStorage.reduce((acc, { name, value }) => { + acc[name] = value; return acc; - }, - {} as Record>, - ), + }, {} as Record); + return acc; + }, {} as Record>), }; core.setOutput("auth_context", JSON.stringify(authContextOutput)); core.debug("Output: 'auth_context'"); From 853ba27002095593e571ee6235ea7027f925a905 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:50:38 +0000 Subject: [PATCH 35/54] Testing reverting file --- .github/actions/file/src/generateIssueBody.ts | 27 +++++++++-------- .github/actions/file/src/index.ts | 11 ++++--- .github/actions/file/src/openIssue.ts | 25 ++++++---------- .github/actions/file/src/reopenIssue.ts | 24 ++++++--------- .github/actions/find/src/findForUrl.ts | 30 +++++++------------ .github/actions/find/src/index.ts | 4 +-- 6 files changed, 50 insertions(+), 71 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 797f496..e82624e 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -1,30 +1,31 @@ import type { Finding } from "./types.d.js"; -export function generateIssueBody(finding: Finding): string { +export function generateIssueBody(finding: Finding, repoWithOwner: string): string { const solutionLong = finding.solutionLong - ?.split("\n") - .map((line: string) => - !line.trim().startsWith("Fix any") && - !line.trim().startsWith("Fix all") && - line.trim() !== "" - ? `- ${line}` - : line, - ) - .join("\n"); - const acceptanceCriteria = `## Acceptance Criteria + ?.split("\n") + .map((line: string) => + !line.trim().startsWith("Fix any") && + !line.trim().startsWith("Fix all") && + line.trim() !== "" + ? `- ${line}` + : line + ) + .join("\n"); + const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific axe 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 axe violation does not regress. - [ ] This PR MUST NOT introduce any new accessibility issues or regressions. `; - const body = `## What + const body = `## What An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. To fix this, ${finding.solutionShort}. - ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ""} + ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} ${acceptanceCriteria} `; return body; } + diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 776253f..0c1304b 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -16,12 +16,12 @@ const OctokitWithThrottling = Octokit.plugin(throttling); export default async function () { core.info("Started 'file' action"); const findings: Finding[] = JSON.parse( - core.getInput("findings", { required: true }), + core.getInput("findings", { required: true }) ); const repoWithOwner = core.getInput("repository", { required: true }); const token = core.getInput("token", { required: true }); const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( - core.getInput("cached_filings", { required: false }) || "[]", + core.getInput("cached_filings", { required: false }) || "[]" ); core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`); core.debug(`Input: 'repository: ${repoWithOwner}'`); @@ -32,7 +32,7 @@ export default async function () { throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { octokit.log.warn( - `Request quota exhausted for request ${options.method} ${options.url}`, + `Request quota exhausted for request ${options.method} ${options.url}` ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); @@ -41,7 +41,7 @@ export default async function () { }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { octokit.log.warn( - `Secondary rate limit hit for request ${options.method} ${options.url}`, + `Secondary rate limit hit for request ${options.method} ${options.url}` ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); @@ -62,7 +62,6 @@ export default async function () { } else if (isNewFiling(filing)) { // Open a new issue for the filing response = await openIssue(octokit, repoWithOwner, filing.findings[0]); - // eslint-disable-next-line @typescript-eslint/no-explicit-any (filing as any).issue = { state: "open" } as Issue; } else if (isRepeatedFiling(filing)) { // Reopen the filing’s issue (if necessary) @@ -76,7 +75,7 @@ export default async function () { 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}`, + `Set issue ${response.data.title} (${repoWithOwner}#${response.data.number}) state to ${filing.issue.state}` ); } } catch (error) { diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index a468ff4..3220ddf 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -1,7 +1,7 @@ -import type { Octokit } from "@octokit/core"; -import type { Finding } from "./types.d.js"; +import type { Octokit } from '@octokit/core'; +import type { Finding } from './types.d.js'; import { generateIssueBody } from "./generateIssueBody.js"; -import * as url from "node:url"; +import * as url from 'node:url' const URL = url.URL; /** Max length for GitHub issue titles */ @@ -14,21 +14,14 @@ const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256; * @returns Either the original text or a truncated version with an ellipsis */ function truncateWithEllipsis(text: string, maxLength: number): string { - return text.length > maxLength ? text.slice(0, maxLength - 1) + "…" : text; + return text.length > maxLength ? text.slice(0, maxLength - 1) + '…' : text; } -export async function openIssue( - octokit: Octokit, - repoWithOwner: string, - finding: Finding, -) { - const owner = repoWithOwner.split("/")[0]; - const repo = repoWithOwner.split("/")[1]; +export async function openIssue(octokit: Octokit, repoWithOwner: string, finding: Finding) { + const owner = repoWithOwner.split('/')[0]; + const repo = repoWithOwner.split('/')[1]; - const labels = [ - `${finding.scannerType} rule: ${finding.ruleId}`, - `${finding.scannerType}-scanning-issue`, - ]; + const labels = [`${finding.scannerType} rule: ${finding.ruleId}`, `${finding.scannerType}-scanning-issue`]; const title = truncateWithEllipsis( `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, GITHUB_ISSUE_TITLE_MAX_LENGTH, @@ -41,6 +34,6 @@ export async function openIssue( repo, title, body, - labels, + labels }); } diff --git a/.github/actions/file/src/reopenIssue.ts b/.github/actions/file/src/reopenIssue.ts index a65cfff..e777bfd 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -1,17 +1,11 @@ -import type { Octokit } from "@octokit/core"; -import type { Issue } from "./Issue.js"; +import type { Octokit } from '@octokit/core'; +import type { Issue } from './Issue.js'; -export async function reopenIssue( - octokit: Octokit, - { owner, repository, issueNumber }: Issue, -) { - return octokit.request( - `PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, - { - owner, - repository, - issue_number: issueNumber, - state: "open", - }, - ); +export async function reopenIssue(octokit: Octokit, { owner, repository, issueNumber}: Issue) { + return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { + owner, + repository, + issue_number: issueNumber, + state: 'open' + }); } diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 7ca7c24..3bcd3fa 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -1,16 +1,10 @@ -import type { Finding } from "./types.d.js"; -import AxeBuilder from "@axe-core/playwright"; -import playwright from "playwright"; -import { AuthContext } from "./AuthContext.js"; +import type { Finding } from './types.d.js'; +import AxeBuilder from '@axe-core/playwright' +import playwright from 'playwright'; +import { AuthContext } from './AuthContext.js'; -export async function findForUrl( - url: string, - authContext?: AuthContext, -): Promise { - const browser = await playwright.chromium.launch({ - headless: true, - executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined, - }); +export async function findForUrl(url: string, authContext?: AuthContext): Promise { + const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined }); const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {}; const context = await browser.newContext(contextOptions); const page = await context.newPage(); @@ -20,19 +14,17 @@ export async function findForUrl( let findings: Finding[] = []; try { const rawFindings = await new AxeBuilder({ page }).analyze(); - findings = rawFindings.violations.map((violation) => ({ - scannerType: "axe", + findings = rawFindings.violations.map(violation => ({ + scannerType: 'axe', url, html: violation.nodes[0].html.replace(/'/g, "'"), problemShort: violation.help.toLowerCase().replace(/'/g, "'"), problemUrl: violation.helpUrl.replace(/'/g, "'"), ruleId: violation.id, - solutionShort: violation.description - .toLowerCase() - .replace(/'/g, "'"), - solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'"), + solutionShort: violation.description.toLowerCase().replace(/'/g, "'"), + solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'") })); - } catch (_e) { + } catch (e) { // do something with the error } await context.close(); diff --git a/.github/actions/find/src/index.ts b/.github/actions/find/src/index.ts index 8cdf836..e596647 100644 --- a/.github/actions/find/src/index.ts +++ b/.github/actions/find/src/index.ts @@ -8,11 +8,11 @@ export default async function () { const urls = core.getMultilineInput("urls", { required: true }); core.debug(`Input: 'urls: ${JSON.stringify(urls)}'`); const authContextInput: AuthContextInput = JSON.parse( - core.getInput("auth_context", { required: false }) || "{}", + core.getInput("auth_context", { required: false }) || "{}" ); const authContext = new AuthContext(authContextInput); - const findings = []; + let findings = []; for (const url of urls) { core.info(`Preparing to scan ${url}`); const findingsForUrl = await findForUrl(url, authContext); From 1300fe4fa0af9b0a8016348654cde32cf5d5b830 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:52:57 +0000 Subject: [PATCH 36/54] Updates find files --- .github/actions/find/src/findForUrl.ts | 12 +++++++----- .github/actions/find/src/index.ts | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 3bcd3fa..a7d02ed 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -14,17 +14,19 @@ export async function findForUrl(url: string, authContext?: AuthContext): Promis let findings: Finding[] = []; try { const rawFindings = await new AxeBuilder({ page }).analyze(); - findings = rawFindings.violations.map(violation => ({ - scannerType: 'axe', + findings = rawFindings.violations.map((violation) => ({ + scannerType: "axe", url, html: violation.nodes[0].html.replace(/'/g, "'"), problemShort: violation.help.toLowerCase().replace(/'/g, "'"), problemUrl: violation.helpUrl.replace(/'/g, "'"), ruleId: violation.id, - solutionShort: violation.description.toLowerCase().replace(/'/g, "'"), - solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'") + solutionShort: violation.description + .toLowerCase() + .replace(/'/g, "'"), + solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'"), })); - } catch (e) { + } catch (_e) { // do something with the error } await context.close(); diff --git a/.github/actions/find/src/index.ts b/.github/actions/find/src/index.ts index e596647..c4c6650 100644 --- a/.github/actions/find/src/index.ts +++ b/.github/actions/find/src/index.ts @@ -12,7 +12,7 @@ export default async function () { ); const authContext = new AuthContext(authContextInput); - let findings = []; + const findings = []; for (const url of urls) { core.info(`Preparing to scan ${url}`); const findingsForUrl = await findForUrl(url, authContext); From e796fc6f4729461208a5935166916511090596a2 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:54:55 +0000 Subject: [PATCH 37/54] Updates a few files --- .github/actions/file/src/generateIssueBody.ts | 26 +++++++++---------- .github/actions/file/src/index.ts | 1 + .github/actions/file/src/openIssue.ts | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index e82624e..6407022 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -1,27 +1,27 @@ import type { Finding } from "./types.d.js"; -export function generateIssueBody(finding: Finding, repoWithOwner: string): string { +export function generateIssueBody(finding: Finding): string { const solutionLong = finding.solutionLong - ?.split("\n") - .map((line: string) => - !line.trim().startsWith("Fix any") && - !line.trim().startsWith("Fix all") && - line.trim() !== "" - ? `- ${line}` - : line - ) - .join("\n"); - const acceptanceCriteria = `## Acceptance Criteria + ?.split("\n") + .map((line: string) => + !line.trim().startsWith("Fix any") && + !line.trim().startsWith("Fix all") && + line.trim() !== "" + ? `- ${line}` + : line, + ) + .join("\n"); + const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific axe 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 axe violation does not regress. - [ ] This PR MUST NOT introduce any new accessibility issues or regressions. `; - const body = `## What + const body = `## What An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. To fix this, ${finding.solutionShort}. - ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} + ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ""} ${acceptanceCriteria} `; diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 0c1304b..87b5b92 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -62,6 +62,7 @@ export default async function () { } else if (isNewFiling(filing)) { // Open a new issue for the filing response = await openIssue(octokit, repoWithOwner, filing.findings[0]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any (filing as any).issue = { state: "open" } as Issue; } else if (isRepeatedFiling(filing)) { // Reopen the filing’s issue (if necessary) diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 3220ddf..384b742 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -27,7 +27,7 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding GITHUB_ISSUE_TITLE_MAX_LENGTH, ); - const body = generateIssueBody(finding, repoWithOwner); + const body = generateIssueBody(finding); return octokit.request(`POST /repos/${owner}/${repo}/issues`, { owner, From d630300a0edbcea37a2bbc11e705c5ef7451e90b Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:56:29 +0000 Subject: [PATCH 38/54] Removes auth unused files --- .github/actions/auth/src/index.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/actions/auth/src/index.ts b/.github/actions/auth/src/index.ts index 4e0e4b1..860f7e7 100644 --- a/.github/actions/auth/src/index.ts +++ b/.github/actions/auth/src/index.ts @@ -1,7 +1,5 @@ import type { AuthContextOutput } from "./types.d.js"; -import crypto from "node:crypto"; import process from "node:process"; -import * as url from "node:url"; import core from "@actions/core"; import playwright from "playwright"; @@ -18,13 +16,6 @@ export default async function () { const password = core.getInput("password", { required: true }); core.setSecret(password); - // Determine storage path for authenticated session state - // Playwright will create missing directories, if needed - const actionDirectory = `${url.fileURLToPath(new URL(import.meta.url))}/..`; - const sessionStatePath = `${ - process.env.RUNNER_TEMP ?? actionDirectory - }/.auth/${crypto.randomUUID()}/sessionState.json`; - // Launch a headless browser browser = await playwright.chromium.launch({ headless: true, @@ -76,13 +67,19 @@ export default async function () { username, password, cookies, - localStorage: origins.reduce((acc, { origin, localStorage }) => { - acc[origin] = localStorage.reduce((acc, { name, value }) => { - acc[name] = value; + localStorage: origins.reduce( + (acc, { origin, localStorage }) => { + acc[origin] = localStorage.reduce( + (acc, { name, value }) => { + acc[name] = value; + return acc; + }, + {} as Record, + ); return acc; - }, {} as Record); - return acc; - }, {} as Record>), + }, + {} as Record>, + ), }; core.setOutput("auth_context", JSON.stringify(authContextOutput)); core.debug("Output: 'auth_context'"); From 310d936dff4082141047969c9b3f536b8b4ee840 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:59:27 +0000 Subject: [PATCH 39/54] Updates generate issue body --- .github/actions/file/src/generateIssueBody.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 6407022..797f496 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -28,4 +28,3 @@ export function generateIssueBody(finding: Finding): string { return body; } - From 41e9a3b3cf19761425dfe801345833d301411560 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:01:36 +0000 Subject: [PATCH 40/54] More files --- .github/actions/file/src/index.ts | 10 +++++----- .github/actions/file/src/openIssue.ts | 25 ++++++++++++++++--------- .github/actions/file/src/reopenIssue.ts | 24 +++++++++++++++--------- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 87b5b92..776253f 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -16,12 +16,12 @@ const OctokitWithThrottling = Octokit.plugin(throttling); export default async function () { core.info("Started 'file' action"); const findings: Finding[] = JSON.parse( - core.getInput("findings", { required: true }) + core.getInput("findings", { required: true }), ); const repoWithOwner = core.getInput("repository", { required: true }); const token = core.getInput("token", { required: true }); const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( - core.getInput("cached_filings", { required: false }) || "[]" + core.getInput("cached_filings", { required: false }) || "[]", ); core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`); core.debug(`Input: 'repository: ${repoWithOwner}'`); @@ -32,7 +32,7 @@ export default async function () { throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { octokit.log.warn( - `Request quota exhausted for request ${options.method} ${options.url}` + `Request quota exhausted for request ${options.method} ${options.url}`, ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); @@ -41,7 +41,7 @@ export default async function () { }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { octokit.log.warn( - `Secondary rate limit hit for request ${options.method} ${options.url}` + `Secondary rate limit hit for request ${options.method} ${options.url}`, ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); @@ -76,7 +76,7 @@ export default async function () { 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}` + `Set issue ${response.data.title} (${repoWithOwner}#${response.data.number}) state to ${filing.issue.state}`, ); } } catch (error) { diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 384b742..d53a350 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -1,7 +1,7 @@ -import type { Octokit } from '@octokit/core'; -import type { Finding } from './types.d.js'; +import type { Octokit } from "@octokit/core"; +import type { Finding } from "./types.d.js"; import { generateIssueBody } from "./generateIssueBody.js"; -import * as url from 'node:url' +import * as url from "node:url"; const URL = url.URL; /** Max length for GitHub issue titles */ @@ -14,14 +14,21 @@ const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256; * @returns Either the original text or a truncated version with an ellipsis */ function truncateWithEllipsis(text: string, maxLength: number): string { - return text.length > maxLength ? text.slice(0, maxLength - 1) + '…' : text; + return text.length > maxLength ? text.slice(0, maxLength - 1) + "…" : text; } -export async function openIssue(octokit: Octokit, repoWithOwner: string, finding: Finding) { - const owner = repoWithOwner.split('/')[0]; - const repo = repoWithOwner.split('/')[1]; +export async function openIssue( + octokit: Octokit, + repoWithOwner: string, + finding: Finding, +) { + const owner = repoWithOwner.split("/")[0]; + const repo = repoWithOwner.split("/")[1]; - const labels = [`${finding.scannerType} rule: ${finding.ruleId}`, `${finding.scannerType}-scanning-issue`]; + const labels = [ + `${finding.scannerType} rule: ${finding.ruleId}`, + `${finding.scannerType}-scanning-issue`, + ]; const title = truncateWithEllipsis( `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, GITHUB_ISSUE_TITLE_MAX_LENGTH, @@ -34,6 +41,6 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding repo, title, body, - labels + labels, }); } diff --git a/.github/actions/file/src/reopenIssue.ts b/.github/actions/file/src/reopenIssue.ts index e777bfd..a65cfff 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -1,11 +1,17 @@ -import type { Octokit } from '@octokit/core'; -import type { Issue } from './Issue.js'; +import type { Octokit } from "@octokit/core"; +import type { Issue } from "./Issue.js"; -export async function reopenIssue(octokit: Octokit, { owner, repository, issueNumber}: Issue) { - return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { - owner, - repository, - issue_number: issueNumber, - state: 'open' - }); +export async function reopenIssue( + octokit: Octokit, + { owner, repository, issueNumber }: Issue, +) { + return octokit.request( + `PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, + { + owner, + repository, + issue_number: issueNumber, + state: "open", + }, + ); } From f052c0b604985cb735d6326263e657b4535e39ce Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:03:01 +0000 Subject: [PATCH 41/54] More --- .github/actions/find/src/findForUrl.ts | 18 ++++++++++++------ .github/actions/find/src/index.ts | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index a7d02ed..7ca7c24 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -1,10 +1,16 @@ -import type { Finding } from './types.d.js'; -import AxeBuilder from '@axe-core/playwright' -import playwright from 'playwright'; -import { AuthContext } from './AuthContext.js'; +import type { Finding } from "./types.d.js"; +import AxeBuilder from "@axe-core/playwright"; +import playwright from "playwright"; +import { AuthContext } from "./AuthContext.js"; -export async function findForUrl(url: string, authContext?: AuthContext): Promise { - const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined }); +export async function findForUrl( + url: string, + authContext?: AuthContext, +): Promise { + const browser = await playwright.chromium.launch({ + headless: true, + executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined, + }); const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {}; const context = await browser.newContext(contextOptions); const page = await context.newPage(); diff --git a/.github/actions/find/src/index.ts b/.github/actions/find/src/index.ts index c4c6650..8cdf836 100644 --- a/.github/actions/find/src/index.ts +++ b/.github/actions/find/src/index.ts @@ -8,7 +8,7 @@ export default async function () { const urls = core.getMultilineInput("urls", { required: true }); core.debug(`Input: 'urls: ${JSON.stringify(urls)}'`); const authContextInput: AuthContextInput = JSON.parse( - core.getInput("auth_context", { required: false }) || "{}" + core.getInput("auth_context", { required: false }) || "{}", ); const authContext = new AuthContext(authContextInput); From 889e90b5a1bf7b04c605c49d5eb51dd7dcb6bab1 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:44:23 +0000 Subject: [PATCH 42/54] Uses GitHub prettier config --- .github/actions/auth/src/index.ts | 93 +++--- .github/actions/auth/src/types.d.ts | 34 +-- .github/actions/file/src/Issue.ts | 62 ++-- .github/actions/file/src/closeIssue.ts | 24 +- .github/actions/file/src/generateIssueBody.ts | 18 +- .github/actions/file/src/index.ts | 102 ++++--- .github/actions/file/src/isNewFiling.ts | 8 +- .github/actions/file/src/isRepeatedFiling.ts | 9 +- .github/actions/file/src/isResolvedFiling.ts | 8 +- .github/actions/file/src/openIssue.ts | 35 +-- .github/actions/file/src/reopenIssue.ts | 24 +- .github/actions/file/src/types.d.ts | 50 ++-- .../file/src/updateFilingsWithNewFindings.ts | 34 +-- .../file/tests/generateIssueBody.test.ts | 80 +++--- .github/actions/find/src/AuthContext.ts | 35 ++- .github/actions/find/src/findForUrl.ts | 55 ++-- .github/actions/find/src/index.ts | 42 ++- .github/actions/find/src/types.d.ts | 48 ++-- .github/actions/fix/src/Issue.ts | 34 ++- .github/actions/fix/src/assignIssue.ts | 71 ++--- .github/actions/fix/src/getLinkedPR.ts | 44 ++- .github/actions/fix/src/index.ts | 86 +++--- .github/actions/fix/src/retry.ts | 16 +- .github/actions/fix/src/types.d.ts | 18 +- .prettierrc.json | 6 - package-lock.json | 9 + package.json | 2 + tests/site-with-errors.test.ts | 271 ++++++++---------- tests/types.d.ts | 44 +-- 29 files changed, 622 insertions(+), 740 deletions(-) delete mode 100644 .prettierrc.json diff --git a/.github/actions/auth/src/index.ts b/.github/actions/auth/src/index.ts index 860f7e7..2543caa 100644 --- a/.github/actions/auth/src/index.ts +++ b/.github/actions/auth/src/index.ts @@ -1,99 +1,96 @@ -import type { AuthContextOutput } from "./types.d.js"; -import process from "node:process"; -import core from "@actions/core"; -import playwright from "playwright"; +import type {AuthContextOutput} from './types.d.js' +import process from 'node:process' +import core from '@actions/core' +import playwright from 'playwright' export default async function () { - core.info("Starting 'auth' action"); + core.info("Starting 'auth' action") - let browser: playwright.Browser | undefined; - let context: playwright.BrowserContext | undefined; - let page: playwright.Page | undefined; + let browser: playwright.Browser | undefined + let context: playwright.BrowserContext | undefined + let page: playwright.Page | undefined try { // Get inputs - const loginUrl = core.getInput("login_url", { required: true }); - const username = core.getInput("username", { required: true }); - const password = core.getInput("password", { required: true }); - core.setSecret(password); + const loginUrl = core.getInput('login_url', {required: true}) + const username = core.getInput('username', {required: true}) + const password = core.getInput('password', {required: true}) + core.setSecret(password) // Launch a headless browser browser = await playwright.chromium.launch({ headless: true, - executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined, - }); + executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined, + }) context = await browser.newContext({ // Try HTTP Basic authentication httpCredentials: { username, password, }, - }); - page = await context.newPage(); + }) + page = await context.newPage() // Navigate to login page - core.info("Navigating to login page"); - await page.goto(loginUrl); + core.info('Navigating to login page') + await page.goto(loginUrl) // Check for a login form. // If no login form is found, then either HTTP Basic auth succeeded, or the page does not require authentication. - core.info("Checking for login form"); + core.info('Checking for login form') const [usernameField, passwordField] = await Promise.all([ page.getByLabel(/user ?name/i).first(), page.getByLabel(/password/i).first(), - ]); - const [usernameFieldExists, passwordFieldExists] = await Promise.all([ - usernameField.count(), - passwordField.count(), - ]); + ]) + const [usernameFieldExists, passwordFieldExists] = await Promise.all([usernameField.count(), passwordField.count()]) if (usernameFieldExists && passwordFieldExists) { // Try form authentication - core.info("Filling username"); - await usernameField.fill(username); - core.info("Filling password"); - await passwordField.fill(password); - core.info("Logging in"); + core.info('Filling username') + await usernameField.fill(username) + core.info('Filling password') + await passwordField.fill(password) + core.info('Logging in') await page .getByLabel(/password/i) - .locator("xpath=ancestor::form") - .evaluate((form) => (form as HTMLFormElement).submit()); + .locator('xpath=ancestor::form') + .evaluate(form => (form as HTMLFormElement).submit()) } else { - core.info("No login form detected"); + core.info('No login form detected') // This occurs if HTTP Basic auth succeeded, or if the page does not require authentication. } // Output authenticated session state - const { cookies, origins } = await context.storageState(); + const {cookies, origins} = await context.storageState() const authContextOutput: AuthContextOutput = { username, password, cookies, localStorage: origins.reduce( - (acc, { origin, localStorage }) => { + (acc, {origin, localStorage}) => { acc[origin] = localStorage.reduce( - (acc, { name, value }) => { - acc[name] = value; - return acc; + (acc, {name, value}) => { + acc[name] = value + return acc }, {} as Record, - ); - return acc; + ) + return acc }, {} as Record>, ), - }; - core.setOutput("auth_context", JSON.stringify(authContextOutput)); - core.debug("Output: 'auth_context'"); + } + core.setOutput('auth_context', JSON.stringify(authContextOutput)) + core.debug("Output: 'auth_context'") } catch (error) { if (page) { - core.info(`Errored at page URL: ${page.url()}`); + core.info(`Errored at page URL: ${page.url()}`) } - core.setFailed(`${error}`); - process.exit(1); + core.setFailed(`${error}`) + process.exit(1) } finally { // Clean up - await context?.close(); - await browser?.close(); + await context?.close() + await browser?.close() } - core.info("Finished 'auth' action"); + core.info("Finished 'auth' action") } diff --git a/.github/actions/auth/src/types.d.ts b/.github/actions/auth/src/types.d.ts index 1c365ac..79db31e 100644 --- a/.github/actions/auth/src/types.d.ts +++ b/.github/actions/auth/src/types.d.ts @@ -1,23 +1,23 @@ export type Cookie = { - name: string; - value: string; - domain: string; - path: string; - expires?: number; - httpOnly?: boolean; - secure?: boolean; - sameSite?: "Strict" | "Lax" | "None"; -}; + name: string + value: string + domain: string + path: string + expires?: number + httpOnly?: boolean + secure?: boolean + sameSite?: 'Strict' | 'Lax' | 'None' +} export type LocalStorage = { [origin: string]: { - [key: string]: string; - }; -}; + [key: string]: string + } +} export type AuthContextOutput = { - username?: string; - password?: string; - cookies?: Cookie[]; - localStorage?: LocalStorage; -}; + username?: string + password?: string + cookies?: Cookie[] + localStorage?: LocalStorage +} diff --git a/.github/actions/file/src/Issue.ts b/.github/actions/file/src/Issue.ts index f622a20..0e5e2af 100644 --- a/.github/actions/file/src/Issue.ts +++ b/.github/actions/file/src/Issue.ts @@ -1,44 +1,44 @@ -import type { Issue as IssueInput } from "./types.d.js"; +import type {Issue as IssueInput} from './types.d.js' export class Issue implements IssueInput { - #url!: string; + #url!: string #parsedUrl!: { - owner: string; - repository: string; - issueNumber: number; - }; - nodeId: string; - id: number; - title: string; - state?: "open" | "reopened" | "closed"; - - constructor({ url, nodeId, id, title, state }: IssueInput) { - this.url = url; - this.nodeId = nodeId; - this.id = id; - this.title = title; - this.state = state; + owner: string + repository: string + issueNumber: number + } + nodeId: string + id: number + title: string + state?: 'open' | 'reopened' | 'closed' + + constructor({url, nodeId, id, title, state}: IssueInput) { + this.url = url + this.nodeId = nodeId + this.id = id + this.title = title + this.state = state } set url(newUrl: string) { - this.#url = newUrl; - this.#parsedUrl = this.#parseUrl(); + this.#url = newUrl + this.#parsedUrl = this.#parseUrl() } get url(): string { - return this.#url; + return this.#url } get owner(): string { - return this.#parsedUrl.owner; + return this.#parsedUrl.owner } get repository(): string { - return this.#parsedUrl.repository; + return this.#parsedUrl.repository } get issueNumber(): number { - return this.#parsedUrl.issueNumber; + return this.#parsedUrl.issueNumber } /** @@ -47,17 +47,15 @@ export class Issue implements IssueInput { * @throws The provided URL is unparseable due to its unexpected format. */ #parseUrl(): { - owner: string; - repository: string; - issueNumber: number; + owner: string + repository: string + issueNumber: number } { - const { owner, repository, issueNumber } = - /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec( - this.#url, - )?.groups || {}; + const {owner, repository, issueNumber} = + /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec(this.#url)?.groups || {} if (!owner || !repository || !issueNumber) { - throw new Error(`Could not parse issue URL: ${this.#url}`); + throw new Error(`Could not parse issue URL: ${this.#url}`) } - return { owner, repository, issueNumber: Number(issueNumber) }; + return {owner, repository, issueNumber: Number(issueNumber)} } } diff --git a/.github/actions/file/src/closeIssue.ts b/.github/actions/file/src/closeIssue.ts index e645fae..e589a6d 100644 --- a/.github/actions/file/src/closeIssue.ts +++ b/.github/actions/file/src/closeIssue.ts @@ -1,17 +1,11 @@ -import type { Octokit } from "@octokit/core"; -import { Issue } from "./Issue.js"; +import type {Octokit} from '@octokit/core' +import {Issue} from './Issue.js' -export async function closeIssue( - octokit: Octokit, - { owner, repository, issueNumber }: Issue, -) { - return octokit.request( - `PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, - { - owner, - repository, - issue_number: issueNumber, - state: "closed", - }, - ); +export async function closeIssue(octokit: Octokit, {owner, repository, issueNumber}: Issue) { + return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { + owner, + repository, + issue_number: issueNumber, + state: 'closed', + }) } diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 797f496..3a0b8fa 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -1,30 +1,28 @@ -import type { Finding } from "./types.d.js"; +import type {Finding} from './types.d.js' export function generateIssueBody(finding: Finding): string { const solutionLong = finding.solutionLong - ?.split("\n") + ?.split('\n') .map((line: string) => - !line.trim().startsWith("Fix any") && - !line.trim().startsWith("Fix all") && - line.trim() !== "" + !line.trim().startsWith('Fix any') && !line.trim().startsWith('Fix all') && line.trim() !== '' ? `- ${line}` : line, ) - .join("\n"); + .join('\n') const acceptanceCriteria = `## Acceptance Criteria - [ ] The specific axe 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 axe violation does not regress. - [ ] This PR MUST NOT introduce any new accessibility issues or regressions. - `; + ` const body = `## What An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. To fix this, ${finding.solutionShort}. - ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ""} + ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} ${acceptanceCriteria} - `; + ` - return body; + return body } diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 776253f..8d2d39c 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -1,91 +1,85 @@ -import type { Finding, ResolvedFiling, RepeatedFiling } from "./types.d.js"; -import process from "node:process"; -import core from "@actions/core"; -import { Octokit } from "@octokit/core"; -import { throttling } from "@octokit/plugin-throttling"; -import { Issue } from "./Issue.js"; -import { closeIssue } from "./closeIssue.js"; -import { isNewFiling } from "./isNewFiling.js"; -import { isRepeatedFiling } from "./isRepeatedFiling.js"; -import { isResolvedFiling } from "./isResolvedFiling.js"; -import { openIssue } from "./openIssue.js"; -import { reopenIssue } from "./reopenIssue.js"; -import { updateFilingsWithNewFindings } from "./updateFilingsWithNewFindings.js"; -const OctokitWithThrottling = Octokit.plugin(throttling); +import type {Finding, ResolvedFiling, RepeatedFiling} from './types.d.js' +import process from 'node:process' +import core from '@actions/core' +import {Octokit} from '@octokit/core' +import {throttling} from '@octokit/plugin-throttling' +import {Issue} from './Issue.js' +import {closeIssue} from './closeIssue.js' +import {isNewFiling} from './isNewFiling.js' +import {isRepeatedFiling} from './isRepeatedFiling.js' +import {isResolvedFiling} from './isResolvedFiling.js' +import {openIssue} from './openIssue.js' +import {reopenIssue} from './reopenIssue.js' +import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' +const OctokitWithThrottling = Octokit.plugin(throttling) export default async function () { - core.info("Started 'file' action"); - const findings: Finding[] = JSON.parse( - core.getInput("findings", { required: true }), - ); - const repoWithOwner = core.getInput("repository", { required: true }); - const token = core.getInput("token", { required: true }); + core.info("Started 'file' action") + const findings: Finding[] = JSON.parse(core.getInput('findings', {required: true})) + const repoWithOwner = core.getInput('repository', {required: true}) + const token = core.getInput('token', {required: true}) const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( - core.getInput("cached_filings", { required: false }) || "[]", - ); - core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`); - core.debug(`Input: 'repository: ${repoWithOwner}'`); - core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`); + core.getInput('cached_filings', {required: false}) || '[]', + ) + core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`) + core.debug(`Input: 'repository: ${repoWithOwner}'`) + core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`) const octokit = new OctokitWithThrottling({ auth: token, throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn( - `Request quota exhausted for request ${options.method} ${options.url}`, - ); + octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn( - `Secondary rate limit hit for request ${options.method} ${options.url}`, - ); + octokit.log.warn(`Secondary rate limit hit for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, }, - }); - const filings = updateFilingsWithNewFindings(cachedFilings, findings); + }) + const filings = updateFilingsWithNewFindings(cachedFilings, findings) for (const filing of filings) { - let response; + let response try { if (isResolvedFiling(filing)) { // Close the filing’s issue (if necessary) - response = await closeIssue(octokit, new Issue(filing.issue)); - filing.issue.state = "closed"; + 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]); + response = await openIssue(octokit, repoWithOwner, filing.findings[0]) // eslint-disable-next-line @typescript-eslint/no-explicit-any - (filing as any).issue = { state: "open" } as Issue; + ;(filing as any).issue = {state: 'open'} as Issue } else if (isRepeatedFiling(filing)) { // Reopen the filing’s issue (if necessary) - response = await reopenIssue(octokit, new Issue(filing.issue)); - filing.issue.state = "reopened"; + response = await reopenIssue(octokit, new Issue(filing.issue)) + 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; + 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: ${filing}\n${error}`); - process.exit(1); + core.setFailed(`Failed on filing: ${filing}\n${error}`) + process.exit(1) } } - core.setOutput("filings", JSON.stringify(filings)); - core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`); - core.info("Finished 'file' action"); + core.setOutput('filings', JSON.stringify(filings)) + core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`) + core.info("Finished 'file' action") } diff --git a/.github/actions/file/src/isNewFiling.ts b/.github/actions/file/src/isNewFiling.ts index 5a0ae20..0d7ebd2 100644 --- a/.github/actions/file/src/isNewFiling.ts +++ b/.github/actions/file/src/isNewFiling.ts @@ -1,10 +1,6 @@ -import type { Filing, NewFiling } from "./types.d.js"; +import type {Filing, NewFiling} from './types.d.js' export function isNewFiling(filing: Filing): filing is NewFiling { // A Filing without an issue is new - return ( - (!("issue" in filing) || !filing.issue?.url) && - "findings" in filing && - filing.findings.length > 0 - ); + return (!('issue' in filing) || !filing.issue?.url) && 'findings' in filing && filing.findings.length > 0 } diff --git a/.github/actions/file/src/isRepeatedFiling.ts b/.github/actions/file/src/isRepeatedFiling.ts index b9dd5f2..e18cee8 100644 --- a/.github/actions/file/src/isRepeatedFiling.ts +++ b/.github/actions/file/src/isRepeatedFiling.ts @@ -1,11 +1,6 @@ -import type { Filing, RepeatedFiling } from "./types.d.js"; +import type {Filing, RepeatedFiling} from './types.d.js' export function isRepeatedFiling(filing: Filing): filing is RepeatedFiling { // A Filing with an issue and findings is a repeated filing - return ( - "findings" in filing && - filing.findings.length > 0 && - "issue" in filing && - !!filing.issue?.url - ); + return 'findings' in filing && filing.findings.length > 0 && 'issue' in filing && !!filing.issue?.url } diff --git a/.github/actions/file/src/isResolvedFiling.ts b/.github/actions/file/src/isResolvedFiling.ts index e3d7aea..544def1 100644 --- a/.github/actions/file/src/isResolvedFiling.ts +++ b/.github/actions/file/src/isResolvedFiling.ts @@ -1,10 +1,6 @@ -import type { Filing, ResolvedFiling } from "./types.d.js"; +import type {Filing, ResolvedFiling} from './types.d.js' export function isResolvedFiling(filing: Filing): filing is ResolvedFiling { // A Filing without findings is resolved - return ( - (!("findings" in filing) || filing.findings.length === 0) && - "issue" in filing && - !!filing.issue?.url - ); + return (!('findings' in filing) || filing.findings.length === 0) && 'issue' in filing && !!filing.issue?.url } diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index d53a350..7e2ccb3 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -1,11 +1,11 @@ -import type { Octokit } from "@octokit/core"; -import type { Finding } from "./types.d.js"; -import { generateIssueBody } from "./generateIssueBody.js"; -import * as url from "node:url"; -const URL = url.URL; +import type {Octokit} from '@octokit/core' +import type {Finding} from './types.d.js' +import {generateIssueBody} from './generateIssueBody.js' +import * as url from 'node:url' +const URL = url.URL /** Max length for GitHub issue titles */ -const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256; +const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256 /** * Truncates text to a maximum length, adding an ellipsis if truncated. @@ -14,27 +14,20 @@ const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256; * @returns Either the original text or a truncated version with an ellipsis */ function truncateWithEllipsis(text: string, maxLength: number): string { - return text.length > maxLength ? text.slice(0, maxLength - 1) + "…" : text; + return text.length > maxLength ? text.slice(0, maxLength - 1) + '…' : text } -export async function openIssue( - octokit: Octokit, - repoWithOwner: string, - finding: Finding, -) { - const owner = repoWithOwner.split("/")[0]; - const repo = repoWithOwner.split("/")[1]; +export async function openIssue(octokit: Octokit, repoWithOwner: string, finding: Finding) { + const owner = repoWithOwner.split('/')[0] + const repo = repoWithOwner.split('/')[1] - const labels = [ - `${finding.scannerType} rule: ${finding.ruleId}`, - `${finding.scannerType}-scanning-issue`, - ]; + const labels = [`${finding.scannerType} rule: ${finding.ruleId}`, `${finding.scannerType}-scanning-issue`] const title = truncateWithEllipsis( `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, GITHUB_ISSUE_TITLE_MAX_LENGTH, - ); + ) - const body = generateIssueBody(finding); + const body = generateIssueBody(finding) return octokit.request(`POST /repos/${owner}/${repo}/issues`, { owner, @@ -42,5 +35,5 @@ export async function openIssue( title, body, labels, - }); + }) } diff --git a/.github/actions/file/src/reopenIssue.ts b/.github/actions/file/src/reopenIssue.ts index a65cfff..4df85b7 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -1,17 +1,11 @@ -import type { Octokit } from "@octokit/core"; -import type { Issue } from "./Issue.js"; +import type {Octokit} from '@octokit/core' +import type {Issue} from './Issue.js' -export async function reopenIssue( - octokit: Octokit, - { owner, repository, issueNumber }: Issue, -) { - return octokit.request( - `PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, - { - owner, - repository, - issue_number: issueNumber, - state: "open", - }, - ); +export async function reopenIssue(octokit: Octokit, {owner, repository, issueNumber}: Issue) { + return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { + owner, + repository, + issue_number: issueNumber, + state: 'open', + }) } diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index bcc52ea..42f420e 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -1,35 +1,35 @@ export type Finding = { - scannerType: string; - ruleId: string; - url: string; - html: string; - problemShort: string; - problemUrl: string; - solutionShort: string; - solutionLong?: string; -}; + scannerType: string + ruleId: string + url: string + html: string + problemShort: string + problemUrl: string + solutionShort: string + solutionLong?: string +} export type Issue = { - id: number; - nodeId: string; - url: string; - title: string; - state?: "open" | "reopened" | "closed"; -}; + id: number + nodeId: string + url: string + title: string + state?: 'open' | 'reopened' | 'closed' +} export type ResolvedFiling = { - findings: never[]; - issue: Issue; -}; + findings: never[] + issue: Issue +} export type NewFiling = { - findings: Finding[]; - issue?: never; -}; + findings: Finding[] + issue?: never +} export type RepeatedFiling = { - findings: Finding[]; - issue: Issue; -}; + findings: Finding[] + issue: Issue +} -export type Filing = ResolvedFiling | NewFiling | RepeatedFiling; +export type Filing = ResolvedFiling | NewFiling | RepeatedFiling diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index 0f93037..a674e68 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -1,17 +1,11 @@ -import type { - Finding, - ResolvedFiling, - NewFiling, - RepeatedFiling, - Filing, -} from "./types.d.js"; +import type {Finding, ResolvedFiling, NewFiling, RepeatedFiling, Filing} from './types.d.js' function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string { - return filing.issue.url; + return filing.issue.url } function getFindingKey(finding: Finding): string { - return `${finding.url};${finding.ruleId};${finding.html}`; + return `${finding.url};${finding.ruleId};${finding.html}` } export function updateFilingsWithNewFindings( @@ -19,10 +13,10 @@ export function updateFilingsWithNewFindings( findings: Finding[], ): Filing[] { const filingKeys: { - [key: string]: ResolvedFiling | RepeatedFiling; - } = {}; - const findingKeys: { [key: string]: string } = {}; - const newFilings: NewFiling[] = []; + [key: string]: ResolvedFiling | RepeatedFiling + } = {} + const findingKeys: {[key: string]: string} = {} + const newFilings: NewFiling[] = [] // Create maps for filing and finding data from previous runs, for quick lookups for (const filing of filings) { @@ -30,23 +24,23 @@ export function updateFilingsWithNewFindings( filingKeys[getFilingKey(filing)] = { issue: filing.issue, findings: [], - }; + } for (const finding of filing.findings) { - findingKeys[getFindingKey(finding)] = getFilingKey(filing); + findingKeys[getFindingKey(finding)] = getFilingKey(filing) } } for (const finding of findings) { - const filingKey = findingKeys[getFindingKey(finding)]; + const filingKey = findingKeys[getFindingKey(finding)] if (filingKey) { // This finding already has an associated filing; add it to that filing's findings - (filingKeys[filingKey] as RepeatedFiling).findings.push(finding); + ;(filingKeys[filingKey] as RepeatedFiling).findings.push(finding) } else { // This finding is new; create a new entry with no associated issue yet - newFilings.push({ findings: [finding] }); + newFilings.push({findings: [finding]}) } } - const updatedFilings = Object.values(filingKeys); - return [...updatedFilings, ...newFilings]; + const updatedFilings = Object.values(filingKeys) + return [...updatedFilings, ...newFilings] } diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 158df86..c4bdca6 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -1,54 +1,50 @@ -import { describe, it, expect } from "vitest"; -import { generateIssueBody } from "../src/generateIssueBody.ts"; +import {describe, it, expect} from 'vitest' +import {generateIssueBody} from '../src/generateIssueBody.ts' const baseFinding = { - 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?application=playwright", - solutionShort: - "ensure the contrast between foreground and background colors meets WCAG thresholds", -}; + 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?application=playwright', + solutionShort: 'ensure the contrast between foreground and background colors meets WCAG thresholds', +} -describe("generateIssueBody", () => { - it("includes acceptance criteria and omits the Specifically section when solutionLong is missing", () => { - const body = generateIssueBody(baseFinding, "github/accessibility-scanner"); +describe('generateIssueBody', () => { + it('includes acceptance criteria and omits the Specifically section when solutionLong is missing', () => { + const body = generateIssueBody(baseFinding, 'github/accessibility-scanner') - expect(body).toContain("## What"); - expect(body).toContain("## Acceptance Criteria"); - expect(body).toContain( - "The specific axe violation reported in this issue is no longer reproducible.", - ); - expect(body).not.toContain("Specifically:"); - }); + expect(body).toContain('## What') + expect(body).toContain('## Acceptance Criteria') + expect(body).toContain('The specific axe violation reported in this issue is no longer reproducible.') + expect(body).not.toContain('Specifically:') + }) - it("formats solutionLong lines into bullets while preserving Fix any/Fix all lines", () => { + it('formats solutionLong lines into bullets while preserving Fix any/Fix all lines', () => { const body = generateIssueBody( { ...baseFinding, solutionLong: [ - "Use a darker foreground color.", - "Fix any of the following:", - "Increase font weight.", - "Fix all of the following:", - "Add a non-color visual indicator.", - "", - ].join("\n"), + 'Use a darker foreground color.', + 'Fix any of the following:', + 'Increase font weight.', + 'Fix all of the following:', + 'Add a non-color visual indicator.', + '', + ].join('\n'), }, - "github/accessibility-scanner", - ); + 'github/accessibility-scanner', + ) - expect(body).toContain("Specifically:"); - expect(body).toContain("- Use a darker foreground color."); - expect(body).toContain("Fix any of the following:"); - expect(body).toContain("- Increase font weight."); - expect(body).toContain("Fix all of the following:"); - expect(body).toContain("- Add a non-color visual indicator."); + expect(body).toContain('Specifically:') + expect(body).toContain('- Use a darker foreground color.') + expect(body).toContain('Fix any of the following:') + expect(body).toContain('- Increase font weight.') + expect(body).toContain('Fix all of the following:') + expect(body).toContain('- Add a non-color visual indicator.') - expect(body).not.toContain("- Fix any of the following:"); - expect(body).not.toContain("- Fix all of the following:"); - }); -}); + expect(body).not.toContain('- Fix any of the following:') + expect(body).not.toContain('- Fix all of the following:') + }) +}) diff --git a/.github/actions/find/src/AuthContext.ts b/.github/actions/find/src/AuthContext.ts index 820425f..5b815e0 100644 --- a/.github/actions/find/src/AuthContext.ts +++ b/.github/actions/find/src/AuthContext.ts @@ -1,37 +1,36 @@ -import type playwright from "playwright"; -import type { Cookie, LocalStorage, AuthContextInput } from "./types.js"; +import type playwright from 'playwright' +import type {Cookie, LocalStorage, AuthContextInput} from './types.js' export class AuthContext implements AuthContextInput { - readonly username?: string; - readonly password?: string; - readonly cookies?: Cookie[]; - readonly localStorage?: LocalStorage; + readonly username?: string + readonly password?: string + readonly cookies?: Cookie[] + readonly localStorage?: LocalStorage - constructor({ username, password, cookies, localStorage }: AuthContextInput) { - this.username = username; - this.password = password; - this.cookies = cookies; - this.localStorage = localStorage; + constructor({username, password, cookies, localStorage}: AuthContextInput) { + this.username = username + this.password = password + this.cookies = cookies + this.localStorage = localStorage } toPlaywrightBrowserContextOptions(): playwright.BrowserContextOptions { - const playwrightBrowserContextOptions: playwright.BrowserContextOptions = - {}; + const playwrightBrowserContextOptions: playwright.BrowserContextOptions = {} if (this.username && this.password) { playwrightBrowserContextOptions.httpCredentials = { username: this.username, password: this.password, - }; + } } if (this.cookies || this.localStorage) { playwrightBrowserContextOptions.storageState = { // Add default values for fields Playwright requires which aren’t actually required by the Cookie API. cookies: - this.cookies?.map((cookie) => ({ + this.cookies?.map(cookie => ({ expires: -1, httpOnly: false, secure: false, - sameSite: "Lax", + sameSite: 'Lax', ...cookie, })) ?? [], // Transform the localStorage object into the shape Playwright expects. @@ -43,8 +42,8 @@ export class AuthContext implements AuthContextInput { value, })), })) ?? [], - }; + } } - return playwrightBrowserContextOptions; + return playwrightBrowserContextOptions } } diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 7ca7c24..a6f0217 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -1,41 +1,36 @@ -import type { Finding } from "./types.d.js"; -import AxeBuilder from "@axe-core/playwright"; -import playwright from "playwright"; -import { AuthContext } from "./AuthContext.js"; +import type {Finding} from './types.d.js' +import AxeBuilder from '@axe-core/playwright' +import playwright from 'playwright' +import {AuthContext} from './AuthContext.js' -export async function findForUrl( - url: string, - authContext?: AuthContext, -): Promise { +export async function findForUrl(url: string, authContext?: AuthContext): Promise { const browser = await playwright.chromium.launch({ headless: true, - executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined, - }); - const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {}; - const context = await browser.newContext(contextOptions); - const page = await context.newPage(); - await page.goto(url); - console.log(`Scanning ${page.url()}`); + executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined, + }) + const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {} + const context = await browser.newContext(contextOptions) + const page = await context.newPage() + await page.goto(url) + console.log(`Scanning ${page.url()}`) - let findings: Finding[] = []; + let findings: Finding[] = [] try { - const rawFindings = await new AxeBuilder({ page }).analyze(); - findings = rawFindings.violations.map((violation) => ({ - scannerType: "axe", + const rawFindings = await new AxeBuilder({page}).analyze() + findings = rawFindings.violations.map(violation => ({ + scannerType: 'axe', url, - html: violation.nodes[0].html.replace(/'/g, "'"), - problemShort: violation.help.toLowerCase().replace(/'/g, "'"), - problemUrl: violation.helpUrl.replace(/'/g, "'"), + html: violation.nodes[0].html.replace(/'/g, '''), + problemShort: violation.help.toLowerCase().replace(/'/g, '''), + problemUrl: violation.helpUrl.replace(/'/g, '''), ruleId: violation.id, - solutionShort: violation.description - .toLowerCase() - .replace(/'/g, "'"), - solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'"), - })); + solutionShort: violation.description.toLowerCase().replace(/'/g, '''), + solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, '''), + })) } catch (_e) { // do something with the error } - await context.close(); - await browser.close(); - return findings; + await context.close() + await browser.close() + return findings } diff --git a/.github/actions/find/src/index.ts b/.github/actions/find/src/index.ts index 8cdf836..453bb0b 100644 --- a/.github/actions/find/src/index.ts +++ b/.github/actions/find/src/index.ts @@ -1,31 +1,29 @@ -import type { AuthContextInput } from "./types.js"; -import core from "@actions/core"; -import { AuthContext } from "./AuthContext.js"; -import { findForUrl } from "./findForUrl.js"; +import type {AuthContextInput} from './types.js' +import core from '@actions/core' +import {AuthContext} from './AuthContext.js' +import {findForUrl} from './findForUrl.js' export default async function () { - core.info("Starting 'find' action"); - const urls = core.getMultilineInput("urls", { required: true }); - core.debug(`Input: 'urls: ${JSON.stringify(urls)}'`); - const authContextInput: AuthContextInput = JSON.parse( - core.getInput("auth_context", { required: false }) || "{}", - ); - const authContext = new AuthContext(authContextInput); + core.info("Starting 'find' action") + const urls = core.getMultilineInput('urls', {required: true}) + core.debug(`Input: 'urls: ${JSON.stringify(urls)}'`) + const authContextInput: AuthContextInput = JSON.parse(core.getInput('auth_context', {required: false}) || '{}') + const authContext = new AuthContext(authContextInput) - const findings = []; + const findings = [] for (const url of urls) { - core.info(`Preparing to scan ${url}`); - const findingsForUrl = await findForUrl(url, authContext); + core.info(`Preparing to scan ${url}`) + const findingsForUrl = await findForUrl(url, authContext) if (findingsForUrl.length === 0) { - core.info(`No accessibility gaps were found on ${url}`); - continue; + core.info(`No accessibility gaps were found on ${url}`) + continue } - findings.push(...findingsForUrl); - core.info(`Found ${findingsForUrl.length} findings for ${url}`); + findings.push(...findingsForUrl) + core.info(`Found ${findingsForUrl.length} findings for ${url}`) } - core.setOutput("findings", JSON.stringify(findings)); - core.debug(`Output: 'findings: ${JSON.stringify(findings)}'`); - core.info(`Found ${findings.length} findings in total`); - core.info("Finished 'find' action"); + core.setOutput('findings', JSON.stringify(findings)) + core.debug(`Output: 'findings: ${JSON.stringify(findings)}'`) + core.info(`Found ${findings.length} findings in total`) + core.info("Finished 'find' action") } diff --git a/.github/actions/find/src/types.d.ts b/.github/actions/find/src/types.d.ts index ee0ea27..ccf0002 100644 --- a/.github/actions/find/src/types.d.ts +++ b/.github/actions/find/src/types.d.ts @@ -1,32 +1,32 @@ export type Finding = { - url: string; - html: string; - problemShort: string; - problemUrl: string; - solutionShort: string; - solutionLong?: string; -}; + url: string + html: string + problemShort: string + problemUrl: string + solutionShort: string + solutionLong?: string +} export type Cookie = { - name: string; - value: string; - domain: string; - path: string; - expires?: number; - httpOnly?: boolean; - secure?: boolean; - sameSite?: "Strict" | "Lax" | "None"; -}; + name: string + value: string + domain: string + path: string + expires?: number + httpOnly?: boolean + secure?: boolean + sameSite?: 'Strict' | 'Lax' | 'None' +} export type LocalStorage = { [origin: string]: { - [key: string]: string; - }; -}; + [key: string]: string + } +} export type AuthContextInput = { - username?: string; - password?: string; - cookies?: Cookie[]; - localStorage?: LocalStorage; -}; + username?: string + password?: string + cookies?: Cookie[] + localStorage?: LocalStorage +} diff --git a/.github/actions/fix/src/Issue.ts b/.github/actions/fix/src/Issue.ts index 6e79194..0f7c35a 100644 --- a/.github/actions/fix/src/Issue.ts +++ b/.github/actions/fix/src/Issue.ts @@ -1,4 +1,4 @@ -import { Issue as IssueInput } from "./types.d.js"; +import {Issue as IssueInput} from './types.d.js' export class Issue implements IssueInput { /** @@ -8,37 +8,35 @@ export class Issue implements IssueInput { * @throws The provided URL is unparseable due to its unexpected format. */ static parseIssueUrl(issueUrl: string): { - owner: string; - repository: string; - issueNumber: number; + owner: string + repository: string + issueNumber: number } { - const { owner, repository, issueNumber } = - /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec( - issueUrl, - )?.groups || {}; + const {owner, repository, issueNumber} = + /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec(issueUrl)?.groups || {} if (!owner || !repository || !issueNumber) { - throw new Error(`Could not parse issue URL: ${issueUrl}`); + throw new Error(`Could not parse issue URL: ${issueUrl}`) } - return { owner, repository, issueNumber: Number(issueNumber) }; + return {owner, repository, issueNumber: Number(issueNumber)} } - url: string; - nodeId?: string; + url: string + nodeId?: string get owner(): string { - return Issue.parseIssueUrl(this.url).owner; + return Issue.parseIssueUrl(this.url).owner } get repository(): string { - return Issue.parseIssueUrl(this.url).repository; + return Issue.parseIssueUrl(this.url).repository } get issueNumber(): number { - return Issue.parseIssueUrl(this.url).issueNumber; + return Issue.parseIssueUrl(this.url).issueNumber } - constructor({ url, nodeId }: IssueInput) { - this.url = url; - this.nodeId = nodeId; + constructor({url, nodeId}: IssueInput) { + this.url = url + this.nodeId = nodeId } } diff --git a/.github/actions/fix/src/assignIssue.ts b/.github/actions/fix/src/assignIssue.ts index b374325..100fe0f 100644 --- a/.github/actions/fix/src/assignIssue.ts +++ b/.github/actions/fix/src/assignIssue.ts @@ -1,18 +1,15 @@ -import type { Octokit } from "@octokit/core"; -import { Issue } from "./Issue.js"; +import type {Octokit} from '@octokit/core' +import {Issue} from './Issue.js' // https://docs.github.com/en/enterprise-cloud@latest/copilot/how-tos/use-copilot-agents/coding-agent/assign-copilot-to-an-issue#assigning-an-existing-issue -export async function assignIssue( - octokit: Octokit, - { owner, repository, issueNumber, nodeId }: Issue, -) { +export async function assignIssue(octokit: Octokit, {owner, repository, issueNumber, nodeId}: Issue) { // Check whether issues can be assigned to Copilot const suggestedActorsResponse = await octokit.graphql<{ repository: { suggestedActors: { - nodes: { login: string; id: string }[]; - }; - }; + nodes: {login: string; id: string}[] + } + } }>( `query ($owner: String!, $repository: String!) { repository(owner: $owner, name: $repository) { @@ -26,58 +23,49 @@ export async function assignIssue( } } }`, - { owner, repository }, - ); - if ( - suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.login !== - "copilot-swe-agent" - ) { - return; + {owner, repository}, + ) + if (suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.login !== 'copilot-swe-agent') { + return } // Get GraphQL identifier for issue (unless already provided) - let issueId = nodeId; + let issueId = nodeId if (!issueId) { - console.debug( - `Fetching identifier for issue ${owner}/${repository}#${issueNumber}`, - ); + console.debug(`Fetching identifier for issue ${owner}/${repository}#${issueNumber}`) const issueResponse = await octokit.graphql<{ repository: { - issue: { id: string }; - }; + issue: {id: string} + } }>( `query($owner: String!, $repository: String!, $issueNumber: Int!) { repository(owner: $owner, name: $repository) { issue(number: $issueNumber) { id } } }`, - { owner, repository, issueNumber }, - ); - issueId = issueResponse?.repository?.issue?.id; - console.debug( - `Fetched identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`, - ); + {owner, repository, issueNumber}, + ) + issueId = issueResponse?.repository?.issue?.id + console.debug(`Fetched identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`) } else { - console.debug( - `Using provided identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`, - ); + console.debug(`Using provided identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`) } if (!issueId) { console.warn( `Couldn’t get identifier for issue ${owner}/${repository}#${issueNumber}. Skipping assignment to Copilot.`, - ); - return; + ) + return } // Assign issue to Copilot await octokit.graphql<{ replaceActorsForAssignable: { assignable: { - id: string; - title: string; + id: string + title: string assignees: { - nodes: { login: string }[]; - }; - }; - }; + nodes: {login: string}[] + } + } + } }>( `mutation($issueId: ID!, $assigneeId: ID!) { replaceActorsForAssignable(input: {assignableId: $issueId, actorIds: [$assigneeId]}) { @@ -96,8 +84,7 @@ export async function assignIssue( }`, { issueId, - assigneeId: - suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.id, + assigneeId: suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.id, }, - ); + ) } diff --git a/.github/actions/fix/src/getLinkedPR.ts b/.github/actions/fix/src/getLinkedPR.ts index 7508d55..f1b0796 100644 --- a/.github/actions/fix/src/getLinkedPR.ts +++ b/.github/actions/fix/src/getLinkedPR.ts @@ -1,22 +1,19 @@ -import type { Octokit } from "@octokit/core"; -import { Issue } from "./Issue.js"; +import type {Octokit} from '@octokit/core' +import {Issue} from './Issue.js' -export async function getLinkedPR( - octokit: Octokit, - { owner, repository, issueNumber }: Issue, -) { +export async function getLinkedPR(octokit: Octokit, {owner, repository, issueNumber}: Issue) { // Check whether issues can be assigned to Copilot const response = await octokit.graphql<{ repository?: { issue?: { timelineItems?: { nodes: ( - | { source: { id: string; url: string; title: string } } - | { subject: { id: string; url: string; title: string } } - )[]; - }; - }; - }; + | {source: {id: string; url: string; title: string}} + | {subject: {id: string; url: string; title: string}} + )[] + } + } + } }>( `query($owner: String!, $repository: String!, $issueNumber: Int!) { repository(owner: $owner, name: $repository) { @@ -30,16 +27,15 @@ export async function getLinkedPR( } } }`, - { owner, repository, issueNumber }, - ); - const timelineNodes = response?.repository?.issue?.timelineItems?.nodes || []; - const pullRequest: { id: string; url: string; title: string } | undefined = - timelineNodes - .map((node) => { - if ("source" in node && node.source?.url) return node.source; - if ("subject" in node && node.subject?.url) return node.subject; - return undefined; - }) - .find((pr) => !!pr); - return pullRequest; + {owner, repository, issueNumber}, + ) + const timelineNodes = response?.repository?.issue?.timelineItems?.nodes || [] + const pullRequest: {id: string; url: string; title: string} | undefined = timelineNodes + .map(node => { + if ('source' in node && node.source?.url) return node.source + if ('subject' in node && node.subject?.url) return node.subject + return undefined + }) + .find(pr => !!pr) + return pullRequest } diff --git a/.github/actions/fix/src/index.ts b/.github/actions/fix/src/index.ts index f503eda..2d4a815 100644 --- a/.github/actions/fix/src/index.ts +++ b/.github/actions/fix/src/index.ts @@ -1,76 +1,62 @@ -import type { Issue as IssueInput, Fixing } from "./types.d.js"; -import process from "node:process"; -import core from "@actions/core"; -import { Octokit } from "@octokit/core"; -import { throttling } from "@octokit/plugin-throttling"; -import { assignIssue } from "./assignIssue.js"; -import { getLinkedPR } from "./getLinkedPR.js"; -import { retry } from "./retry.js"; -import { Issue } from "./Issue.js"; -const OctokitWithThrottling = Octokit.plugin(throttling); +import type {Issue as IssueInput, Fixing} from './types.d.js' +import process from 'node:process' +import core from '@actions/core' +import {Octokit} from '@octokit/core' +import {throttling} from '@octokit/plugin-throttling' +import {assignIssue} from './assignIssue.js' +import {getLinkedPR} from './getLinkedPR.js' +import {retry} from './retry.js' +import {Issue} from './Issue.js' +const OctokitWithThrottling = Octokit.plugin(throttling) export default async function () { - core.info("Started 'fix' action"); - const issues: IssueInput[] = JSON.parse( - core.getInput("issues", { required: true }) || "[]", - ); - const repoWithOwner = core.getInput("repository", { required: true }); - const token = core.getInput("token", { required: true }); - core.debug(`Input: 'issues: ${JSON.stringify(issues)}'`); - core.debug(`Input: 'repository: ${repoWithOwner}'`); + core.info("Started 'fix' action") + const issues: IssueInput[] = JSON.parse(core.getInput('issues', {required: true}) || '[]') + const repoWithOwner = core.getInput('repository', {required: true}) + const token = core.getInput('token', {required: true}) + core.debug(`Input: 'issues: ${JSON.stringify(issues)}'`) + core.debug(`Input: 'repository: ${repoWithOwner}'`) const octokit = new OctokitWithThrottling({ auth: token, throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn( - `Request quota exhausted for request ${options.method} ${options.url}`, - ); + octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn( - `Secondary rate limit hit for request ${options.method} ${options.url}`, - ); + octokit.log.warn(`Secondary rate limit hit for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, }, - }); - const fixings: Fixing[] = issues.map((issue) => ({ issue })) as Fixing[]; + }) + const fixings: Fixing[] = issues.map(issue => ({issue})) as Fixing[] for (const fixing of fixings) { try { - const issue = new Issue(fixing.issue); - await assignIssue(octokit, issue); - core.info( - `Assigned ${issue.owner}/${issue.repository}#${issue.issueNumber} to Copilot!`, - ); - const pullRequest = await retry(() => getLinkedPR(octokit, issue)); + const issue = new Issue(fixing.issue) + await assignIssue(octokit, issue) + core.info(`Assigned ${issue.owner}/${issue.repository}#${issue.issueNumber} to Copilot!`) + const pullRequest = await retry(() => getLinkedPR(octokit, issue)) if (pullRequest) { - fixing.pullRequest = pullRequest; - core.info( - `Found linked PR for ${issue.owner}/${issue.repository}#${issue.issueNumber}: ${pullRequest.url}`, - ); + fixing.pullRequest = pullRequest + core.info(`Found linked PR for ${issue.owner}/${issue.repository}#${issue.issueNumber}: ${pullRequest.url}`) } else { - core.info( - `No linked PR was found for ${issue.owner}/${issue.repository}#${issue.issueNumber}`, - ); + core.info(`No linked PR was found for ${issue.owner}/${issue.repository}#${issue.issueNumber}`) } } catch (error) { - core.setFailed( - `Failed to assign ${fixing.issue.url} to Copilot: ${error}`, - ); - process.exit(1); + core.setFailed(`Failed to assign ${fixing.issue.url} to Copilot: ${error}`) + process.exit(1) } } - core.setOutput("fixings", JSON.stringify(fixings)); - core.debug(`Output: 'fixings: ${JSON.stringify(fixings)}'`); - core.info("Finished 'fix' action"); + core.setOutput('fixings', JSON.stringify(fixings)) + core.debug(`Output: 'fixings: ${JSON.stringify(fixings)}'`) + core.info("Finished 'fix' action") } diff --git a/.github/actions/fix/src/retry.ts b/.github/actions/fix/src/retry.ts index 4165133..5630e4d 100644 --- a/.github/actions/fix/src/retry.ts +++ b/.github/actions/fix/src/retry.ts @@ -3,7 +3,7 @@ * @param ms Time to sleep, in milliseconds. */ function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(() => resolve(), ms)); + return new Promise(resolve => setTimeout(() => resolve(), ms)) } /** @@ -20,13 +20,13 @@ export async function retry( baseDelay = 2000, attempt = 1, ): Promise { - const value = await fn(); - if (value != null) return value; - if (attempt >= maxAttempts) return undefined; + const value = await fn() + if (value != null) return value + if (attempt >= maxAttempts) return undefined /** Exponential backoff, capped at 30s */ - const delay = Math.min(30000, baseDelay * 2 ** (attempt - 1)); + const delay = Math.min(30000, baseDelay * 2 ** (attempt - 1)) /** ±10% jitter */ - const jitter = 1 + (Math.random() - 0.5) * 0.2; - await sleep(Math.round(delay * jitter)); - return retry(fn, maxAttempts, baseDelay, attempt + 1); + const jitter = 1 + (Math.random() - 0.5) * 0.2 + await sleep(Math.round(delay * jitter)) + return retry(fn, maxAttempts, baseDelay, attempt + 1) } diff --git a/.github/actions/fix/src/types.d.ts b/.github/actions/fix/src/types.d.ts index 4886425..bd94d2c 100644 --- a/.github/actions/fix/src/types.d.ts +++ b/.github/actions/fix/src/types.d.ts @@ -1,14 +1,14 @@ export type Issue = { - url: string; - nodeId?: string; -}; + url: string + nodeId?: string +} export type PullRequest = { - url: string; - nodeId?: string; -}; + url: string + nodeId?: string +} export type Fixing = { - issue: Issue; - pullRequest: PullRequest; -}; + issue: Issue + pullRequest: PullRequest +} diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index d61ae01..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "trailingComma": "all", - "singleQuote": false, - "semi": true, - "tabWidth": 2 -} diff --git a/package-lock.json b/package-lock.json index 4dcc13b..7aab2de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "devDependencies": { "@actions/core": "^3.0.0", + "@github/prettier-config": "^0.0.6", "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", @@ -609,6 +610,13 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@github/prettier-config": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@github/prettier-config/-/prettier-config-0.0.6.tgz", + "integrity": "sha512-Sdb089z+QbGnFF2NivbDeaJ62ooPlD31wE6Fkb/ESjAOXSjNJo+gjqzYYhlM7G3ERJmKFZRUJYMlsqB7Tym8lQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2368,6 +2376,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 200f6c6..3d9a1e5 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,10 @@ }, "homepage": "https://github.com/github/accessibility-scanner#readme", "type": "module", + "prettier": "@github/prettier-config", "devDependencies": { "@actions/core": "^3.0.0", + "@github/prettier-config": "^0.0.6", "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index 8e6b6ea..ecfefaa 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -1,215 +1,188 @@ -import type { Endpoints } from "@octokit/types"; -import type { Result } from "./types.d.js"; -import fs from "node:fs"; -import { describe, it, expect, beforeAll } from "vitest"; -import { Octokit } from "@octokit/core"; -import { throttling } from "@octokit/plugin-throttling"; -const OctokitWithThrottling = Octokit.plugin(throttling); +import type {Endpoints} from '@octokit/types' +import type {Result} from './types.d.js' +import fs from 'node:fs' +import {describe, it, expect, beforeAll} from 'vitest' +import {Octokit} from '@octokit/core' +import {throttling} from '@octokit/plugin-throttling' +const OctokitWithThrottling = Octokit.plugin(throttling) -describe("site-with-errors", () => { - let results: Result[]; +describe('site-with-errors', () => { + let results: Result[] beforeAll(() => { - expect(process.env.CACHE_PATH).toBeDefined(); - expect(fs.existsSync(process.env.CACHE_PATH!)).toBe(true); - results = JSON.parse(fs.readFileSync(process.env.CACHE_PATH!, "utf-8")); - }); + expect(process.env.CACHE_PATH).toBeDefined() + expect(fs.existsSync(process.env.CACHE_PATH!)).toBe(true) + results = JSON.parse(fs.readFileSync(process.env.CACHE_PATH!, 'utf-8')) + }) - it("cache has expected results", () => { - const actual = results.map( - ({ - issue: { url: issueUrl }, - pullRequest: { url: pullRequestUrl }, - findings, - }) => { - const { problemUrl, solutionLong, ...finding } = findings[0]; - // Check volatile fields for existence only - expect(issueUrl).toBeDefined(); - expect(pullRequestUrl).toBeDefined(); - expect(problemUrl).toBeDefined(); - expect(solutionLong).toBeDefined(); - // Check `problemUrl`, ignoring axe version - expect( - problemUrl.startsWith("https://dequeuniversity.com/rules/axe/"), - ).toBe(true); - expect( - problemUrl.endsWith(`/${finding.ruleId}?application=playwright`), - ).toBe(true); - return finding; - }, - ); + it('cache has expected results', () => { + const actual = results.map(({issue: {url: issueUrl}, pullRequest: {url: pullRequestUrl}, findings}) => { + const {problemUrl, solutionLong, ...finding} = findings[0] + // Check volatile fields for existence only + expect(issueUrl).toBeDefined() + expect(pullRequestUrl).toBeDefined() + expect(problemUrl).toBeDefined() + expect(solutionLong).toBeDefined() + // Check `problemUrl`, ignoring axe version + expect(problemUrl.startsWith('https://dequeuniversity.com/rules/axe/')).toBe(true) + expect(problemUrl.endsWith(`/${finding.ruleId}?application=playwright`)).toBe(true) + return finding + }) const expected = [ { - scannerType: "axe", - url: "http://127.0.0.1:4000/", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/', html: '', - problemShort: - "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', solutionShort: - "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/', html: '', - problemShort: "page should contain a level-one heading", - ruleId: "page-has-heading-one", - solutionShort: - "ensure that the page, or at least one of its frames contains a level-one heading", + problemShort: 'page should contain a level-one heading', + ruleId: 'page-has-heading-one', + solutionShort: 'ensure that the page, or at least one of its frames contains a level-one heading', }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html', html: ``, - problemShort: - "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', solutionShort: - "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/about/", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/about/', html: 'jekyllrb.com', - problemShort: - "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', solutionShort: - "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/404.html", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/404.html', html: '
  • Accessibility Scanner Demo
  • ', - problemShort: - "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', solutionShort: - "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/404.html", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/404.html', html: '

    ', - problemShort: "headings should not be empty", - ruleId: "empty-heading", - solutionShort: "ensure headings have discernible text", + problemShort: 'headings should not be empty', + ruleId: 'empty-heading', + solutionShort: 'ensure headings have discernible text', }, - ]; + ] // Check that: // - every expected object exists (no more and no fewer), and // - each object has all fields, and // - field values match expectations exactly // A specific order is _not_ enforced. - expect(actual).toHaveLength(expected.length); - expect(actual).toEqual(expect.arrayContaining(expected)); - }); + expect(actual).toHaveLength(expected.length) + expect(actual).toEqual(expect.arrayContaining(expected)) + }) - it("GITHUB_TOKEN environment variable is set", () => { - expect(process.env.GITHUB_TOKEN).toBeDefined(); - }); + it('GITHUB_TOKEN environment variable is set', () => { + expect(process.env.GITHUB_TOKEN).toBeDefined() + }) - describe.runIf(!!process.env.GITHUB_TOKEN)("—", () => { - let octokit: Octokit; - let issues: Endpoints["GET /repos/{owner}/{repo}/issues/{issue_number}"]["response"]["data"][]; - let pullRequests: Endpoints["GET /repos/{owner}/{repo}/pulls/{pull_number}"]["response"]["data"][]; + describe.runIf(!!process.env.GITHUB_TOKEN)('—', () => { + let octokit: Octokit + let issues: Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}']['response']['data'][] + let pullRequests: Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}']['response']['data'][] beforeAll(async () => { octokit = new OctokitWithThrottling({ auth: process.env.GITHUB_TOKEN, throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn( - `Request quota exhausted for request ${options.method} ${options.url}`, - ); + octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn( - `Secondary rate limit hit for request ${options.method} ${options.url}`, - ); + octokit.log.warn(`Secondary rate limit hit for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, }, - }); + }) // Fetch issues referenced in the cache file issues = await Promise.all( - results.map(async ({ issue: { url: issueUrl } }) => { - expect(issueUrl).toBeDefined(); - const { owner, repo, issueNumber } = + results.map(async ({issue: {url: issueUrl}}) => { + expect(issueUrl).toBeDefined() + const {owner, repo, issueNumber} = /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)/.exec( issueUrl!, - )!.groups!; - const { data: issue } = await octokit.request( - "GET /repos/{owner}/{repo}/issues/{issue_number}", - { - owner, - repo, - issue_number: parseInt(issueNumber, 10), - }, - ); - expect(issue).toBeDefined(); - return issue; + )!.groups! + const {data: issue} = await octokit.request('GET /repos/{owner}/{repo}/issues/{issue_number}', { + owner, + repo, + issue_number: parseInt(issueNumber, 10), + }) + expect(issue).toBeDefined() + return issue }), - ); + ) // Fetch pull requests referenced in the findings file pullRequests = await Promise.all( - results.map(async ({ pullRequest: { url: pullRequestUrl } }) => { - expect(pullRequestUrl).toBeDefined(); - const { owner, repo, pullNumber } = + results.map(async ({pullRequest: {url: pullRequestUrl}}) => { + expect(pullRequestUrl).toBeDefined() + const {owner, repo, pullNumber} = /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/pull\/(?\d+)/.exec( pullRequestUrl!, - )!.groups!; - const { data: pullRequest } = await octokit.request( - "GET /repos/{owner}/{repo}/pulls/{pull_number}", - { - owner, - repo, - pull_number: parseInt(pullNumber, 10), - }, - ); - expect(pullRequest).toBeDefined(); - return pullRequest; + )!.groups! + const {data: pullRequest} = await octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', { + owner, + repo, + pull_number: parseInt(pullNumber, 10), + }) + expect(pullRequest).toBeDefined() + return pullRequest }), - ); - }); + ) + }) - it("issues exist and have expected title, state, and assignee", async () => { - const actualTitles = issues.map(({ title }) => title); + it('issues exist and have expected title, state, and assignee', async () => { + const actualTitles = issues.map(({title}) => title) const expectedTitles = [ - "Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /", - "Accessibility issue: Page should contain a level-one heading on /", - "Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /404.html", - "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", - ]; - expect(actualTitles).toHaveLength(expectedTitles.length); - expect(actualTitles).toEqual(expect.arrayContaining(expectedTitles)); + 'Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /', + 'Accessibility issue: Page should contain a level-one heading on /', + 'Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /404.html', + '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', + ] + expect(actualTitles).toHaveLength(expectedTitles.length) + expect(actualTitles).toEqual(expect.arrayContaining(expectedTitles)) for (const issue of issues) { - expect(issue.state).toBe("open"); - expect(issue.assignees).toBeDefined(); - expect(issue.assignees!.some((a) => a.login === "Copilot")).toBe(true); + expect(issue.state).toBe('open') + expect(issue.assignees).toBeDefined() + expect(issue.assignees!.some(a => a.login === 'Copilot')).toBe(true) } - }); + }) - it("pull requests exist and have expected author, state, and assignee", async () => { + it('pull requests exist and have expected author, state, and assignee', async () => { for (const pullRequest of pullRequests) { - expect(pullRequest.user.login).toBe("Copilot"); - expect(pullRequest.state).toBe("open"); - expect(pullRequest.assignees).toBeDefined(); - expect(pullRequest.assignees!.some((a) => a.login === "Copilot")).toBe( - true, - ); + expect(pullRequest.user.login).toBe('Copilot') + expect(pullRequest.state).toBe('open') + expect(pullRequest.assignees).toBeDefined() + expect(pullRequest.assignees!.some(a => a.login === 'Copilot')).toBe(true) } - }); - }); -}); + }) + }) +}) diff --git a/tests/types.d.ts b/tests/types.d.ts index 684ab0e..fe12a2c 100644 --- a/tests/types.d.ts +++ b/tests/types.d.ts @@ -1,29 +1,29 @@ export type Finding = { - scannerType: string; - ruleId: string; - url: string; - html: string; - problemShort: string; - problemUrl: string; - solutionShort: string; - solutionLong?: string; -}; + scannerType: string + ruleId: string + url: string + html: string + problemShort: string + problemUrl: string + solutionShort: string + solutionLong?: string +} export type Issue = { - id: number; - nodeId: string; - url: string; - title: string; - state?: "open" | "reopened" | "closed"; -}; + id: number + nodeId: string + url: string + title: string + state?: 'open' | 'reopened' | 'closed' +} export type PullRequest = { - url: string; - nodeId: string; -}; + url: string + nodeId: string +} export type Result = { - findings: Finding[]; - issue: Issue; - pullRequest: PullRequest; -}; + findings: Finding[] + issue: Issue + pullRequest: PullRequest +} From 4f3fa07485513acdf56f8bd105e834ff3db53b6d Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:12:20 +0000 Subject: [PATCH 43/54] Initial updates from PR review --- .github/actions/file/action.yml | 2 +- .github/actions/file/src/index.ts | 6 ++++-- .github/actions/find/src/findForUrl.ts | 2 ++ action.yml | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index 8337bb2..c5db9d5 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -14,7 +14,7 @@ inputs: cached_filings: description: "Cached filings from previous runs, as stringified JSON. Without this, duplicate issues may be filed." required: false - screenshot_repo: + 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." required: false diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 88eaad3..749c0b8 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -20,13 +20,15 @@ export default async function () { ); const repoWithOwner = core.getInput("repository", { required: true }); const token = core.getInput("token", { required: true }); - const screenshotRepo = core.getInput("screenshot_repo", { required: false }) || repoWithOwner; + const screenshotRepo = + core.getInput("screenshot_repository", { required: false }) || + repoWithOwner; const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( core.getInput("cached_filings", { required: false }) || "[]" ); core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`); core.debug(`Input: 'repository: ${repoWithOwner}'`); - core.debug(`Input: 'screenshot_repo: ${screenshotRepo}'`); + core.debug(`Input: 'screenshot_repository: ${screenshotRepo}'`); core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`); const octokit = new OctokitWithThrottling({ diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 3e3d2cc..92d6422 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -39,6 +39,8 @@ export async function findForUrl( if (!fs.existsSync(SCREENSHOT_DIR)) { fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); console.log(`Created screenshot directory: ${SCREENSHOT_DIR}`); + } else { + console.log(`Using existing screenshot directory ${SCREENSHOT_DIR}`); } try { diff --git a/action.yml b/action.yml index 1ece0f5..fdca9c9 100644 --- a/action.yml +++ b/action.yml @@ -93,7 +93,7 @@ runs: repository: ${{ inputs.repository }} token: ${{ inputs.token }} cached_filings: ${{ steps.normalize_cache.outputs.value }} - screenshot_repo: ${{ github.repository }} + screenshot_repository: ${{ github.repository }} - if: ${{ steps.file.outputs.filings }} name: Get issues from filings id: get_issues_from_filings From 4dd03c7781b3645df08076c0946b7b00fd77c5df Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:13:28 +0000 Subject: [PATCH 44/54] Removes page.waitForLoadState --- .github/actions/find/src/findForUrl.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 92d6422..0a2d5ca 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -26,7 +26,6 @@ export async function findForUrl( const context = await browser.newContext(contextOptions); const page = await context.newPage(); await page.goto(url); - await page.waitForLoadState("domcontentloaded"); console.log(`Scanning ${page.url()}`); let findings: Finding[] = []; From 0fbb9d7510ed8208a961919922da6de130518f9c Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:33:30 +0000 Subject: [PATCH 45/54] Extracts generareScreenshots into another function --- .github/actions/find/src/findForUrl.ts | 38 ++--------------- .../actions/find/src/generateScreenshot.ts | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 35 deletions(-) create mode 100644 .github/actions/find/src/generateScreenshot.ts diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 0a2d5ca..ae6fbb3 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -2,16 +2,7 @@ import type { Finding } from './types.d.js'; import AxeBuilder from '@axe-core/playwright' import playwright from 'playwright'; import { AuthContext } from './AuthContext.js'; -import fs from "node:fs"; -import path from "node:path"; -import crypto from "node:crypto"; - -// Use GITHUB_WORKSPACE to ensure screenshots are saved in the workflow workspace root -// where the artifact upload step can find them -const SCREENSHOT_DIR = path.join( - process.env.GITHUB_WORKSPACE || process.cwd(), - ".screenshots", -); +import { generateScreenshots } from "./generateScreenshots.js"; export async function findForUrl( url: string, @@ -31,33 +22,10 @@ export async function findForUrl( let findings: Finding[] = []; try { const rawFindings = await new AxeBuilder({ page }).analyze(); - let screenshotId: string | undefined; + let screenshotId: string | undefined; if (includeScreenshots) { - // Ensure screenshot directory exists - if (!fs.existsSync(SCREENSHOT_DIR)) { - fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); - console.log(`Created screenshot directory: ${SCREENSHOT_DIR}`); - } else { - console.log(`Using existing screenshot directory ${SCREENSHOT_DIR}`); - } - - try { - const screenshotBuffer = await page.screenshot({ - fullPage: true, - type: "png", - }); - - screenshotId = crypto.randomUUID(); - const filename = `${screenshotId}.png`; - const filepath = path.join(SCREENSHOT_DIR, filename); - - fs.writeFileSync(filepath, screenshotBuffer); - console.log(`Screenshot saved: ${filename}`); - } catch (error) { - console.error("Failed to capture/save screenshot:", error); - screenshotId = undefined; - } + screenshotId = await generateScreenshots(page); } findings = rawFindings.violations.map((violation) => ({ diff --git a/.github/actions/find/src/generateScreenshot.ts b/.github/actions/find/src/generateScreenshot.ts new file mode 100644 index 0000000..3669bff --- /dev/null +++ b/.github/actions/find/src/generateScreenshot.ts @@ -0,0 +1,41 @@ +import fs from "node:fs"; +import path from "node:path"; +import crypto from "node:crypto"; +import type { Page } from "playwright"; + +// Use GITHUB_WORKSPACE to ensure screenshots are saved in the workflow workspace root +// where the artifact upload step can find them +const SCREENSHOT_DIR = path.join( + process.env.GITHUB_WORKSPACE || process.cwd(), + ".screenshots", +); + +export const generateScreenshots = async function (page: Page) { + let screenshotId: string | undefined; + // Ensure screenshot directory exists + if (!fs.existsSync(SCREENSHOT_DIR)) { + fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); + console.log(`Created screenshot directory: ${SCREENSHOT_DIR}`); + } else { + console.log(`Using existing screenshot directory ${SCREENSHOT_DIR}`); + } + + try { + const screenshotBuffer = await page.screenshot({ + fullPage: true, + type: "png", + }); + + screenshotId = crypto.randomUUID(); + const filename = `${screenshotId}.png`; + const filepath = path.join(SCREENSHOT_DIR, filename); + + fs.writeFileSync(filepath, screenshotBuffer); + console.log(`Screenshot saved: ${filename}`); + } catch (error) { + console.error("Failed to capture/save screenshot:", error); + screenshotId = undefined; + } + + return screenshotId +} From 124d17cb02307defe58834bc47cbcc3fcedb9f06 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:44:39 +0000 Subject: [PATCH 46/54] Fixes naming --- .../find/src/{generateScreenshot.ts => generateScreenshots.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/actions/find/src/{generateScreenshot.ts => generateScreenshots.ts} (100%) diff --git a/.github/actions/find/src/generateScreenshot.ts b/.github/actions/find/src/generateScreenshots.ts similarity index 100% rename from .github/actions/find/src/generateScreenshot.ts rename to .github/actions/find/src/generateScreenshots.ts From dfc8c4b8da40d5111abb27f22ffcd87a8fc02fb1 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:55:40 +0000 Subject: [PATCH 47/54] Updates ternary --- .github/actions/file/src/reopenIssue.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/actions/file/src/reopenIssue.ts b/.github/actions/file/src/reopenIssue.ts index 70f2ae3..966e782 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -10,10 +10,13 @@ export async function reopenIssue( repoWithOwner?: string, screenshotRepo?: string, ) { - const body = - finding && repoWithOwner - ? generateIssueBody(finding, screenshotRepo ?? repoWithOwner) - : undefined; + let body = {}; + if (finding && repoWithOwner) { + body = { + body: generateIssueBody(finding, screenshotRepo ?? repoWithOwner), + }; + } + return octokit.request( `PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { @@ -21,7 +24,7 @@ export async function reopenIssue( repository, issue_number: issueNumber, state: "open", - ...(body ? { body } : {}), + ...body, }, ); } From a0e058eebb7b8bb0f7df732f9472abe12b42d825 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:07:56 +0000 Subject: [PATCH 48/54] Formatting / linting --- .github/actions/file/src/generateIssueBody.ts | 28 ++-- .github/actions/file/src/index.ts | 35 +++-- .github/actions/file/src/openIssue.ts | 6 +- .github/actions/file/src/reopenIssue.ts | 31 ++--- .github/actions/file/src/types.d.ts | 20 +-- .../file/tests/generateIssueBody.test.ts | 31 ++--- .github/actions/file/tests/openIssue.test.ts | 108 +++++++-------- .../actions/file/tests/reopenIssue.test.ts | 128 +++++++++--------- .github/actions/find/src/findForUrl.ts | 40 +++--- .../actions/find/src/generateScreenshots.ts | 39 +++--- .github/actions/find/src/index.ts | 13 +- .github/actions/find/src/types.d.ts | 16 +-- tests/site-with-errors.test.ts | 92 ++++++------- tests/types.d.ts | 20 +-- 14 files changed, 284 insertions(+), 323 deletions(-) diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts index 5fcd61d..a216cdd 100644 --- a/.github/actions/file/src/generateIssueBody.ts +++ b/.github/actions/file/src/generateIssueBody.ts @@ -2,22 +2,20 @@ import type {Finding} from './types.d.js' export function generateIssueBody(finding: Finding, screenshotRepo: string): string { const solutionLong = finding.solutionLong - ?.split("\n") - .map((line: string) => - !line.trim().startsWith("Fix any") && - !line.trim().startsWith("Fix all") && - line.trim() !== "" - ? `- ${line}` - : line - ) - .join("\n"); + ?.split('\n') + .map((line: string) => + !line.trim().startsWith('Fix any') && !line.trim().startsWith('Fix all') && line.trim() !== '' + ? `- ${line}` + : line, + ) + .join('\n') - let screenshotSection; + let screenshotSection if (finding.screenshotId) { - const screenshotUrl = `https://github.com/${screenshotRepo}/blob/gh-cache/.screenshots/${finding.screenshotId}.png`; + const screenshotUrl = `https://github.com/${screenshotRepo}/blob/gh-cache/.screenshots/${finding.screenshotId}.png` screenshotSection = ` [View screenshot](${screenshotUrl}) -`; +` } const acceptanceCriteria = `## Acceptance Criteria @@ -25,14 +23,14 @@ export function generateIssueBody(finding: Finding, screenshotRepo: string): str - [ ] 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 axe violation does not regress. - [ ] This PR MUST NOT introduce any new accessibility issues or regressions. - `; + ` const body = `## What An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. - ${screenshotSection ?? ""} + ${screenshotSection ?? ''} To fix this, ${finding.solutionShort}. - ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ""} + ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} ${acceptanceCriteria} ` diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index f217b0a..e95f6d4 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -14,22 +14,18 @@ import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' const OctokitWithThrottling = Octokit.plugin(throttling) export default async function () { - core.info("Started 'file' action"); - const findings: Finding[] = JSON.parse( - core.getInput("findings", { required: true }) - ); - const repoWithOwner = core.getInput("repository", { required: true }); - const token = core.getInput("token", { required: true }); - const screenshotRepo = - core.getInput("screenshot_repository", { required: false }) || - repoWithOwner; + core.info("Started 'file' action") + const findings: Finding[] = JSON.parse(core.getInput('findings', {required: true})) + const repoWithOwner = core.getInput('repository', {required: true}) + const token = core.getInput('token', {required: true}) + const screenshotRepo = core.getInput('screenshot_repository', {required: false}) || repoWithOwner const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( - core.getInput("cached_filings", { required: false }) || "[]" - ); - core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`); - core.debug(`Input: 'repository: ${repoWithOwner}'`); - core.debug(`Input: 'screenshot_repository: ${screenshotRepo}'`); - core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`); + core.getInput('cached_filings', {required: false}) || '[]', + ) + core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`) + core.debug(`Input: 'repository: ${repoWithOwner}'`) + core.debug(`Input: 'screenshot_repository: ${screenshotRepo}'`) + core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`) const octokit = new OctokitWithThrottling({ auth: token, @@ -61,8 +57,9 @@ 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); - (filing as any).issue = { state: "open" } as Issue; + response = await openIssue(octokit, repoWithOwner, filing.findings[0], screenshotRepo) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(filing as any).issue = {state: 'open'} as Issue } else if (isRepeatedFiling(filing)) { // Reopen the filing's issue (if necessary) and update the body with the latest finding response = await reopenIssue( @@ -71,8 +68,8 @@ export default async function () { filing.findings[0], repoWithOwner, screenshotRepo, - ); - filing.issue.state = "reopened"; + ) + filing.issue.state = 'reopened' } if (response?.data && filing.issue) { // Update the filing with the latest issue data diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 7e8f35e..2297daa 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -18,8 +18,8 @@ function truncateWithEllipsis(text: string, maxLength: number): string { } export async function openIssue(octokit: Octokit, repoWithOwner: string, finding: Finding, screenshotRepo?: string) { - const owner = repoWithOwner.split('/')[0]; - const repo = repoWithOwner.split('/')[1]; + const owner = repoWithOwner.split('/')[0] + const repo = repoWithOwner.split('/')[1] const labels = [`${finding.scannerType} rule: ${finding.ruleId}`, `${finding.scannerType}-scanning-issue`] const title = truncateWithEllipsis( @@ -27,7 +27,7 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding GITHUB_ISSUE_TITLE_MAX_LENGTH, ) - const body = generateIssueBody(finding, screenshotRepo ?? repoWithOwner); + const body = generateIssueBody(finding, 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 966e782..8c1d494 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -1,30 +1,27 @@ -import type { Octokit } from '@octokit/core'; -import type { Issue } from './Issue.js'; -import type { Finding } from "./types.d.js"; -import { generateIssueBody } from "./generateIssueBody.js"; +import type {Octokit} from '@octokit/core' +import type {Issue} from './Issue.js' +import type {Finding} from './types.d.js' +import {generateIssueBody} from './generateIssueBody.js' export async function reopenIssue( octokit: Octokit, - { owner, repository, issueNumber }: Issue, + {owner, repository, issueNumber}: Issue, finding?: Finding, repoWithOwner?: string, screenshotRepo?: string, ) { - let body = {}; + let body = {} if (finding && repoWithOwner) { body = { body: generateIssueBody(finding, screenshotRepo ?? repoWithOwner), - }; + } } - return octokit.request( - `PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, - { - owner, - repository, - issue_number: issueNumber, - state: "open", - ...body, - }, - ); + return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { + owner, + repository, + issue_number: issueNumber, + state: 'open', + ...body, + }) } diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index d104267..36069a5 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -1,14 +1,14 @@ export type Finding = { - scannerType: string; - ruleId: string; - url: string; - html: string; - problemShort: string; - problemUrl: string; - solutionShort: string; - solutionLong?: string; - screenshotId?: string; -}; + scannerType: string + ruleId: string + url: string + html: string + problemShort: string + problemUrl: string + solutionShort: string + solutionLong?: string + screenshotId?: string +} export type Issue = { id: number diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts index 3fae692..96023b6 100644 --- a/.github/actions/file/tests/generateIssueBody.test.ts +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -44,24 +44,21 @@ describe('generateIssueBody', () => { expect(body).toContain('Fix all of the following:') expect(body).toContain('- Add a non-color visual indicator.') - expect(body).not.toContain("- Fix any of the following:"); - expect(body).not.toContain("- Fix all of the following:"); - }); + expect(body).not.toContain('- Fix any of the following:') + expect(body).not.toContain('- Fix all of the following:') + }) - it("uses the screenshotRepo for the screenshot URL, not the filing repo", () => { - const body = generateIssueBody( - { ...baseFinding, screenshotId: "abc123" }, - "github/my-workflow-repo", - ); + it('uses the screenshotRepo for the screenshot URL, not the filing repo', () => { + const body = generateIssueBody({...baseFinding, screenshotId: 'abc123'}, 'github/my-workflow-repo') - expect(body).toContain("github/my-workflow-repo/blob/gh-cache/.screenshots/abc123.png"); - expect(body).not.toContain("github/accessibility-scanner"); - }); + expect(body).toContain('github/my-workflow-repo/blob/gh-cache/.screenshots/abc123.png') + expect(body).not.toContain('github/accessibility-scanner') + }) - it("omits screenshot section when screenshotId is not present", () => { - const body = generateIssueBody(baseFinding, "github/accessibility-scanner"); + it('omits screenshot section when screenshotId is not present', () => { + const body = generateIssueBody(baseFinding, 'github/accessibility-scanner') - expect(body).not.toContain("View screenshot"); - expect(body).not.toContain(".screenshots"); - }); -}); + expect(body).not.toContain('View screenshot') + expect(body).not.toContain('.screenshots') + }) +}) diff --git a/.github/actions/file/tests/openIssue.test.ts b/.github/actions/file/tests/openIssue.test.ts index e4b8bab..1757381 100644 --- a/.github/actions/file/tests/openIssue.test.ts +++ b/.github/actions/file/tests/openIssue.test.ts @@ -1,84 +1,80 @@ -import { describe, it, expect, vi } from "vitest"; +import {describe, it, expect, vi} from 'vitest' // Mock generateIssueBody so we can inspect what screenshotRepo is passed -vi.mock("../src/generateIssueBody.js", () => ({ - generateIssueBody: vi.fn( - (_finding, screenshotRepo: string) => - `body with screenshotRepo=${screenshotRepo}`, - ), -})); +vi.mock('../src/generateIssueBody.js', () => ({ + generateIssueBody: vi.fn((_finding, screenshotRepo: string) => `body with screenshotRepo=${screenshotRepo}`), +})) -import { openIssue } from "../src/openIssue.ts"; -import { generateIssueBody } from "../src/generateIssueBody.ts"; +import {openIssue} from '../src/openIssue.ts' +import {generateIssueBody} from '../src/generateIssueBody.ts' const baseFinding = { - 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?application=playwright", - solutionShort: - "ensure the contrast between foreground and background colors meets WCAG thresholds", -}; + 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?application=playwright', + solutionShort: 'ensure the contrast between foreground and background colors meets WCAG thresholds', +} function mockOctokit() { return { - request: vi.fn().mockResolvedValue({ data: { id: 1, html_url: "https://github.com/org/repo/issues/1" } }), - } as any; + request: vi.fn().mockResolvedValue({data: {id: 1, html_url: 'https://github.com/org/repo/issues/1'}}), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any } -describe("openIssue", () => { - it("passes screenshotRepo to generateIssueBody when provided", async () => { - const octokit = mockOctokit(); - await openIssue(octokit, "org/filing-repo", baseFinding, "org/workflow-repo"); +describe('openIssue', () => { + it('passes screenshotRepo to generateIssueBody when provided', async () => { + const octokit = mockOctokit() + 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); + it('falls back to repoWithOwner when screenshotRepo is not provided', async () => { + const octokit = mockOctokit() + 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"); + 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') expect(octokit.request).toHaveBeenCalledWith( - "POST /repos/org/filing-repo/issues", + 'POST /repos/org/filing-repo/issues', expect.objectContaining({ - owner: "org", - repo: "filing-repo", + owner: 'org', + repo: 'filing-repo', }), - ); - }); + ) + }) - it("includes the correct labels based on the finding", async () => { - const octokit = mockOctokit(); - await openIssue(octokit, "org/repo", baseFinding); + it('includes the correct labels based on the finding', async () => { + const octokit = mockOctokit() + await openIssue(octokit, 'org/repo', baseFinding) expect(octokit.request).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - labels: ["axe rule: color-contrast", "axe-scanning-issue"], + labels: ['axe rule: color-contrast', 'axe-scanning-issue'], }), - ); - }); + ) + }) - it("truncates long titles with ellipsis", async () => { - const octokit = mockOctokit(); + it('truncates long titles with ellipsis', async () => { + const octokit = mockOctokit() const longFinding = { ...baseFinding, - problemShort: "a".repeat(300), - }; - await openIssue(octokit, "org/repo", longFinding); + problemShort: 'a'.repeat(300), + } + await openIssue(octokit, 'org/repo', longFinding) - const callArgs = octokit.request.mock.calls[0][1]; - expect(callArgs.title.length).toBeLessThanOrEqual(256); - expect(callArgs.title).toMatch(/…$/); - }); -}); + const callArgs = octokit.request.mock.calls[0][1] + expect(callArgs.title.length).toBeLessThanOrEqual(256) + expect(callArgs.title).toMatch(/…$/) + }) +}) diff --git a/.github/actions/file/tests/reopenIssue.test.ts b/.github/actions/file/tests/reopenIssue.test.ts index 6d613a3..f5b34ef 100644 --- a/.github/actions/file/tests/reopenIssue.test.ts +++ b/.github/actions/file/tests/reopenIssue.test.ts @@ -1,102 +1,98 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import {describe, it, expect, vi, beforeEach} from 'vitest' // Mock generateIssueBody so we can inspect what screenshotRepo is passed -vi.mock("../src/generateIssueBody.js", () => ({ - generateIssueBody: vi.fn( - (_finding, screenshotRepo: string) => - `body with screenshotRepo=${screenshotRepo}`, - ), -})); +vi.mock('../src/generateIssueBody.js', () => ({ + generateIssueBody: vi.fn((_finding, screenshotRepo: string) => `body with screenshotRepo=${screenshotRepo}`), +})) -import { reopenIssue } from "../src/reopenIssue.ts"; -import { generateIssueBody } from "../src/generateIssueBody.ts"; -import { Issue } from "../src/Issue.ts"; +import {reopenIssue} from '../src/reopenIssue.ts' +import {generateIssueBody} from '../src/generateIssueBody.ts' +import {Issue} from '../src/Issue.ts' const baseFinding = { - 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?application=playwright", - solutionShort: - "ensure the contrast between foreground and background colors meets WCAG thresholds", -}; + 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?application=playwright', + solutionShort: 'ensure the contrast between foreground and background colors meets WCAG thresholds', +} const testIssue = new Issue({ id: 42, - nodeId: "MDU6SXNzdWU0Mg==", - url: "https://github.com/org/filing-repo/issues/7", - title: "Accessibility issue: test", - state: "closed", -}); + nodeId: 'MDU6SXNzdWU0Mg==', + url: 'https://github.com/org/filing-repo/issues/7', + title: 'Accessibility issue: test', + state: 'closed', +}) function mockOctokit() { return { - request: vi.fn().mockResolvedValue({ data: { id: 42, html_url: "https://github.com/org/filing-repo/issues/7" } }), - } as any; + request: vi.fn().mockResolvedValue({data: {id: 42, html_url: 'https://github.com/org/filing-repo/issues/7'}}), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any } -describe("reopenIssue", () => { +describe('reopenIssue', () => { beforeEach(() => { - vi.clearAllMocks(); - }); + vi.clearAllMocks() + }) - it("passes screenshotRepo to generateIssueBody when provided", async () => { - const octokit = mockOctokit(); - await reopenIssue(octokit, testIssue, baseFinding, "org/filing-repo", "org/workflow-repo"); + it('passes screenshotRepo to generateIssueBody when provided', async () => { + const octokit = mockOctokit() + 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"); + it('falls back to repoWithOwner when screenshotRepo is not provided', async () => { + const octokit = mockOctokit() + 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 () => { - const octokit = mockOctokit(); - await reopenIssue(octokit, testIssue); + it('does not generate a body when finding is not provided', async () => { + const octokit = mockOctokit() + await reopenIssue(octokit, testIssue) - expect(generateIssueBody).not.toHaveBeenCalled(); + expect(generateIssueBody).not.toHaveBeenCalled() expect(octokit.request).toHaveBeenCalledWith( expect.any(String), - expect.not.objectContaining({ body: expect.anything() }), - ); - }); + expect.not.objectContaining({body: expect.anything()}), + ) + }) - it("does not generate a body when repoWithOwner is not provided", async () => { - const octokit = mockOctokit(); - await reopenIssue(octokit, testIssue, baseFinding); + it('does not generate a body when repoWithOwner is not provided', async () => { + const octokit = mockOctokit() + await reopenIssue(octokit, testIssue, baseFinding) - expect(generateIssueBody).not.toHaveBeenCalled(); - }); + 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"); + 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') expect(octokit.request).toHaveBeenCalledWith( - "PATCH /repos/org/filing-repo/issues/7", + 'PATCH /repos/org/filing-repo/issues/7', expect.objectContaining({ - state: "open", + state: 'open', issue_number: 7, }), - ); - }); + ) + }) - 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"); + 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') expect(octokit.request).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - body: "body with screenshotRepo=org/workflow-repo", + body: 'body with screenshotRepo=org/workflow-repo', }), - ); - }); -}); + ) + }) +}) diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 5ce7da2..8380fb3 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -1,8 +1,8 @@ import type {Finding} from './types.d.js' import AxeBuilder from '@axe-core/playwright' -import playwright from 'playwright'; -import { AuthContext } from './AuthContext.js'; -import { generateScreenshots } from "./generateScreenshots.js"; +import playwright from 'playwright' +import {AuthContext} from './AuthContext.js' +import {generateScreenshots} from './generateScreenshots.js' export async function findForUrl( url: string, @@ -11,38 +11,36 @@ export async function findForUrl( ): Promise { const browser = await playwright.chromium.launch({ headless: true, - executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined, - }); - const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {}; - const context = await browser.newContext(contextOptions); - const page = await context.newPage(); - await page.goto(url); - console.log(`Scanning ${page.url()}`); + executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined, + }) + const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {} + const context = await browser.newContext(contextOptions) + const page = await context.newPage() + await page.goto(url) + console.log(`Scanning ${page.url()}`) let findings: Finding[] = [] try { - const rawFindings = await new AxeBuilder({ page }).analyze(); + const rawFindings = await new AxeBuilder({page}).analyze() - let screenshotId: string | undefined; + let screenshotId: string | undefined if (includeScreenshots) { - screenshotId = await generateScreenshots(page); + screenshotId = await generateScreenshots(page) } - findings = rawFindings.violations.map((violation) => ({ - scannerType: "axe", + findings = rawFindings.violations.map(violation => ({ + scannerType: 'axe', url, html: violation.nodes[0].html.replace(/'/g, '''), problemShort: violation.help.toLowerCase().replace(/'/g, '''), problemUrl: violation.helpUrl.replace(/'/g, '''), ruleId: violation.id, - solutionShort: violation.description - .toLowerCase() - .replace(/'/g, "'"), - solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'"), + solutionShort: violation.description.toLowerCase().replace(/'/g, '''), + solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, '''), screenshotId, - })); + })) } catch (e) { - console.error("Error during accessibility scan:", e); + console.error('Error during accessibility scan:', e) } await context.close() await browser.close() diff --git a/.github/actions/find/src/generateScreenshots.ts b/.github/actions/find/src/generateScreenshots.ts index 3669bff..d1dd353 100644 --- a/.github/actions/find/src/generateScreenshots.ts +++ b/.github/actions/find/src/generateScreenshots.ts @@ -1,40 +1,37 @@ -import fs from "node:fs"; -import path from "node:path"; -import crypto from "node:crypto"; -import type { Page } from "playwright"; +import fs from 'node:fs' +import path from 'node:path' +import crypto from 'node:crypto' +import type {Page} from 'playwright' // Use GITHUB_WORKSPACE to ensure screenshots are saved in the workflow workspace root // where the artifact upload step can find them -const SCREENSHOT_DIR = path.join( - process.env.GITHUB_WORKSPACE || process.cwd(), - ".screenshots", -); +const SCREENSHOT_DIR = path.join(process.env.GITHUB_WORKSPACE || process.cwd(), '.screenshots') export const generateScreenshots = async function (page: Page) { - let screenshotId: string | undefined; + let screenshotId: string | undefined // Ensure screenshot directory exists if (!fs.existsSync(SCREENSHOT_DIR)) { - fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); - console.log(`Created screenshot directory: ${SCREENSHOT_DIR}`); + fs.mkdirSync(SCREENSHOT_DIR, {recursive: true}) + console.log(`Created screenshot directory: ${SCREENSHOT_DIR}`) } else { - console.log(`Using existing screenshot directory ${SCREENSHOT_DIR}`); + console.log(`Using existing screenshot directory ${SCREENSHOT_DIR}`) } try { const screenshotBuffer = await page.screenshot({ fullPage: true, - type: "png", - }); + type: 'png', + }) - screenshotId = crypto.randomUUID(); - const filename = `${screenshotId}.png`; - const filepath = path.join(SCREENSHOT_DIR, filename); + screenshotId = crypto.randomUUID() + const filename = `${screenshotId}.png` + const filepath = path.join(SCREENSHOT_DIR, filename) - fs.writeFileSync(filepath, screenshotBuffer); - console.log(`Screenshot saved: ${filename}`); + fs.writeFileSync(filepath, screenshotBuffer) + console.log(`Screenshot saved: ${filename}`) } catch (error) { - console.error("Failed to capture/save screenshot:", error); - screenshotId = undefined; + console.error('Failed to capture/save screenshot:', error) + screenshotId = undefined } return screenshotId diff --git a/.github/actions/find/src/index.ts b/.github/actions/find/src/index.ts index 6dc9884..d5f53da 100644 --- a/.github/actions/find/src/index.ts +++ b/.github/actions/find/src/index.ts @@ -10,17 +10,12 @@ export default async function () { const authContextInput: AuthContextInput = JSON.parse(core.getInput('auth_context', {required: false}) || '{}') const authContext = new AuthContext(authContextInput) - const includeScreenshots = - core.getInput("include_screenshots", { required: false }) !== "false"; + const includeScreenshots = core.getInput('include_screenshots', {required: false}) !== 'false' - const findings = []; + const findings = [] for (const url of urls) { - core.info(`Preparing to scan ${url}`); - const findingsForUrl = await findForUrl( - url, - authContext, - includeScreenshots, - ); + core.info(`Preparing to scan ${url}`) + const findingsForUrl = await findForUrl(url, authContext, includeScreenshots) if (findingsForUrl.length === 0) { core.info(`No accessibility gaps were found on ${url}`) continue diff --git a/.github/actions/find/src/types.d.ts b/.github/actions/find/src/types.d.ts index a34b06f..72582c4 100644 --- a/.github/actions/find/src/types.d.ts +++ b/.github/actions/find/src/types.d.ts @@ -1,12 +1,12 @@ export type Finding = { - url: string; - html: string; - problemShort: string; - problemUrl: string; - solutionShort: string; - solutionLong?: string; - screenshotId?: string; -}; + url: string + html: string + problemShort: string + problemUrl: string + solutionShort: string + solutionLong?: string + screenshotId?: string +} export type Cookie = { name: string diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index de9e5f9..c5861ae 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -15,84 +15,74 @@ describe('site-with-errors', () => { results = JSON.parse(fs.readFileSync(process.env.CACHE_PATH!, 'utf-8')) }) - it("cache has expected results", () => { - const actual = results.map(({ issue: { url: issueUrl }, pullRequest: { url: pullRequestUrl }, findings }) => { - const { problemUrl, solutionLong, screenshotId, ...finding } = - findings[0]; + it('cache has expected results', () => { + const actual = results.map(({issue: {url: issueUrl}, pullRequest: {url: pullRequestUrl}, findings}) => { + const {problemUrl, solutionLong, screenshotId, ...finding} = findings[0] // Check volatile fields for existence only - expect(issueUrl).toBeDefined(); - expect(pullRequestUrl).toBeDefined(); - expect(problemUrl).toBeDefined(); - expect(solutionLong).toBeDefined(); - expect(screenshotId).toBeDefined(); + expect(issueUrl).toBeDefined() + expect(pullRequestUrl).toBeDefined() + expect(problemUrl).toBeDefined() + expect(solutionLong).toBeDefined() + expect(screenshotId).toBeDefined() // Check `problemUrl`, ignoring axe version - expect( - problemUrl.startsWith("https://dequeuniversity.com/rules/axe/"), - ).toBe(true); - expect( - problemUrl.endsWith(`/${finding.ruleId}?application=playwright`), - ).toBe(true); - expect(screenshotId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); - return finding; - }); + expect(problemUrl.startsWith('https://dequeuniversity.com/rules/axe/')).toBe(true) + expect(problemUrl.endsWith(`/${finding.ruleId}?application=playwright`)).toBe(true) + expect(screenshotId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) + return finding + }) const expected = [ { scannerType: 'axe', url: 'http://127.0.0.1:4000/', html: '', - problemShort: - "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', solutionShort: - "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/', html: '', - problemShort: "page should contain a level-one heading", - ruleId: "page-has-heading-one", - solutionShort: - "ensure that the page, or at least one of its frames contains a level-one heading", + problemShort: 'page should contain a level-one heading', + ruleId: 'page-has-heading-one', + solutionShort: 'ensure that the page, or at least one of its frames contains a level-one heading', }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html', html: ``, - problemShort: - "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', solutionShort: - "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/about/", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/about/', html: 'jekyllrb.com', - problemShort: - "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', solutionShort: - "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/404.html", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/404.html', html: '
  • Accessibility Scanner Demo
  • ', - problemShort: - "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', solutionShort: - "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/404.html", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/404.html', html: '

    ', - problemShort: "headings should not be empty", - ruleId: "empty-heading", - solutionShort: "ensure headings have discernible text", + problemShort: 'headings should not be empty', + ruleId: 'empty-heading', + solutionShort: 'ensure headings have discernible text', }, { scannerType: 'axe', diff --git a/tests/types.d.ts b/tests/types.d.ts index ec6bc0d..cc2c15e 100644 --- a/tests/types.d.ts +++ b/tests/types.d.ts @@ -1,14 +1,14 @@ export type Finding = { - scannerType: string; - ruleId: string; - url: string; - html: string; - problemShort: string; - problemUrl: string; - solutionShort: string; - solutionLong?: string; - screenshotId?: string; -}; + scannerType: string + ruleId: string + url: string + html: string + problemShort: string + problemUrl: string + solutionShort: string + solutionLong?: string + screenshotId?: string +} export type Issue = { id: number From f03c192587db8a15e14b39c9d45b05629ec3b64b Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:18:00 +0000 Subject: [PATCH 49/54] Changes include_screenshots from true to false --- .github/actions/find/action.yml | 2 +- .github/actions/find/src/findForUrl.ts | 2 +- action.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/find/action.yml b/.github/actions/find/action.yml index e238a00..2ab8dcb 100644 --- a/.github/actions/find/action.yml +++ b/.github/actions/find/action.yml @@ -12,7 +12,7 @@ inputs: include_screenshots: description: "Whether to capture screenshots of scanned pages and include links to them in the issue" required: false - default: "true" + default: "false" outputs: findings: diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 8380fb3..185f58d 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -7,7 +7,7 @@ import {generateScreenshots} from './generateScreenshots.js' export async function findForUrl( url: string, authContext?: AuthContext, - includeScreenshots: boolean = true, + includeScreenshots: boolean = false, ): Promise { const browser = await playwright.chromium.launch({ headless: true, diff --git a/action.yml b/action.yml index fdca9c9..932bc27 100644 --- a/action.yml +++ b/action.yml @@ -34,7 +34,7 @@ inputs: include_screenshots: description: "Whether to capture screenshots and include links to them in the issue" required: false - default: "true" + default: "false" outputs: results: From 51b0cc5922d410dc04fb0d0476bf46b35aafec55 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:19:51 +0000 Subject: [PATCH 50/54] Updates README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 62c3cf8..3c9cebc 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ jobs: # password: ${{ secrets.PASSWORD }} # Optional: Password for authentication (use secrets!) # auth_context: # Optional: Stringified JSON object for complex authentication # 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 ``` > 👉 Update all `REPLACE_THIS` placeholders with your actual values. See [Action Inputs](#action-inputs) for details. @@ -113,6 +114,7 @@ Trigger the workflow manually or automatically based on your configuration. The | `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` | --- From 035975fcc0628d7b0038a50be31ac0a046cd2711 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:28:27 +0000 Subject: [PATCH 51/54] Update tests --- tests/site-with-errors.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index c5861ae..fb75b66 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -16,18 +16,20 @@ describe('site-with-errors', () => { }) it('cache has expected results', () => { - const actual = results.map(({issue: {url: issueUrl}, pullRequest: {url: pullRequestUrl}, findings}) => { + const actual = results.map(;({issue: {url: issueUrl}, pullRequest: {url: pullRequestUrl}, findings}) => { const {problemUrl, solutionLong, screenshotId, ...finding} = findings[0] // Check volatile fields for existence only expect(issueUrl).toBeDefined() expect(pullRequestUrl).toBeDefined() expect(problemUrl).toBeDefined() expect(solutionLong).toBeDefined() - expect(screenshotId).toBeDefined() // Check `problemUrl`, ignoring axe version expect(problemUrl.startsWith('https://dequeuniversity.com/rules/axe/')).toBe(true) expect(problemUrl.endsWith(`/${finding.ruleId}?application=playwright`)).toBe(true) - expect(screenshotId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) + // screenshotId is only present when include_screenshots is enabled + if (screenshotId !== undefined) { + expect(screenshotId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) + } return finding }) const expected = [ From 8dee19cc769da6f9f48cef8b76b1f4784d82a129 Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:30:37 +0000 Subject: [PATCH 52/54] Fixes bad semi --- 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 fb75b66..853c441 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -16,7 +16,7 @@ describe('site-with-errors', () => { }) it('cache has expected results', () => { - const actual = results.map(;({issue: {url: issueUrl}, pullRequest: {url: pullRequestUrl}, findings}) => { + const actual = results.map(({issue: {url: issueUrl}, pullRequest: {url: pullRequestUrl}, findings}) => { const {problemUrl, solutionLong, screenshotId, ...finding} = findings[0] // Check volatile fields for existence only expect(issueUrl).toBeDefined() From 594d6b6fd3f4ceeba2166a4fa037e423e95fdcec Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:35:04 +0000 Subject: [PATCH 53/54] Removes extra tests --- tests/site-with-errors.test.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index 853c441..5ce4696 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -86,32 +86,6 @@ describe('site-with-errors', () => { ruleId: 'empty-heading', solutionShort: 'ensure headings have discernible text', }, - { - scannerType: 'axe', - url: 'http://127.0.0.1:4000/about/', - html: 'jekyllrb.com', - problemShort: 'elements must meet minimum color contrast ratio thresholds', - ruleId: 'color-contrast', - solutionShort: - 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', - }, - { - scannerType: 'axe', - url: 'http://127.0.0.1:4000/404.html', - html: '
  • Accessibility Scanner Demo
  • ', - problemShort: 'elements must meet minimum color contrast ratio thresholds', - ruleId: 'color-contrast', - solutionShort: - 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', - }, - { - scannerType: 'axe', - url: 'http://127.0.0.1:4000/404.html', - html: '

    ', - problemShort: 'headings should not be empty', - ruleId: 'empty-heading', - solutionShort: 'ensure headings have discernible text', - }, ] // Check that: // - every expected object exists (no more and no fewer), and From 0b1f8916c19284308d8184aa32e94b4740a983eb Mon Sep 17 00:00:00 2001 From: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:33:34 +0000 Subject: [PATCH 54/54] Removes silly destructure --- .github/actions/file/src/reopenIssue.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/actions/file/src/reopenIssue.ts b/.github/actions/file/src/reopenIssue.ts index 8c1d494..329c695 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -10,11 +10,9 @@ export async function reopenIssue( repoWithOwner?: string, screenshotRepo?: string, ) { - let body = {} + let body: string | undefined if (finding && repoWithOwner) { - body = { - body: generateIssueBody(finding, screenshotRepo ?? repoWithOwner), - } + body = generateIssueBody(finding, screenshotRepo ?? repoWithOwner) } return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { @@ -22,6 +20,6 @@ export async function reopenIssue( repository, issue_number: issueNumber, state: 'open', - ...body, + body, }) }