From 909fda1468f8ef25624848914b92c3faeba7cfe7 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 6 Oct 2026 13:36:35 +0100 Subject: [PATCH 1/9] Allow logging functions to accept `string[]` directly --- lib/entry-points.js | 20 ++++++++++++++++---- src/logging.test.ts | 17 +++++++++++++++++ src/logging.ts | 43 +++++++++++++++++++++++++++++++++++-------- src/testing-utils.ts | 14 +++++++------- 4 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 src/logging.test.ts diff --git a/lib/entry-points.js b/lib/entry-points.js index ad7ac988a1..1a75678e97 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -170435,12 +170435,24 @@ function fixCodeQualityCategory(logger2, category, env = getEnv()) { // src/logging.ts var core4 = __toESM(require_core()); +var joinMessageStrings = (message) => { + if (Array.isArray(message)) { + return message.join(" "); + } else { + return message; + } +}; +var autoJoinMessageStrings = (fn) => { + return (message) => { + return fn(joinMessageStrings(message)); + }; +}; function getActionsLogger() { return { - debug: core4.debug, - info: core4.info, - warning: core4.warning, - error: core4.error, + debug: autoJoinMessageStrings(core4.debug), + info: autoJoinMessageStrings(core4.info), + warning: autoJoinMessageStrings(core4.warning), + error: autoJoinMessageStrings(core4.error), isDebug: core4.isDebug, startGroup: core4.startGroup, endGroup: core4.endGroup diff --git a/src/logging.test.ts b/src/logging.test.ts new file mode 100644 index 0000000000..33f6f8c162 --- /dev/null +++ b/src/logging.test.ts @@ -0,0 +1,17 @@ +import test from "ava"; + +import { joinMessageStrings } from "./logging"; +import { setupTests } from "./testing-utils"; + +setupTests(test); + +test("joinMessageStrings", async (t) => { + // For strings and errors, it is the identity function. + t.deepEqual(joinMessageStrings("Hello"), "Hello"); + + const error = new Error("Some error"); + t.deepEqual(joinMessageStrings(error), error); + + // For arrays of strings, we get a join-ed string. + t.deepEqual(joinMessageStrings(["foo", "bar"]), "foo bar"); +}); diff --git a/src/logging.ts b/src/logging.ts index 2c34cb54d4..177aca6c7b 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,10 +1,16 @@ import * as core from "@actions/core"; +/** Types that all of our logging functions accept. */ +export type Loggable = string | string[]; + +/** Some logging functions also accept errors directly. */ +export type LoggableError = Loggable | Error; + export interface Logger { - debug: (message: string) => void; - info: (message: string) => void; - warning: (message: string | Error) => void; - error: (message: string | Error) => void; + debug: (message: Loggable) => void; + info: (message: Loggable) => void; + warning: (message: LoggableError) => void; + error: (message: LoggableError) => void; isDebug: () => boolean; @@ -12,12 +18,33 @@ export interface Logger { endGroup: () => void; } +/** If `message` is an array of strings, the array is `join`-ed into one string separated by spaces. */ +export const joinMessageStrings = ( + message: string[] | string | T, +): string | T => { + if (Array.isArray(message)) { + return message.join(" "); + } else { + return message; + } +}; + +/** + * Wraps a logging function so that `joinMessageStrings` is applied to the input before + * calling the wrapped logging function. + */ +const autoJoinMessageStrings = (fn: (msg: string | T) => void) => { + return (message: string[] | string | T) => { + return fn(joinMessageStrings(message)); + }; +}; + export function getActionsLogger(): Logger { return { - debug: core.debug, - info: core.info, - warning: core.warning, - error: core.error, + debug: autoJoinMessageStrings(core.debug), + info: autoJoinMessageStrings(core.info), + warning: autoJoinMessageStrings(core.warning), + error: autoJoinMessageStrings(core.error), isDebug: core.isDebug, startGroup: core.startGroup, endGroup: core.endGroup, diff --git a/src/testing-utils.ts b/src/testing-utils.ts index a5ba50554c..df81f51bb4 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -30,7 +30,7 @@ import { featureConfig, FeatureEnablement, } from "./feature-flags"; -import { Logger } from "./logging"; +import { joinMessageStrings, Loggable, LoggableError, Logger } from "./logging"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; import { getBundlePlatform } from "./platform"; import { ActionName } from "./status-report"; @@ -622,8 +622,8 @@ export class RecordingLogger implements Logger { constructor(private readonly logToConsole: boolean = true) {} - private addMessage(level: LogLevel, message: string | Error): void { - this.messages.push({ type: level, message }); + private addMessage(level: LogLevel, message: LoggableError): void { + this.messages.push({ type: level, message: joinMessageStrings(message) }); if (this.logToConsole) { // eslint-disable-next-line no-console @@ -648,19 +648,19 @@ export class RecordingLogger implements Logger { return true; } - debug(message: string) { + debug(message: Loggable) { this.addMessage("debug", message); } - info(message: string) { + info(message: Loggable) { this.addMessage("info", message); } - warning(message: string | Error) { + warning(message: LoggableError) { this.addMessage("warning", message); } - error(message: string | Error) { + error(message: LoggableError) { this.addMessage("error", message); } From cb7acf539626ded596e80ac0f4d7c38fc25a89d6 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 6 Oct 2026 13:45:35 +0100 Subject: [PATCH 2/9] Refactor `getCodeQLActionRepository` out of `setup-codeql` --- lib/entry-points.js | 22 ++++++++++++---------- src/setup-codeql.test.ts | 18 ------------------ src/setup-codeql.ts | 20 ++++---------------- src/setup/repository.test.ts | 31 +++++++++++++++++++++++++++++++ src/setup/repository.ts | 19 +++++++++++++++++++ 5 files changed, 66 insertions(+), 44 deletions(-) create mode 100644 src/setup/repository.test.ts create mode 100644 src/setup/repository.ts diff --git a/lib/entry-points.js b/lib/entry-points.js index 1a75678e97..f0cc985d11 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -175634,6 +175634,18 @@ function logPerLanguageBundleFallback({ logger: logger2 }, language, location) { ); } +// src/setup/repository.ts +var CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action"; +function getCodeQLActionRepository(logger2) { + if (isRunningLocalAction()) { + logger2.info( + "The CodeQL Action is checked out locally. Using the default CodeQL Action repository." + ); + return CODEQL_DEFAULT_ACTION_REPOSITORY; + } + return getRequiredEnvParam("GITHUB_ACTION_REPOSITORY"); +} + // src/tar.ts var import_child_process = require("child_process"); var fs12 = __toESM(require("fs")); @@ -176025,21 +176037,11 @@ function writeToolcacheMarkerFile(extractedPath, logger2) { } // src/setup-codeql.ts -var CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action"; var CODEQL_NIGHTLIES_REPOSITORY_OWNER = "dsp-testing"; var CODEQL_NIGHTLIES_REPOSITORY_NAME = "codeql-cli-nightlies"; var CODEQL_BUNDLE_VERSION_ALIAS = ["linked", "latest"]; var CODEQL_NIGHTLY_TOOLS_INPUTS = ["nightly", "nightly-latest"]; var CODEQL_TOOLCACHE_INPUT = "toolcache"; -function getCodeQLActionRepository(logger2) { - if (isRunningLocalAction()) { - logger2.info( - "The CodeQL Action is checked out locally. Using the default CodeQL Action repository." - ); - return CODEQL_DEFAULT_ACTION_REPOSITORY; - } - return getRequiredEnvParam("GITHUB_ACTION_REPOSITORY"); -} async function getCodeQLBundleDownloadURL(tagName, apiDetails, codeQLBundleName, logger2) { const codeQLActionRepository = getCodeQLActionRepository(logger2); const potentialDownloadSources = [ diff --git a/src/setup-codeql.test.ts b/src/setup-codeql.test.ts index c33ac0700a..e7a93455fe 100644 --- a/src/setup-codeql.test.ts +++ b/src/setup-codeql.test.ts @@ -126,24 +126,6 @@ test.serial("convert to semver", (t) => { } }); -test.serial("getCodeQLActionRepository", (t) => { - const logger = getRunnerLogger(true); - - initializeEnvironment("1.2.3"); - - // isRunningLocalAction() === true - delete process.env["GITHUB_ACTION_REPOSITORY"]; - process.env["RUNNER_TEMP"] = path.dirname(__dirname); - const repoLocalRunner = setupCodeql.getCodeQLActionRepository(logger); - t.deepEqual(repoLocalRunner, "github/codeql-action"); - - // isRunningLocalAction() === false - sinon.stub(actionsUtil, "isRunningLocalAction").returns(false); - process.env["GITHUB_ACTION_REPOSITORY"] = "xxx/yyy"; - const repoEnv = setupCodeql.getCodeQLActionRepository(logger); - t.deepEqual(repoEnv, "xxx/yyy"); -}); - test.serial( "getCodeQLSource sets CLI version for a semver tagged bundle", async (t) => { diff --git a/src/setup-codeql.ts b/src/setup-codeql.ts index d306d40d63..793191b8b1 100644 --- a/src/setup-codeql.ts +++ b/src/setup-codeql.ts @@ -14,7 +14,6 @@ import { isAnalyzingPullRequest, isDynamicWorkflow, isGitHubHostedRunner, - isRunningLocalAction, } from "./actions-util"; import * as api from "./api-client"; import { @@ -45,6 +44,10 @@ import { logPerLanguageBundleFallback, } from "./per-language-bundles"; import { getBundlePlatform } from "./platform"; +import { + CODEQL_DEFAULT_ACTION_REPOSITORY, + getCodeQLActionRepository, +} from "./setup/repository"; import * as tar from "./tar"; import { deleteToolcacheBundles, @@ -66,7 +69,6 @@ export enum ToolsSource { Download = "DOWNLOAD", } -const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action"; const CODEQL_NIGHTLIES_REPOSITORY_OWNER = "dsp-testing"; const CODEQL_NIGHTLIES_REPOSITORY_NAME = "codeql-cli-nightlies"; @@ -74,20 +76,6 @@ const CODEQL_BUNDLE_VERSION_ALIAS: string[] = ["linked", "latest"]; const CODEQL_NIGHTLY_TOOLS_INPUTS = ["nightly", "nightly-latest"]; const CODEQL_TOOLCACHE_INPUT = "toolcache"; -export function getCodeQLActionRepository(logger: Logger): string { - if (isRunningLocalAction()) { - // This handles the case where the Action does not come from an Action repository, - // e.g. our integration tests which use the Action code from the current checkout. - // In these cases, the GITHUB_ACTION_REPOSITORY environment variable is not set. - logger.info( - "The CodeQL Action is checked out locally. Using the default CodeQL Action repository.", - ); - return CODEQL_DEFAULT_ACTION_REPOSITORY; - } - - return util.getRequiredEnvParam("GITHUB_ACTION_REPOSITORY"); -} - async function getCodeQLBundleDownloadURL( tagName: string, apiDetails: api.GitHubApiDetails, diff --git a/src/setup/repository.test.ts b/src/setup/repository.test.ts new file mode 100644 index 0000000000..c3dccbf9c2 --- /dev/null +++ b/src/setup/repository.test.ts @@ -0,0 +1,31 @@ +import * as path from "path"; + +import test from "ava"; +import * as sinon from "sinon"; + +import * as actionsUtil from "../actions-util"; +import { getRunnerLogger } from "../logging"; +import { setupTests } from "../testing-utils"; +import { initializeEnvironment } from "../util"; + +import { getCodeQLActionRepository } from "./repository"; + +setupTests(test); + +test.serial("getCodeQLActionRepository", (t) => { + const logger = getRunnerLogger(true); + + initializeEnvironment("1.2.3"); + + // isRunningLocalAction() === true + delete process.env["GITHUB_ACTION_REPOSITORY"]; + process.env["RUNNER_TEMP"] = path.dirname(__dirname); + const repoLocalRunner = getCodeQLActionRepository(logger); + t.deepEqual(repoLocalRunner, "github/codeql-action"); + + // isRunningLocalAction() === false + sinon.stub(actionsUtil, "isRunningLocalAction").returns(false); + process.env["GITHUB_ACTION_REPOSITORY"] = "xxx/yyy"; + const repoEnv = getCodeQLActionRepository(logger); + t.deepEqual(repoEnv, "xxx/yyy"); +}); diff --git a/src/setup/repository.ts b/src/setup/repository.ts new file mode 100644 index 0000000000..908cc39ee7 --- /dev/null +++ b/src/setup/repository.ts @@ -0,0 +1,19 @@ +import { isRunningLocalAction } from "../actions-util"; +import { Logger } from "../logging"; +import * as util from "../util"; + +export const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action"; + +export function getCodeQLActionRepository(logger: Logger): string { + if (isRunningLocalAction()) { + // This handles the case where the Action does not come from an Action repository, + // e.g. our integration tests which use the Action code from the current checkout. + // In these cases, the GITHUB_ACTION_REPOSITORY environment variable is not set. + logger.info( + "The CodeQL Action is checked out locally. Using the default CodeQL Action repository.", + ); + return CODEQL_DEFAULT_ACTION_REPOSITORY; + } + + return util.getRequiredEnvParam("GITHUB_ACTION_REPOSITORY"); +} From 3f57a005543dac0b43f0fa212b72e5c09612cfa0 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 6 Oct 2026 14:00:12 +0100 Subject: [PATCH 3/9] Use `ActionState` for `getCodeQLActionRepository` --- lib/entry-points.js | 13 ++++++++----- src/actions-util.ts | 4 ++-- src/setup-codeql.ts | 5 ++++- src/setup/repository.test.ts | 27 +++++++++++++++------------ src/setup/repository.ts | 23 +++++++++++++++++------ 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index f0cc985d11..3a4bdcb05c 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -175636,14 +175636,14 @@ function logPerLanguageBundleFallback({ logger: logger2 }, language, location) { // src/setup/repository.ts var CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action"; -function getCodeQLActionRepository(logger2) { - if (isRunningLocalAction()) { - logger2.info( +function getCodeQLActionRepository(action) { + if (isRunningLocalAction(action.env)) { + action.logger.info( "The CodeQL Action is checked out locally. Using the default CodeQL Action repository." ); return CODEQL_DEFAULT_ACTION_REPOSITORY; } - return getRequiredEnvParam("GITHUB_ACTION_REPOSITORY"); + return action.env.getRequired("GITHUB_ACTION_REPOSITORY" /* GITHUB_ACTION_REPOSITORY */); } // src/tar.ts @@ -176043,7 +176043,10 @@ var CODEQL_BUNDLE_VERSION_ALIAS = ["linked", "latest"]; var CODEQL_NIGHTLY_TOOLS_INPUTS = ["nightly", "nightly-latest"]; var CODEQL_TOOLCACHE_INPUT = "toolcache"; async function getCodeQLBundleDownloadURL(tagName, apiDetails, codeQLBundleName, logger2) { - const codeQLActionRepository = getCodeQLActionRepository(logger2); + const codeQLActionRepository = getCodeQLActionRepository({ + logger: logger2, + env: getEnv() + }); const potentialDownloadSources = [ // This GitHub instance, and this Action. [apiDetails.url, codeQLActionRepository], diff --git a/src/actions-util.ts b/src/actions-util.ts index 6c7d6a78d2..d51fdbe1da 100644 --- a/src/actions-util.ts +++ b/src/actions-util.ts @@ -103,7 +103,7 @@ export function getWorkflowEventName(env: Env = getEnv()) { * Returns whether the current workflow is executing a local copy of the Action, e.g. we're running * a workflow on the codeql-action repo itself. */ -export function isRunningLocalAction(env: Env = getEnv()): boolean { +export function isRunningLocalAction(env: ReadOnlyEnv = getEnv()): boolean { const relativeScriptPath = getRelativeScriptPath(env); return ( relativeScriptPath.startsWith("..") || path.isAbsolute(relativeScriptPath) @@ -115,7 +115,7 @@ export function isRunningLocalAction(env: Env = getEnv()): boolean { * * This can be used to get the Action's name or tell if we're running a local Action. */ -function getRelativeScriptPath(env: Env): string { +function getRelativeScriptPath(env: ReadOnlyEnv): string { const runnerTemp = env.getRequired(ActionsEnvVars.RUNNER_TEMP); const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions"); return path.relative(actionsDirectory, __filename); diff --git a/src/setup-codeql.ts b/src/setup-codeql.ts index 793191b8b1..c97729471f 100644 --- a/src/setup-codeql.ts +++ b/src/setup-codeql.ts @@ -82,7 +82,10 @@ async function getCodeQLBundleDownloadURL( codeQLBundleName: string, logger: Logger, ): Promise { - const codeQLActionRepository = getCodeQLActionRepository(logger); + const codeQLActionRepository = getCodeQLActionRepository({ + logger, + env: getEnv(), + }); const potentialDownloadSources = [ // This GitHub instance, and this Action. [apiDetails.url, codeQLActionRepository], diff --git a/src/setup/repository.test.ts b/src/setup/repository.test.ts index c3dccbf9c2..56eb74786f 100644 --- a/src/setup/repository.test.ts +++ b/src/setup/repository.test.ts @@ -4,28 +4,31 @@ import test from "ava"; import * as sinon from "sinon"; import * as actionsUtil from "../actions-util"; -import { getRunnerLogger } from "../logging"; -import { setupTests } from "../testing-utils"; +import { ActionsEnvVars } from "../environment"; +import { callee, setupTests } from "../testing-utils"; import { initializeEnvironment } from "../util"; import { getCodeQLActionRepository } from "./repository"; setupTests(test); -test.serial("getCodeQLActionRepository", (t) => { - const logger = getRunnerLogger(true); - +test.serial("getCodeQLActionRepository", async (t) => { initializeEnvironment("1.2.3"); + const target = callee(getCodeQLActionRepository) + .withArgs() + .withEnv((env) => { + env.set(ActionsEnvVars.RUNNER_TEMP, path.dirname(__dirname)); + }); + // isRunningLocalAction() === true - delete process.env["GITHUB_ACTION_REPOSITORY"]; - process.env["RUNNER_TEMP"] = path.dirname(__dirname); - const repoLocalRunner = getCodeQLActionRepository(logger); - t.deepEqual(repoLocalRunner, "github/codeql-action"); + await target.passes(t.deepEqual, "github/codeql-action"); // isRunningLocalAction() === false sinon.stub(actionsUtil, "isRunningLocalAction").returns(false); - process.env["GITHUB_ACTION_REPOSITORY"] = "xxx/yyy"; - const repoEnv = getCodeQLActionRepository(logger); - t.deepEqual(repoEnv, "xxx/yyy"); + await target + .withEnv((env) => { + env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "xxx/yyy"); + }) + .passes(t.deepEqual, "xxx/yyy"); }); diff --git a/src/setup/repository.ts b/src/setup/repository.ts index 908cc39ee7..c58470630b 100644 --- a/src/setup/repository.ts +++ b/src/setup/repository.ts @@ -1,19 +1,30 @@ +import { ActionState } from "../action-common"; import { isRunningLocalAction } from "../actions-util"; -import { Logger } from "../logging"; -import * as util from "../util"; +import { ActionsEnvVars } from "../environment"; +/** The NWO of the standard CodeQL Action repository. */ export const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action"; -export function getCodeQLActionRepository(logger: Logger): string { - if (isRunningLocalAction()) { +/** + * Retrieves the NWO of the repository that the CodeQL Action is loaded from. + * Typically, this will be `github/codeql-action`, but it may be something else for + * forks of the Action or in environments other than GitHub.com. + * + * If this function detects that we are running the Action locally (e.g. in tests), + * then we default to `CODEQL_DEFAULT_ACTION_REPOSITORY`. + */ +export function getCodeQLActionRepository( + action: ActionState<["ReadOnlyEnv", "Logger"]>, +): string { + if (isRunningLocalAction(action.env)) { // This handles the case where the Action does not come from an Action repository, // e.g. our integration tests which use the Action code from the current checkout. // In these cases, the GITHUB_ACTION_REPOSITORY environment variable is not set. - logger.info( + action.logger.info( "The CodeQL Action is checked out locally. Using the default CodeQL Action repository.", ); return CODEQL_DEFAULT_ACTION_REPOSITORY; } - return util.getRequiredEnvParam("GITHUB_ACTION_REPOSITORY"); + return action.env.getRequired(ActionsEnvVars.GITHUB_ACTION_REPOSITORY); } From da07d40a0f387325a497ae40e0e02b45044b7f38 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 6 Oct 2026 14:29:42 +0100 Subject: [PATCH 4/9] Refactor `getCodeQLBundleDownloadURL` out of `setup-codeql` --- lib/entry-points.js | 172 ++++++++++++++++------------------------ package-lock.json | 2 +- package.json | 1 - src/setup-codeql.ts | 73 ++--------------- src/setup/repository.ts | 99 +++++++++++++++++++++++ 5 files changed, 176 insertions(+), 171 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 3a4bdcb05c..71b6915a9c 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -42967,11 +42967,11 @@ var require_tracingPolicy = __commonJS({ var import_log = require_log3(); var import_core_util = require_commonjs4(); var import_restError = require_restError3(); - var import_util58 = require_internal3(); + var import_util59 = require_internal3(); var tracingPolicyName2 = "tracingPolicy"; function tracingPolicy2(options = {}) { const userAgentPromise = (0, import_userAgent.getUserAgentValue)(options.userAgentPrefix); - const sanitizer = new import_util58.Sanitizer({ + const sanitizer = new import_util59.Sanitizer({ additionalAllowedQueryParameters: options.additionalAllowedQueryParameters }); const tracingClient = tryCreateTracingClient(); @@ -46597,11 +46597,11 @@ var require_response2 = __commonJS({ }); module2.exports = __toCommonJS2(response_exports); var import_core_rest_pipeline = require_commonjs6(); - var import_util58 = require_util9(); + var import_util59 = require_util9(); var originalResponse = /* @__PURE__ */ Symbol("Original FullOperationResponse"); function toCompatResponse2(response, options) { - let request3 = (0, import_util58.toWebResourceLike)(response.request); - let headers = (0, import_util58.toHttpHeadersLike)(response.headers); + let request3 = (0, import_util59.toWebResourceLike)(response.request); + let headers = (0, import_util59.toHttpHeadersLike)(response.headers); if (options?.createProxy) { return new Proxy(response, { get(target, prop, receiver) { @@ -46642,7 +46642,7 @@ var require_response2 = __commonJS({ return { ...compatResponse, headers, - request: (0, import_util58.toPipelineRequest)(compatResponse.request) + request: (0, import_util59.toPipelineRequest)(compatResponse.request) }; } } @@ -46749,7 +46749,7 @@ var require_requestPolicyFactoryPolicy = __commonJS({ requestPolicyFactoryPolicyName: () => requestPolicyFactoryPolicyName2 }); module2.exports = __toCommonJS2(requestPolicyFactoryPolicy_exports); - var import_util58 = require_util9(); + var import_util59 = require_util9(); var import_response = require_response2(); var HttpPipelineLogLevel2 = /* @__PURE__ */ ((HttpPipelineLogLevel22) => { HttpPipelineLogLevel22[HttpPipelineLogLevel22["ERROR"] = 1] = "ERROR"; @@ -46773,14 +46773,14 @@ var require_requestPolicyFactoryPolicy = __commonJS({ async sendRequest(request3, next) { let httpPipeline = { async sendRequest(httpRequest) { - const response2 = await next((0, import_util58.toPipelineRequest)(httpRequest)); + const response2 = await next((0, import_util59.toPipelineRequest)(httpRequest)); return (0, import_response.toCompatResponse)(response2, { createProxy: true }); } }; for (const factory of orderedFactories) { httpPipeline = factory.create(httpPipeline, mockRequestPolicyOptions); } - const webResourceLike = (0, import_util58.toWebResourceLike)(request3, { createProxy: true }); + const webResourceLike = (0, import_util59.toWebResourceLike)(request3, { createProxy: true }); const response = await httpPipeline.sendRequest(webResourceLike); return (0, import_response.toPipelineResponse)(response); } @@ -46815,12 +46815,12 @@ var require_httpClientAdapter = __commonJS({ }); module2.exports = __toCommonJS2(httpClientAdapter_exports); var import_response = require_response2(); - var import_util58 = require_util9(); + var import_util59 = require_util9(); function convertHttpClient2(requestPolicyClient) { return { sendRequest: async (request3) => { const response = await requestPolicyClient.sendRequest( - (0, import_util58.toWebResourceLike)(request3, { createProxy: true }) + (0, import_util59.toWebResourceLike)(request3, { createProxy: true }) ); return (0, import_response.toPipelineResponse)(response); } @@ -46858,7 +46858,7 @@ var require_commonjs9 = __commonJS({ disableKeepAlivePolicyName: () => import_disableKeepAlivePolicy.disableKeepAlivePolicyName, requestPolicyFactoryPolicyName: () => import_requestPolicyFactoryPolicy.requestPolicyFactoryPolicyName, toCompatResponse: () => import_response.toCompatResponse, - toHttpHeadersLike: () => import_util58.toHttpHeadersLike + toHttpHeadersLike: () => import_util59.toHttpHeadersLike }); module2.exports = __toCommonJS2(src_exports); var import_extendedClient = require_extendedClient(); @@ -46866,7 +46866,7 @@ var require_commonjs9 = __commonJS({ var import_requestPolicyFactoryPolicy = require_requestPolicyFactoryPolicy(); var import_disableKeepAlivePolicy = require_disableKeepAlivePolicy(); var import_httpClientAdapter = require_httpClientAdapter(); - var import_util58 = require_util9(); + var import_util59 = require_util9(); } }); @@ -107096,41 +107096,6 @@ var require_tool_cache = __commonJS({ } }); -// node_modules/fast-deep-equal/index.js -var require_fast_deep_equal = __commonJS({ - "node_modules/fast-deep-equal/index.js"(exports2, module2) { - "use strict"; - module2.exports = function equal(a, b) { - if (a === b) return true; - if (a && b && typeof a == "object" && typeof b == "object") { - if (a.constructor !== b.constructor) return false; - var length, i, keys; - if (Array.isArray(a)) { - length = a.length; - if (length != b.length) return false; - for (i = length; i-- !== 0; ) - if (!equal(a[i], b[i])) return false; - return true; - } - if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; - if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); - if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); - keys = Object.keys(a); - length = keys.length; - if (length !== Object.keys(b).length) return false; - for (i = length; i-- !== 0; ) - if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; - for (i = length; i-- !== 0; ) { - var key = keys[i]; - if (!equal(a[key], b[key])) return false; - } - return true; - } - return a !== a && b !== b; - }; - } -}); - // node_modules/follow-redirects/debug.js var require_debug3 = __commonJS({ "node_modules/follow-redirects/debug.js"(exports2, module2) { @@ -175209,7 +175174,6 @@ var path13 = __toESM(require("path")); var import_perf_hooks4 = require("perf_hooks"); var core12 = __toESM(require_core()); var toolcache3 = __toESM(require_tool_cache()); -var import_fast_deep_equal = __toESM(require_fast_deep_equal()); var semver10 = __toESM(require_semver2()); // src/codeql-bundle.ts @@ -175645,6 +175609,55 @@ function getCodeQLActionRepository(action) { } return action.env.getRequired("GITHUB_ACTION_REPOSITORY" /* GITHUB_ACTION_REPOSITORY */); } +var DEFAULT_DOWNLOAD_SOURCE = [ + GITHUB_DOTCOM_URL, + CODEQL_DEFAULT_ACTION_REPOSITORY +]; +var isSameDownloadSource = ([srcUrl, srcRepo], [otherUrl, otherRepo]) => { + return srcUrl === otherUrl && srcRepo === otherRepo; +}; +async function getCodeQLAssetDownloadURL(action, apiDetails, tagName, assetName, assetKind = "CodeQL bundle") { + const codeQLActionRepository = getCodeQLActionRepository(action); + const potentialDownloadSources = [ + // This GitHub instance, and this Action. + [apiDetails.url, codeQLActionRepository], + // This GitHub instance, and the canonical Action. + [apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY], + // GitHub.com, and the canonical Action. + DEFAULT_DOWNLOAD_SOURCE + ]; + const uniqueDownloadSources = potentialDownloadSources.filter( + (source, index2, self2) => { + return !self2.slice(0, index2).some((other) => isSameDownloadSource(source, other)); + } + ); + for (const [apiURL, repository] of uniqueDownloadSources) { + if (isSameDownloadSource(DEFAULT_DOWNLOAD_SOURCE, [apiURL, repository])) { + break; + } + const [repositoryOwner, repositoryName] = repository.split("/"); + try { + const release2 = await getApiClient().rest.repos.getReleaseByTag({ + owner: repositoryOwner, + repo: repositoryName, + tag: tagName + }); + for (const asset of release2.data.assets) { + if (asset.name === assetName) { + action.logger.info( + `Found ${assetKind} ${assetName} in ${repository} on ${apiURL} with URL ${asset.url}.` + ); + return asset.url; + } + } + } catch (e) { + action.logger.info( + `Looked for ${assetKind} ${assetName} in ${repository} on ${apiURL} but got error ${e}.` + ); + } + } + return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${assetName}`; +} // src/tar.ts var import_child_process = require("child_process"); @@ -176042,52 +176055,6 @@ var CODEQL_NIGHTLIES_REPOSITORY_NAME = "codeql-cli-nightlies"; var CODEQL_BUNDLE_VERSION_ALIAS = ["linked", "latest"]; var CODEQL_NIGHTLY_TOOLS_INPUTS = ["nightly", "nightly-latest"]; var CODEQL_TOOLCACHE_INPUT = "toolcache"; -async function getCodeQLBundleDownloadURL(tagName, apiDetails, codeQLBundleName, logger2) { - const codeQLActionRepository = getCodeQLActionRepository({ - logger: logger2, - env: getEnv() - }); - const potentialDownloadSources = [ - // This GitHub instance, and this Action. - [apiDetails.url, codeQLActionRepository], - // This GitHub instance, and the canonical Action. - [apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY], - // GitHub.com, and the canonical Action. - [GITHUB_DOTCOM_URL, CODEQL_DEFAULT_ACTION_REPOSITORY] - ]; - const uniqueDownloadSources = potentialDownloadSources.filter( - (source, index2, self2) => { - return !self2.slice(0, index2).some((other) => (0, import_fast_deep_equal.default)(source, other)); - } - ); - for (const downloadSource of uniqueDownloadSources) { - const [apiURL, repository] = downloadSource; - if (apiURL === GITHUB_DOTCOM_URL && repository === CODEQL_DEFAULT_ACTION_REPOSITORY) { - break; - } - const [repositoryOwner, repositoryName] = repository.split("/"); - try { - const release2 = await getApiClient().rest.repos.getReleaseByTag({ - owner: repositoryOwner, - repo: repositoryName, - tag: tagName - }); - for (const asset of release2.data.assets) { - if (asset.name === codeQLBundleName) { - logger2.info( - `Found CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} with URL ${asset.url}.` - ); - return asset.url; - } - } - } catch (e) { - logger2.info( - `Looked for CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} but got error ${e}.` - ); - } - } - return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`; -} function tryGetBundleVersionFromTagName(tagName, logger2) { const match2 = tagName.match(/^codeql-bundle-(.+)$/); if (match2 === null || match2.length < 2) { @@ -176444,9 +176411,10 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO ); } compressionMethod = cliVersion2 !== void 0 && await useZstdBundle(cliVersion2, tarSupportsZstd) ? "zstd" : "gzip"; + const action = { env: getEnv(), logger: logger2 }; const platform2 = getBundlePlatform(); const perLanguageBundleLanguage = await getPerLanguageBundleLanguage( - { env: getEnv(), features, logger: logger2 }, + { ...action, features }, { rawLanguages, cliVersion: cliVersion2, @@ -176455,11 +176423,11 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO variant } ); - const resolveBundleURL = (language) => getCodeQLBundleDownloadURL( - bundleTagName, + const resolveBundleURL = (language) => getCodeQLAssetDownloadURL( + action, apiDetails, - getCodeQLBundleName(compressionMethod, platform2, language), - logger2 + bundleTagName, + getCodeQLBundleName(compressionMethod, platform2, language) ); const combinedBundleURL = await resolveBundleURL(); if (perLanguageBundleLanguage !== void 0) { @@ -182926,7 +182894,7 @@ var import_async = __toESM(require_async(), 1); var import_path7 = require("path"); // node_modules/archiver/lib/error.js -var import_util36 = __toESM(require("util"), 1); +var import_util37 = __toESM(require("util"), 1); var ERROR_CODES = { ABORTED: "archive was aborted", DIRECTORYDIRPATHREQUIRED: "diretory dirpath argument must be a non-empty string value", @@ -182951,7 +182919,7 @@ function ArchiverError(code, data) { this.code = code; this.data = data; } -import_util36.default.inherits(ArchiverError, Error); +import_util37.default.inherits(ArchiverError, Error); // node_modules/archiver/lib/core.js var import_readable_stream2 = __toESM(require_ours(), 1); diff --git a/package-lock.json b/package-lock.json index 27bf393bfd..8d3781fe5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "@octokit/plugin-rest-endpoint-methods": "^18.0.0", "@octokit/plugin-retry": "^8.1.1", "archiver": "^8.0.0", - "fast-deep-equal": "^3.1.3", "follow-redirects": "^1.16.0", "get-folder-size": "^5.0.0", "https-proxy-agent": "^7.0.6", @@ -5817,6 +5816,7 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "dev": true, "license": "MIT" }, "node_modules/fast-diff": { diff --git a/package.json b/package.json index 5ebf14292b..52c11027f8 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "@octokit/plugin-rest-endpoint-methods": "^18.0.0", "@octokit/plugin-retry": "^8.1.1", "archiver": "^8.0.0", - "fast-deep-equal": "^3.1.3", "follow-redirects": "^1.16.0", "get-folder-size": "^5.0.0", "https-proxy-agent": "^7.0.6", diff --git a/src/setup-codeql.ts b/src/setup-codeql.ts index c97729471f..29a1809e4b 100644 --- a/src/setup-codeql.ts +++ b/src/setup-codeql.ts @@ -5,7 +5,6 @@ import { performance } from "perf_hooks"; import * as core from "@actions/core"; import * as toolcache from "@actions/tool-cache"; -import { default as deepEqual } from "fast-deep-equal"; import * as semver from "semver"; import { v4 as uuidV4 } from "uuid"; @@ -44,10 +43,7 @@ import { logPerLanguageBundleFallback, } from "./per-language-bundles"; import { getBundlePlatform } from "./platform"; -import { - CODEQL_DEFAULT_ACTION_REPOSITORY, - getCodeQLActionRepository, -} from "./setup/repository"; +import { getCodeQLAssetDownloadURL } from "./setup/repository"; import * as tar from "./tar"; import { deleteToolcacheBundles, @@ -76,64 +72,6 @@ const CODEQL_BUNDLE_VERSION_ALIAS: string[] = ["linked", "latest"]; const CODEQL_NIGHTLY_TOOLS_INPUTS = ["nightly", "nightly-latest"]; const CODEQL_TOOLCACHE_INPUT = "toolcache"; -async function getCodeQLBundleDownloadURL( - tagName: string, - apiDetails: api.GitHubApiDetails, - codeQLBundleName: string, - logger: Logger, -): Promise { - const codeQLActionRepository = getCodeQLActionRepository({ - logger, - env: getEnv(), - }); - const potentialDownloadSources = [ - // This GitHub instance, and this Action. - [apiDetails.url, codeQLActionRepository], - // This GitHub instance, and the canonical Action. - [apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY], - // GitHub.com, and the canonical Action. - [util.GITHUB_DOTCOM_URL, CODEQL_DEFAULT_ACTION_REPOSITORY], - ]; - // We now filter out any duplicates. - // Duplicates will happen either because the GitHub instance is GitHub.com, or because the Action is not a fork. - const uniqueDownloadSources = potentialDownloadSources.filter( - (source, index, self) => { - return !self.slice(0, index).some((other) => deepEqual(source, other)); - }, - ); - for (const downloadSource of uniqueDownloadSources) { - const [apiURL, repository] = downloadSource; - // If we've reached the final case, short-circuit the API check since we know the bundle exists and is public. - if ( - apiURL === util.GITHUB_DOTCOM_URL && - repository === CODEQL_DEFAULT_ACTION_REPOSITORY - ) { - break; - } - const [repositoryOwner, repositoryName] = repository.split("/"); - try { - const release = await api.getApiClient().rest.repos.getReleaseByTag({ - owner: repositoryOwner, - repo: repositoryName, - tag: tagName, - }); - for (const asset of release.data.assets) { - if (asset.name === codeQLBundleName) { - logger.info( - `Found CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} with URL ${asset.url}.`, - ); - return asset.url; - } - } - } catch (e) { - logger.info( - `Looked for CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} but got error ${e}.`, - ); - } - } - return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`; -} - function tryGetBundleVersionFromTagName( tagName: string, logger: Logger, @@ -707,9 +645,10 @@ export async function getCodeQLSource( ? "zstd" : "gzip"; + const action = { env: getEnv(), logger }; const platform = getBundlePlatform(); const perLanguageBundleLanguage = await getPerLanguageBundleLanguage( - { env: getEnv(), features, logger }, + { ...action, features }, { rawLanguages, cliVersion, @@ -721,11 +660,11 @@ export async function getCodeQLSource( // Resolves the combined or per-language bundle URL for the requested release. const resolveBundleURL = (language?: BuiltInLanguage) => - getCodeQLBundleDownloadURL( - bundleTagName, + getCodeQLAssetDownloadURL( + action, apiDetails, + bundleTagName, getCodeQLBundleName(compressionMethod, platform, language), - logger, ); const combinedBundleURL = await resolveBundleURL(); diff --git a/src/setup/repository.ts b/src/setup/repository.ts index c58470630b..53ca44f3e2 100644 --- a/src/setup/repository.ts +++ b/src/setup/repository.ts @@ -1,6 +1,8 @@ import { ActionState } from "../action-common"; import { isRunningLocalAction } from "../actions-util"; +import * as api from "../api-client"; import { ActionsEnvVars } from "../environment"; +import { GITHUB_DOTCOM_URL } from "../util"; /** The NWO of the standard CodeQL Action repository. */ export const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action"; @@ -28,3 +30,100 @@ export function getCodeQLActionRepository( return action.env.getRequired(ActionsEnvVars.GITHUB_ACTION_REPOSITORY); } + +/**A download source is a pair of strings. */ +export type DownloadSource = [string, string]; + +/** GitHub.com, and the canonical Action. */ +export const DEFAULT_DOWNLOAD_SOURCE: DownloadSource = [ + GITHUB_DOTCOM_URL, + CODEQL_DEFAULT_ACTION_REPOSITORY, +]; + +/** Decides if the two provided download sources are the same. */ +const isSameDownloadSource = ( + [srcUrl, srcRepo]: DownloadSource, + [otherUrl, otherRepo]: DownloadSource, +) => { + return srcUrl === otherUrl && srcRepo === otherRepo; +}; + +/** A download URL is represented as a string. */ +export type DownloadURL = string; + +/** + * Tries to find a download URL for `assetName` in a release tagged with `tagName`. + * + * Depending on where and how the CodeQL Action is running, we may be using different `apiDetails` and there may be + * different options for where to source CodeQL releases from. + * + * This function either returns the download URL for the asset for the first release we find, or + * defaults to the assumed download URL for the asset on the default CodeQL Action repository on GitHub.com. + * In the latter case, this function does not guarantee that the asset actually exists. + * + * @param action The Action state. + * @param apiDetails The details of the GitHub API in use. + * @param tagName The name of the release tag we want to obtain the asset from. + * @param assetName The name of the asset we should look for in the release. + * @param [assetKind="CodeQL bundle"] The kind of asset we are looking for to show in log messages. + * @returns A URL that we can use to download the asset. + */ +export async function getCodeQLAssetDownloadURL( + action: ActionState<["ReadOnlyEnv", "Logger"]>, + apiDetails: api.GitHubApiDetails, + tagName: string, + assetName: string, + assetKind: "CodeQL bundle" = "CodeQL bundle", +): Promise { + const codeQLActionRepository = getCodeQLActionRepository(action); + + const potentialDownloadSources: Array<[string, string]> = [ + // This GitHub instance, and this Action. + [apiDetails.url, codeQLActionRepository], + // This GitHub instance, and the canonical Action. + [apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY], + // GitHub.com, and the canonical Action. + DEFAULT_DOWNLOAD_SOURCE, + ]; + + // We now filter out any duplicates. + // Duplicates will happen either because the GitHub instance is GitHub.com, or because the Action is not a fork. + const uniqueDownloadSources = potentialDownloadSources.filter( + (source, index, self) => { + return !self + .slice(0, index) + .some((other) => isSameDownloadSource(source, other)); + }, + ); + + for (const [apiURL, repository] of uniqueDownloadSources) { + // If we've reached the final case, short-circuit the API check since we know the bundle exists and is public. + if (isSameDownloadSource(DEFAULT_DOWNLOAD_SOURCE, [apiURL, repository])) { + break; + } + + const [repositoryOwner, repositoryName] = repository.split("/"); + try { + const release = await api.getApiClient().rest.repos.getReleaseByTag({ + owner: repositoryOwner, + repo: repositoryName, + tag: tagName, + }); + + for (const asset of release.data.assets) { + if (asset.name === assetName) { + action.logger.info( + `Found ${assetKind} ${assetName} in ${repository} on ${apiURL} with URL ${asset.url}.`, + ); + return asset.url; + } + } + } catch (e) { + action.logger.info( + `Looked for ${assetKind} ${assetName} in ${repository} on ${apiURL} but got error ${e}.`, + ); + } + } + + return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${assetName}`; +} From 35f12ded428e57cae1cfa0e0c84b789eb44a4fda Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 6 Oct 2026 16:10:21 +0100 Subject: [PATCH 5/9] Add utility for assertions on sinon stubs with better output by default --- src/testing-utils.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index df81f51bb4..2b188857a9 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -163,6 +163,34 @@ export function setupTests(testFn: TestFn) { }); } +export interface AvaSinonAssertions { + /** + * Asserts that `stub` must have been called once with at least the `expected` arguments. + */ + onceWith: ( + stub: sinon.SinonStub, + ...expected: sinon.MatchPartialArguments + ) => boolean; +} + +/** Adds additional assertions for use with `sinon` to `t`. */ +export function addSinonAssertions( + t: ExecutionContext, +): ExecutionContext & AvaSinonAssertions { + return { + ...t, + onceWith: (stub, ...expected) => { + if (stub.callCount > 0) { + // Reduce the arguments of the first call to the same number that was provided, + // so that extra arguments don't lead to a failure. + return t.deepEqual(stub.args[0].slice(0, expected.length), expected); + } else { + return t.fail("The stub wasn't called."); + } + }, + }; +} + /** * Declare a reusable test implementation, with better type safety than `test.macro`. */ From a4f9f6fc121605c2b77d8dd9d562eb148dba7c13 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 6 Oct 2026 16:11:27 +0100 Subject: [PATCH 6/9] Type the `assetKind` parameter --- src/setup/repository.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/setup/repository.ts b/src/setup/repository.ts index 53ca44f3e2..4101be6770 100644 --- a/src/setup/repository.ts +++ b/src/setup/repository.ts @@ -51,6 +51,9 @@ const isSameDownloadSource = ( /** A download URL is represented as a string. */ export type DownloadURL = string; +/** Enumerates log-friendly descriptions of different kinds of assets we might look for in releases. */ +export type ReleaseAssetKind = "CodeQL bundle" | "private registry proxy"; + /** * Tries to find a download URL for `assetName` in a release tagged with `tagName`. * @@ -73,7 +76,7 @@ export async function getCodeQLAssetDownloadURL( apiDetails: api.GitHubApiDetails, tagName: string, assetName: string, - assetKind: "CodeQL bundle" = "CodeQL bundle", + assetKind: ReleaseAssetKind = "CodeQL bundle", ): Promise { const codeQLActionRepository = getCodeQLActionRepository(action); From de99f0ee4b789bac85c60c23cf5617af3a93e08f Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 6 Oct 2026 16:30:20 +0100 Subject: [PATCH 7/9] Add `getDefaultDotComDownloadURL` --- lib/entry-points.js | 5 ++++- src/setup/repository.ts | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 71b6915a9c..49d38c393f 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -175616,6 +175616,9 @@ var DEFAULT_DOWNLOAD_SOURCE = [ var isSameDownloadSource = ([srcUrl, srcRepo], [otherUrl, otherRepo]) => { return srcUrl === otherUrl && srcRepo === otherRepo; }; +function getDefaultDotComDownloadURL(tagName, assetName) { + return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${assetName}`; +} async function getCodeQLAssetDownloadURL(action, apiDetails, tagName, assetName, assetKind = "CodeQL bundle") { const codeQLActionRepository = getCodeQLActionRepository(action); const potentialDownloadSources = [ @@ -175656,7 +175659,7 @@ async function getCodeQLAssetDownloadURL(action, apiDetails, tagName, assetName, ); } } - return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${assetName}`; + return getDefaultDotComDownloadURL(tagName, assetName); } // src/tar.ts diff --git a/src/setup/repository.ts b/src/setup/repository.ts index 4101be6770..3d760858e7 100644 --- a/src/setup/repository.ts +++ b/src/setup/repository.ts @@ -51,6 +51,14 @@ const isSameDownloadSource = ( /** A download URL is represented as a string. */ export type DownloadURL = string; +/** Gets the default GitHub.com download URL for `assetName` in a release tagged `tagName`. */ +export function getDefaultDotComDownloadURL( + tagName: string, + assetName: string, +): DownloadURL { + return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${assetName}`; +} + /** Enumerates log-friendly descriptions of different kinds of assets we might look for in releases. */ export type ReleaseAssetKind = "CodeQL bundle" | "private registry proxy"; @@ -128,5 +136,5 @@ export async function getCodeQLAssetDownloadURL( } } - return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${assetName}`; + return getDefaultDotComDownloadURL(tagName, assetName); } From 133ffebe62eccf0a18bf2fa6f884063a7fd6ff7d Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 6 Oct 2026 16:13:32 +0100 Subject: [PATCH 8/9] Use `getCodeQLAssetDownloadURL` for `start-proxy` --- lib/entry-points.js | 62 +++++--------- src/start-proxy-action.ts | 7 +- src/start-proxy.test.ts | 173 +++++++++++++++++++++++++------------- src/start-proxy.ts | 82 +++++------------- 4 files changed, 166 insertions(+), 158 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 49d38c393f..84c57c813b 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -187489,8 +187489,6 @@ async function sendFailedStatusReport(logger2, startedAt, language, unwrappedErr } } var UPDATEJOB_PROXY = "update-job-proxy"; -var UPDATEJOB_PROXY_VERSION = "v2.0.20250624110901"; -var UPDATEJOB_PROXY_URL_PREFIX = "https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.22.0/"; function isPAT(value) { return isAuthToken(value, [ GITHUB_PAT_CLASSIC_PATTERN, @@ -187608,21 +187606,11 @@ function getCredentials(logger2, registrySecrets, registriesCredentials, languag function getProxyPackage(platform2) { return `${UPDATEJOB_PROXY}-${platform2}.tar.gz`; } -function getFallbackUrl(proxyPackage) { - return `${UPDATEJOB_PROXY_URL_PREFIX}${proxyPackage}`; -} -async function getReleaseByVersion(version) { - return getApiClient().rest.repos.getReleaseByTag({ - owner: "github", - repo: "codeql-action", - tag: version - }); -} async function getCliVersionFromFeatures(features) { const gitHubVersion = await getGitHubVersion(); return await features.getEnabledDefaultCliVersions(gitHubVersion.type); } -async function getDownloadUrl(action) { +async function getDownloadUrl(action, apiDetails) { let platform2 = getBundlePlatform(action.platform, action.arch); if (platform2 === void 0) { action.logger.warning( @@ -187639,33 +187627,25 @@ async function getDownloadUrl(action) { cliVersion, tagName: bundleVersion }; - const cliRelease = await getReleaseByVersion(versionInfo.tagName); - for (const asset of cliRelease.data.assets) { - if (asset.name === proxyPackage) { - action.logger.info( - `Found '${proxyPackage}' in release '${versionInfo.tagName}' at '${asset.url}'` - ); - return { - url: asset.url, - // The `update-job-proxy` doesn't have a version as such. Since we now bundle it - // with CodeQL CLI bundle releases, we use the corresponding CLI version to - // differentiate between (potentially) different versions of `update-job-proxy`. - version: versionInfo.cliVersion - }; - } - } + const proxyAssetUrl = await getCodeQLAssetDownloadURL( + action, + apiDetails, + versionInfo.tagName, + proxyPackage, + "private registry proxy" + ); + return { + url: proxyAssetUrl, + // The `update-job-proxy` doesn't have a version as such. Since we now bundle it + // with CodeQL CLI bundle releases, we use the corresponding CLI version to + // differentiate between (potentially) different versions of `update-job-proxy`. + version: versionInfo.cliVersion + }; } catch (ex) { - action.logger.warning( + throw new Error( `Failed to retrieve information about the linked release: ${getErrorMessage(ex)}` ); } - action.logger.info( - `Did not find '${proxyPackage}' in the linked release, falling back to hard-coded version.` - ); - return { - url: getFallbackUrl(proxyPackage), - version: UPDATEJOB_PROXY_VERSION - }; } async function downloadProxy(logger2, url2, authorization) { try { @@ -187708,13 +187688,12 @@ async function cacheProxy(logger2, source, filename, version) { function getProxyFilename() { return process.platform === "win32" ? `${UPDATEJOB_PROXY}.exe` : UPDATEJOB_PROXY; } -async function getProxyBinaryPath(action) { +async function getProxyBinaryPath(action, apiDetails) { const logger2 = action.logger; const proxyFileName = getProxyFilename(); - const proxyInfo = await getDownloadUrl(action); + const proxyInfo = await getDownloadUrl(action, apiDetails); let proxyBin = toolcache4.find(proxyFileName, proxyInfo.version); if (!proxyBin) { - const apiDetails = getApiDetails(); const authorization = getAuthorizationHeaderFor( logger2, apiDetails, @@ -188091,7 +188070,10 @@ async function run7(action) { all_credentials: credentials, ca }; - const proxyBin = await getProxyBinaryPath({ ...action, features }); + const proxyBin = await getProxyBinaryPath( + { ...action, features }, + getApiDetails(action.env) + ); const proxyInfo = await startProxy( proxyBin, proxyConfig, diff --git a/src/start-proxy-action.ts b/src/start-proxy-action.ts index d965a2dd6b..b9c7795ef1 100644 --- a/src/start-proxy-action.ts +++ b/src/start-proxy-action.ts @@ -5,7 +5,7 @@ import * as core from "@actions/core"; import { Action, ActionState, runInActions } from "./action-common"; import * as actionsUtil from "./actions-util"; -import { getGitHubVersion } from "./api-client"; +import { getApiDetails, getGitHubVersion } from "./api-client"; import { FeatureEnablement, initFeatures } from "./feature-flags"; import { BuiltInLanguage, parseBuiltInLanguage } from "./languages"; import { Logger } from "./logging"; @@ -98,7 +98,10 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) { }; // Start the Proxy - const proxyBin = await getProxyBinaryPath({ ...action, features }); + const proxyBin = await getProxyBinaryPath( + { ...action, features }, + getApiDetails(action.env), + ); const proxyInfo = await startProxy( proxyBin, proxyConfig, diff --git a/src/start-proxy.test.ts b/src/start-proxy.test.ts index bd24897136..b0bdd7bd33 100644 --- a/src/start-proxy.test.ts +++ b/src/start-proxy.test.ts @@ -6,17 +6,21 @@ import test, { ExecutionContext } from "ava"; import sinon from "sinon"; import { ActionState } from "./action-common"; +import * as actionsUtil from "./actions-util"; import * as apiClient from "./api-client"; import * as defaults from "./defaults.json"; +import { ActionsEnvVars } from "./environment"; import { setUpFeatureFlagTests } from "./feature-flags/testing-util"; import { UnvalidatedObject, validateSchema } from "./json"; import { makeFromSchema } from "./json/testing-util"; import { BuiltInLanguage } from "./languages"; import { getRunnerLogger, Logger } from "./logging"; import { BundlePlatform, getBundlePlatform } from "./platform"; +import * as repository from "./setup/repository"; import * as startProxyExports from "./start-proxy"; import * as statusReport from "./status-report"; import { + addSinonAssertions, assertNotLogged, checkExpectedLogMessages, createFeatures, @@ -24,6 +28,7 @@ import { makeMacro, makeTestToken, RecordingLogger, + SAMPLE_DOTCOM_API_DETAILS, setupTests, withRecordingLoggerAsync, } from "./testing-utils"; @@ -705,15 +710,23 @@ function mockGetApiClient(endpoints: any) { type ReleaseAssets = Array<{ name: string; url?: string }>; -function mockGetReleaseByTag(assets?: ReleaseAssets) { +function mockGetReleaseByTag(expectedTag?: string, assets?: ReleaseAssets) { const getReleaseByTag = assets === undefined ? sinon.stub().rejects() - : sinon.stub().resolves({ - status: 200, - data: { assets }, - headers: {}, - url: "GET /repos/:owner/:repo/releases/tags/:tag", + : sinon.stub().callsFake(({ tag }) => { + if (tag === expectedTag) { + return { + status: 200, + data: { assets }, + headers: {}, + url: "GET /repos/:owner/:repo/releases/tags/:tag", + }; + } else { + return { + status: 404, + }; + } }); return mockGetApiClient({ repos: { getReleaseByTag } }); @@ -751,25 +764,33 @@ test.serial( logger, features, }); - const info = await startProxyExports.getDownloadUrl(state); - t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION); - t.is( - info.url, - startProxyExports.getFallbackUrl( - startProxyExports.getProxyPackage(BundlePlatform.Linux64), - ), + // Stub this to always return `true` so we get the default repo for the tests. + sinon.stub(actionsUtil, "isRunningLocalAction").returns(true); + + const info = await startProxyExports.getDownloadUrl( + state, + SAMPLE_DOTCOM_API_DETAILS, ); t.true( logger.hasMessage(`Unsupported platform android on architecture ppc`), ); + + t.is(info.version, defaults.cliVersion); + t.is( + info.url, + repository.getDefaultDotComDownloadURL( + defaults.bundleVersion, + startProxyExports.getProxyPackage(BundlePlatform.Linux64), + ), + ); }); }, ); test.serial( - "getDownloadUrl returns fallback when `getReleaseByVersion` rejects", + "getDownloadUrl returns default bundle URL for linked version", async (t) => { const logger = new RecordingLogger(); mockGetReleaseByTag(); @@ -777,12 +798,20 @@ test.serial( await withTmpDir(async (tempDir) => { const features = mockOfflineFeatures(tempDir, logger); const state = initAllState({ logger, features }); - const info = await startProxyExports.getDownloadUrl(state); - t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION); + // Stub this to always return `true` so we get the default repo for the tests. + sinon.stub(actionsUtil, "isRunningLocalAction").returns(true); + + const info = await startProxyExports.getDownloadUrl( + state, + SAMPLE_DOTCOM_API_DETAILS, + ); + + t.is(info.version, defaults.cliVersion); t.is( info.url, - startProxyExports.getFallbackUrl( + repository.getDefaultDotComDownloadURL( + defaults.bundleVersion, startProxyExports.getProxyPackage(getTestPlatform(state)), ), ); @@ -791,7 +820,7 @@ test.serial( ); test.serial( - "getDownloadUrl returns fallback when there's no matching release asset", + "getDownloadUrl doesn't check that the default asset exists", async (t) => { const logger = new RecordingLogger(); const testAssets = [[], [{ name: "foo" }]]; @@ -800,14 +829,21 @@ test.serial( const features = mockOfflineFeatures(tempDir, logger); const state = initAllState({ logger, features }); + // Stub this to always return `true` so we get the default repo for the tests. + sinon.stub(actionsUtil, "isRunningLocalAction").returns(true); + for (const assets of testAssets) { - const stub = mockGetReleaseByTag(assets); - const info = await startProxyExports.getDownloadUrl(state); + const stub = mockGetReleaseByTag(defaults.bundleVersion, assets); + const info = await startProxyExports.getDownloadUrl( + state, + SAMPLE_DOTCOM_API_DETAILS, + ); - t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION); + t.is(info.version, defaults.cliVersion); t.is( info.url, - startProxyExports.getFallbackUrl( + repository.getDefaultDotComDownloadURL( + defaults.bundleVersion, startProxyExports.getProxyPackage(getTestPlatform(state)), ), ); @@ -828,11 +864,22 @@ test.serial("getDownloadUrl returns matching release asset", async (t) => { url: "url-we-want", }, ]; - mockGetReleaseByTag(assets); + const getReleaseByTag = mockGetReleaseByTag(defaults.bundleVersion, assets); await withTmpDir(async (tempDir) => { + state.env.set(ActionsEnvVars.RUNNER_TEMP, tempDir); const features = mockOfflineFeatures(tempDir, logger); - const info = await startProxyExports.getDownloadUrl({ ...state, features }); + + // Stub this to always return `false` so we force `getCodeQLAssetDownloadURL` to use the (mocked) API. + sinon.stub(actionsUtil, "isRunningLocalAction").returns(false); + state.env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "test/repo"); + + const info = await startProxyExports.getDownloadUrl( + { ...state, features }, + SAMPLE_DOTCOM_API_DETAILS, + ); + + t.is(getReleaseByTag.callCount, 1); t.is(info.version, defaults.cliVersion); t.is(info.url, "url-we-want"); @@ -965,9 +1012,13 @@ test.serial( const toolcachePath = "/path/to/proxy/dir"; sinon.stub(toolcache, "find").returns(toolcachePath); + // Stub this to always return `true` so we get the default repo for the tests. + sinon.stub(actionsUtil, "isRunningLocalAction").returns(true); + const features = mockOfflineFeatures(tempDir, logger); const path = await startProxyExports.getProxyBinaryPath( initAllState({ logger, features }), + SAMPLE_DOTCOM_API_DETAILS, ); t.assert(path); @@ -985,7 +1036,7 @@ test.serial( const logger = new RecordingLogger(); const state = initAllState({ logger }); const downloadUrl = "url-we-want"; - mockGetReleaseByTag([ + const getApiClient = mockGetReleaseByTag(defaults.bundleVersion, [ { name: startProxyExports.getProxyPackage(getTestPlatform(state)), url: downloadUrl, @@ -994,11 +1045,6 @@ test.serial( const toolcachePath = "/path/to/proxy/dir"; const find = sinon.stub(toolcache, "find").returns(""); - const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({ - auth: "", - url: "", - apiURL: "", - }); const getAuthorizationHeaderFor = sinon .stub(apiClient, "getAuthorizationHeaderFor") .returns(undefined); @@ -1012,15 +1058,27 @@ test.serial( .resolves(extractedPath); const cacheDir = sinon.stub(toolcache, "cacheDir").resolves(toolcachePath); - const path = await startProxyExports.getProxyBinaryPath({ - ...state, - features: createFeatures([]), - }); + // Stub this to always return `false` so we force `getCodeQLAssetDownloadURL` to use the (mocked) API. + sinon.stub(actionsUtil, "isRunningLocalAction").returns(false); + state.env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "test/repo"); + const path = await startProxyExports.getProxyBinaryPath( + { + ...state, + features: createFeatures([]), + }, + SAMPLE_DOTCOM_API_DETAILS, + ); + + t.is(getApiClient.callCount, 1); t.assert(find.calledOnce); - t.assert(getApiDetails.calledOnce); t.assert(getAuthorizationHeaderFor.calledOnce); - t.assert(downloadTool.calledOnceWith(downloadUrl)); + + const st = addSinonAssertions(t); + st.onceWith(downloadTool, downloadUrl); + st.onceWith(extractTar, archivePath); + st.onceWith(cacheDir, extractedPath); + t.assert(extractTar.calledOnceWith(archivePath)); t.assert(cacheDir.calledOnceWith(extractedPath)); t.assert(path); @@ -1030,7 +1088,7 @@ test.serial( ); checkExpectedLogMessages(t, logger.messages, [ - `Found '${startProxyExports.getProxyPackage(getTestPlatform(state))}' in release '${defaults.bundleVersion}' at '${downloadUrl}'`, + `Found private registry proxy ${startProxyExports.getProxyPackage(getTestPlatform(state))} in test/repo on https://github.com with URL ${downloadUrl}`, ]); }, ); @@ -1042,8 +1100,8 @@ test.serial( const state = initAllState({ logger }); const expectedTag = "codeql-bundle-v2.20.1"; const expectedParams = { - owner: "github", - repo: "codeql-action", + owner: "test", + repo: "repo", tag: expectedTag, }; const downloadUrl = "url-we-want"; @@ -1066,11 +1124,6 @@ test.serial( await withTmpDir(async (tempDir) => { const toolcachePath = "/path/to/proxy/dir"; const find = sinon.stub(toolcache, "find").returns(""); - const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({ - auth: "", - url: "", - apiURL: "", - }); const getAuthorizationHeaderFor = sinon .stub(apiClient, "getAuthorizationHeaderFor") .returns(undefined); @@ -1100,22 +1153,28 @@ test.serial( .resolves({ enabledVersions: [{ cliVersion: "2.20.1", tagName: expectedTag }], }); - const path = await startProxyExports.getProxyBinaryPath({ - ...state, - features, - }); - t.assert(getDefaultCliVersion.calledOnce); - sinon.assert.calledOnceWithMatch( - getReleaseByTag, - sinon.match(expectedParams), + // Stub this to always return `false` so we force `getCodeQLAssetDownloadURL` to use the (mocked) API. + sinon.stub(actionsUtil, "isRunningLocalAction").returns(false); + state.env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "test/repo"); + + const path = await startProxyExports.getProxyBinaryPath( + { + ...state, + features, + }, + SAMPLE_DOTCOM_API_DETAILS, ); + + t.assert(getDefaultCliVersion.calledOnce); + + const st = addSinonAssertions(t); + st.onceWith(getReleaseByTag, expectedParams); t.assert(find.calledOnce); - t.assert(getApiDetails.calledOnce); t.assert(getAuthorizationHeaderFor.calledOnce); - t.assert(downloadTool.calledOnceWith(downloadUrl)); - t.assert(extractTar.calledOnceWith(archivePath)); - t.assert(cacheDir.calledOnceWith(extractedPath)); + st.onceWith(downloadTool, downloadUrl); + st.onceWith(extractTar, archivePath); + st.onceWith(cacheDir, extractedPath); t.assert(path); t.is( @@ -1125,7 +1184,7 @@ test.serial( }); checkExpectedLogMessages(t, logger.messages, [ - `Found '${startProxyExports.getProxyPackage(getTestPlatform(state))}' in release '${expectedTag}' at '${downloadUrl}'`, + `Found private registry proxy ${startProxyExports.getProxyPackage(getTestPlatform(state))} in test/repo on https://github.com with URL url-we-want.`, ]); }, ); diff --git a/src/start-proxy.ts b/src/start-proxy.ts index 6221c9940f..9558b119ee 100644 --- a/src/start-proxy.ts +++ b/src/start-proxy.ts @@ -5,10 +5,9 @@ import * as toolcache from "@actions/tool-cache"; import { ActionState } from "./action-common"; import { - getApiClient, - getApiDetails, getAuthorizationHeaderFor, getGitHubVersion, + GitHubApiDetails, } from "./api-client"; import * as artifactScanner from "./artifact-scanner"; import { Config } from "./config-utils"; @@ -22,6 +21,7 @@ import * as json from "./json"; import { BuiltInLanguage } from "./languages"; import { Logger } from "./logging"; import { BundlePlatform, getBundlePlatform } from "./platform"; +import { getCodeQLAssetDownloadURL } from "./setup/repository"; import { Address, Registry, @@ -171,10 +171,8 @@ export async function sendFailedStatusReport( } } +/** The basename of the proxy artifacts. */ export const UPDATEJOB_PROXY = "update-job-proxy"; -export const UPDATEJOB_PROXY_VERSION = "v2.0.20250624110901"; -const UPDATEJOB_PROXY_URL_PREFIX = - "https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.22.0/"; function isPAT(value: string) { return artifactScanner.isAuthToken(value, [ @@ -379,30 +377,6 @@ export function getProxyPackage(platform: BundlePlatform): string { return `${UPDATEJOB_PROXY}-${platform}.tar.gz`; } -/** - * Gets the fallback URL for downloading the proxy release asset. - * - * @param proxyPackage The asset name. - * @returns The full URL to download the specified asset from the fallback release. - */ -export function getFallbackUrl(proxyPackage: string): string { - return `${UPDATEJOB_PROXY_URL_PREFIX}${proxyPackage}`; -} - -/** - * Uses the GitHub API to obtain information about the CodeQL CLI bundle release - * that is tagged by `version`. - * - * @returns The response from the GitHub API. - */ -async function getReleaseByVersion(version: string) { - return getApiClient().rest.repos.getReleaseByTag({ - owner: "github", - repo: "codeql-action", - tag: version, - }); -} - /** Uses `features` to determine the default CLI version. */ async function getCliVersionFromFeatures( features: FeatureEnablement, @@ -419,7 +393,8 @@ async function getCliVersionFromFeatures( * @returns Returns the download URL and version of the proxy package we plan to use. */ export async function getDownloadUrl( - action: ActionState<["Base", "Logger", "FeatureFlags"]>, + action: ActionState<["Base", "ReadOnlyEnv", "Logger", "FeatureFlags"]>, + apiDetails: GitHubApiDetails, ): Promise<{ url: string; version: string }> { // Default to linux64 if we don't recognise the platform+arch pair. // This maintains the behaviour we had before switching to `getBundlePlatform` here. @@ -448,37 +423,26 @@ export async function getDownloadUrl( }; // Try to retrieve information about the CLI bundle release identified by `versionInfo`. - const cliRelease = await getReleaseByVersion(versionInfo.tagName); + const proxyAssetUrl = await getCodeQLAssetDownloadURL( + action, + apiDetails, + versionInfo.tagName, + proxyPackage, + "private registry proxy", + ); - // Search the release's assets to find the one we are looking for. - for (const asset of cliRelease.data.assets) { - if (asset.name === proxyPackage) { - action.logger.info( - `Found '${proxyPackage}' in release '${versionInfo.tagName}' at '${asset.url}'`, - ); - return { - url: asset.url, - // The `update-job-proxy` doesn't have a version as such. Since we now bundle it - // with CodeQL CLI bundle releases, we use the corresponding CLI version to - // differentiate between (potentially) different versions of `update-job-proxy`. - version: versionInfo.cliVersion, - }; - } - } + return { + url: proxyAssetUrl, + // The `update-job-proxy` doesn't have a version as such. Since we now bundle it + // with CodeQL CLI bundle releases, we use the corresponding CLI version to + // differentiate between (potentially) different versions of `update-job-proxy`. + version: versionInfo.cliVersion, + }; } catch (ex) { - action.logger.warning( + throw new Error( `Failed to retrieve information about the linked release: ${getErrorMessage(ex)}`, ); } - - // Fallback to the hard-coded URL. - action.logger.info( - `Did not find '${proxyPackage}' in the linked release, falling back to hard-coded version.`, - ); - return { - url: getFallbackUrl(proxyPackage), - version: UPDATEJOB_PROXY_VERSION, - }; } /** @@ -569,15 +533,15 @@ export function getProxyFilename() { * @returns The path to the proxy binary. */ export async function getProxyBinaryPath( - action: ActionState<["Base", "Logger", "FeatureFlags"]>, + action: ActionState<["Base", "ReadOnlyEnv", "Logger", "FeatureFlags"]>, + apiDetails: GitHubApiDetails, ): Promise { const logger = action.logger; const proxyFileName = getProxyFilename(); - const proxyInfo = await getDownloadUrl(action); + const proxyInfo = await getDownloadUrl(action, apiDetails); let proxyBin = toolcache.find(proxyFileName, proxyInfo.version); if (!proxyBin) { - const apiDetails = getApiDetails(); const authorization = getAuthorizationHeaderFor( logger, apiDetails, From f9a9962aa30c39b96d0879722aeb8c3fb5bb9bbf Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 6 Oct 2026 17:26:09 +0100 Subject: [PATCH 9/9] Fix `onceWith` not asserting the `once` part --- src/testing-utils.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 2b188857a9..4e8d175200 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -179,13 +179,25 @@ export function addSinonAssertions( ): ExecutionContext & AvaSinonAssertions { return { ...t, - onceWith: (stub, ...expected) => { - if (stub.callCount > 0) { - // Reduce the arguments of the first call to the same number that was provided, - // so that extra arguments don't lead to a failure. - return t.deepEqual(stub.args[0].slice(0, expected.length), expected); + onceWith: ( + stub: sinon.SinonStub, + ...expected: sinon.MatchPartialArguments + ) => { + // Fail if the stub hasn't been called at all so that we can safely compare + // the arguments of the first call in the else branch. + if (stub.callCount === 0) { + t.fail("The stub wasn't called."); } else { - return t.fail("The stub wasn't called."); + // Reduce the arguments of each to the same number that was provided, + // so that extra arguments don't lead to a failure. + const actual = stub.args.map>( + (args: TArgs) => + args.slice( + 0, + expected.length, + ) as sinon.MatchPartialArguments, + ); + return t.deepEqual(actual, [expected]); } }, };