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 diff --git a/lib/entry-points.js b/lib/entry-points.js index f6b126c84..20715ae2a 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -19897,10 +19897,10 @@ var require_oidc_utils = __commonJS({ var _a2; const httpclient = _OidcClient.createHttpClient(); const res = yield httpclient.getJson(id_token_url).catch((error3) => { - throw new Error(`Failed to get ID Token. - + throw new Error(`Failed to get ID Token. + Error Code : ${error3.statusCode} - + Error Message: ${error3.message}`); }); const id_token = (_a2 = res.result) === null || _a2 === void 0 ? void 0 : _a2.value; @@ -35739,7 +35739,7 @@ var require_restError = __commonJS({ } : void 0; Object.defineProperty(this, inspect_js_1.custom, { value: () => { - return `RestError: ${this.message} + return `RestError: ${this.message} ${errorSanitizer.sanitize({ ...this, request: { ...this.request, agent }, @@ -75738,7 +75738,7 @@ var require_cacheHttpClient = __commonJS({ const cacheListResult = response.result; const totalCount = cacheListResult === null || cacheListResult === void 0 ? void 0 : cacheListResult.totalCount; if (totalCount && totalCount > 0) { - core31.debug(`No matching cache found for cache key '${key}', version '${version} and scope ${process.env["GITHUB_REF"]}. There exist one or more cache(s) with similar key but they have different version or scope. See more info on cache matching here: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key + core31.debug(`No matching cache found for cache key '${key}', version '${version} and scope ${process.env["GITHUB_REF"]}. There exist one or more cache(s) with similar key but they have different version or scope. See more info on cache matching here: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key Other caches with similar key:`); for (const cacheEntry of (cacheListResult === null || cacheListResult === void 0 ? void 0 : cacheListResult.artifactCaches) || []) { core31.debug(`Cache Key: ${cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.cacheKey}, Cache Version: ${cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.cacheVersion}, Cache Scope: ${cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.scope}, Cache Created: ${cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.creationTime}`); @@ -88178,9 +88178,9 @@ var require_path_and_artifact_name_validation = __commonJS({ for (const [invalidCharacterKey, errorMessageForCharacter] of invalidArtifactNameCharacters) { if (name.includes(invalidCharacterKey)) { throw new Error(`The artifact name is not valid: ${name}. Contains the following character: ${errorMessageForCharacter} - + Invalid characters include: ${Array.from(invalidArtifactNameCharacters.values()).toString()} - + These characters are not allowed in the artifact name due to limitations with certain file systems such as NTFS. To maintain file system agnostic behavior, these characters are intentionally not allowed to prevent potential problems with downloads on different file systems.`); } } @@ -88194,9 +88194,9 @@ These characters are not allowed in the artifact name due to limitations with ce for (const [invalidCharacterKey, errorMessageForCharacter] of invalidArtifactFilePathCharacters) { if (path30.includes(invalidCharacterKey)) { throw new Error(`The path for one of the files in artifact is not valid: ${path30}. Contains the following character: ${errorMessageForCharacter} - + Invalid characters include: ${Array.from(invalidArtifactFilePathCharacters.values()).toString()} - + The following characters are not allowed in files that are uploaded due to limitations with certain file systems such as NTFS. To maintain file system agnostic behavior, these characters are intentionally not allowed to prevent potential problems with downloads on different file systems. `); } @@ -119373,10 +119373,10 @@ var require_oidc_utils2 = __commonJS({ return __awaiter2(this, void 0, void 0, function* () { const httpclient = _OidcClient.createHttpClient(); const res = yield httpclient.getJson(id_token_url).catch((error3) => { - throw new Error(`Failed to get ID Token. - + throw new Error(`Failed to get ID Token. + Error Code : ${error3.statusCode} - + Error Message: ${error3.message}`); }); const id_token = (_a2 = res.result) === null || _a2 === void 0 ? void 0 : _a2.value; @@ -121141,9 +121141,9 @@ var require_path_and_artifact_name_validation2 = __commonJS({ for (const [invalidCharacterKey, errorMessageForCharacter] of invalidArtifactNameCharacters) { if (name.includes(invalidCharacterKey)) { throw new Error(`Artifact name is not valid: ${name}. Contains the following character: ${errorMessageForCharacter} - + Invalid characters include: ${Array.from(invalidArtifactNameCharacters.values()).toString()} - + These characters are not allowed in the artifact name due to limitations with certain file systems such as NTFS. To maintain file system agnostic behavior, these characters are intentionally not allowed to prevent potential problems with downloads on different file systems.`); } } @@ -121157,9 +121157,9 @@ These characters are not allowed in the artifact name due to limitations with ce for (const [invalidCharacterKey, errorMessageForCharacter] of invalidArtifactFilePathCharacters) { if (path30.includes(invalidCharacterKey)) { throw new Error(`Artifact path is not valid: ${path30}. Contains the following character: ${errorMessageForCharacter} - + Invalid characters include: ${Array.from(invalidArtifactFilePathCharacters.values()).toString()} - + The following characters are not allowed in files that are uploaded due to limitations with certain file systems such as NTFS. To maintain file system agnostic behavior, these characters are intentionally not allowed to prevent potential problems with downloads on different file systems. `); } diff --git a/package-lock.json b/package-lock.json index a580a2c5b..208964cbc 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", @@ -47,21 +47,21 @@ "@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", "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": { @@ -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": { @@ -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..b1190b043 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", @@ -55,21 +55,21 @@ "@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", "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": { 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}' }}`, diff --git a/src/config/file.test.ts b/src/config/file.test.ts index 3362d26c6..4b7b88c9c 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 = { @@ -22,47 +22,29 @@ 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. - const targetWithArgs = target - .withActions(actionsEnv) - .withArgs(repositoryProperties); - await targetWithArgs.passes(async (fn) => t.is(await fn(), testInput)); - - // Check for the expected log message. - t.true( - targetWithArgs - .getLogger() - .hasMessage("Using configuration file input from workflow"), - ); + 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); }); 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(async (fn) => - t.is(await fn(), 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) => { @@ -70,27 +52,18 @@ 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) => { // 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(async (fn) => t.is(await fn(), 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/config/remote-file.test.ts b/src/config/remote-file.test.ts index ac1492d90..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 { @@ -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,28 +78,23 @@ 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); } }); 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[] = [ { @@ -141,33 +136,33 @@ 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 .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); } }); 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]) - .passes(async (fn) => t.throwsAsync(fn, { instanceOf: Error })); + .throws(t, { instanceOf: Error }); t.assert(getRequired.calledOnceWith(ActionsEnvVars.GITHUB_REPOSITORY)); }); @@ -202,14 +197,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,28 +210,25 @@ 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) => { - 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 = [ " ", @@ -262,21 +252,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/environment.ts b/src/environment.ts index d75eff933..ef9897529 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -187,10 +187,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/json/index.test.ts b/src/json/index.test.ts index eb259c757..80edbedec 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -67,3 +67,76 @@ test("validateSchema - optional properties are optional", async (t) => { t.true(json.validateSchema(optionalSchema, { optionalKey: "" })); 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] })); +}); + +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 })); +}); + +const checkSchemaTestSchema = { + rootKey: json.object(objectSchema), +}; + +test("checkSchema - reports unknown keys", async (t) => { + const result = json.checkSchema(checkSchemaTestSchema, { + rootKey: { + objectKey: { + arrayKey: [], + }, + nestedExtraKey: "foo", + }, + extraKey: "bar", + }); + + t.true(result.valid); + t.deepEqual( + result.unknownKeys.sort(), + [".extraKey", ".rootKey.nestedExtraKey"].sort(), + ); +}); + +test("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 a55de5b58..78923f8ba 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -48,23 +48,107 @@ export function isStringOrUndefined( */ export type Validator = { validate: (val: unknown) => val is T; + check: ( + val: unknown, + opts: CheckSchemaOptions, + path: string, + ) => CheckSchemaResult; required: boolean; }; +function defaultCheck( + validate: (val: unknown) => val is any, +): (arg: unknown) => CheckSchemaResult { + return (arg) => ({ unknownKeys: [], invalidKeys: [], 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, + check: (val: unknown, opts: CheckSchemaOptions, 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 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; + + // 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; + } + + continue; + } + } + + return result; + }, + required: true, + } as const satisfies Validator; +} + +/** A validator for objects. */ +export function object< + S extends Schema, + T extends UnvalidatedObject = FromSchema, +>(schema: S) { + return { + validate: (val: unknown) => { + return isObject(val) && validateSchema(schema, val); + }, + check: (val, opts, path) => { + if (!isObject(val)) { + return invalidCheckSchema(); + } + return checkSchema(schema, val, opts, path); + }, + required: true, + } as const satisfies Validator; +} /** * Transforms a validator to be optional, accepting `undefined` or `null` for an @@ -75,6 +159,12 @@ export function optionalOrNull(validator: Validator) { validate: (val: unknown) => { return val === undefined || val === null || validator.validate(val); }, + check: (val, opts, path) => { + if (val === undefined || val === null) { + return successfulCheckSchema(); + } + return validator.check(val, opts, path); + }, required: false, } as const satisfies Validator; } @@ -88,6 +178,12 @@ export function optional(validator: Validator) { validate: (val: unknown): val is T | undefined => { return val === undefined || validator.validate(val); }, + check: (val, opts, path) => { + if (val === undefined) { + return successfulCheckSchema(); + } + return validator.check(val, opts, path); + }, required: false, } as const satisfies Validator; } @@ -117,28 +213,133 @@ 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( +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; +} + +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[]; + /** Known keys that failed validation. */ + invalidKeys: string[]; +} + +/** + * Convenience function to produce a `CheckSchemaResult` where `valid: true`. + */ +function successfulCheckSchema(): CheckSchemaResult { + return { + valid: true, + unknownKeys: [], + invalidKeys: [], + }; +} + +/** + * Convenience function to produce a `CheckSchemaResult` where `valid: false`. + */ +function invalidCheckSchema(): CheckSchemaResult { + return { + valid: false, + unknownKeys: [], + invalidKeys: [], + }; +} + +export function checkSchema( schema: S, obj: UnvalidatedObject, -): obj is FromSchema { + options: CheckSchemaOptions = {}, + 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; + // 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) { - return false; + result.valid = false; + + if (options.failFast) { + break; + } + 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) { + break; + } + continue; } // If the property is present, validate it. - if (hasKey && !validator.validate(obj[key])) { - return false; + if (hasKey) { + const checkResult = validator.check(obj[key], options, `${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; + + if (options.failFast) { + break; + } + continue; + } } + + // If we reach this point, the key has been successfully validated. + invalidKeys.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}`); + } + + // 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; } 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..03056ac4c 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"] as const; + 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, @@ -291,8 +300,11 @@ 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 ( + !ALWAYS_ENABLED_REGISTRY_TYPE.some((t) => t === e.type) && registryTypeForLanguage && !registryTypeForLanguage.some((t) => t === e.type) ) { diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 46650f12d..9f2157ade 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, @@ -209,35 +211,50 @@ export function initAllState( }; } +type DelayedCheck< + Args extends readonly any[], + R, + 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. */ -export class TestEnv< +abstract class BaseEnvBuilder< Args extends readonly any[], 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; + protected checks: Array>; constructor( fn: (state: ActionState, ...args: Args) => R, - cloneFrom?: TestEnv, + cloneFrom?: BaseEnvBuilder, ) { this.fn = fn; - this.args = cloneFrom?.args; this.logger = new RecordingLogger(); this.state = cloneFrom !== undefined - ? { ...cloneFrom.state, logger: this.logger } + ? ({ + ...cloneFrom.state, + env: Object.create(cloneFrom.state.env), + actions: Object.create(cloneFrom.state.actions), + logger: this.logger, + } satisfies ActionState) : initAllState({ logger: this.logger }); + this.checks = [...(cloneFrom?.checks ?? [])]; } - private clone(): TestEnv { - return new TestEnv(this.fn, this); - } + /** + * Creates a clone of this object. Used internally. + * Must be overridden by subclasses. + */ + protected abstract clone(): this; public getLogger(): RecordingLogger { return this.logger; @@ -247,48 +264,181 @@ export class TestEnv< 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[]): TestEnv { + public withFeatures(enabled: Feature[]): this { const result = this.clone(); result.state.features = createFeatures(enabled); return result; } - public withEnv(env: Env): TestEnv { + /** + * 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(); - result.state.env = env; + setupBaseActionsVars(overrides, result.state.env); return result; } - public withActions(actions: ActionsEnv): TestEnv { + /** + * 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(); - result.state.actions = actions; + setupActionsVars(tempDir, toolsDir, overrides, result.state.env); return result; } + public withEnv(arg: ValueOrMutation): this { + const result = this.clone(); + if (typeof arg === "function") { + arg(result.state.env); + } else { + result.state.env = arg; + } + return result; + } + + public withActions(arg: ValueOrMutation): this { + const result = this.clone(); + if (typeof arg === "function") { + arg(result.state.actions); + } else { + result.state.actions = arg; + } + 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< + 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 BaseEnvBuilder { + private args: Args; + + constructor( + fn: (state: ActionState, ...args: Args) => R, + args: Args, + cloneFrom?: BaseEnvBuilder, + ) { + 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); } - public passes( - assertion: (makeCall: () => R) => T | Promise, - ): T | 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 { + // 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()); + + // 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; + } + + /** + * 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> { + // Run the main assertion. + const error = await t.throwsAsync( + async () => Promise.resolve(this.call()), + expectations, + ); + + // Run other delayed checks. + for (const delayedCheck of this.checks) { + await delayedCheck(this); + } + + // Return the error. + return error; } } @@ -297,8 +447,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); } /** @@ -350,16 +500,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"; @@ -493,6 +645,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`. */