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 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 09/13] 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 10/13] 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 11/13] 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 12/13] 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 13/13] 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 +}