From ef79560bc8ad37858e1afd5bb5b9d072fa7ee972 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 8 Jul 2026 17:13:20 +0100 Subject: [PATCH 01/34] Always enable `git_source` registries --- lib/entry-points.js | 3 ++- src/start-proxy.test.ts | 54 ++++++++++++++++++++++++++++++++++++----- src/start-proxy.ts | 20 ++++++++++++--- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 321b9ded4..0310338e1 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -161527,6 +161527,7 @@ function isPAT(value) { GITHUB_PAT_FINE_GRAINED_PATTERN ]); } +var ALWAYS_ENABLED_REGISTRY_TYPE = ["git_source"]; var LANGUAGE_TO_REGISTRY_TYPE = { actions: [], cpp: [], @@ -161591,7 +161592,7 @@ function getCredentials(logger, registrySecrets, registriesCredentials, language } const authConfig = getAuthConfig(e); const address = getRegistryAddress(e); - if (registryTypeForLanguage && !registryTypeForLanguage.some((t) => t === e.type)) { + if (!ALWAYS_ENABLED_REGISTRY_TYPE.some((t) => t === e.type) && registryTypeForLanguage && !registryTypeForLanguage.some((t) => t === e.type)) { continue; } const isPrintable2 = (str) => { diff --git a/src/start-proxy.test.ts b/src/start-proxy.test.ts index 6a905f16b..178f7b462 100644 --- a/src/start-proxy.test.ts +++ b/src/start-proxy.test.ts @@ -120,9 +120,14 @@ const mixedCredentials = [ { type: "maven_repository", host: "maven.pkg.github.com", token: "def" }, { type: "nuget_feed", host: "nuget.pkg.github.com", token: "ghi" }, { type: "goproxy_server", host: "goproxy.example.com", token: "jkl" }, - { type: "git_source", host: "github.com/github", token: "mno" }, ]; +const gitSourceCredential = { + type: "git_source", + host: "github.com/github", + token: "mno", +}; + test("getCredentials prefers registriesCredentials over registrySecrets", async (t) => { const registryCredentials = Buffer.from( JSON.stringify([ @@ -241,7 +246,7 @@ test("getCredentials returns all for a language when specified", async (t) => { const credentials = startProxyExports.getCredentials( getRunnerLogger(true), undefined, - toEncodedJSON(mixedCredentials), + toEncodedJSON([...mixedCredentials, gitSourceCredential]), BuiltInLanguage.go, ); t.is(credentials.length, 2); @@ -284,7 +289,7 @@ test("getCredentials returns all maven_repositories for Java when specified", as host: "maven2.pkg.github.com", token: "token2", }, - { type: "git_source", host: "github.com/github", token: "mno" }, + { type: "goproxy_server", host: "github.com/github", token: "mno" }, ]; const credentials = startProxyExports.getCredentials( @@ -624,8 +629,11 @@ test("getCredentials validates 'replaces-base' correctly", async (t) => { ); }); -test("getCredentials returns no credentials for Actions", async (t) => { - const credentialsInput = toEncodedJSON(mixedCredentials); +test("getCredentials returns only ALWAYS_ENABLED_REGISTRY_TYPE credentials for Actions", async (t) => { + const credentialsInput = toEncodedJSON([ + ...mixedCredentials, + gitSourceCredential, + ]); const credentials = startProxyExports.getCredentials( getRunnerLogger(true), @@ -633,7 +641,41 @@ test("getCredentials returns no credentials for Actions", async (t) => { credentialsInput, BuiltInLanguage.actions, ); - t.deepEqual(credentials, []); + + for (const credential of credentials) { + t.true( + startProxyExports.ALWAYS_ENABLED_REGISTRY_TYPE.some( + (ty) => ty === credential.type, + ), + ); + } +}); + +test("getCredentials always returns ALWAYS_ENABLED_REGISTRY_TYPE credentials for all languages", async (t) => { + const alwaysEnabledCredentials: startProxyExports.Credential[] = []; + + for (const alwaysEnabled of startProxyExports.ALWAYS_ENABLED_REGISTRY_TYPE) { + alwaysEnabledCredentials.push({ + type: alwaysEnabled, + host: `host-${alwaysEnabled}`, + token: `bar-${alwaysEnabled}`, + url: `url-${alwaysEnabled}`, + }); + } + + const credentialsInput = toEncodedJSON(alwaysEnabledCredentials); + + // Test all languages. + for (const language of Object.values(BuiltInLanguage)) { + const credentials = startProxyExports.getCredentials( + getRunnerLogger(true), + undefined, + credentialsInput, + language, + ); + + t.deepEqual(credentials, alwaysEnabledCredentials); + } }); function mockGetApiClient(endpoints: any) { diff --git a/src/start-proxy.ts b/src/start-proxy.ts index 6b956d647..c492ae6e1 100644 --- a/src/start-proxy.ts +++ b/src/start-proxy.ts @@ -187,9 +187,16 @@ function isPAT(value: string) { ]); } +/** + * A list of always-enabled registry types. The registry types in this list are always + * enabled, because generic CodeQL workflow components may use them rather than just + * language-specific components. + */ +export const ALWAYS_ENABLED_REGISTRY_TYPE = ["git_source"]; + type RegistryMapping = Partial>; -const LANGUAGE_TO_REGISTRY_TYPE: Required = { +export const LANGUAGE_TO_REGISTRY_TYPE: Required = { actions: [], cpp: [], java: ["maven_repository"], @@ -233,9 +240,11 @@ function getRegistryAddress( } } -// getCredentials returns registry credentials from action inputs. -// It prefers `registries_credentials` over `registry_secrets`. -// If neither is set, it returns an empty array. +/** + * Returns registry credentials from action inputs. + * It prefers `registriesCredentials` over `registrySecrets`. + * If neither is set, it returns an empty array. + */ export function getCredentials( logger: Logger, registrySecrets: string | undefined, @@ -292,7 +301,10 @@ export function getCredentials( // Filter credentials based on language if specified. `type` is the registry type. // E.g., "maven_feed" for Java/Kotlin, "nuget_repository" for C#. + // We always allow types in `ALWAYS_ENABLED_REGISTRY_TYPE` since they can be used by + // other parts of the workflow. if ( + !ALWAYS_ENABLED_REGISTRY_TYPE.some((t) => t === e.type) && registryTypeForLanguage && !registryTypeForLanguage.some((t) => t === e.type) ) { From 1c42f210612566d6c5c566384feb2ad62f8e49ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:55:10 +0000 Subject: [PATCH 02/34] Bump the npm-minor group across 1 directory with 4 updates Bumps the npm-minor group with 4 updates in the / directory: [js-yaml](https://github.com/nodeca/js-yaml), [eslint-plugin-import-x](https://github.com/un-ts/eslint-plugin-import-x), [nock](https://github.com/nock/nock) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint). Updates `js-yaml` from 5.1.0 to 5.2.0 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/5.1.0...5.2.0) Updates `eslint-plugin-import-x` from 4.17.0 to 4.17.1 - [Release notes](https://github.com/un-ts/eslint-plugin-import-x/releases) - [Changelog](https://github.com/un-ts/eslint-plugin-import-x/blob/master/CHANGELOG.md) - [Commits](https://github.com/un-ts/eslint-plugin-import-x/compare/v4.17.0...v4.17.1) Updates `nock` from 14.0.15 to 14.0.16 - [Release notes](https://github.com/nock/nock/releases) - [Changelog](https://github.com/nock/nock/blob/main/CHANGELOG.md) - [Commits](https://github.com/nock/nock/compare/v14.0.15...v14.0.16) Updates `typescript-eslint` from 8.62.0 to 8.62.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/typescript-eslint) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 5.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor - dependency-name: eslint-plugin-import-x dependency-version: 4.17.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor - dependency-name: nock dependency-version: 14.0.16 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor - dependency-name: typescript-eslint dependency-version: 8.62.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 148 +++++++++++++++++++++++----------------------- package.json | 8 +-- 2 files changed, 78 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index a580a2c5b..55c539a4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "follow-redirects": "^1.16.0", "get-folder-size": "^5.0.0", "https-proxy-agent": "^7.0.6", - "js-yaml": "^5.1.0", + "js-yaml": "^5.2.0", "jsonschema": "1.5.0", "long": "^5.3.2", "node-forge": "^1.4.0", @@ -53,15 +53,15 @@ "eslint": "^9.39.4", "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-github": "^6.0.0", - "eslint-plugin-import-x": "^4.17.0", + "eslint-plugin-import-x": "^4.17.1", "eslint-plugin-jsdoc": "^62.9.0", "eslint-plugin-no-async-foreach": "^0.1.1", "glob": "^13.0.6", "globals": "^17.7.0", - "nock": "^14.0.15", + "nock": "^14.0.16", "sinon": "^22.0.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.62.0" + "typescript-eslint": "^8.62.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2587,17 +2587,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", - "integrity": "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", + "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.62.0", - "@typescript-eslint/type-utils": "8.62.0", - "@typescript-eslint/utils": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/type-utils": "8.62.1", + "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -2610,7 +2610,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.62.0", + "@typescript-eslint/parser": "^8.62.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -2626,16 +2626,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz", - "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", + "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.62.0", - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", "debug": "^4.4.3" }, "engines": { @@ -2669,14 +2669,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz", - "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", + "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.62.0", - "@typescript-eslint/types": "^8.62.0", + "@typescript-eslint/tsconfig-utils": "^8.62.1", + "@typescript-eslint/types": "^8.62.1", "debug": "^4.4.3" }, "engines": { @@ -2709,14 +2709,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz", - "integrity": "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", + "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0" + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2727,9 +2727,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz", - "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", + "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", "dev": true, "license": "MIT", "engines": { @@ -2744,15 +2744,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz", - "integrity": "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz", + "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0", - "@typescript-eslint/utils": "8.62.0", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2787,9 +2787,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz", - "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", + "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -2801,16 +2801,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz", - "integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", + "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.62.0", - "@typescript-eslint/tsconfig-utils": "8.62.0", - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0", + "@typescript-eslint/project-service": "8.62.1", + "@typescript-eslint/tsconfig-utils": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2886,16 +2886,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz", - "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz", + "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.62.0", - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0" + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2910,13 +2910,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz", - "integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", + "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/types": "8.62.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -5125,9 +5125,9 @@ } }, "node_modules/eslint-plugin-import-x": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.17.0.tgz", - "integrity": "sha512-aM7V25Bg6YuYxtEhwjafzfS0NTMds1D2PMQI0K4KqJxQJRtkP4CO+MQTWRdBq2qAnmPxTxLevhXUBtByxJqS1w==", + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.17.1.tgz", + "integrity": "sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==", "dev": true, "license": "MIT", "dependencies": { @@ -6978,9 +6978,9 @@ } }, "node_modules/js-yaml": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.1.0.tgz", - "integrity": "sha512-s8VA5jkR8f22S3NAXmhKPFqGUduqZGlsufabVOgN14iTdw/RXcym7bKkbwjxLK9Yw2lEvvmJjFp119+KPeo8Kg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.0.tgz", + "integrity": "sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==", "funding": [ { "type": "github", @@ -7443,9 +7443,9 @@ "license": "MIT" }, "node_modules/nock": { - "version": "14.0.15", - "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.15.tgz", - "integrity": "sha512-S0a47C9pLvcYx/Ugf0H30BVBEcUgMMBDk9VJIDlJ8XGrfH2QDUD4Tgdp45qDIiHttokBG+IbsOtsvIjGR/j3bg==", + "version": "14.0.16", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.16.tgz", + "integrity": "sha512-8r4KEc6nT1D/fdLD/R1BO1CPaVEL8o40u/guFRJlXabN7vr3RmMqyjsY5Krt0nMwhsOAwXQ/mtN5vy5Jh3aErg==", "dev": true, "license": "MIT", "dependencies": { @@ -9317,16 +9317,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.0.tgz", - "integrity": "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz", + "integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.62.0", - "@typescript-eslint/parser": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0", - "@typescript-eslint/utils": "8.62.0" + "@typescript-eslint/eslint-plugin": "8.62.1", + "@typescript-eslint/parser": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 46fb37e98..4b60d0dc8 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "follow-redirects": "^1.16.0", "get-folder-size": "^5.0.0", "https-proxy-agent": "^7.0.6", - "js-yaml": "^5.1.0", + "js-yaml": "^5.2.0", "jsonschema": "1.5.0", "long": "^5.3.2", "node-forge": "^1.4.0", @@ -61,15 +61,15 @@ "eslint": "^9.39.4", "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-github": "^6.0.0", - "eslint-plugin-import-x": "^4.17.0", + "eslint-plugin-import-x": "^4.17.1", "eslint-plugin-jsdoc": "^62.9.0", "eslint-plugin-no-async-foreach": "^0.1.1", "glob": "^13.0.6", "globals": "^17.7.0", - "nock": "^14.0.15", + "nock": "^14.0.16", "sinon": "^22.0.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.62.0" + "typescript-eslint": "^8.62.1" }, "overrides": { "@actions/tool-cache": { From 9fdfd3e4c83e1014fa1bee1623a05ed76c14655e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:55:17 +0000 Subject: [PATCH 03/34] Bump @types/sinon from 21.0.1 to 22.0.0 Bumps [@types/sinon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sinon) from 21.0.1 to 22.0.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sinon) --- updated-dependencies: - dependency-name: "@types/sinon" dependency-version: 22.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a580a2c5b..77f1db9f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "@types/node-forge": "^1.3.14", "@types/sarif": "^2.1.7", "@types/semver": "^7.7.1", - "@types/sinon": "^21.0.1", + "@types/sinon": "^22.0.0", "ava": "^6.4.1", "esbuild": "^0.28.1", "eslint": "^9.39.4", @@ -2572,9 +2572,9 @@ "license": "MIT" }, "node_modules/@types/sinon": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.1.tgz", - "integrity": "sha512-5yoJSqLbjH8T9V2bksgRayuhpZy+723/z6wBOR+Soe4ZlXC0eW8Na71TeaZPUWDQvM7LYKa9UGFc6LRqxiR5fQ==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-22.0.0.tgz", + "integrity": "sha512-TDbVpbccc2HfiqHR09Argj3mHV1KMW7sCCKj52fsl8lbRLkEn7fB1966EWhOKWUBcqfBueZuPoA7/OK1CKiy3g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 46fb37e98..fe539cf9a 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@types/node-forge": "^1.3.14", "@types/sarif": "^2.1.7", "@types/semver": "^7.7.1", - "@types/sinon": "^21.0.1", + "@types/sinon": "^22.0.0", "ava": "^6.4.1", "esbuild": "^0.28.1", "eslint": "^9.39.4", From ac36b7d725ad21845873be2e7449422053b0e7f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:57:09 +0000 Subject: [PATCH 04/34] Bump the actions-minor group across 1 directory with 2 updates Bumps the actions-minor group with 2 updates in the /.github/workflows directory: [actions/setup-java](https://github.com/actions/setup-java) and [ruby/setup-ruby](https://github.com/ruby/setup-ruby). Updates `actions/setup-java` from 5.4.0 to 5.5.0 - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/1bcf9fb12cf4aa7d266a90ae39939e61372fe520...0f481fcb613427c0f801b606911222b5b6f3083a) Updates `ruby/setup-ruby` from 1.314.0 to 1.315.0 - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/9eb537ca036ebaed86729dcb9309076e4c5c3b74...0dafeac902942906541bc140009cdbf32665b601) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: 5.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-minor - dependency-name: ruby/setup-ruby dependency-version: 1.315.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-minor ... Signed-off-by: dependabot[bot] --- .../workflows/__autobuild-direct-tracing-with-working-dir.yml | 2 +- .github/workflows/__build-mode-autobuild.yml | 2 +- .github/workflows/__rubocop-multi-language.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/__autobuild-direct-tracing-with-working-dir.yml b/.github/workflows/__autobuild-direct-tracing-with-working-dir.yml index c5856df04..13918e539 100644 --- a/.github/workflows/__autobuild-direct-tracing-with-working-dir.yml +++ b/.github/workflows/__autobuild-direct-tracing-with-working-dir.yml @@ -63,7 +63,7 @@ jobs: - name: Check out repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install Java - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0 with: java-version: ${{ inputs.java-version || '17' }} distribution: temurin diff --git a/.github/workflows/__build-mode-autobuild.yml b/.github/workflows/__build-mode-autobuild.yml index 1ecceee86..74881301c 100644 --- a/.github/workflows/__build-mode-autobuild.yml +++ b/.github/workflows/__build-mode-autobuild.yml @@ -63,7 +63,7 @@ jobs: - name: Check out repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install Java - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0 with: java-version: ${{ inputs.java-version || '17' }} distribution: temurin diff --git a/.github/workflows/__rubocop-multi-language.yml b/.github/workflows/__rubocop-multi-language.yml index bc8499d60..08f667d7c 100644 --- a/.github/workflows/__rubocop-multi-language.yml +++ b/.github/workflows/__rubocop-multi-language.yml @@ -54,7 +54,7 @@ jobs: use-all-platform-bundle: 'false' setup-kotlin: 'true' - name: Set up Ruby - uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314.0 + uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 with: ruby-version: 2.6 - name: Install Code Scanning integration From d8168ab6553574ff9ac06f90714b0325bf5136dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:57:12 +0000 Subject: [PATCH 05/34] Rebuild --- lib/entry-points.js | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 406916cf6..664f590f7 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -141750,7 +141750,7 @@ var intCoreTag = defineScalarTag("tag:yaml.org,2002:int", { ..."0123456789" ], resolve: resolveYamlInteger$2, - identify: (object) => Object.prototype.toString.call(object) === "[object Number]" && object % 1 === 0 && !Object.is(object, -0), + identify: (object) => Number.isInteger(object) && !Object.is(object, -0) && object.toString(10).indexOf("e") < 0, represent: (object) => object.toString(10) }); var YAML_INTEGER_IMPLICIT_PATTERN = /* @__PURE__ */ new RegExp("^-?(?:0|[1-9][0-9]*)$"); @@ -141778,7 +141778,7 @@ var intJsonTag = defineScalarTag("tag:yaml.org,2002:int", { implicit: true, implicitFirstChars: ["-", ..."0123456789"], resolve: resolveYamlInteger$1, - identify: (object) => Object.prototype.toString.call(object) === "[object Number]" && object % 1 === 0 && !Object.is(object, -0), + identify: (object) => Number.isInteger(object) && !Object.is(object, -0) && object.toString(10).indexOf("e") < 0, represent: (object) => object.toString(10) }); var YAML_INTEGER_PATTERN = /* @__PURE__ */ new RegExp("^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?0x[0-9a-fA-F_]+|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+|[-+]?(?:0|[1-9][0-9_]*))$"); @@ -141812,7 +141812,7 @@ var intYaml11Tag = defineScalarTag("tag:yaml.org,2002:int", { ..."0123456789" ], resolve: resolveYamlInteger, - identify: (object) => Object.prototype.toString.call(object) === "[object Number]" && object % 1 === 0 && !Object.is(object, -0), + identify: (object) => Number.isInteger(object) && !Object.is(object, -0) && object.toString(10).indexOf("e") < 0, represent: (object) => object.toString(10) }); var YAML_FLOAT_PATTERN$1 = /* @__PURE__ */ new RegExp("^(?:[-+]?[0-9]+(?:\\.[0-9]*)?(?:[eE][-+]?[0-9]+)?|[-+]?\\.[0-9]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"); @@ -141845,7 +141845,7 @@ var floatCoreTag = defineScalarTag("tag:yaml.org,2002:float", { ..."0123456789" ], resolve: resolveYamlFloat$2, - identify: (object) => Object.prototype.toString.call(object) === "[object Number]" && (object % 1 !== 0 || Object.is(object, -0)), + identify: (object) => typeof object === "number" && (!Number.isInteger(object) || Object.is(object, -0) || object.toString(10).indexOf("e") >= 0), represent: representYamlFloat$2 }); var YAML_FLOAT_IMPLICIT_PATTERN = /* @__PURE__ */ new RegExp("^-?(?:0|[1-9][0-9]*)(?:\\.[0-9]*)?(?:[eE][-+]?[0-9]+)?$"); @@ -141878,7 +141878,7 @@ var floatJsonTag = defineScalarTag("tag:yaml.org,2002:float", { implicit: true, implicitFirstChars: ["-", ..."0123456789"], resolve: resolveYamlFloat$1, - identify: (object) => Object.prototype.toString.call(object) === "[object Number]" && (object % 1 !== 0 || Object.is(object, -0)), + identify: (object) => typeof object === "number" && (!Number.isInteger(object) || Object.is(object, -0) || object.toString(10).indexOf("e") >= 0), represent: representYamlFloat$1 }); var YAML_FLOAT_PATTERN = /* @__PURE__ */ new RegExp("^(?:[-+]?(?:(?:[0-9][0-9_]*)?\\.[0-9_]*)(?:[eE][-+][0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"); @@ -141915,7 +141915,7 @@ var floatYaml11Tag = defineScalarTag("tag:yaml.org,2002:float", { ..."0123456789" ], resolve: resolveYamlFloat, - identify: (object) => Object.prototype.toString.call(object) === "[object Number]" && (object % 1 !== 0 || Object.is(object, -0)), + identify: (object) => typeof object === "number" && (!Number.isInteger(object) || Object.is(object, -0) || object.toString(10).indexOf("e") >= 0), represent: representYamlFloat }); var mergeTag = defineScalarTag("tag:yaml.org,2002:merge", { @@ -142614,7 +142614,8 @@ var DEFAULT_CONSTRUCTOR_OPTIONS = { filename: "", schema: CORE_SCHEMA, json: false, - maxMergeSeqLength: 20 + maxTotalMergeKeys: 1e4, + maxAliases: -1 }; function eventPosition$1(event) { if ("tagStart" in event && event.tagStart !== NO_RANGE$2) return event.tagStart; @@ -142702,6 +142703,7 @@ function isMappingTag(tag) { } function mergeKeys(state, frame, source, sourceTag) { for (const sourceKey of sourceTag.keys(source)) { + if (state.maxTotalMergeKeys !== -1 && ++state.totalMergeKeys > state.maxTotalMergeKeys) throwError$1(state, `merge keys exceeded maxTotalMergeKeys (${state.maxTotalMergeKeys})`); if (frame.tag.has(frame.value, sourceKey)) continue; const err = frame.tag.addPair(frame.value, sourceKey, sourceTag.get(source, sourceKey)); if (err) throwError$1(state, err); @@ -142711,14 +142713,8 @@ function mergeKeys(state, frame, source, sourceTag) { function mergeSource(state, frame, source, sourceTag) { state.position = frame.keyPosition; if (isMappingTag(sourceTag)) mergeKeys(state, frame, source, sourceTag); - else if (sourceTag.nodeKind === "sequence" && Array.isArray(source)) { - const seen = /* @__PURE__ */ new Set(); - for (const element of source) { - if (seen.has(element)) continue; - seen.add(element); - mergeKeys(state, frame, element, frame.tag); - } - } else throwError$1(state, "cannot merge mappings; the provided source object is unacceptable"); + else if (sourceTag.nodeKind === "sequence" && Array.isArray(source)) for (const element of source) mergeKeys(state, frame, element, frame.tag); + else throwError$1(state, "cannot merge mappings; the provided source object is unacceptable"); } function addMappingValue(state, frame, key, value, tag) { state.position = frame.keyPosition; @@ -142739,7 +142735,6 @@ function addValue(state, value, tag) { } else if (frame.kind === "sequence") { if (frame.merge) { if (!isMappingTag(tag)) throwError$1(state, "cannot merge mappings; the provided source object is unacceptable"); - if (frame.index >= state.maxMergeSeqLength) throwError$1(state, `merge sequence length exceeded maxMergeSeqLength (${state.maxMergeSeqLength})`); } const err = frame.tag.addItem(frame.value, value, frame.index++); if (err) throwError$1(state, err); @@ -142776,7 +142771,9 @@ function constructFromEvents(events, options) { position: 0, frames: [], anchors: /* @__PURE__ */ new Map(), - tagHandlers: /* @__PURE__ */ Object.create(null) + tagHandlers: /* @__PURE__ */ Object.create(null), + totalMergeKeys: 0, + aliasCount: 0 }; while (state.eventIndex < state.events.length) { const event = state.events[state.eventIndex++]; @@ -142784,6 +142781,7 @@ function constructFromEvents(events, options) { switch (event.type) { case 1: state.anchors = /* @__PURE__ */ new Map(); + state.aliasCount = 0; state.tagHandlers = /* @__PURE__ */ Object.create(null); for (const directive of event.directives) if (directive.kind === "tag") state.tagHandlers[directive.handle] = directive.prefix; state.frames.push({ @@ -142834,6 +142832,7 @@ function constructFromEvents(events, options) { break; } case 5: { + if (state.maxAliases !== -1 && ++state.aliasCount > state.maxAliases) throwError$1(state, `aliases exceeded maxAliases (${state.maxAliases})`); const name = state.source.slice(event.anchorStart, event.anchorEnd); const anchor = state.anchors.get(name); if (!anchor) throwError$1(state, `unidentified alias "${name}"`); @@ -162682,7 +162681,7 @@ tmp/lib/tmp.js: *) js-yaml/dist/js-yaml.mjs: - (*! js-yaml 5.1.0 https://github.com/nodeca/js-yaml @license MIT *) + (*! js-yaml 5.2.0 https://github.com/nodeca/js-yaml @license MIT *) long/index.js: (** From 258af3df601bfc60214920b71511f2abb099fa20 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:59:16 +0000 Subject: [PATCH 06/34] Rebuild --- pr-checks/checks/rubocop-multi-language.yml | 2 +- pr-checks/sync.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pr-checks/checks/rubocop-multi-language.yml b/pr-checks/checks/rubocop-multi-language.yml index dd9c21828..faec4fe21 100644 --- a/pr-checks/checks/rubocop-multi-language.yml +++ b/pr-checks/checks/rubocop-multi-language.yml @@ -5,7 +5,7 @@ versions: - default steps: - name: Set up Ruby - uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314.0 + uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 with: ruby-version: 2.6 - name: Install Code Scanning integration diff --git a/pr-checks/sync.ts b/pr-checks/sync.ts index cc4f66946..f3520449e 100755 --- a/pr-checks/sync.ts +++ b/pr-checks/sync.ts @@ -253,8 +253,8 @@ const languageSetups: LanguageSetups = { name: "Install Java", uses: pinnedUses( "actions/setup-java", - "1bcf9fb12cf4aa7d266a90ae39939e61372fe520", - "v5.4.0", + "0f481fcb613427c0f801b606911222b5b6f3083a", + "v5.5.0", ), with: { "java-version": `\${{ inputs.java-version || '${defaultLanguageVersions.java}' }}`, From 2831d9a0e9f85152662b38294eb6107770d8bdbe Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 12:03:31 +0100 Subject: [PATCH 07/34] Fix previously wrong comment --- src/start-proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/start-proxy.ts b/src/start-proxy.ts index c492ae6e1..4753af4b2 100644 --- a/src/start-proxy.ts +++ b/src/start-proxy.ts @@ -300,7 +300,7 @@ export function getCredentials( const address = getRegistryAddress(e); // Filter credentials based on language if specified. `type` is the registry type. - // E.g., "maven_feed" for Java/Kotlin, "nuget_repository" for C#. + // E.g., "maven_repository" for Java/Kotlin, "nuget_feed" for C#. // We always allow types in `ALWAYS_ENABLED_REGISTRY_TYPE` since they can be used by // other parts of the workflow. if ( From c7d9083db4c09d99b5c4eafd45577307b6e8e68f Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 12:05:14 +0100 Subject: [PATCH 08/34] Make `ALWAYS_ENABLED_REGISTRY_TYPE` read-only --- src/start-proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/start-proxy.ts b/src/start-proxy.ts index 4753af4b2..03056ac4c 100644 --- a/src/start-proxy.ts +++ b/src/start-proxy.ts @@ -192,7 +192,7 @@ function isPAT(value: string) { * enabled, because generic CodeQL workflow components may use them rather than just * language-specific components. */ -export const ALWAYS_ENABLED_REGISTRY_TYPE = ["git_source"]; +export const ALWAYS_ENABLED_REGISTRY_TYPE = ["git_source"] as const; type RegistryMapping = Partial>; From 31842a7e069dcda6d42abeb3aba3e22d2df3fdb0 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 17:17:15 +0100 Subject: [PATCH 09/34] Simplify return types for `passes` --- src/testing-utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index ec518853f..f340d6dc1 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -281,9 +281,7 @@ export class TestEnv< return this.fn(this.state as unknown as ActionState, ...this.args); } - public passes( - assertion: (makeCall: () => R) => T | Promise, - ): T | Promise { + public passes(assertion: (makeCall: () => R) => Promise): Promise { return assertion(() => { const result = this.call(); return result; From b571644f21e73dcfee0fe9c660f852948c0defda Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 17:16:06 +0100 Subject: [PATCH 10/34] Clean-up `passes` by splitting it into `passes` and `throws` --- src/config/file.test.ts | 13 +++---- src/config/remote-file.test.ts | 62 ++++++++++++++-------------------- src/testing-utils.ts | 35 ++++++++++++++++--- 3 files changed, 62 insertions(+), 48 deletions(-) diff --git a/src/config/file.test.ts b/src/config/file.test.ts index 3362d26c6..086edbf38 100644 --- a/src/config/file.test.ts +++ b/src/config/file.test.ts @@ -13,7 +13,7 @@ test("getConfigFileInput returns undefined by default", async (t) => { await callee(getConfigFileInput) .withArgs({}) .withFeatures([Feature.ConfigFileRepositoryProperty]) - .passes(async (fn) => t.is(await fn(), undefined)); + .passes(t.is, undefined); }); const repositoryProperties = { @@ -37,7 +37,7 @@ test("getConfigFileInput returns input value", async (t) => { const targetWithArgs = target .withActions(actionsEnv) .withArgs(repositoryProperties); - await targetWithArgs.passes(async (fn) => t.is(await fn(), testInput)); + await targetWithArgs.passes(t.is, testInput); // Check for the expected log message. t.true( @@ -53,8 +53,9 @@ test("getConfigFileInput returns repository property value", async (t) => { .withFeatures([Feature.ConfigFileRepositoryProperty]) .withArgs(repositoryProperties); - await target.passes(async (fn) => - t.is(await fn(), repositoryProperties[RepositoryPropertyName.CONFIG_FILE]), + await target.passes( + t.is, + repositoryProperties[RepositoryPropertyName.CONFIG_FILE], ); // Check for the expected log message. @@ -70,7 +71,7 @@ test("getConfigFileInput ignores empty repository property value", async (t) => await callee(getConfigFileInput) .withFeatures([Feature.ConfigFileRepositoryProperty]) .withArgs({ [RepositoryPropertyName.CONFIG_FILE]: " " }) - .passes(async (fn) => t.is(await fn(), undefined)); + .passes(t.is, undefined); }); test("getConfigFileInput ignores repository property value when FF is off", async (t) => { @@ -79,7 +80,7 @@ test("getConfigFileInput ignores repository property value when FF is off", asyn .withFeatures([]) .withArgs(repositoryProperties); - await target.passes(async (fn) => t.is(await fn(), undefined)); + await target.passes(t.is, undefined); t.false( target diff --git a/src/config/remote-file.test.ts b/src/config/remote-file.test.ts index ac1492d90..df55a8e75 100644 --- a/src/config/remote-file.test.ts +++ b/src/config/remote-file.test.ts @@ -50,7 +50,7 @@ test("parseRemoteFileAddress accepts full remote addresses", async (t) => { for (const oldFormatInput of oldFormatInputs) { await target .withArgs(oldFormatInput.input) - .passes(async (fn) => t.deepEqual(await fn(), oldFormatInput.expected)); + .passes(t.deepEqual, oldFormatInput.expected); } // New format. @@ -78,14 +78,12 @@ test("parseRemoteFileAddress accepts full remote addresses", async (t) => { // Should fail when the FF is not enabled. await targetWithArgs .withFeatures([]) - .passes(async (fn) => - t.throwsAsync(fn, { instanceOf: ConfigurationError }), - ); + .throws(t, { instanceOf: ConfigurationError }); // And pass when the FF is enabled. await targetWithArgs .withFeatures([Feature.NewRemoteFileAddresses]) - .passes(async (fn) => t.deepEqual(await fn(), newFormatInput.expected)); + .passes(t.deepEqual, newFormatInput.expected); } }); @@ -146,14 +144,12 @@ test("parseRemoteFileAddress accepts remote address without an owner", async (t) // Should fail when the FF is not enabled. await targetWithArgs .withFeatures([]) - .passes(async (fn) => - t.throwsAsync(fn, { instanceOf: ConfigurationError }), - ); + .throws(t, { instanceOf: ConfigurationError }); // And pass when the FF is enabled. await targetWithArgs .withFeatures([Feature.NewRemoteFileAddresses]) - .passes(async (fn) => t.deepEqual(await fn(), testCase.expected)); + .passes(t.deepEqual, testCase.expected); } }); @@ -167,7 +163,7 @@ test("parseRemoteFileAddress throws for invalid `GITHUB_REPOSITORY`", async (t) await target .withEnv(env) .withFeatures([Feature.NewRemoteFileAddresses]) - .passes(async (fn) => t.throwsAsync(fn, { instanceOf: Error })); + .throws(t, { instanceOf: Error }); t.assert(getRequired.calledOnceWith(ActionsEnvVars.GITHUB_REPOSITORY)); }); @@ -202,14 +198,12 @@ test("parseRemoteFileAddress accepts remote address without a path", async (t) = // Should fail when the FF is not enabled. await targetWithArgs .withFeatures([]) - .passes(async (fn) => - t.throwsAsync(fn, { instanceOf: ConfigurationError }), - ); + .throws(t, { instanceOf: ConfigurationError }); // And pass when the FF is enabled. await targetWithArgs .withFeatures([Feature.NewRemoteFileAddresses]) - .passes(async (fn) => t.deepEqual(await fn(), testCase.expected)); + .passes(t.deepEqual, testCase.expected); } }); @@ -217,17 +211,15 @@ test("parseRemoteFileAddress accepts remote address without a ref", async (t) => const target = callee(parseRemoteFileAddress).withArgs("owner/repo:path"); // Should only accept the input if the FF is enabled. - await target.withFeatures([]).passes(t.throwsAsync); + await target.withFeatures([]).throws(t); await target .withFeatures([Feature.NewRemoteFileAddresses]) - .passes(async (fn) => - t.deepEqual(await fn(), { - owner: "owner", - repo: "repo", - path: "path", - ref: DEFAULT_CONFIG_FILE_REF, - } satisfies RemoteFileAddress), - ); + .passes(t.deepEqual, { + owner: "owner", + repo: "repo", + path: "path", + ref: DEFAULT_CONFIG_FILE_REF, + } satisfies RemoteFileAddress); }); test("parseRemoteFileAddress rejects invalid values", async (t) => { @@ -262,21 +254,17 @@ test("parseRemoteFileAddress rejects invalid values", async (t) => { const targetWithArgs = target.withArgs(testInput); // Should throw both when the new format is and isn't accepted. - await targetWithArgs.withFeatures([]).passes(async (fn) => - t.throwsAsync(fn, { - instanceOf: ConfigurationError, - message: errors.getConfigFileRepoOldFormatInvalidMessage(testInput), - }), - ); + await targetWithArgs.withFeatures([]).throws(t, { + instanceOf: ConfigurationError, + message: errors.getConfigFileRepoOldFormatInvalidMessage(testInput), + }); await targetWithArgs .withFeatures([Feature.NewRemoteFileAddresses]) - .passes(async (fn) => - t.throwsAsync(fn, { - // When the new format is accepted, there are some more specific - // errors in some cases. It is sufficient for us to check that - // an exception is thrown. - instanceOf: ConfigurationError, - }), - ); + .throws(t, { + // When the new format is accepted, there are some more specific + // errors in some cases. It is sufficient for us to check that + // an exception is thrown. + instanceOf: ConfigurationError, + }); } }); diff --git a/src/testing-utils.ts b/src/testing-utils.ts index f340d6dc1..086c732f8 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -3,6 +3,8 @@ import path from "path"; import * as github from "@actions/github"; import test, { + type ThrownError, + type ThrowsExpectation, type ExecutionContext, type MacroDeclarationOptions, type TestFn, @@ -281,11 +283,34 @@ export class TestEnv< return this.fn(this.state as unknown as ActionState, ...this.args); } - public passes(assertion: (makeCall: () => R) => Promise): Promise { - return assertion(() => { - const result = this.call(); - return result; - }); + /** + * Calls the underlying function in the configured environment and passes + * the result to `assertion` along with extra `assertionArgs`. + * + * @param assertion The assertion to apply to the result. + * @param assertionArgs Extra arguments for the assertion. + * @returns The result of the assertion. + */ + public async passes( + assertion: (val: Awaited, ...assertionArgs: AArgs) => AResult, + ...assertionArgs: AArgs + ): Promise { + const result = await Promise.resolve(this.call()); + return assertion(result, ...assertionArgs); + } + + /** + * Asserts that calling the underlying function should throw an exception. + * + * @param t The execution context for the assertion. + * @param expectations Expectations for the error. + * @returns The error that was thrown. + */ + public async throws( + t: ExecutionContext, + expectations?: ThrowsExpectation, + ): Promise> { + return t.throwsAsync(() => Promise.resolve(this.call()), expectations); } } From a8ac1e029c86b0975c1ef16020d95ad4d1a8eca4 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 17:23:44 +0100 Subject: [PATCH 11/34] Rename `TestEnv` to `EnvBuilder` --- src/testing-utils.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 086c732f8..ae07cb1ac 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -213,7 +213,7 @@ export function initAllState( /** * Wraps a function that accepts an `ActionState` for testing in different environments. */ -export class TestEnv< +class EnvBuilder< Args extends readonly any[], R, Fs extends ReadonlyArray, @@ -225,7 +225,7 @@ export class TestEnv< constructor( fn: (state: ActionState, ...args: Args) => R, - cloneFrom?: TestEnv, + cloneFrom?: EnvBuilder, ) { this.fn = fn; this.args = cloneFrom?.args; @@ -236,8 +236,8 @@ export class TestEnv< : initAllState({ logger: this.logger }); } - private clone(): TestEnv { - return new TestEnv(this.fn, this); + private clone(): EnvBuilder { + return new EnvBuilder(this.fn, this); } public getLogger(): RecordingLogger { @@ -258,19 +258,19 @@ export class TestEnv< return result; } - public withFeatures(enabled: Feature[]): TestEnv { + public withFeatures(enabled: Feature[]): EnvBuilder { const result = this.clone(); result.state.features = createFeatures(enabled); return result; } - public withEnv(env: Env): TestEnv { + public withEnv(env: Env): EnvBuilder { const result = this.clone(); result.state.env = env; return result; } - public withActions(actions: ActionsEnv): TestEnv { + public withActions(actions: ActionsEnv): EnvBuilder { const result = this.clone(); result.state.actions = actions; return result; @@ -319,8 +319,8 @@ export function callee< Args extends readonly any[], R, Fs extends readonly StateFeature[], ->(fn: (state: ActionState, ...args: Args) => R): TestEnv { - return new TestEnv(fn); +>(fn: (state: ActionState, ...args: Args) => R): EnvBuilder { + return new EnvBuilder(fn); } /** From a4319e3365c9df14e348c373b3597325fce3f675 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 17:48:19 +0100 Subject: [PATCH 12/34] Split `EnvBuilder` into it + `CallableEnvBuilder` --- src/testing-utils.ts | 57 +++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index ae07cb1ac..24c856cda 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -218,17 +218,15 @@ class EnvBuilder< R, Fs extends ReadonlyArray, > { - private readonly fn: (state: ActionState, ...args: Args) => R; - private args?: Args; + protected readonly fn: (state: ActionState, ...args: Args) => R; private logger: RecordingLogger; - private state: ActionState; + protected state: ActionState; constructor( fn: (state: ActionState, ...args: Args) => R, cloneFrom?: EnvBuilder, ) { this.fn = fn; - this.args = cloneFrom?.args; this.logger = new RecordingLogger(); this.state = cloneFrom !== undefined @@ -236,8 +234,12 @@ class EnvBuilder< : initAllState({ logger: this.logger }); } - private clone(): EnvBuilder { - return new EnvBuilder(this.fn, this); + /** + * Creates a clone of this object. Used internally. + * Must be overriden by subclasses. + */ + protected clone(): this { + return new EnvBuilder(this.fn, this) as this; } public getLogger(): RecordingLogger { @@ -248,38 +250,55 @@ class EnvBuilder< return this.state; } - public getArgs(): Args | undefined { - return this.args; - } - - public withArgs(...args: Args) { - const result = this.clone(); - result.args = args; + public withArgs(...args: Args): CallableEnvBuilder { + const result = new CallableEnvBuilder(this.fn, args, this.clone()); return result; } - public withFeatures(enabled: Feature[]): EnvBuilder { + public withFeatures(enabled: Feature[]): this { const result = this.clone(); result.state.features = createFeatures(enabled); return result; } - public withEnv(env: Env): EnvBuilder { + public withEnv(env: Env): this { const result = this.clone(); result.state.env = env; return result; } - public withActions(actions: ActionsEnv): EnvBuilder { + public withActions(actions: ActionsEnv): this { const result = this.clone(); result.state.actions = actions; return result; } +} + +class CallableEnvBuilder< + Args extends readonly any[], + R, + Fs extends ReadonlyArray, +> extends EnvBuilder { + private args: Args; + + constructor( + fn: (state: ActionState, ...args: Args) => R, + args: Args, + cloneFrom?: EnvBuilder, + ) { + super(fn, cloneFrom); + this.args = args; + } + + protected clone(): this { + return new CallableEnvBuilder(this.fn, this.args, this) as this; + } + + public getArgs(): Args { + return this.args; + } call(): R { - if (!this.args) { - throw new Error("Trying to call function in TestEnv without arguments."); - } return this.fn(this.state as unknown as ActionState, ...this.args); } From 6a782906dc08721600c0cf840db9764bd7e009b9 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 17:52:26 +0100 Subject: [PATCH 13/34] Add abstract `BaseEnvBuilder` to enforce that `clone` is overriden by subclasses --- src/testing-utils.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 24c856cda..7a4496ec4 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -213,7 +213,7 @@ export function initAllState( /** * Wraps a function that accepts an `ActionState` for testing in different environments. */ -class EnvBuilder< +abstract class BaseEnvBuilder< Args extends readonly any[], R, Fs extends ReadonlyArray, @@ -224,7 +224,7 @@ class EnvBuilder< constructor( fn: (state: ActionState, ...args: Args) => R, - cloneFrom?: EnvBuilder, + cloneFrom?: BaseEnvBuilder, ) { this.fn = fn; this.logger = new RecordingLogger(); @@ -238,9 +238,7 @@ class EnvBuilder< * Creates a clone of this object. Used internally. * Must be overriden by subclasses. */ - protected clone(): this { - return new EnvBuilder(this.fn, this) as this; - } + protected abstract clone(): this; public getLogger(): RecordingLogger { return this.logger; @@ -274,17 +272,27 @@ class EnvBuilder< } } +class EnvBuilder< + Args extends readonly any[], + R, + Fs extends ReadonlyArray, +> extends BaseEnvBuilder { + protected clone(): this { + return new EnvBuilder(this.fn, this) as this; + } +} + class CallableEnvBuilder< Args extends readonly any[], R, Fs extends ReadonlyArray, -> extends EnvBuilder { +> extends BaseEnvBuilder { private args: Args; constructor( fn: (state: ActionState, ...args: Args) => R, args: Args, - cloneFrom?: EnvBuilder, + cloneFrom?: BaseEnvBuilder, ) { super(fn, cloneFrom); this.args = args; From 1a57d7ae115fdf043b7f0053708978529d62655a Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 19:11:53 +0100 Subject: [PATCH 14/34] Simplify checking logs --- src/config/file.test.ts | 57 ++++++++------------------ src/testing-utils.ts | 89 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 43 deletions(-) diff --git a/src/config/file.test.ts b/src/config/file.test.ts index 086edbf38..6632385e4 100644 --- a/src/config/file.test.ts +++ b/src/config/file.test.ts @@ -34,36 +34,20 @@ test("getConfigFileInput returns input value", async (t) => { // Even though both an input and repository property are configured, // we prefer the direct input to the Action. - const targetWithArgs = target + await target .withActions(actionsEnv) - .withArgs(repositoryProperties); - await targetWithArgs.passes(t.is, testInput); - - // Check for the expected log message. - t.true( - targetWithArgs - .getLogger() - .hasMessage("Using configuration file input from workflow"), - ); + .withArgs(repositoryProperties) + .logs(t, "Using configuration file input from workflow") + .passes(t.is, testInput); }); test("getConfigFileInput returns repository property value", async (t) => { // Since there is no direct input, we should use the repository property. - const target = callee(getConfigFileInput) + await callee(getConfigFileInput) .withFeatures([Feature.ConfigFileRepositoryProperty]) - .withArgs(repositoryProperties); - - await target.passes( - t.is, - repositoryProperties[RepositoryPropertyName.CONFIG_FILE], - ); - - // Check for the expected log message. - t.true( - target - .getLogger() - .hasMessage("Using configuration file input from repository property"), - ); + .withArgs(repositoryProperties) + .logs(t, "Using configuration file input from repository property") + .passes(t.is, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]); }); test("getConfigFileInput ignores empty repository property value", async (t) => { @@ -76,22 +60,13 @@ test("getConfigFileInput ignores empty repository property value", async (t) => test("getConfigFileInput ignores repository property value when FF is off", async (t) => { // Since the FF is off, we should ignore the repository property value. - const target = callee(getConfigFileInput) + await callee(getConfigFileInput) .withFeatures([]) - .withArgs(repositoryProperties); - - await target.passes(t.is, undefined); - - t.false( - target - .getLogger() - .hasMessage("Using configuration file input from repository property"), - ); - t.true( - target - .getLogger() - .hasMessage( - "Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.", - ), - ); + .withArgs(repositoryProperties) + .notLogs(t, "Using configuration file input from repository property") + .logs( + t, + "Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.", + ) + .passes(t.is, undefined); }); diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 7a4496ec4..653914d53 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -210,6 +210,12 @@ export function initAllState( }; } +type DelayedCheck< + Args extends readonly any[], + R, + Fs extends ReadonlyArray, +> = (env: Readonly>) => Promise; + /** * Wraps a function that accepts an `ActionState` for testing in different environments. */ @@ -221,6 +227,7 @@ abstract class BaseEnvBuilder< protected readonly fn: (state: ActionState, ...args: Args) => R; private logger: RecordingLogger; protected state: ActionState; + protected checks: Array>; constructor( fn: (state: ActionState, ...args: Args) => R, @@ -232,6 +239,7 @@ abstract class BaseEnvBuilder< cloneFrom !== undefined ? { ...cloneFrom.state, logger: this.logger } : initAllState({ logger: this.logger }); + this.checks = [...(cloneFrom?.checks ?? [])]; } /** @@ -270,6 +278,30 @@ abstract class BaseEnvBuilder< result.state.actions = actions; return result; } + + /** + * Adds a delayed check that `messages` are logged. The check will be + * performed after the main assertion passes. + */ + public logs(t: ExecutionContext, ...messages: string[]): this { + const result = this.clone(); + result.checks.push(async (env) => { + checkExpectedLogMessages(t, env.getLogger().messages, messages); + }); + return result; + } + + /** + * Adds a delayed check that `messages` are not logged. The check will be + * performed after the main assertion passes. + */ + public notLogs(t: ExecutionContext, ...messages: string[]): this { + const result = this.clone(); + result.checks.push(async (env) => { + checkUnexpectedLogMessages(t, env.getLogger().messages, messages); + }); + return result; + } } class EnvBuilder< @@ -322,8 +354,21 @@ class CallableEnvBuilder< assertion: (val: Awaited, ...assertionArgs: AArgs) => AResult, ...assertionArgs: AArgs ): Promise { + // this.call() may or may not return a promise, + // `Promise.resolve` turns the result into one if it isn't already, + // and we then await it. That ensures that `result` is an `Awaited`. const result = await Promise.resolve(this.call()); - return assertion(result, ...assertionArgs); + + // Run the main assertion on the `result`. + const assertionResult = await assertion(result, ...assertionArgs); + + // Run other delayed checks. + for (const delayedCheck of this.checks) { + await delayedCheck(this); + } + + // Return the result of the main assertion. + return assertionResult; } /** @@ -337,7 +382,19 @@ class CallableEnvBuilder< t: ExecutionContext, expectations?: ThrowsExpectation, ): Promise> { - return t.throwsAsync(() => Promise.resolve(this.call()), expectations); + // Run the main assertion. + const error = await t.throwsAsync( + () => Promise.resolve(this.call()), + expectations, + ); + + // Run other delayed checks. + for (const delayedCheck of this.checks) { + await delayedCheck(this); + } + + // Return the error. + return error; } } @@ -542,6 +599,34 @@ export function checkExpectedLogMessages( } } +/** + * Checks that `messages` contains none of `unexpectedMessages`. + */ +export function checkUnexpectedLogMessages( + t: ExecutionContext, + messages: LoggedMessage[], + unexpectedMessages: string[], +) { + const presentMessages: string[] = []; + + for (const unexpectedMessage of unexpectedMessages) { + if (hasLoggedMessage(messages, unexpectedMessage)) { + presentMessages.push(unexpectedMessage); + } + } + + if (presentMessages.length > 0) { + const listify = (lines: string[]) => + lines.map((m) => ` - '${m}'`).join("\n"); + + t.fail( + `Did not expect\n\n${listify(presentMessages)}\n\nin the logger output, but found them in:\n\n${messages.map((m) => ` - '${m.message}'`).join("\n")}`, + ); + } else { + t.pass(); + } +} + /** * Asserts that `message` should not have been logged to `logger`. */ From c77051d879862fb9b5ce83f35a979035ec59dea1 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 19:30:29 +0100 Subject: [PATCH 15/34] Allow `withActions` and `withEnv` to mutate values --- src/config/file.test.ts | 19 +++++++-------- src/config/remote-file.test.ts | 44 ++++++++++++++++------------------ src/testing-utils.ts | 18 ++++++++++---- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/config/file.test.ts b/src/config/file.test.ts index 6632385e4..4b7b88c9c 100644 --- a/src/config/file.test.ts +++ b/src/config/file.test.ts @@ -22,20 +22,17 @@ const repositoryProperties = { test("getConfigFileInput returns input value", async (t) => { const testInput = "/some/path"; - const target = callee(getConfigFileInput).withFeatures([ - Feature.ConfigFileRepositoryProperty, - ]); - - const actionsEnv = target.getState().actions; - sinon - .stub(actionsEnv, "getOptionalInput") - .withArgs("config-file") - .returns(testInput); // Even though both an input and repository property are configured, // we prefer the direct input to the Action. - await target - .withActions(actionsEnv) + await callee(getConfigFileInput) + .withFeatures([Feature.ConfigFileRepositoryProperty]) + .withActions((actionsEnv) => { + sinon + .stub(actionsEnv, "getOptionalInput") + .withArgs("config-file") + .returns(testInput); + }) .withArgs(repositoryProperties) .logs(t, "Using configuration file input from workflow") .passes(t.is, testInput); diff --git a/src/config/remote-file.test.ts b/src/config/remote-file.test.ts index df55a8e75..fd1fcf3c1 100644 --- a/src/config/remote-file.test.ts +++ b/src/config/remote-file.test.ts @@ -4,7 +4,7 @@ import sinon from "sinon"; import { ActionsEnvVars } from "../environment"; import * as errors from "../error-messages"; import { Feature } from "../feature-flags"; -import { callee, getTestEnv } from "../testing-utils"; +import { callee } from "../testing-utils"; import { ConfigurationError } from "../util"; import { @@ -88,16 +88,13 @@ test("parseRemoteFileAddress accepts full remote addresses", async (t) => { }); test("parseRemoteFileAddress accepts remote address without an owner", async (t) => { - const target = callee(parseRemoteFileAddress); - - const env = target.getState().env; const owner = "test-owner"; - const getRequired = sinon.stub(env, "getRequired"); - getRequired - .withArgs(ActionsEnvVars.GITHUB_REPOSITORY) - .returns(`${owner}/current-repo`); - - const targetWithEnv = target.withEnv(env); + const target = callee(parseRemoteFileAddress).withEnv((env) => { + const getRequired = sinon.stub(env, "getRequired"); + getRequired + .withArgs(ActionsEnvVars.GITHUB_REPOSITORY) + .returns(`${owner}/current-repo`); + }); const testCases: ParseRemoteFileAddressTest[] = [ { @@ -139,7 +136,7 @@ test("parseRemoteFileAddress accepts remote address without an owner", async (t) ]; for (const testCase of testCases) { - const targetWithArgs = targetWithEnv.withArgs(testCase.input); + const targetWithArgs = target.withArgs(testCase.input); // Should fail when the FF is not enabled. await targetWithArgs @@ -154,14 +151,16 @@ test("parseRemoteFileAddress accepts remote address without an owner", async (t) }); test("parseRemoteFileAddress throws for invalid `GITHUB_REPOSITORY`", async (t) => { - const target = callee(parseRemoteFileAddress).withArgs("repo@ref"); - - const env = target.getState().env; - const getRequired = sinon.stub(env, "getRequired"); + const getRequired: sinon.SinonStub = sinon.stub(); getRequired.withArgs(ActionsEnvVars.GITHUB_REPOSITORY).returns(`not-valid`); + const target = callee(parseRemoteFileAddress) + .withArgs("repo@ref") + .withEnv((env) => { + sinon.define(env, "getRequired", getRequired); + }); + await target - .withEnv(env) .withFeatures([Feature.NewRemoteFileAddresses]) .throws(t, { instanceOf: Error }); @@ -223,14 +222,13 @@ test("parseRemoteFileAddress accepts remote address without a ref", async (t) => }); test("parseRemoteFileAddress rejects invalid values", async (t) => { - const env = getTestEnv(); const owner = "owner"; - const getRequired = sinon.stub(env, "getRequired"); - getRequired - .withArgs(ActionsEnvVars.GITHUB_REPOSITORY) - .returns(`${owner}/current-repo`); - - const target = callee(parseRemoteFileAddress).withEnv(env); + const target = callee(parseRemoteFileAddress).withEnv((env) => { + const getRequired = sinon.stub(env, "getRequired"); + getRequired + .withArgs(ActionsEnvVars.GITHUB_REPOSITORY) + .returns(`${owner}/current-repo`); + }); const testInputs = [ " ", diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 653914d53..20cbadeab 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -216,6 +216,8 @@ type DelayedCheck< Fs extends ReadonlyArray, > = (env: Readonly>) => Promise; +export type ValueOrMutation = T | ((val: T) => void); + /** * Wraps a function that accepts an `ActionState` for testing in different environments. */ @@ -267,15 +269,23 @@ abstract class BaseEnvBuilder< return result; } - public withEnv(env: Env): this { + public withEnv(arg: ValueOrMutation): this { const result = this.clone(); - result.state.env = env; + if (typeof arg === "function") { + arg(result.state.env); + } else { + result.state.env = arg; + } return result; } - public withActions(actions: ActionsEnv): this { + public withActions(arg: ValueOrMutation): this { const result = this.clone(); - result.state.actions = actions; + if (typeof arg === "function") { + arg(result.state.actions); + } else { + result.state.actions = arg; + } return result; } From ab6fd520bd413bd9cdf9d676cedc14373fc93d1d Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 19:41:17 +0100 Subject: [PATCH 16/34] Accept `Env` for `setupActionsVars` Also extract env var strings into `ActionsEnvVars` --- src/environment.ts | 2 ++ src/testing-utils.ts | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/environment.ts b/src/environment.ts index 6ea0e87e4..80759d6d6 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -193,10 +193,12 @@ export enum ActionsEnvVars { GITHUB_SERVER_URL = "GITHUB_SERVER_URL", GITHUB_SHA = "GITHUB_SHA", GITHUB_WORKFLOW = "GITHUB_WORKFLOW", + GITHUB_WORKSPACE = "GITHUB_WORKSPACE", RUNNER_ENVIRONMENT = "RUNNER_ENVIRONMENT", RUNNER_NAME = "RUNNER_NAME", RUNNER_OS = "RUNNER_OS", RUNNER_TEMP = "RUNNER_TEMP", + RUNNER_TOOL_CACHE = "RUNNER_TOOL_CACHE", } /** A type representing all known environment variables. */ diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 20cbadeab..fab26d57b 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -466,16 +466,18 @@ export function setupBaseActionsVars( * @param tempDir A value for `RUNNER_TEMP` and `GITHUB_WORKSPACE`. * @param toolsDir A value for `RUNNER_TOOL_CACHE`. * @param overrides Overrides for the defaults. + * @param env The environment to set the variables for. */ export function setupActionsVars( tempDir: string, toolsDir: string, overrides?: ActionVarOverrides, + env: Env = getEnv(), ) { - setupBaseActionsVars(overrides); - process.env["RUNNER_TEMP"] = tempDir; - process.env["RUNNER_TOOL_CACHE"] = toolsDir; - process.env["GITHUB_WORKSPACE"] = tempDir; + setupBaseActionsVars(overrides, env); + env.set(ActionsEnvVars.RUNNER_TEMP, tempDir); + env.set(ActionsEnvVars.RUNNER_TOOL_CACHE, toolsDir); + env.set(ActionsEnvVars.GITHUB_WORKSPACE, tempDir); } type LogLevel = "debug" | "info" | "warning" | "error"; From da75ca58fadd1ffcf5d76f9946c2fe9d28951a3c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 19:44:22 +0100 Subject: [PATCH 17/34] Add action vars helpers to `BaseEnvBuilder` --- src/testing-utils.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index fab26d57b..b81b9b97b 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -269,6 +269,34 @@ abstract class BaseEnvBuilder< return result; } + /** + * Sets environment variables that are always available to GitHub Actions, + * excluding some that are expected to be set to paths. + * + * @param overrides Overrides for the defaults. + */ + public withDefaultActionsEnv(overrides?: ActionVarOverrides): this { + const result = this.clone(); + setupBaseActionsVars(overrides, result.state.env); + return result; + } + + /** + * Sets environment variables that are always available to GitHub Actions. + * @param tempDir A value for `RUNNER_TEMP` and `GITHUB_WORKSPACE`. + * @param toolsDir A value for `RUNNER_TOOL_CACHE`. + * @param overrides Overrides for the defaults. + */ + public withActionsEnv( + tempDir: string, + toolsDir: string, + overrides?: ActionVarOverrides, + ): this { + const result = this.clone(); + setupActionsVars(tempDir, toolsDir, overrides); + return result; + } + public withEnv(arg: ValueOrMutation): this { const result = this.clone(); if (typeof arg === "function") { From 6b3ba17c47e85b81d534445ccffaea9290793120 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 19:44:48 +0100 Subject: [PATCH 18/34] Fix typo --- src/testing-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index b81b9b97b..fbe844be9 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -246,7 +246,7 @@ abstract class BaseEnvBuilder< /** * Creates a clone of this object. Used internally. - * Must be overriden by subclasses. + * Must be overridden by subclasses. */ protected abstract clone(): this; From a1e3f70a6f946fe5ae2c4352ba83cdda29e09347 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 19:51:19 +0100 Subject: [PATCH 19/34] Add `satisfies` for state in `BaseEnvBuilder` constructor --- src/testing-utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index fbe844be9..eb94e642c 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -239,7 +239,10 @@ abstract class BaseEnvBuilder< this.logger = new RecordingLogger(); this.state = cloneFrom !== undefined - ? { ...cloneFrom.state, logger: this.logger } + ? ({ + ...cloneFrom.state, + logger: this.logger, + } satisfies ActionState) : initAllState({ logger: this.logger }); this.checks = [...(cloneFrom?.checks ?? [])]; } From d0a69d77dcb6f0a7cad5853f33556f01c4d33812 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 20:02:02 +0100 Subject: [PATCH 20/34] Clone `env` and `actions` so that sinon stubs survive --- src/testing-utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index eb94e642c..e90029566 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -241,6 +241,8 @@ abstract class BaseEnvBuilder< cloneFrom !== undefined ? ({ ...cloneFrom.state, + env: Object.create(cloneFrom.state.env), + actions: Object.create(cloneFrom.state.actions), logger: this.logger, } satisfies ActionState) : initAllState({ logger: this.logger }); From d8866c2578e70ca106cd84888d2709f1e7c00d9c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 22:40:38 +0100 Subject: [PATCH 21/34] fixup! Add action vars helpers to `BaseEnvBuilder` --- src/testing-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index e90029566..91a0b7a88 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -298,7 +298,7 @@ abstract class BaseEnvBuilder< overrides?: ActionVarOverrides, ): this { const result = this.clone(); - setupActionsVars(tempDir, toolsDir, overrides); + setupActionsVars(tempDir, toolsDir, overrides, result.state.env); return result; } From 9a9ab4cc7d69af435ae72d7f5a7d9f4733361a0c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 22:45:33 +0100 Subject: [PATCH 22/34] Make `throwsAsync` arg `async` --- src/testing-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 91a0b7a88..60e8fb554 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -427,7 +427,7 @@ class CallableEnvBuilder< ): Promise> { // Run the main assertion. const error = await t.throwsAsync( - () => Promise.resolve(this.call()), + async () => Promise.resolve(this.call()), expectations, ); From fb5048ee886585db7ef2210fe32b077efa9a74a7 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 23:27:42 +0100 Subject: [PATCH 23/34] Add `checkSchema` function --- lib/entry-points.js | 30 +++++++++++++++++++--- src/json/index.test.ts | 10 ++++++++ src/json/index.ts | 56 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index ee64cb6a5..5bda904e4 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144518,19 +144518,41 @@ function optionalOrNull(validator) { }; } function validateSchema(schema, obj) { + const result = checkSchema(schema, obj, { failFast: true }); + return result.valid; +} +function checkSchema(schema, obj, options = {}, path29 = "") { + const result = { valid: true, unknownKeys: [] }; + const inputKeys = new Set(Object.keys(obj)); for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; if (validator.required && !hasKey) { - return false; + result.valid = false; + if (options.failFast) { + return result; + } + continue; } if (validator.required && (obj[key] === void 0 || obj[key] === null)) { - return false; + result.valid = false; + if (options.failFast) { + return result; + } + continue; } if (hasKey && !validator.validate(obj[key])) { - return false; + result.valid = false; + if (options.failFast) { + return result; + } + continue; } + inputKeys.delete(key); } - return true; + for (const remainingKey of inputKeys) { + result.unknownKeys.push(`${path29}${remainingKey}`); + } + return result; } // src/util.ts diff --git a/src/json/index.test.ts b/src/json/index.test.ts index eb259c757..5234872e5 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -67,3 +67,13 @@ test("validateSchema - optional properties are optional", async (t) => { t.true(json.validateSchema(optionalSchema, { optionalKey: "" })); t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" })); }); + +test("validateSchema - checkSchema reports unknown keys", async (t) => { + const result = json.checkSchema(testSchema, { + requiredKey: "foo", + extraKey: "bar", + }); + + t.true(result.valid); + t.deepEqual(result.unknownKeys, ["extraKey"]); +}); diff --git a/src/json/index.ts b/src/json/index.ts index a55de5b58..d0a8edb51 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -121,24 +121,72 @@ export function validateSchema( schema: S, obj: UnvalidatedObject, ): obj is FromSchema { + const result = checkSchema(schema, obj, { failFast: true }); + return result.valid; +} + +export interface CheckSchemaOptions { + /** Whether to stop validation after the first error. */ + failFast?: boolean; +} + +export interface CheckSchemaResult { + /** Whether the `obj` satisfies the schema. */ + valid: boolean; + /** Unknown keys that were found during validation. */ + unknownKeys: string[]; +} + +export function checkSchema( + schema: S, + obj: UnvalidatedObject, + options: CheckSchemaOptions = {}, + path: string = "", +): CheckSchemaResult { + const result: CheckSchemaResult = { valid: true, unknownKeys: [] }; + const inputKeys = new Set(Object.keys(obj)); + for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; // If the property is required, but absent, fail. if (validator.required && !hasKey) { - return false; + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } // If the property is required, but undefined or null, fail. if (validator.required && (obj[key] === undefined || obj[key] === null)) { - return false; + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } // If the property is present, validate it. if (hasKey && !validator.validate(obj[key])) { - return false; + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } + + // If we reach this point, the key has been successfully validated. + inputKeys.delete(key); } - return true; + // If there are any remaining keys in `inputKeys`, add them to `unknownKeys`. + for (const remainingKey of inputKeys) { + result.unknownKeys.push(`${path}${remainingKey}`); + } + + return result; } From 6a62e99aa7aa907eadba5fc98937bd786ee1fa79 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 23:36:50 +0100 Subject: [PATCH 24/34] Add an `array` `Validator` --- src/json/index.test.ts | 17 +++++++++++++++++ src/json/index.ts | 10 ++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/json/index.test.ts b/src/json/index.test.ts index 5234872e5..a238d8071 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -68,6 +68,23 @@ test("validateSchema - optional properties are optional", async (t) => { t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" })); }); +const arraySchema = { + arrayKey: json.array(json.number), +}; + +test("validateSchema - validates arrays", async (t) => { + // Arrays of numeric elements are accepted. + t.true(json.validateSchema(arraySchema, { arrayKey: [] })); + t.true(json.validateSchema(arraySchema, { arrayKey: [4] })); + t.true(json.validateSchema(arraySchema, { arrayKey: [4, 8] })); + t.true(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15] })); + + // Other array elements are not accepted. + t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, "bar"] })); + t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, undefined] })); + t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, null] })); +}); + test("validateSchema - checkSchema reports unknown keys", async (t) => { const result = json.checkSchema(testSchema, { requiredKey: "foo", diff --git a/src/json/index.ts b/src/json/index.ts index d0a8edb51..e9fbbd8a7 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -66,6 +66,16 @@ export const number = { required: true, } as const satisfies Validator; +/** A validator for arrays. */ +export function array(validator: Validator) { + return { + validate: (val: unknown) => { + return isArray(val) && val.every((e) => validator.validate(e)); + }, + required: true, + } as const satisfies Validator; +} + /** * Transforms a validator to be optional, accepting `undefined` or `null` for an * absent value. From 50d76e2fa3a057fbae009851a89d7a4b22309ee9 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 23:42:28 +0100 Subject: [PATCH 25/34] Add an `object` `Validator` --- src/json/index.test.ts | 18 ++++++++++++++++++ src/json/index.ts | 10 ++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/json/index.test.ts b/src/json/index.test.ts index a238d8071..71f800003 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -85,6 +85,24 @@ test("validateSchema - validates arrays", async (t) => { t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, null] })); }); +const objectSchema = { + objectKey: json.object(arraySchema), +}; + +test("validateSchema - validates objects", async (t) => { + // Objects of the given schema are accepted. + t.true(json.validateSchema(objectSchema, { objectKey: { arrayKey: [] } })); + t.true(json.validateSchema(objectSchema, { objectKey: { arrayKey: [4] } })); + + // Other values are not accepted. + t.false(json.validateSchema(objectSchema, {})); + t.false(json.validateSchema(objectSchema, { objectKey: [] })); + t.false(json.validateSchema(objectSchema, { objectKey: undefined })); + t.false(json.validateSchema(objectSchema, { objectKey: null })); + t.false(json.validateSchema(objectSchema, { objectKey: "foo" })); + t.false(json.validateSchema(objectSchema, { objectKey: 123 })); +}); + test("validateSchema - checkSchema reports unknown keys", async (t) => { const result = json.checkSchema(testSchema, { requiredKey: "foo", diff --git a/src/json/index.ts b/src/json/index.ts index e9fbbd8a7..083bd49bb 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -76,6 +76,16 @@ export function array(validator: Validator) { } as const satisfies Validator; } +/** A validator for objects. */ +export function object(schema: Schema) { + return { + validate: (val: unknown) => { + return isObject(val) && validateSchema(schema, val); + }, + required: true, + } as const satisfies Validator>; +} + /** * Transforms a validator to be optional, accepting `undefined` or `null` for an * absent value. From 36ef8893297f149d1c086c48694c50bdb31b21dc Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 00:01:13 +0100 Subject: [PATCH 26/34] Improve type inference for `object` and `validateSchema` --- src/json/index.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/json/index.ts b/src/json/index.ts index 083bd49bb..325689d33 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -77,13 +77,16 @@ export function array(validator: Validator) { } /** A validator for objects. */ -export function object(schema: Schema) { +export function object< + S extends Schema, + T extends UnvalidatedObject = FromSchema, +>(schema: S) { return { validate: (val: unknown) => { - return isObject(val) && validateSchema(schema, val); + return isObject(val) && validateSchema(schema, val); }, required: true, - } as const satisfies Validator>; + } as const satisfies Validator; } /** @@ -137,10 +140,10 @@ export type FromSchema = { * @param obj The object to validate. * @returns Asserts that `obj` is of the `schema`'s type if validation is successful. */ -export function validateSchema( - schema: S, - obj: UnvalidatedObject, -): obj is FromSchema { +export function validateSchema< + S extends Schema, + T extends UnvalidatedObject = FromSchema, +>(schema: S, obj: UnvalidatedObject): obj is T { const result = checkSchema(schema, obj, { failFast: true }); return result.valid; } From ec0b8ae45feb17387add946b85d30fb71f1d489b Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 00:43:05 +0100 Subject: [PATCH 27/34] Allow nested checking --- lib/entry-points.js | 89 +++++++++++++++++++++++++---------------- src/json/index.test.ts | 18 +++++++-- src/json/index.ts | 90 +++++++++++++++++++++++++++++++++++------- 3 files changed, 145 insertions(+), 52 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 5bda904e4..f8de6f868 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -29647,13 +29647,13 @@ var require_helpers = __commonJS({ exports2.encodePath = function encodePointer(a) { return a.map(pathEncoder).join(""); }; - exports2.getDecimalPlaces = function getDecimalPlaces(number) { + exports2.getDecimalPlaces = function getDecimalPlaces(number2) { var decimalPlaces = 0; - if (isNaN(number)) return decimalPlaces; - if (typeof number !== "number") { - number = Number(number); + if (isNaN(number2)) return decimalPlaces; + if (typeof number2 !== "number") { + number2 = Number(number2); } - var parts = number.toString().split("e"); + var parts = number2.toString().split("e"); if (parts.length === 2) { if (parts[1][0] !== "-") { return decimalPlaces; @@ -78733,9 +78733,9 @@ var require_enum_object = __commonJS({ if (!isEnumObject(enumObject)) throw new Error("not a typescript enum object"); let values = []; - for (let [name, number] of Object.entries(enumObject)) - if (typeof number == "number") - values.push({ name, number }); + for (let [name, number2] of Object.entries(enumObject)) + if (typeof number2 == "number") + values.push({ name, number: number2 }); return values; } exports2.listEnumValues = listEnumValues; @@ -92751,10 +92751,10 @@ var require_util12 = __commonJS({ return arg == null; } exports2.isNullOrUndefined = isNullOrUndefined; - function isNumber(arg) { + function isNumber2(arg) { return typeof arg === "number"; } - exports2.isNumber = isNumber; + exports2.isNumber = isNumber2; function isString3(arg) { return typeof arg === "string"; } @@ -101524,24 +101524,24 @@ var require_operators = __commonJS({ } }.call(this); } - function toIntegerOrInfinity(number) { - number = Number2(number); - if (NumberIsNaN(number)) { + function toIntegerOrInfinity(number2) { + number2 = Number2(number2); + if (NumberIsNaN(number2)) { return 0; } - if (number < 0) { - throw new ERR_OUT_OF_RANGE("number", ">= 0", number); + if (number2 < 0) { + throw new ERR_OUT_OF_RANGE("number", ">= 0", number2); } - return number; + return number2; } - function drop(number, options = void 0) { + function drop(number2, options = void 0) { if (options != null) { validateObject(options, "options"); } if ((options === null || options === void 0 ? void 0 : options.signal) != null) { validateAbortSignal(options.signal, "options.signal"); } - number = toIntegerOrInfinity(number); + number2 = toIntegerOrInfinity(number2); return async function* drop2() { var _options$signal5; if (options !== null && options !== void 0 && (_options$signal5 = options.signal) !== null && _options$signal5 !== void 0 && _options$signal5.aborted) { @@ -101552,20 +101552,20 @@ var require_operators = __commonJS({ if (options !== null && options !== void 0 && (_options$signal6 = options.signal) !== null && _options$signal6 !== void 0 && _options$signal6.aborted) { throw new AbortError(); } - if (number-- <= 0) { + if (number2-- <= 0) { yield val; } } }.call(this); } - function take(number, options = void 0) { + function take(number2, options = void 0) { if (options != null) { validateObject(options, "options"); } if ((options === null || options === void 0 ? void 0 : options.signal) != null) { validateAbortSignal(options.signal, "options.signal"); } - number = toIntegerOrInfinity(number); + number2 = toIntegerOrInfinity(number2); return async function* take2() { var _options$signal7; if (options !== null && options !== void 0 && (_options$signal7 = options.signal) !== null && _options$signal7 !== void 0 && _options$signal7.aborted) { @@ -101576,10 +101576,10 @@ var require_operators = __commonJS({ if (options !== null && options !== void 0 && (_options$signal8 = options.signal) !== null && _options$signal8 !== void 0 && _options$signal8.aborted) { throw new AbortError(); } - if (number-- > 0) { + if (number2-- > 0) { yield val; } - if (number <= 0) { + if (number2 <= 0) { return; } } @@ -124893,8 +124893,8 @@ var require_util16 = __commonJS({ parts.push(format.substring(last)); return parts.join(""); }; - util3.formatNumber = function(number, decimals, dec_point, thousands_sep) { - var n = number, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals; + util3.formatNumber = function(number2, decimals, dec_point, thousands_sep) { + var n = number2, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals; var d = dec_point === void 0 ? "," : dec_point; var t = thousands_sep === void 0 ? "." : thousands_sep, s = n < 0 ? "-" : ""; var i = parseInt(n = Math.abs(+n || 0).toFixed(c), 10) + ""; @@ -144502,18 +144502,35 @@ function isArray(value) { function isString(value) { return typeof value === "string"; } +function isNumber(value) { + return typeof value === "number"; +} function isStringOrUndefined(value) { return value === void 0 || isString(value); } -var string = { - validate: isString, - required: true -}; +function defaultCheck(validate) { + return (arg) => ({ unknownKeys: [], valid: validate(arg) }); +} +function makeValidator(validate, required = true) { + return { + validate, + check: defaultCheck(validate), + required + }; +} +var string = makeValidator(isString); +var number = makeValidator(isNumber); function optionalOrNull(validator) { return { validate: (val) => { return val === void 0 || val === null || validator.validate(val); }, + check: (val, path29) => { + if (val === void 0 || val === null) { + return { valid: true, unknownKeys: [] }; + } + return validator.check(val, path29); + }, required: false }; } @@ -144540,12 +144557,16 @@ function checkSchema(schema, obj, options = {}, path29 = "") { } continue; } - if (hasKey && !validator.validate(obj[key])) { - result.valid = false; - if (options.failFast) { - return result; + if (hasKey) { + const checkResult = validator.check(obj[key], `${path29}${key}.`); + result.unknownKeys.push(...checkResult.unknownKeys); + if (!checkResult.valid) { + result.valid = false; + if (options.failFast) { + return result; + } + continue; } - continue; } inputKeys.delete(key); } diff --git a/src/json/index.test.ts b/src/json/index.test.ts index 71f800003..47956c938 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -103,12 +103,24 @@ test("validateSchema - validates objects", async (t) => { t.false(json.validateSchema(objectSchema, { objectKey: 123 })); }); +const checkSchemaTestSchema = { + rootKey: json.object(objectSchema), +}; + test("validateSchema - checkSchema reports unknown keys", async (t) => { - const result = json.checkSchema(testSchema, { - requiredKey: "foo", + const result = json.checkSchema(checkSchemaTestSchema, { + rootKey: { + objectKey: { + arrayKey: [], + }, + nestedExtraKey: "foo", + }, extraKey: "bar", }); t.true(result.valid); - t.deepEqual(result.unknownKeys, ["extraKey"]); + t.deepEqual( + result.unknownKeys.sort(), + ["extraKey", "rootKey.nestedExtraKey"].sort(), + ); }); diff --git a/src/json/index.ts b/src/json/index.ts index 325689d33..032952ad5 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -48,29 +48,65 @@ export function isStringOrUndefined( */ export type Validator = { validate: (val: unknown) => val is T; + check: (val: unknown, path: string) => CheckSchemaResult; required: boolean; }; +function defaultCheck( + validate: (val: unknown) => val is any, +): (arg: unknown) => CheckSchemaResult { + return (arg) => ({ unknownKeys: [], valid: validate(arg) }); +} + +function makeValidator( + validate: (arg: unknown) => arg is T, + required: boolean = true, +) { + return { + validate, + check: defaultCheck(validate), + required, + } as const satisfies Validator; +} + /** Extracts `T` from `Validator`. */ export type UnwrapValidator = V extends Validator ? A : never; /** A validator for string fields in schemas. */ -export const string = { - validate: isString, - required: true, -} as const satisfies Validator; +export const string = makeValidator(isString); /** A validator for number fields in schemas. */ -export const number = { - validate: isNumber, - required: true, -} as const satisfies Validator; +export const number = makeValidator(isNumber); /** A validator for arrays. */ export function array(validator: Validator) { + const validate = (val: unknown) => { + return isArray(val) && val.every((e) => validator.validate(e)); + }; return { - validate: (val: unknown) => { - return isArray(val) && val.every((e) => validator.validate(e)); + validate, + check: (val: unknown, path: string) => { + const result: CheckSchemaResult = { valid: true, unknownKeys: [] }; + + if (!isArray(val)) { + result.valid = false; + return result; + } + + let index = 0; + for (const e of val) { + const eResult = validator.check(e, `${path}[${index}].`); + + result.unknownKeys.push(...eResult.unknownKeys); + index++; + + if (!eResult.valid) { + result.valid = false; + continue; + } + } + + return result; }, required: true, } as const satisfies Validator; @@ -85,6 +121,12 @@ export function object< validate: (val: unknown) => { return isObject(val) && validateSchema(schema, val); }, + check: (val, path) => { + if (!isObject(val)) { + return { valid: false, unknownKeys: [] }; + } + return checkSchema(schema, val, {}, path); + }, required: true, } as const satisfies Validator; } @@ -98,6 +140,12 @@ export function optionalOrNull(validator: Validator) { validate: (val: unknown) => { return val === undefined || val === null || validator.validate(val); }, + check: (val, path) => { + if (val === undefined || val === null) { + return { valid: true, unknownKeys: [] }; + } + return validator.check(val, path); + }, required: false, } as const satisfies Validator; } @@ -111,6 +159,12 @@ export function optional(validator: Validator) { validate: (val: unknown): val is T | undefined => { return val === undefined || validator.validate(val); }, + check: (val, path) => { + if (val === undefined) { + return { valid: true, unknownKeys: [] }; + } + return validator.check(val, path); + }, required: false, } as const satisfies Validator; } @@ -193,13 +247,19 @@ export function checkSchema( } // If the property is present, validate it. - if (hasKey && !validator.validate(obj[key])) { - result.valid = false; + if (hasKey) { + const checkResult = validator.check(obj[key], `${path}${key}.`); - if (options.failFast) { - return result; + result.unknownKeys.push(...checkResult.unknownKeys); + + if (!checkResult.valid) { + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } - continue; } // If we reach this point, the key has been successfully validated. From f8ed33e0674f37a4ad1f68fa2b0c2d7496695c92 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 13:10:25 +0100 Subject: [PATCH 28/34] Remove keys from unrecognised set as soon as found --- lib/entry-points.js | 2 +- src/json/index.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index f8de6f868..191e25b23 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144543,6 +144543,7 @@ function checkSchema(schema, obj, options = {}, path29 = "") { const inputKeys = new Set(Object.keys(obj)); for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; + inputKeys.delete(key); if (validator.required && !hasKey) { result.valid = false; if (options.failFast) { @@ -144568,7 +144569,6 @@ function checkSchema(schema, obj, options = {}, path29 = "") { continue; } } - inputKeys.delete(key); } for (const remainingKey of inputKeys) { result.unknownKeys.push(`${path29}${remainingKey}`); diff --git a/src/json/index.ts b/src/json/index.ts index 032952ad5..b5c965098 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -226,6 +226,9 @@ export function checkSchema( for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; + // Remove key from set of unrecognised keys. + inputKeys.delete(key); + // If the property is required, but absent, fail. if (validator.required && !hasKey) { result.valid = false; @@ -263,7 +266,6 @@ export function checkSchema( } // If we reach this point, the key has been successfully validated. - inputKeys.delete(key); } // If there are any remaining keys in `inputKeys`, add them to `unknownKeys`. From aec5441507d8875bdf068af4b4d8a25728f8ac06 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 13:18:45 +0100 Subject: [PATCH 29/34] Add convenience functions to produce `CheckSchemaResult` values --- lib/entry-points.js | 10 ++++++++-- src/json/index.ts | 30 +++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 191e25b23..e8b673085 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144527,7 +144527,7 @@ function optionalOrNull(validator) { }, check: (val, path29) => { if (val === void 0 || val === null) { - return { valid: true, unknownKeys: [] }; + return successfulCheckSchema(); } return validator.check(val, path29); }, @@ -144538,8 +144538,14 @@ function validateSchema(schema, obj) { const result = checkSchema(schema, obj, { failFast: true }); return result.valid; } +function successfulCheckSchema() { + return { + valid: true, + unknownKeys: [] + }; +} function checkSchema(schema, obj, options = {}, path29 = "") { - const result = { valid: true, unknownKeys: [] }; + const result = successfulCheckSchema(); const inputKeys = new Set(Object.keys(obj)); for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; diff --git a/src/json/index.ts b/src/json/index.ts index b5c965098..7bc23271e 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -86,7 +86,7 @@ export function array(validator: Validator) { return { validate, check: (val: unknown, path: string) => { - const result: CheckSchemaResult = { valid: true, unknownKeys: [] }; + const result: CheckSchemaResult = successfulCheckSchema(); if (!isArray(val)) { result.valid = false; @@ -123,7 +123,7 @@ export function object< }, check: (val, path) => { if (!isObject(val)) { - return { valid: false, unknownKeys: [] }; + return invalidCheckSchema(); } return checkSchema(schema, val, {}, path); }, @@ -142,7 +142,7 @@ export function optionalOrNull(validator: Validator) { }, check: (val, path) => { if (val === undefined || val === null) { - return { valid: true, unknownKeys: [] }; + return successfulCheckSchema(); } return validator.check(val, path); }, @@ -161,7 +161,7 @@ export function optional(validator: Validator) { }, check: (val, path) => { if (val === undefined) { - return { valid: true, unknownKeys: [] }; + return successfulCheckSchema(); } return validator.check(val, path); }, @@ -214,13 +214,33 @@ export interface CheckSchemaResult { unknownKeys: string[]; } +/** + * Convenience function to produce a `CheckSchemaResult` where `valid: true`. + */ +function successfulCheckSchema(): CheckSchemaResult { + return { + valid: true, + unknownKeys: [], + }; +} + +/** + * Convenience function to produce a `CheckSchemaResult` where `valid: false`. + */ +function invalidCheckSchema(): CheckSchemaResult { + return { + valid: false, + unknownKeys: [], + }; +} + export function checkSchema( schema: S, obj: UnvalidatedObject, options: CheckSchemaOptions = {}, path: string = "", ): CheckSchemaResult { - const result: CheckSchemaResult = { valid: true, unknownKeys: [] }; + const result: CheckSchemaResult = successfulCheckSchema(); const inputKeys = new Set(Object.keys(obj)); for (const [key, validator] of Object.entries(schema)) { From 9a1225df6182678c271eb9c2500302669150d86a Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 14:21:41 +0100 Subject: [PATCH 30/34] Track invalid keys, and use more standard JSON path notation --- lib/entry-points.js | 19 +++++++++++++++---- src/json/index.test.ts | 18 +++++++++++++++++- src/json/index.ts | 34 ++++++++++++++++++++++++++++++---- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index e8b673085..1aa30ba06 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144509,7 +144509,7 @@ function isStringOrUndefined(value) { return value === void 0 || isString(value); } function defaultCheck(validate) { - return (arg) => ({ unknownKeys: [], valid: validate(arg) }); + return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) }); } function makeValidator(validate, required = true) { return { @@ -144541,15 +144541,18 @@ function validateSchema(schema, obj) { function successfulCheckSchema() { return { valid: true, - unknownKeys: [] + unknownKeys: [], + invalidKeys: [] }; } function checkSchema(schema, obj, options = {}, path29 = "") { const result = successfulCheckSchema(); const inputKeys = new Set(Object.keys(obj)); + const invalidKeys = /* @__PURE__ */ new Set(); for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; inputKeys.delete(key); + invalidKeys.add(key); if (validator.required && !hasKey) { result.valid = false; if (options.failFast) { @@ -144565,8 +144568,12 @@ function checkSchema(schema, obj, options = {}, path29 = "") { continue; } if (hasKey) { - const checkResult = validator.check(obj[key], `${path29}${key}.`); + const checkResult = validator.check(obj[key], `${path29}.${key}`); result.unknownKeys.push(...checkResult.unknownKeys); + result.invalidKeys.push(...checkResult.invalidKeys); + if (checkResult.invalidKeys.length > 0) { + invalidKeys.delete(key); + } if (!checkResult.valid) { result.valid = false; if (options.failFast) { @@ -144575,9 +144582,13 @@ function checkSchema(schema, obj, options = {}, path29 = "") { continue; } } + invalidKeys.delete(key); } for (const remainingKey of inputKeys) { - result.unknownKeys.push(`${path29}${remainingKey}`); + result.unknownKeys.push(`${path29}.${remainingKey}`); + } + for (const invalidKey of invalidKeys) { + result.invalidKeys.push(`${path29}.${invalidKey}`); } return result; } diff --git a/src/json/index.test.ts b/src/json/index.test.ts index 47956c938..df682fd8b 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -121,6 +121,22 @@ test("validateSchema - checkSchema reports unknown keys", async (t) => { t.true(result.valid); t.deepEqual( result.unknownKeys.sort(), - ["extraKey", "rootKey.nestedExtraKey"].sort(), + [".extraKey", ".rootKey.nestedExtraKey"].sort(), + ); +}); + +test("validateSchema - checkSchema reports invalid keys", async (t) => { + const result = json.checkSchema(checkSchemaTestSchema, { + rootKey: { + objectKey: { + arrayKey: ["foo"], + }, + }, + }); + + t.false(result.valid); + t.deepEqual( + result.invalidKeys.sort(), + [".rootKey.objectKey.arrayKey[0]"].sort(), ); }); diff --git a/src/json/index.ts b/src/json/index.ts index 7bc23271e..a5c89f722 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -55,7 +55,7 @@ export type Validator = { function defaultCheck( validate: (val: unknown) => val is any, ): (arg: unknown) => CheckSchemaResult { - return (arg) => ({ unknownKeys: [], valid: validate(arg) }); + return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) }); } function makeValidator( @@ -88,20 +88,24 @@ export function array(validator: Validator) { check: (val: unknown, path: string) => { const result: CheckSchemaResult = successfulCheckSchema(); + // The value must be an array. if (!isArray(val)) { result.valid = false; return result; } + // Validate all elements of the array. let index = 0; for (const e of val) { - const eResult = validator.check(e, `${path}[${index}].`); + const elementPath = `${path}[${index}]`; + const eResult = validator.check(e, `${elementPath}`); result.unknownKeys.push(...eResult.unknownKeys); index++; if (!eResult.valid) { result.valid = false; + result.invalidKeys.push(elementPath); continue; } } @@ -212,6 +216,8 @@ export interface CheckSchemaResult { valid: boolean; /** Unknown keys that were found during validation. */ unknownKeys: string[]; + /** Known keys that failed validation. */ + invalidKeys: string[]; } /** @@ -221,6 +227,7 @@ function successfulCheckSchema(): CheckSchemaResult { return { valid: true, unknownKeys: [], + invalidKeys: [], }; } @@ -231,6 +238,7 @@ function invalidCheckSchema(): CheckSchemaResult { return { valid: false, unknownKeys: [], + invalidKeys: [], }; } @@ -242,6 +250,7 @@ export function checkSchema( ): CheckSchemaResult { const result: CheckSchemaResult = successfulCheckSchema(); const inputKeys = new Set(Object.keys(obj)); + const invalidKeys = new Set(); for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; @@ -249,6 +258,10 @@ export function checkSchema( // Remove key from set of unrecognised keys. inputKeys.delete(key); + // Add the key to the set of invalid keys. We remove it later once + // it passes validation. + invalidKeys.add(key); + // If the property is required, but absent, fail. if (validator.required && !hasKey) { result.valid = false; @@ -271,9 +284,16 @@ export function checkSchema( // If the property is present, validate it. if (hasKey) { - const checkResult = validator.check(obj[key], `${path}${key}.`); + const checkResult = validator.check(obj[key], `${path}.${key}`); result.unknownKeys.push(...checkResult.unknownKeys); + result.invalidKeys.push(...checkResult.invalidKeys); + + // If we have invalid keys from the validator, then that means that + // we have a more specific key than `key`. Remove `key` from the results. + if (checkResult.invalidKeys.length > 0) { + invalidKeys.delete(key); + } if (!checkResult.valid) { result.valid = false; @@ -286,11 +306,17 @@ export function checkSchema( } // If we reach this point, the key has been successfully validated. + invalidKeys.delete(key); } // If there are any remaining keys in `inputKeys`, add them to `unknownKeys`. for (const remainingKey of inputKeys) { - result.unknownKeys.push(`${path}${remainingKey}`); + result.unknownKeys.push(`${path}.${remainingKey}`); + } + + // If there are any remaining keys in `invalidKeys`, add them to the result. + for (const invalidKey of invalidKeys) { + result.invalidKeys.push(`${path}.${invalidKey}`); } return result; From ba5fbf159aa9e3107b3b5df37853415b9e26835f Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 14:24:33 +0100 Subject: [PATCH 31/34] Propagate `CheckSchemaOptions` to allow `failFast` to work as intended --- lib/entry-points.js | 6 +++--- src/json/index.ts | 29 +++++++++++++++++++---------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 1aa30ba06..aefa923c0 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144525,11 +144525,11 @@ function optionalOrNull(validator) { validate: (val) => { return val === void 0 || val === null || validator.validate(val); }, - check: (val, path29) => { + check: (val, opts, path29) => { if (val === void 0 || val === null) { return successfulCheckSchema(); } - return validator.check(val, path29); + return validator.check(val, opts, path29); }, required: false }; @@ -144568,7 +144568,7 @@ function checkSchema(schema, obj, options = {}, path29 = "") { continue; } if (hasKey) { - const checkResult = validator.check(obj[key], `${path29}.${key}`); + const checkResult = validator.check(obj[key], options, `${path29}.${key}`); result.unknownKeys.push(...checkResult.unknownKeys); result.invalidKeys.push(...checkResult.invalidKeys); if (checkResult.invalidKeys.length > 0) { diff --git a/src/json/index.ts b/src/json/index.ts index a5c89f722..590a1eb6b 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -48,7 +48,11 @@ export function isStringOrUndefined( */ export type Validator = { validate: (val: unknown) => val is T; - check: (val: unknown, path: string) => CheckSchemaResult; + check: ( + val: unknown, + opts: CheckSchemaOptions, + path: string, + ) => CheckSchemaResult; required: boolean; }; @@ -85,7 +89,7 @@ export function array(validator: Validator) { }; return { validate, - check: (val: unknown, path: string) => { + check: (val: unknown, opts: CheckSchemaOptions, path: string) => { const result: CheckSchemaResult = successfulCheckSchema(); // The value must be an array. @@ -98,7 +102,7 @@ export function array(validator: Validator) { let index = 0; for (const e of val) { const elementPath = `${path}[${index}]`; - const eResult = validator.check(e, `${elementPath}`); + const eResult = validator.check(e, opts, `${elementPath}`); result.unknownKeys.push(...eResult.unknownKeys); index++; @@ -106,6 +110,11 @@ export function array(validator: Validator) { if (!eResult.valid) { result.valid = false; result.invalidKeys.push(elementPath); + + if (opts.failFast) { + return result; + } + continue; } } @@ -125,11 +134,11 @@ export function object< validate: (val: unknown) => { return isObject(val) && validateSchema(schema, val); }, - check: (val, path) => { + check: (val, opts, path) => { if (!isObject(val)) { return invalidCheckSchema(); } - return checkSchema(schema, val, {}, path); + return checkSchema(schema, val, opts, path); }, required: true, } as const satisfies Validator; @@ -144,11 +153,11 @@ export function optionalOrNull(validator: Validator) { validate: (val: unknown) => { return val === undefined || val === null || validator.validate(val); }, - check: (val, path) => { + check: (val, opts, path) => { if (val === undefined || val === null) { return successfulCheckSchema(); } - return validator.check(val, path); + return validator.check(val, opts, path); }, required: false, } as const satisfies Validator; @@ -163,11 +172,11 @@ export function optional(validator: Validator) { validate: (val: unknown): val is T | undefined => { return val === undefined || validator.validate(val); }, - check: (val, path) => { + check: (val, opts, path) => { if (val === undefined) { return successfulCheckSchema(); } - return validator.check(val, path); + return validator.check(val, opts, path); }, required: false, } as const satisfies Validator; @@ -284,7 +293,7 @@ export function checkSchema( // If the property is present, validate it. if (hasKey) { - const checkResult = validator.check(obj[key], `${path}.${key}`); + const checkResult = validator.check(obj[key], options, `${path}.${key}`); result.unknownKeys.push(...checkResult.unknownKeys); result.invalidKeys.push(...checkResult.invalidKeys); From 89343ee1aace9f33f7489527e8d566f907e71ec0 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 13 Jul 2026 10:22:19 +0100 Subject: [PATCH 32/34] Fix `checkSchema` test names --- src/json/index.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/json/index.test.ts b/src/json/index.test.ts index df682fd8b..80edbedec 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -107,7 +107,7 @@ const checkSchemaTestSchema = { rootKey: json.object(objectSchema), }; -test("validateSchema - checkSchema reports unknown keys", async (t) => { +test("checkSchema - reports unknown keys", async (t) => { const result = json.checkSchema(checkSchemaTestSchema, { rootKey: { objectKey: { @@ -125,7 +125,7 @@ test("validateSchema - checkSchema reports unknown keys", async (t) => { ); }); -test("validateSchema - checkSchema reports invalid keys", async (t) => { +test("checkSchema - reports invalid keys", async (t) => { const result = json.checkSchema(checkSchemaTestSchema, { rootKey: { objectKey: { From 3f1e4da48dc2f81e92a17fb80b28102e0ece722a Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 13 Jul 2026 10:23:24 +0100 Subject: [PATCH 33/34] Propagate and prefer `invalidKeys` from element validator in `array` validator --- src/json/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/json/index.ts b/src/json/index.ts index 590a1eb6b..2d50ff121 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -104,12 +104,18 @@ export function array(validator: Validator) { const elementPath = `${path}[${index}]`; const eResult = validator.check(e, opts, `${elementPath}`); + result.invalidKeys.push(...eResult.invalidKeys); result.unknownKeys.push(...eResult.unknownKeys); index++; if (!eResult.valid) { result.valid = false; - result.invalidKeys.push(elementPath); + + // Add the element path to `invalidKeys` if we didn't get + // any more specific ones from the element validator. + if (eResult.invalidKeys.length === 0) { + result.invalidKeys.push(elementPath); + } if (opts.failFast) { return result; From 0c2cea31ba55de6dab97ca4fc06e27396fb71841 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 13 Jul 2026 14:47:38 +0100 Subject: [PATCH 34/34] Break out of loop to allow `result` object to be updated after failing fast --- lib/entry-points.js | 6 +++--- src/json/index.ts | 13 ++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index aefa923c0..083567dfb 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144556,14 +144556,14 @@ function checkSchema(schema, obj, options = {}, path29 = "") { if (validator.required && !hasKey) { result.valid = false; if (options.failFast) { - return result; + break; } continue; } if (validator.required && (obj[key] === void 0 || obj[key] === null)) { result.valid = false; if (options.failFast) { - return result; + break; } continue; } @@ -144577,7 +144577,7 @@ function checkSchema(schema, obj, options = {}, path29 = "") { if (!checkResult.valid) { result.valid = false; if (options.failFast) { - return result; + break; } continue; } diff --git a/src/json/index.ts b/src/json/index.ts index 2d50ff121..78923f8ba 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -264,9 +264,16 @@ export function checkSchema( path: string = "", ): CheckSchemaResult { const result: CheckSchemaResult = successfulCheckSchema(); + + // Track the set of input keys. We remove keys from this set as we recognise them + // during validation. const inputKeys = new Set(Object.keys(obj)); + + // Track keys that have failed validation, starting with the empty set. const invalidKeys = new Set(); + // Loop through all keys in the object schema and validate that the given object + // satisfies the schema key. for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; @@ -282,7 +289,7 @@ export function checkSchema( result.valid = false; if (options.failFast) { - return result; + break; } continue; } @@ -292,7 +299,7 @@ export function checkSchema( result.valid = false; if (options.failFast) { - return result; + break; } continue; } @@ -314,7 +321,7 @@ export function checkSchema( result.valid = false; if (options.failFast) { - return result; + break; } continue; }