From 1f9caf0118e0ac28ca8795236cb9d8db9c5c4658 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 28 Jul 2026 18:37:01 +0100 Subject: [PATCH 01/19] Refactor `jobRunUuid` init into a function Use in `init` and `setup-codeql` actions --- lib/entry-points.js | 102 +++++++++++++++++++------------------ src/init-action.ts | 6 +-- src/setup-codeql-action.ts | 7 ++- src/status-report.ts | 13 +++++ 4 files changed, 70 insertions(+), 58 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 46c44a8183..287d8a127e 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -145728,6 +145728,50 @@ function formatDuration(durationMs) { var os3 = __toESM(require("os")); var core7 = __toESM(require_core()); +// node_modules/uuid/dist-node/stringify.js +var byteToHex = []; +for (let i = 0; i < 256; ++i) { + byteToHex.push((i + 256).toString(16).slice(1)); +} +function unsafeStringify(arr, offset = 0) { + return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); +} + +// node_modules/uuid/dist-node/rng.js +var rnds8 = new Uint8Array(16); +function rng() { + return crypto.getRandomValues(rnds8); +} + +// node_modules/uuid/dist-node/v4.js +function v4(options, buf, offset) { + if (!buf && !options && crypto.randomUUID) { + return crypto.randomUUID(); + } + return _v4(options, buf, offset); +} +function _v4(options, buf, offset) { + options = options || {}; + const rnds = options.random ?? options.rng?.() ?? rng(); + if (rnds.length < 16) { + throw new Error("Random bytes length must be >= 16"); + } + rnds[6] = rnds[6] & 15 | 64; + rnds[8] = rnds[8] & 63 | 128; + if (buf) { + offset = offset || 0; + if (offset < 0 || offset + 16 > buf.length) { + throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`); + } + for (let i = 0; i < 16; ++i) { + buf[offset + i] = rnds[i]; + } + return buf; + } + return unsafeStringify(rnds); +} +var v4_default = v4; + // src/api-client.ts var core5 = __toESM(require_core()); var githubUtils = __toESM(require_utils4()); @@ -146346,6 +146390,12 @@ function getDisplayActionName(actionName) { } return actionName; } +function getJobUUID(logger) { + const jobRunUuid = v4_default(); + logger.info(`Job run UUID is ${jobRunUuid}.`); + core7.exportVariable("JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid); + return jobRunUuid; +} function isFirstPartyAnalysis(actionName) { if (actionName !== "upload-sarif" /* UploadSarif */) { return true; @@ -150068,50 +150118,6 @@ var toolcache3 = __toESM(require_tool_cache()); var import_fast_deep_equal = __toESM(require_fast_deep_equal()); var semver9 = __toESM(require_semver2()); -// node_modules/uuid/dist-node/stringify.js -var byteToHex = []; -for (let i = 0; i < 256; ++i) { - byteToHex.push((i + 256).toString(16).slice(1)); -} -function unsafeStringify(arr, offset = 0) { - return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); -} - -// node_modules/uuid/dist-node/rng.js -var rnds8 = new Uint8Array(16); -function rng() { - return crypto.getRandomValues(rnds8); -} - -// node_modules/uuid/dist-node/v4.js -function v4(options, buf, offset) { - if (!buf && !options && crypto.randomUUID) { - return crypto.randomUUID(); - } - return _v4(options, buf, offset); -} -function _v4(options, buf, offset) { - options = options || {}; - const rnds = options.random ?? options.rng?.() ?? rng(); - if (rnds.length < 16) { - throw new Error("Random bytes length must be >= 16"); - } - rnds[6] = rnds[6] & 15 | 64; - rnds[8] = rnds[8] & 63 | 128; - if (buf) { - offset = offset || 0; - if (offset < 0 || offset + 16 > buf.length) { - throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`); - } - for (let i = 0; i < 16; ++i) { - buf[offset + i] = rnds[i]; - } - return buf; - } - return unsafeStringify(rnds); -} -var v4_default = v4; - // src/overlay/caching.ts var fs10 = __toESM(require("fs")); var actionsCache3 = __toESM(require_cache4()); @@ -160751,9 +160757,7 @@ async function run3(actionState) { logger ); const repositoryProperties = repositoryPropertiesResult.orElse({}); - const jobRunUuid = v4_default(); - logger.info(`Job run UUID is ${jobRunUuid}.`); - core21.exportVariable("JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid); + getJobUUID(logger); core21.exportVariable("CODEQL_ACTION_INIT_HAS_RUN" /* INIT_ACTION_HAS_RUN */, "true"); sourceRoot = path24.resolve( getRequiredEnvParam("GITHUB_WORKSPACE"), @@ -161751,9 +161755,7 @@ async function run6(actionState) { ); const repositoryProperties = repositoryPropertiesResult.orElse({}); const actionStateWithFeatures = { ...actionState, features }; - const jobRunUuid = v4_default(); - logger.info(`Job run UUID is ${jobRunUuid}.`); - core24.exportVariable("JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid); + getJobUUID(logger); const statusReportBase = await createStatusReportBase( "setup-codeql" /* SetupCodeQL */, "starting", diff --git a/src/init-action.ts b/src/init-action.ts index 4b52ba6ec6..a2ae0918be 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -4,7 +4,6 @@ import * as path from "path"; import * as core from "@actions/core"; import * as io from "@actions/io"; import * as semver from "semver"; -import { v4 as uuidV4 } from "uuid"; import { Action, ActionState, runInActions } from "./action-common"; import { @@ -69,6 +68,7 @@ import { createInitWithConfigStatusReport, createStatusReportBase, getActionsStatus, + getJobUUID, sendStatusReport, } from "./status-report"; import { ToolsDownloadStatusReport } from "./tools-download"; @@ -256,9 +256,7 @@ async function run( const repositoryProperties = repositoryPropertiesResult.orElse({}); // Create a unique identifier for this run. - const jobRunUuid = uuidV4(); - logger.info(`Job run UUID is ${jobRunUuid}.`); - core.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid); + getJobUUID(logger); core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true"); diff --git a/src/setup-codeql-action.ts b/src/setup-codeql-action.ts index b2a9e90f36..810931f672 100644 --- a/src/setup-codeql-action.ts +++ b/src/setup-codeql-action.ts @@ -1,5 +1,4 @@ import * as core from "@actions/core"; -import { v4 as uuidV4 } from "uuid"; import { Action, ActionState, runInActions } from "./action-common"; import { @@ -26,6 +25,7 @@ import { InitToolsDownloadFields, createStatusReportBase, getActionsStatus, + getJobUUID, sendStatusReport, } from "./status-report"; import { ToolsDownloadStatusReport } from "./tools-download"; @@ -140,9 +140,8 @@ async function run( const actionStateWithFeatures = { ...actionState, features }; - const jobRunUuid = uuidV4(); - logger.info(`Job run UUID is ${jobRunUuid}.`); - core.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid); + // Create a unique identifier for this run. + getJobUUID(logger); const statusReportBase = await createStatusReportBase( ActionName.SetupCodeQL, diff --git a/src/status-report.ts b/src/status-report.ts index d9d2a7ba4c..13cfe8ac39 100644 --- a/src/status-report.ts +++ b/src/status-report.ts @@ -1,6 +1,7 @@ import * as os from "os"; import * as core from "@actions/core"; +import { v4 as uuidV4 } from "uuid"; import { getWorkflowEventName, @@ -59,6 +60,18 @@ export function getDisplayActionName(actionName: ActionName): string { return actionName; } +/** + * Creates a UUIDv4 for the analysis and returns it. + * The generated UUID is also exported as an environment variable. + */ +export function getJobUUID(logger: Logger) { + const jobRunUuid = uuidV4(); + logger.info(`Job run UUID is ${jobRunUuid}.`); + + core.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid); + return jobRunUuid; +} + /** * @returns a boolean indicating whether the analysis is considered to be first party. * From c7ae51bb2daea524f6967b00fec6cceaa7b607b5 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 28 Jul 2026 18:41:41 +0100 Subject: [PATCH 02/19] Make `ActionState` available and add test --- src/init-action.ts | 2 +- src/setup-codeql-action.ts | 4 ++-- src/status-report.test.ts | 9 +++++++++ src/status-report.ts | 5 +++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/init-action.ts b/src/init-action.ts index a2ae0918be..f1c3916318 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -256,7 +256,7 @@ async function run( const repositoryProperties = repositoryPropertiesResult.orElse({}); // Create a unique identifier for this run. - getJobUUID(logger); + getJobUUID(actionState); core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true"); diff --git a/src/setup-codeql-action.ts b/src/setup-codeql-action.ts index 810931f672..d2f8c6104b 100644 --- a/src/setup-codeql-action.ts +++ b/src/setup-codeql-action.ts @@ -95,7 +95,7 @@ async function sendCompletedStatusReport( /** The main behaviour of this action. */ async function run( - actionState: ActionState<["Base", "Logger", "Actions"]>, + actionState: ActionState<["Base", "Logger", "Env", "Actions"]>, ): Promise { // To capture errors appropriately, keep as much code within the try-catch as // possible, and only use safe functions outside. @@ -141,7 +141,7 @@ async function run( const actionStateWithFeatures = { ...actionState, features }; // Create a unique identifier for this run. - getJobUUID(logger); + getJobUUID(actionState); const statusReportBase = await createStatusReportBase( ActionName.SetupCodeQL, diff --git a/src/status-report.test.ts b/src/status-report.test.ts index 9086dd34ef..0d8fe8108e 100644 --- a/src/status-report.test.ts +++ b/src/status-report.test.ts @@ -1,5 +1,6 @@ import test from "ava"; import * as sinon from "sinon"; +import * as uuid from "uuid"; import * as actionsUtil from "./actions-util"; import { Config } from "./config-utils"; @@ -12,6 +13,7 @@ import { createInitWithConfigStatusReport, createStatusReportBase, getActionsStatus, + getJobUUID, InitStatusReport, InitWithConfigStatusReport, } from "./status-report"; @@ -20,11 +22,18 @@ import { setupActionsVars, createTestConfig, makeMacro, + callee, } from "./testing-utils"; import { BuildMode, ConfigurationError, withTmpDir, wrapError } from "./util"; setupTests(test); +test("getJobUUID - generates valid UUIDs", async (t) => { + await callee(getJobUUID) + .withArgs() + .passes((val) => t.true(uuid.validate(val))); +}); + function setupEnvironmentAndStub(tmpDir: string) { setupActionsVars(tmpDir, tmpDir, { GITHUB_EVENT_NAME: "dynamic", diff --git a/src/status-report.ts b/src/status-report.ts index 13cfe8ac39..08cb05ff93 100644 --- a/src/status-report.ts +++ b/src/status-report.ts @@ -3,6 +3,7 @@ import * as os from "os"; import * as core from "@actions/core"; import { v4 as uuidV4 } from "uuid"; +import type { ActionState } from "./action-common"; import { getWorkflowEventName, getOptionalInput, @@ -64,9 +65,9 @@ export function getDisplayActionName(actionName: ActionName): string { * Creates a UUIDv4 for the analysis and returns it. * The generated UUID is also exported as an environment variable. */ -export function getJobUUID(logger: Logger) { +export function getJobUUID(action: ActionState<["Logger", "ReadOnlyEnv"]>) { const jobRunUuid = uuidV4(); - logger.info(`Job run UUID is ${jobRunUuid}.`); + action.logger.info(`Job run UUID is ${jobRunUuid}.`); core.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid); return jobRunUuid; From 049af32c592249a000289bd518b3592923901db3 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 28 Jul 2026 18:47:23 +0100 Subject: [PATCH 03/19] Allow `getJobUUID` to retrieve the UUID from the environment --- lib/entry-points.js | 38 ++++++++++++++++++++++++++------------ src/status-report.test.ts | 12 ++++++++++++ src/status-report.ts | 18 ++++++++++++++---- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 287d8a127e..ade2488356 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -30477,7 +30477,7 @@ var require_validator = __commonJS({ Validator3.prototype.getSchema = function getSchema(urn) { return this.schemas[urn]; }; - Validator3.prototype.validate = function validate(instance, schema, options, ctx) { + Validator3.prototype.validate = function validate2(instance, schema, options, ctx) { if (typeof schema !== "boolean" && typeof schema !== "object" || schema === null) { throw new SchemaError("Expected `schema` to be an object or boolean"); } @@ -144595,24 +144595,24 @@ function isNumber(value) { function isStringOrUndefined(value) { return value === void 0 || isString(value); } -function defaultCheck(validate) { - return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) }); +function defaultCheck(validate2) { + return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate2(arg) }); } -function makeValidator(validate, required = true) { +function makeValidator(validate2, required = true) { return { - validate, - check: defaultCheck(validate), + validate: validate2, + check: defaultCheck(validate2), required }; } var string = makeValidator(isString); var number = makeValidator(isNumber); function array(validator) { - const validate = (val) => { + const validate2 = (val) => { return isArray(val) && val.every((e) => validator.validate(e)); }; return { - validate, + validate: validate2, check: (val, opts, path29) => { const result = successfulCheckSchema(); if (!isArray(val)) { @@ -145728,6 +145728,15 @@ function formatDuration(durationMs) { var os3 = __toESM(require("os")); var core7 = __toESM(require_core()); +// node_modules/uuid/dist-node/regex.js +var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i; + +// node_modules/uuid/dist-node/validate.js +function validate(uuid) { + return typeof uuid === "string" && regex_default.test(uuid); +} +var validate_default = validate; + // node_modules/uuid/dist-node/stringify.js var byteToHex = []; for (let i = 0; i < 256; ++i) { @@ -146390,9 +146399,14 @@ function getDisplayActionName(actionName) { } return actionName; } -function getJobUUID(logger) { +function getJobUUID(action) { + const existingJobRunUuid = action.env.getOptional("JOB_RUN_UUID" /* JOB_RUN_UUID */); + if (existingJobRunUuid !== void 0 && validate_default(existingJobRunUuid)) { + action.logger.info(`Existing job run UUID is ${existingJobRunUuid}.`); + return existingJobRunUuid; + } const jobRunUuid = v4_default(); - logger.info(`Job run UUID is ${jobRunUuid}.`); + action.logger.info(`Job run UUID is ${jobRunUuid}.`); core7.exportVariable("JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid); return jobRunUuid; } @@ -160757,7 +160771,7 @@ async function run3(actionState) { logger ); const repositoryProperties = repositoryPropertiesResult.orElse({}); - getJobUUID(logger); + getJobUUID(actionState); core21.exportVariable("CODEQL_ACTION_INIT_HAS_RUN" /* INIT_ACTION_HAS_RUN */, "true"); sourceRoot = path24.resolve( getRequiredEnvParam("GITHUB_WORKSPACE"), @@ -161755,7 +161769,7 @@ async function run6(actionState) { ); const repositoryProperties = repositoryPropertiesResult.orElse({}); const actionStateWithFeatures = { ...actionState, features }; - getJobUUID(logger); + getJobUUID(actionState); const statusReportBase = await createStatusReportBase( "setup-codeql" /* SetupCodeQL */, "starting", diff --git a/src/status-report.test.ts b/src/status-report.test.ts index 0d8fe8108e..6f2c0164b1 100644 --- a/src/status-report.test.ts +++ b/src/status-report.test.ts @@ -31,9 +31,21 @@ setupTests(test); test("getJobUUID - generates valid UUIDs", async (t) => { await callee(getJobUUID) .withArgs() + .logs(t, "Job run UUID is ") .passes((val) => t.true(uuid.validate(val))); }); +test("getJobUUID - retrieves existing job UUIDs", async (t) => { + const existingJobUuid = uuid.v4(); + await callee(getJobUUID) + .withArgs() + .withEnv((env) => { + env.set(EnvVar.JOB_RUN_UUID, existingJobUuid); + }) + .logs(t, `Existing job run UUID is ${existingJobUuid}.`) + .passes(t.deepEqual, existingJobUuid); +}); + function setupEnvironmentAndStub(tmpDir: string) { setupActionsVars(tmpDir, tmpDir, { GITHUB_EVENT_NAME: "dynamic", diff --git a/src/status-report.ts b/src/status-report.ts index 08cb05ff93..ae1e0172ce 100644 --- a/src/status-report.ts +++ b/src/status-report.ts @@ -1,7 +1,7 @@ import * as os from "os"; import * as core from "@actions/core"; -import { v4 as uuidV4 } from "uuid"; +import * as uuid from "uuid"; import type { ActionState } from "./action-common"; import { @@ -62,11 +62,21 @@ export function getDisplayActionName(actionName: ActionName): string { } /** - * Creates a UUIDv4 for the analysis and returns it. - * The generated UUID is also exported as an environment variable. + * Either creates a UUIDv4 for the analysis or retrieves an existing one from the + * environment and returns it. + * If a new UUID is generated, it is also exported as an environment variable. */ export function getJobUUID(action: ActionState<["Logger", "ReadOnlyEnv"]>) { - const jobRunUuid = uuidV4(); + // Check if we already have a UUID for the analysis and return it if so. + const existingJobRunUuid = action.env.getOptional(EnvVar.JOB_RUN_UUID); + + if (existingJobRunUuid !== undefined && uuid.validate(existingJobRunUuid)) { + action.logger.info(`Existing job run UUID is ${existingJobRunUuid}.`); + return existingJobRunUuid; + } + + // Otherwise generate a new UUID. + const jobRunUuid = uuid.v4(); action.logger.info(`Job run UUID is ${jobRunUuid}.`); core.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid); From 766928d055114dfca04d7ad722bbc9fe4b928c3e Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 28 Jul 2026 18:50:56 +0100 Subject: [PATCH 04/19] Call `getJobUUID` in `start-proxy` The `start-proxy` step precedes `init` in Default Setup --- lib/entry-points.js | 5 +++++ src/start-proxy-action.ts | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index ade2488356..6eee65e05d 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -162645,6 +162645,11 @@ async function run7(startedAt) { let features; let language; try { + const action = { + logger, + env: new Env(process.env) + }; + getJobUUID(action); persistInputs(); const tempDir = getTemporaryDirectory(); const proxyLogFilePath = path28.resolve(tempDir, "proxy.log"); diff --git a/src/start-proxy-action.ts b/src/start-proxy-action.ts index 3e376ec64f..9da2069df9 100644 --- a/src/start-proxy-action.ts +++ b/src/start-proxy-action.ts @@ -3,8 +3,10 @@ import * as path from "path"; import * as core from "@actions/core"; +import { ActionState } from "./action-common"; import * as actionsUtil from "./actions-util"; import { getGitHubVersion } from "./api-client"; +import { Env } from "./environment"; import { FeatureEnablement, initFeatures } from "./feature-flags"; import { BuiltInLanguage, parseBuiltInLanguage } from "./languages"; import { getActionsLogger, Logger } from "./logging"; @@ -23,7 +25,11 @@ import { import { generateCertificateAuthority } from "./start-proxy/ca"; import { checkProxyEnvironment } from "./start-proxy/environment"; import { checkConnections } from "./start-proxy/reachability"; -import { ActionName, sendUnhandledErrorStatusReport } from "./status-report"; +import { + ActionName, + getJobUUID, + sendUnhandledErrorStatusReport, +} from "./status-report"; import * as util from "./util"; async function run(startedAt: Date) { @@ -35,6 +41,14 @@ async function run(startedAt: Date) { let language: BuiltInLanguage | undefined; try { + const action: ActionState<["Logger", "Env"]> = { + logger, + env: new Env(process.env), + }; + + // Create a unique identifier for this run. + getJobUUID(action); + // Make inputs accessible in the `post` step. actionsUtil.persistInputs(); From e9831f72a27e863fb32ccac5d65261114809b6fd Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 24 Jul 2026 17:40:26 +0100 Subject: [PATCH 05/19] Add `getRequiredInput` to `ActionsEnv` --- lib/entry-points.js | 2 +- src/actions-util.ts | 3 ++- src/testing-utils.ts | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 6eee65e05d..35a0b20922 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -145383,7 +145383,7 @@ var Failure = class { // src/actions-util.ts function getActionsEnv() { - return { getOptionalInput }; + return { getRequiredInput, getOptionalInput }; } var getRequiredInput = function(name) { const value = core3.getInput(name); diff --git a/src/actions-util.ts b/src/actions-util.ts index 5fd1ebc4fe..6731f8ef4e 100644 --- a/src/actions-util.ts +++ b/src/actions-util.ts @@ -27,6 +27,7 @@ declare const __CODEQL_ACTION_VERSION__: string; * global functions in tests. */ export interface ActionsEnv { + getRequiredInput: (name: string) => string; getOptionalInput: (name: string) => string | undefined; } @@ -34,7 +35,7 @@ export interface ActionsEnv { * Gets the real `ActionsEnv` used by production code. */ export function getActionsEnv(): ActionsEnv { - return { getOptionalInput }; + return { getRequiredInput, getOptionalInput }; } /** diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 03354653ac..e4fb9adf6f 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -187,6 +187,9 @@ export function getTestEnv(testEnv: NodeJS.ProcessEnv = {}): Env { */ export function getTestActionsEnv(): ActionsEnv { return { + getRequiredInput: (name) => { + throw new Error(`Input required and not supplied: ${name}`); + }, getOptionalInput: () => undefined, }; } From 60834a0cd9645a12daf4e2e76f667afb0f4cbed2 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 24 Jul 2026 22:02:49 +0100 Subject: [PATCH 06/19] Add `exportVariable` to `ActionsEnv` --- lib/entry-points.js | 14 +++++++++----- src/actions-util.ts | 7 ++++++- src/testing-utils.ts | 1 + 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 35a0b20922..7ad13ddc55 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -21559,7 +21559,7 @@ var require_core = __commonJS({ }; Object.defineProperty(exports2, "__esModule", { value: true }); exports2.platform = exports2.toPlatformPath = exports2.toWin32Path = exports2.toPosixPath = exports2.markdownSummary = exports2.summary = exports2.ExitCode = void 0; - exports2.exportVariable = exportVariable15; + exports2.exportVariable = exportVariable16; exports2.setSecret = setSecret2; exports2.addPath = addPath2; exports2.getInput = getInput2; @@ -21591,7 +21591,7 @@ var require_core = __commonJS({ ExitCode2[ExitCode2["Success"] = 0] = "Success"; ExitCode2[ExitCode2["Failure"] = 1] = "Failure"; })(ExitCode || (exports2.ExitCode = ExitCode = {})); - function exportVariable15(name, val) { + function exportVariable16(name, val) { const convertedVal = (0, utils_1.toCommandValue)(val); process.env[name] = convertedVal; const filePath = process.env["GITHUB_ENV"] || ""; @@ -121026,7 +121026,7 @@ var require_core3 = __commonJS({ ExitCode2[ExitCode2["Success"] = 0] = "Success"; ExitCode2[ExitCode2["Failure"] = 1] = "Failure"; })(ExitCode || (exports2.ExitCode = ExitCode = {})); - function exportVariable15(name, val) { + function exportVariable16(name, val) { const convertedVal = (0, utils_1.toCommandValue)(val); process.env[name] = convertedVal; const filePath = process.env["GITHUB_ENV"] || ""; @@ -121035,7 +121035,7 @@ var require_core3 = __commonJS({ } (0, command_1.issueCommand)("set-env", { name }, convertedVal); } - exports2.exportVariable = exportVariable15; + exports2.exportVariable = exportVariable16; function setSecret2(secret) { (0, command_1.issueCommand)("add-mask", {}, secret); } @@ -145383,7 +145383,11 @@ var Failure = class { // src/actions-util.ts function getActionsEnv() { - return { getRequiredInput, getOptionalInput }; + return { + getRequiredInput, + getOptionalInput, + exportVariable: core3.exportVariable + }; } var getRequiredInput = function(name) { const value = core3.getInput(name); diff --git a/src/actions-util.ts b/src/actions-util.ts index 6731f8ef4e..dd5124620d 100644 --- a/src/actions-util.ts +++ b/src/actions-util.ts @@ -29,13 +29,18 @@ declare const __CODEQL_ACTION_VERSION__: string; export interface ActionsEnv { getRequiredInput: (name: string) => string; getOptionalInput: (name: string) => string | undefined; + exportVariable: (name: string, value: string) => void; } /** * Gets the real `ActionsEnv` used by production code. */ export function getActionsEnv(): ActionsEnv { - return { getRequiredInput, getOptionalInput }; + return { + getRequiredInput, + getOptionalInput, + exportVariable: core.exportVariable, + }; } /** diff --git a/src/testing-utils.ts b/src/testing-utils.ts index e4fb9adf6f..4402458d82 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -191,6 +191,7 @@ export function getTestActionsEnv(): ActionsEnv { throw new Error(`Input required and not supplied: ${name}`); }, getOptionalInput: () => undefined, + exportVariable: () => {}, }; } From e28cbacfa115612a23d42a9425bfa0aa072443df Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 28 Jul 2026 19:13:35 +0100 Subject: [PATCH 07/19] Test that `getJobUUID` calls `exportVariable` --- lib/entry-points.js | 5 +++-- src/start-proxy-action.ts | 3 ++- src/status-report.test.ts | 14 +++++++++++++- src/status-report.ts | 6 ++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 7ad13ddc55..0bc32b8bfa 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -146411,7 +146411,7 @@ function getJobUUID(action) { } const jobRunUuid = v4_default(); action.logger.info(`Job run UUID is ${jobRunUuid}.`); - core7.exportVariable("JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid); + action.actions.exportVariable("JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid); return jobRunUuid; } function isFirstPartyAnalysis(actionName) { @@ -162651,7 +162651,8 @@ async function run7(startedAt) { try { const action = { logger, - env: new Env(process.env) + env: new Env(process.env), + actions: getActionsEnv() }; getJobUUID(action); persistInputs(); diff --git a/src/start-proxy-action.ts b/src/start-proxy-action.ts index 9da2069df9..ee587c04df 100644 --- a/src/start-proxy-action.ts +++ b/src/start-proxy-action.ts @@ -41,9 +41,10 @@ async function run(startedAt: Date) { let language: BuiltInLanguage | undefined; try { - const action: ActionState<["Logger", "Env"]> = { + const action: ActionState<["Logger", "Env", "Actions"]> = { logger, env: new Env(process.env), + actions: actionsUtil.getActionsEnv(), }; // Create a unique identifier for this run. diff --git a/src/status-report.test.ts b/src/status-report.test.ts index 6f2c0164b1..efe272faeb 100644 --- a/src/status-report.test.ts +++ b/src/status-report.test.ts @@ -29,10 +29,22 @@ import { BuildMode, ConfigurationError, withTmpDir, wrapError } from "./util"; setupTests(test); test("getJobUUID - generates valid UUIDs", async (t) => { + const exportVariableStub: sinon.SinonStub<[string, string], void> = + sinon.stub(); + await callee(getJobUUID) .withArgs() + .withActions((env) => { + env.exportVariable = exportVariableStub; + }) .logs(t, "Job run UUID is ") - .passes((val) => t.true(uuid.validate(val))); + .passes((val) => { + t.true(uuid.validate(val)); + + const calls = exportVariableStub.getCalls(); + t.is(calls.length, 1); + t.deepEqual(calls[0].args, [EnvVar.JOB_RUN_UUID, val]); + }); }); test("getJobUUID - retrieves existing job UUIDs", async (t) => { diff --git a/src/status-report.ts b/src/status-report.ts index ae1e0172ce..69cac8a05d 100644 --- a/src/status-report.ts +++ b/src/status-report.ts @@ -66,7 +66,9 @@ export function getDisplayActionName(actionName: ActionName): string { * environment and returns it. * If a new UUID is generated, it is also exported as an environment variable. */ -export function getJobUUID(action: ActionState<["Logger", "ReadOnlyEnv"]>) { +export function getJobUUID( + action: ActionState<["Logger", "ReadOnlyEnv", "Actions"]>, +) { // Check if we already have a UUID for the analysis and return it if so. const existingJobRunUuid = action.env.getOptional(EnvVar.JOB_RUN_UUID); @@ -79,7 +81,7 @@ export function getJobUUID(action: ActionState<["Logger", "ReadOnlyEnv"]>) { const jobRunUuid = uuid.v4(); action.logger.info(`Job run UUID is ${jobRunUuid}.`); - core.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid); + action.actions.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid); return jobRunUuid; } From 94a12eb6f6fa716ef39d1cd9c61231f08577b870 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 28 Jul 2026 19:14:57 +0100 Subject: [PATCH 08/19] Add a test for invalid values --- src/status-report.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/status-report.test.ts b/src/status-report.test.ts index efe272faeb..9d0c62efb1 100644 --- a/src/status-report.test.ts +++ b/src/status-report.test.ts @@ -58,6 +58,18 @@ test("getJobUUID - retrieves existing job UUIDs", async (t) => { .passes(t.deepEqual, existingJobUuid); }); +test("getJobUUID - doesn't retrieve invalid UUIDs", async (t) => { + const existingJobUuid = "not-a-uuid"; + await callee(getJobUUID) + .withArgs() + .withEnv((env) => { + env.set(EnvVar.JOB_RUN_UUID, existingJobUuid); + }) + .logs(t, `Job run UUID is `) + .notLogs(t, `Existing job run UUID is ${existingJobUuid}.`) + .passes(t.notDeepEqual, existingJobUuid); +}); + function setupEnvironmentAndStub(tmpDir: string) { setupActionsVars(tmpDir, tmpDir, { GITHUB_EVENT_NAME: "dynamic", From 2e251072b0a905f36699df95f6deabbff6a6ec5a Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 29 Jul 2026 10:19:14 +0100 Subject: [PATCH 09/19] Use `getEnv()` --- lib/entry-points.js | 2 +- src/start-proxy-action.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 0bc32b8bfa..0f35b11dfd 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -162651,7 +162651,7 @@ async function run7(startedAt) { try { const action = { logger, - env: new Env(process.env), + env: getEnv(), actions: getActionsEnv() }; getJobUUID(action); diff --git a/src/start-proxy-action.ts b/src/start-proxy-action.ts index ee587c04df..67f6d50177 100644 --- a/src/start-proxy-action.ts +++ b/src/start-proxy-action.ts @@ -6,7 +6,6 @@ import * as core from "@actions/core"; import { ActionState } from "./action-common"; import * as actionsUtil from "./actions-util"; import { getGitHubVersion } from "./api-client"; -import { Env } from "./environment"; import { FeatureEnablement, initFeatures } from "./feature-flags"; import { BuiltInLanguage, parseBuiltInLanguage } from "./languages"; import { getActionsLogger, Logger } from "./logging"; @@ -43,7 +42,7 @@ async function run(startedAt: Date) { try { const action: ActionState<["Logger", "Env", "Actions"]> = { logger, - env: new Env(process.env), + env: util.getEnv(), actions: actionsUtil.getActionsEnv(), }; From 42a3b947902ef5aef0cda3594c9e0cb60f8f4820 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 29 Jul 2026 11:58:44 +0100 Subject: [PATCH 10/19] Add `CODEQL_ACTION_` prefix to `JOB_RUN_UUID` --- .github/workflows/__job-run-uuid-sarif.yml | 4 ++-- lib/entry-points.js | 8 ++++---- pr-checks/checks/job-run-uuid-sarif.yml | 4 ++-- src/environment.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/__job-run-uuid-sarif.yml b/.github/workflows/__job-run-uuid-sarif.yml index cd47fb577e..429a694947 100644 --- a/.github/workflows/__job-run-uuid-sarif.yml +++ b/.github/workflows/__job-run-uuid-sarif.yml @@ -71,8 +71,8 @@ jobs: run: | cd "$RUNNER_TEMP/results" actual=$(jq -r '.runs[0].properties.jobRunUuid' javascript.sarif) - if [[ "$actual" != "$JOB_RUN_UUID" ]]; then - echo "Expected SARIF output to contain job run UUID '$JOB_RUN_UUID', but found '$actual'." + if [[ "$actual" != "$CODEQL_ACTION_JOB_RUN_UUID" ]]; then + echo "Expected SARIF output to contain job run UUID '$CODEQL_ACTION_JOB_RUN_UUID', but found '$actual'." exit 1 else echo "Found job run UUID '$actual'." diff --git a/lib/entry-points.js b/lib/entry-points.js index 0f35b11dfd..a2f6d22c52 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -146404,14 +146404,14 @@ function getDisplayActionName(actionName) { return actionName; } function getJobUUID(action) { - const existingJobRunUuid = action.env.getOptional("JOB_RUN_UUID" /* JOB_RUN_UUID */); + const existingJobRunUuid = action.env.getOptional("CODEQL_ACTION_JOB_RUN_UUID" /* JOB_RUN_UUID */); if (existingJobRunUuid !== void 0 && validate_default(existingJobRunUuid)) { action.logger.info(`Existing job run UUID is ${existingJobRunUuid}.`); return existingJobRunUuid; } const jobRunUuid = v4_default(); action.logger.info(`Job run UUID is ${jobRunUuid}.`); - action.actions.exportVariable("JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid); + action.actions.exportVariable("CODEQL_ACTION_JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid); return jobRunUuid; } function isFirstPartyAnalysis(actionName) { @@ -146468,7 +146468,7 @@ async function createStatusReportBase(actionName, status, actionStartedAt, confi try { const commitOid = getOptionalInput("sha") || process.env["GITHUB_SHA"] || ""; const ref = await getRef(); - const jobRunUUID = process.env["JOB_RUN_UUID" /* JOB_RUN_UUID */] || ""; + const jobRunUUID = process.env["CODEQL_ACTION_JOB_RUN_UUID" /* JOB_RUN_UUID */] || ""; const workflowRunID = getWorkflowRunID(); const workflowRunAttempt = getWorkflowRunAttempt(); const workflowName = process.env["GITHUB_WORKFLOW"] || ""; @@ -152008,7 +152008,7 @@ function applyAutobuildAzurePipelinesTimeoutFix() { ].join(" "); } async function getJobRunUuidSarifOptions() { - const jobRunUuid = process.env["JOB_RUN_UUID" /* JOB_RUN_UUID */]; + const jobRunUuid = process.env["CODEQL_ACTION_JOB_RUN_UUID" /* JOB_RUN_UUID */]; return jobRunUuid ? [`--sarif-run-property=jobRunUuid=${jobRunUuid}`] : []; } diff --git a/pr-checks/checks/job-run-uuid-sarif.yml b/pr-checks/checks/job-run-uuid-sarif.yml index dc1dd02d43..b86725d944 100644 --- a/pr-checks/checks/job-run-uuid-sarif.yml +++ b/pr-checks/checks/job-run-uuid-sarif.yml @@ -21,8 +21,8 @@ steps: run: | cd "$RUNNER_TEMP/results" actual=$(jq -r '.runs[0].properties.jobRunUuid' javascript.sarif) - if [[ "$actual" != "$JOB_RUN_UUID" ]]; then - echo "Expected SARIF output to contain job run UUID '$JOB_RUN_UUID', but found '$actual'." + if [[ "$actual" != "$CODEQL_ACTION_JOB_RUN_UUID" ]]; then + echo "Expected SARIF output to contain job run UUID '$CODEQL_ACTION_JOB_RUN_UUID', but found '$actual'." exit 1 else echo "Found job run UUID '$actual'." diff --git a/src/environment.ts b/src/environment.ts index 1b00ab7cfb..fea553d602 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -88,7 +88,7 @@ export enum EnvVar { LOG_VERSION_DEPRECATION = "CODEQL_ACTION_DID_LOG_VERSION_DEPRECATION", /** UUID representing the current job run. */ - JOB_RUN_UUID = "JOB_RUN_UUID", + JOB_RUN_UUID = "CODEQL_ACTION_JOB_RUN_UUID", /** Status for the entire job, submitted to the status report in `init-post` */ JOB_STATUS = "CODEQL_ACTION_JOB_STATUS", From 3ca82bb259b52fe4d0f27055fa58f0fb99694b80 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 29 Jul 2026 12:55:16 +0100 Subject: [PATCH 11/19] Change `withActions` to only allow mutations --- src/config/inputs.test.ts | 18 +++++------------- src/testing-utils.ts | 12 +++++------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/config/inputs.test.ts b/src/config/inputs.test.ts index a91dc258ea..851dd72e2f 100644 --- a/src/config/inputs.test.ts +++ b/src/config/inputs.test.ts @@ -1,7 +1,7 @@ import test from "ava"; import sinon from "sinon"; -import { getActionsEnv } from "../actions-util"; +import { ActionsEnv } from "../actions-util"; import { Feature } from "../feature-flags"; import { RepositoryPropertyName } from "../feature-flags/properties"; import { callee } from "../testing-utils"; @@ -22,32 +22,26 @@ const expectedRepositoryPropertyResult: ComputedInput = { value: "repo-property-input-value", }; -function stubGetToolsInput() { - const actions = getActionsEnv(); +function stubGetToolsInput(actions: ActionsEnv) { sinon .stub(actions, "getOptionalInput") .withArgs(InputName.Tools) .returns(expectedWorkflowResult.value); - return actions; } const workflowLogMessage = `Using ${InputName.Tools} input from workflow:`; test("getToolsInput - returns workflow input if available", async (t) => { - const actions = stubGetToolsInput(); - await callee(getToolsInput) - .withActions(actions) + .withActions(stubGetToolsInput) .withArgs({}) .logs(t, workflowLogMessage) .passes(t.deepEqual, expectedWorkflowResult); }); test("getToolsInput - returns repository property value if enforced", async (t) => { - const actions = stubGetToolsInput(); - const target = callee(getToolsInput) - .withActions(actions) + .withActions(stubGetToolsInput) .withArgs({ [RepositoryPropertyName.TOOLS]: `!${expectedRepositoryPropertyResult.value}`, }); @@ -65,10 +59,8 @@ test("getToolsInput - returns repository property value if enforced", async (t) }); test("getToolsInput - prefers workflow input", async (t) => { - const actions = stubGetToolsInput(); - const target = callee(getToolsInput) - .withActions(actions) + .withActions(stubGetToolsInput) .withArgs({ [RepositoryPropertyName.TOOLS]: expectedRepositoryPropertyResult.value, }); diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 4402458d82..553a775e93 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -228,7 +228,8 @@ type DelayedCheck< Fs extends ReadonlyArray, > = (env: Readonly>) => Promise; -export type ValueOrMutation = T | ((val: T) => void); +export type Mutation = (val: T) => void; +export type ValueOrMutation = T | Mutation; /** * Wraps a function that accepts an `ActionState` for testing in different environments. @@ -324,13 +325,10 @@ abstract class BaseEnvBuilder< return result; } - public withActions(arg: ValueOrMutation): this { + /** Applies `fn` to the `ActionsEnv`. */ + public withActions(fn: Mutation): this { const result = this.clone(); - if (typeof arg === "function") { - arg(result.state.actions); - } else { - result.state.actions = arg; - } + fn(result.state.actions); return result; } From 30c33c9286fa4a7c5325301b5a2aa0c5b67a51ec Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 29 Jul 2026 12:56:51 +0100 Subject: [PATCH 12/19] Make results of function call available to delayed checks --- src/testing-utils.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 553a775e93..d6fffb69b1 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -34,11 +34,14 @@ import { ActionName } from "./status-report"; import { DEFAULT_DEBUG_ARTIFACT_NAME, DEFAULT_DEBUG_DATABASE_NAME, + Failure, getEnv, GitHubVariant, GitHubVersion, HTTPError, resetCachedCodeQlVersion, + Result, + Success, } from "./util"; export const SAMPLE_DOTCOM_API_DETAILS = { @@ -226,7 +229,10 @@ type DelayedCheck< Args extends readonly any[], R, Fs extends ReadonlyArray, -> = (env: Readonly>) => Promise; +> = ( + env: Readonly>, + result: Result, ThrownError>, +) => Promise; export type Mutation = (val: T) => void; export type ValueOrMutation = T | Mutation; @@ -441,7 +447,7 @@ class CallableEnvBuilder< // Run other delayed checks. for (const delayedCheck of this.checks) { - await delayedCheck(this); + await delayedCheck(this, new Success(result)); } // Return the results of the function call and the main assertion. @@ -467,7 +473,7 @@ class CallableEnvBuilder< // Run other delayed checks. for (const delayedCheck of this.checks) { - await delayedCheck(this); + await delayedCheck(this, new Failure(error)); } // Return the error. From 36737508ece41f7da5ed9862108928b78f5c24ff Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 29 Jul 2026 13:00:31 +0100 Subject: [PATCH 13/19] Add `Env`-backed `ActionsEnv` implementation for tests --- src/testing-utils.ts | 66 +++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/src/testing-utils.ts b/src/testing-utils.ts index d6fffb69b1..94c8435218 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -185,17 +185,32 @@ export function getTestEnv(testEnv: NodeJS.ProcessEnv = {}): Env { return getEnv(testEnv); } +/** An implementation of `ActionsEnv` for use in tests. */ +class TestActionsEnv implements ActionsEnv { + constructor(private readonly env: Env) {} + + public clone(env: Env): this { + return Object.create(this, { env: { value: env } }) as this; + } + + public getRequiredInput(name: string): string { + throw new Error(`Input required and not supplied: ${name}`); + } + + public getOptionalInput(_name: string): string | undefined { + return undefined; + } + + public exportVariable(name: string, value: string): void { + this.env.set(name, value); + } +} + /** * Gets an `ActionsEnv` instance for use in tests. */ -export function getTestActionsEnv(): ActionsEnv { - return { - getRequiredInput: (name) => { - throw new Error(`Input required and not supplied: ${name}`); - }, - getOptionalInput: () => undefined, - exportVariable: () => {}, - }; +export function getTestActionsEnv(env: Env): TestActionsEnv { + return new TestActionsEnv(env); } /** For testing purposes, we make all available state features accessible in `TestEnv`. */ @@ -213,12 +228,13 @@ type AllState = [ export function initAllState( overrides?: Partial>, ): ActionState { + const env = getTestEnv(); return { name: ActionName.Init, startedAt: new Date(), logger: new RecordingLogger(), - env: getTestEnv(), - actions: getTestActionsEnv(), + env, + actions: getTestActionsEnv(env), apiClient: github.getOctokit("123"), features: createFeatures([]), ...overrides, @@ -247,6 +263,7 @@ abstract class BaseEnvBuilder< > { protected readonly fn: (state: ActionState, ...args: Args) => R; private logger: RecordingLogger; + private actions: TestActionsEnv; protected state: ActionState; protected checks: Array>; @@ -256,15 +273,26 @@ abstract class BaseEnvBuilder< ) { this.fn = fn; this.logger = new RecordingLogger(); - this.state = - cloneFrom !== undefined - ? ({ - ...cloneFrom.state, - env: cloneFrom.state.env.clone(), - actions: Object.create(cloneFrom.state.actions), - logger: this.logger, - } satisfies ActionState) - : initAllState({ logger: this.logger }); + + if (cloneFrom !== undefined) { + const env = cloneFrom.state.env.clone(); + this.actions = cloneFrom.actions.clone(env); + this.state = { + ...cloneFrom.state, + env, + actions: this.actions, + logger: this.logger, + } satisfies ActionState; + } else { + const env = getTestEnv(); + this.actions = getTestActionsEnv(env); + this.state = initAllState({ + logger: this.logger, + env, + actions: this.actions, + }); + } + this.checks = [...(cloneFrom?.checks ?? [])]; } From d2f5cbbe919141b077e54396de7bb0c31da73912 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 29 Jul 2026 13:22:46 +0100 Subject: [PATCH 14/19] Add `get` method to `ReadOnlyEnv` --- lib/entry-points.js | 4 ++++ src/environment.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/lib/entry-points.js b/lib/entry-points.js index a2f6d22c52..9a7fbaa8e2 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -141565,6 +141565,10 @@ var ReadOnlyEnv = class { clone() { return Object.create(this, { vars: { value: { ...this.vars } } }); } + /** Gets a copy of the underlying environment. */ + get() { + return { ...this.vars }; + } /** Tries to get the value for `name` and throws if there isn't one. */ getRequired(name) { return getRequiredEnvVar(this.vars, name); diff --git a/src/environment.ts b/src/environment.ts index fea553d602..d6ff20391a 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -270,6 +270,11 @@ export class ReadOnlyEnv { return Object.create(this, { vars: { value: { ...this.vars } } }) as this; } + /** Gets a copy of the underlying environment. */ + public get(): Record { + return { ...this.vars }; + } + /** Tries to get the value for `name` and throws if there isn't one. */ public getRequired(name: string): string { return getRequiredEnvVar(this.vars, name); From 0cebd1d28d761cf2fceb2a3a9ed79dff79ea5a8c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 29 Jul 2026 13:24:33 +0100 Subject: [PATCH 15/19] Add `hasEnv` delayed assertion and use for `getJobUUID` test --- src/status-report.test.ts | 15 +++++---------- src/testing-utils.ts | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/status-report.test.ts b/src/status-report.test.ts index 9d0c62efb1..17490a60cb 100644 --- a/src/status-report.test.ts +++ b/src/status-report.test.ts @@ -29,21 +29,16 @@ import { BuildMode, ConfigurationError, withTmpDir, wrapError } from "./util"; setupTests(test); test("getJobUUID - generates valid UUIDs", async (t) => { - const exportVariableStub: sinon.SinonStub<[string, string], void> = - sinon.stub(); - await callee(getJobUUID) .withArgs() - .withActions((env) => { - env.exportVariable = exportVariableStub; - }) .logs(t, "Job run UUID is ") + .hasEnv(t, (val) => { + return { + [EnvVar.JOB_RUN_UUID]: val, + }; + }) .passes((val) => { t.true(uuid.validate(val)); - - const calls = exportVariableStub.getCalls(); - t.is(calls.length, 1); - t.deepEqual(calls[0].args, [EnvVar.JOB_RUN_UUID, val]); }); }); diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 94c8435218..279459275d 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -378,6 +378,28 @@ abstract class BaseEnvBuilder< return result; } + /** + * Adds a delayed check that the environment variables returned by `fn` + * are present in the environment after the main assertion passes. + */ + public hasEnv( + t: ExecutionContext, + fn: ( + value: Awaited | undefined, + error: ThrownError | undefined, + ) => Record, + ): this { + const result = this.clone(); + result.checks.push(async (env, r) => { + const value = r.orElse(undefined); + const error = r.isFailure() ? r.value : undefined; + const expected = fn(value, error); + + t.like(env.getState().env.get(), expected); + }); + return result; + } + /** * Adds a delayed check that `messages` are not logged. The check will be * performed after the main assertion passes. From b411bbcd4ad96437e66f359abc5b628477549c83 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 29 Jul 2026 14:27:24 +0100 Subject: [PATCH 16/19] Move `getJobUUID` call into `runInActions` for `init` and `setup-codeql` --- lib/entry-points.js | 8 ++++---- src/action-common.ts | 10 ++++++++-- src/init-action.ts | 4 ---- src/setup-codeql-action.ts | 4 ---- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 9a7fbaa8e2..c56f671098 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -146700,13 +146700,15 @@ async function runInActions(action) { const env = getEnv(); const actionsEnv = getActionsEnv(); try { - await action.run({ + const actionState = { name: action.name, startedAt, logger, env, actions: actionsEnv - }); + }; + getJobUUID(actionState); + await action.run(actionState); } catch (error3) { core8.setFailed( `${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error3)}` @@ -160779,7 +160781,6 @@ async function run3(actionState) { logger ); const repositoryProperties = repositoryPropertiesResult.orElse({}); - getJobUUID(actionState); core21.exportVariable("CODEQL_ACTION_INIT_HAS_RUN" /* INIT_ACTION_HAS_RUN */, "true"); sourceRoot = path24.resolve( getRequiredEnvParam("GITHUB_WORKSPACE"), @@ -161777,7 +161778,6 @@ async function run6(actionState) { ); const repositoryProperties = repositoryPropertiesResult.orElse({}); const actionStateWithFeatures = { ...actionState, features }; - getJobUUID(actionState); const statusReportBase = await createStatusReportBase( "setup-codeql" /* SetupCodeQL */, "starting", diff --git a/src/action-common.ts b/src/action-common.ts index cbb1cd3422..be8629addf 100644 --- a/src/action-common.ts +++ b/src/action-common.ts @@ -8,6 +8,7 @@ import { getActionsLogger, Logger } from "./logging"; import { ActionName, getDisplayActionName, + getJobUUID, sendUnhandledErrorStatusReport, } from "./status-report"; import { getEnv, getErrorMessage } from "./util"; @@ -88,13 +89,18 @@ export async function runInActions(action: Action) { const actionsEnv = getActionsEnv(); try { - await action.run({ + const actionState = { name: action.name, startedAt, logger, env, actions: actionsEnv, - }); + }; + + // Create a unique identifier for this run. + getJobUUID(actionState); + + await action.run(actionState); } catch (error) { core.setFailed( `${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error)}`, diff --git a/src/init-action.ts b/src/init-action.ts index f1c3916318..00143df427 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -68,7 +68,6 @@ import { createInitWithConfigStatusReport, createStatusReportBase, getActionsStatus, - getJobUUID, sendStatusReport, } from "./status-report"; import { ToolsDownloadStatusReport } from "./tools-download"; @@ -255,9 +254,6 @@ async function run( ); const repositoryProperties = repositoryPropertiesResult.orElse({}); - // Create a unique identifier for this run. - getJobUUID(actionState); - core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true"); // path.resolve() respects the intended semantics of source-root. If diff --git a/src/setup-codeql-action.ts b/src/setup-codeql-action.ts index d2f8c6104b..7873449f9c 100644 --- a/src/setup-codeql-action.ts +++ b/src/setup-codeql-action.ts @@ -25,7 +25,6 @@ import { InitToolsDownloadFields, createStatusReportBase, getActionsStatus, - getJobUUID, sendStatusReport, } from "./status-report"; import { ToolsDownloadStatusReport } from "./tools-download"; @@ -140,9 +139,6 @@ async function run( const actionStateWithFeatures = { ...actionState, features }; - // Create a unique identifier for this run. - getJobUUID(actionState); - const statusReportBase = await createStatusReportBase( ActionName.SetupCodeQL, "starting", From ba46ff760e2acb42dc881f443ad2bb3cd9de9d28 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 29 Jul 2026 14:39:30 +0100 Subject: [PATCH 17/19] Add `transformTelemetryError` option to `Action` --- lib/entry-points.js | 8 +++++++- src/action-common.ts | 20 ++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index c56f671098..456ce1be7f 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -146713,7 +146713,13 @@ async function runInActions(action) { core8.setFailed( `${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error3)}` ); - await sendUnhandledErrorStatusReport(action.name, startedAt, error3, logger); + const statusReportError = action.transformTelemetryError !== void 0 ? action.transformTelemetryError(wrapError(error3)) : error3; + await sendUnhandledErrorStatusReport( + action.name, + startedAt, + statusReportError, + logger + ); } } diff --git a/src/action-common.ts b/src/action-common.ts index be8629addf..95323e7f2a 100644 --- a/src/action-common.ts +++ b/src/action-common.ts @@ -11,7 +11,7 @@ import { getJobUUID, sendUnhandledErrorStatusReport, } from "./status-report"; -import { getEnv, getErrorMessage } from "./util"; +import { getEnv, getErrorMessage, wrapError } from "./util"; /** Base state that is available to an Action on startup. */ export interface BaseState { @@ -79,6 +79,12 @@ export interface Action { name: ActionName; /** The entry point for the Action. */ run: ActionMain; + /** + * An optional function that transforms a caught error into a message suitable for + * inclusion in a status report. This is primarily intended for the `start-proxy` + * action to replace the thrown `Error`'s message with a safe one. + */ + transformTelemetryError?: (error: Error) => string; } /** A generic entry point that sets up the basic environment for the `action` and runs it. */ @@ -105,6 +111,16 @@ export async function runInActions(action: Action) { core.setFailed( `${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error)}`, ); - await sendUnhandledErrorStatusReport(action.name, startedAt, error, logger); + + const statusReportError = + action.transformTelemetryError !== undefined + ? action.transformTelemetryError(wrapError(error)) + : error; + await sendUnhandledErrorStatusReport( + action.name, + startedAt, + statusReportError, + logger, + ); } } From 8e6fdffc3205654e6d9f7e9a5976eaf55dee895b Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 29 Jul 2026 14:44:24 +0100 Subject: [PATCH 18/19] Use `runInActions` for `start-proxy` --- lib/entry-points.js | 38 +++++++++++-------------------- src/start-proxy-action.ts | 47 ++++++++++++--------------------------- 2 files changed, 27 insertions(+), 58 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 456ce1be7f..66e153b792 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -21567,7 +21567,7 @@ var require_core = __commonJS({ exports2.getBooleanInput = getBooleanInput; exports2.setOutput = setOutput7; exports2.setCommandEcho = setCommandEcho; - exports2.setFailed = setFailed13; + exports2.setFailed = setFailed12; exports2.isDebug = isDebug5; exports2.debug = debug6; exports2.error = error3; @@ -21651,7 +21651,7 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); function setCommandEcho(enabled) { (0, command_1.issue)("echo", enabled ? "on" : "off"); } - function setFailed13(message) { + function setFailed12(message) { process.exitCode = ExitCode.Failure; error3(message); } @@ -121094,11 +121094,11 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); (0, command_1.issue)("echo", enabled ? "on" : "off"); } exports2.setCommandEcho = setCommandEcho; - function setFailed13(message) { + function setFailed12(message) { process.exitCode = ExitCode.Failure; error3(message); } - exports2.setFailed = setFailed13; + exports2.setFailed = setFailed12; function isDebug5() { return process.env["RUNNER_DEBUG"] === "1"; } @@ -162654,17 +162654,12 @@ async function checkConnections(logger, proxy, backend) { } // src/start-proxy-action.ts -async function run7(startedAt) { - const logger = getActionsLogger(); +async function run7(action) { + const startedAt = action.startedAt; + const logger = action.logger; let features; let language; try { - const action = { - logger, - env: getEnv(), - actions: getActionsEnv() - }; - getJobUUID(action); persistInputs(); const tempDir = getTemporaryDirectory(); const proxyLogFilePath = path28.resolve(tempDir, "proxy.log"); @@ -162727,20 +162722,13 @@ async function run7(startedAt) { await sendFailedStatusReport(logger, startedAt, language, unwrappedError); } } +var startProxyAction = { + name: "start-proxy" /* StartProxy */, + run: run7, + transformTelemetryError: getSafeErrorMessage +}; async function runWrapper8() { - const startedAt = /* @__PURE__ */ new Date(); - const logger = getActionsLogger(); - try { - await run7(startedAt); - } catch (error3) { - core27.setFailed(`start-proxy action failed: ${getErrorMessage(error3)}`); - await sendUnhandledErrorStatusReport( - "start-proxy" /* StartProxy */, - startedAt, - getSafeErrorMessage(wrapError(error3)), - logger - ); - } + await runInActions(startProxyAction); } async function startProxy(binPath, config, logFilePath, logger) { const host = "127.0.0.1"; diff --git a/src/start-proxy-action.ts b/src/start-proxy-action.ts index 67f6d50177..e8b89732f7 100644 --- a/src/start-proxy-action.ts +++ b/src/start-proxy-action.ts @@ -3,12 +3,12 @@ import * as path from "path"; import * as core from "@actions/core"; -import { ActionState } from "./action-common"; +import { Action, ActionState, runInActions } from "./action-common"; import * as actionsUtil from "./actions-util"; import { getGitHubVersion } from "./api-client"; import { FeatureEnablement, initFeatures } from "./feature-flags"; import { BuiltInLanguage, parseBuiltInLanguage } from "./languages"; -import { getActionsLogger, Logger } from "./logging"; +import { Logger } from "./logging"; import { getRepositoryNwo } from "./repository"; import { credentialToStr, @@ -24,31 +24,18 @@ import { import { generateCertificateAuthority } from "./start-proxy/ca"; import { checkProxyEnvironment } from "./start-proxy/environment"; import { checkConnections } from "./start-proxy/reachability"; -import { - ActionName, - getJobUUID, - sendUnhandledErrorStatusReport, -} from "./status-report"; +import { ActionName } from "./status-report"; import * as util from "./util"; -async function run(startedAt: Date) { +async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) { // To capture errors appropriately, keep as much code within the try-catch as // possible, and only use safe functions outside. - - const logger = getActionsLogger(); + const startedAt = action.startedAt; + const logger = action.logger; let features: FeatureEnablement | undefined; let language: BuiltInLanguage | undefined; try { - const action: ActionState<["Logger", "Env", "Actions"]> = { - logger, - env: util.getEnv(), - actions: actionsUtil.getActionsEnv(), - }; - - // Create a unique identifier for this run. - getJobUUID(action); - // Make inputs accessible in the `post` step. actionsUtil.persistInputs(); @@ -136,21 +123,15 @@ async function run(startedAt: Date) { } } -export async function runWrapper() { - const startedAt = new Date(); - const logger = getActionsLogger(); +/** Defines the `start-proxy` Action. */ +const startProxyAction: Action = { + name: ActionName.StartProxy, + run, + transformTelemetryError: getSafeErrorMessage, +}; - try { - await run(startedAt); - } catch (error) { - core.setFailed(`start-proxy action failed: ${util.getErrorMessage(error)}`); - await sendUnhandledErrorStatusReport( - ActionName.StartProxy, - startedAt, - getSafeErrorMessage(util.wrapError(error)), - logger, - ); - } +export async function runWrapper() { + await runInActions(startProxyAction); } async function startProxy( From d57c3ffcba10414c396e4bc89f526c3875e18a0d Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 29 Jul 2026 15:38:27 +0100 Subject: [PATCH 19/19] Add tests for `runInActions` --- src/action-common.test.ts | 123 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/action-common.test.ts diff --git a/src/action-common.test.ts b/src/action-common.test.ts new file mode 100644 index 0000000000..fc2e0a9aaa --- /dev/null +++ b/src/action-common.test.ts @@ -0,0 +1,123 @@ +import * as core from "@actions/core"; +import test from "ava"; +import sinon from "sinon"; + +import * as common from "./action-common"; +import * as actionsUtil from "./actions-util"; +import * as environment from "./environment"; +import * as logging from "./logging"; +import { ActionName } from "./status-report"; +import * as statusReport from "./status-report"; +import { + getTestActionsEnv, + getTestEnv, + makeMacro, + RecordingLogger, + setupTests, +} from "./testing-utils"; +import { getErrorMessage } from "./util"; + +setupTests(test); + +interface RunInActionsTestOpts { + runFn?: () => Promise; + expectedErrorMessage?: string; + expectedTelemetryError?: string; +} + +const runInActionsMacro = makeMacro({ + exec: async (t, opts: RunInActionsTestOpts) => { + const expectFailure = opts?.expectedErrorMessage !== undefined; + + const logger = new RecordingLogger(); + const getActionsLogger = sinon + .stub(logging, "getActionsLogger") + .returns(logger); + + const env = getTestEnv(); + const getEnv = sinon.stub(environment, "getEnv").returns(env); + + const actionsEnv = getTestActionsEnv(env); + const getActionsEnv = sinon + .stub(actionsUtil, "getActionsEnv") + .returns(actionsEnv); + + const getJobUUID = sinon + .stub(statusReport, "getJobUUID") + .returns("test-job-uuid"); + + const setFailed = sinon.stub(core, "setFailed"); + const sendUnhandledErrorStatusReport = sinon.stub( + statusReport, + "sendUnhandledErrorStatusReport", + ); + + const name = ActionName.Init; + const run = sinon.stub(); + + if (opts?.runFn) { + run.callsFake(opts.runFn); + } + + const transformTelemetryError = sinon + .stub() + .callsFake((err) => opts?.expectedTelemetryError ?? getErrorMessage(err)); + const testAction: common.Action = { + name, + run, + transformTelemetryError, + }; + + await common.runInActions(testAction); + + // These always should have been called once. + t.true(getActionsLogger.calledOnce); + t.true(getEnv.calledOnce); + t.true(getActionsEnv.calledOnce); + + const expectedActionState = { + actions: actionsEnv, + env, + logger, + name: ActionName.Init, + }; + + t.true(getJobUUID.calledOnceWithExactly(sinon.match(expectedActionState))); + t.true(run.calledOnceWithExactly(sinon.match(expectedActionState))); + + t.is(setFailed.calledOnce, expectFailure ?? false); + t.is(sendUnhandledErrorStatusReport.calledOnce, expectFailure ?? false); + + if (expectFailure) { + t.true( + setFailed.calledOnceWithExactly( + `${statusReport.getDisplayActionName(name)} action failed: ${opts?.expectedErrorMessage}`, + ), + ); + t.true( + sendUnhandledErrorStatusReport.calledOnceWithExactly( + name, + sinon.match.any, + opts?.expectedTelemetryError ?? opts?.expectedErrorMessage, + logger, + ), + ); + } + }, + title: (providedTitle) => `runInActions - ${providedTitle}`, +}); + +runInActionsMacro.serial("calls run", {}); +runInActionsMacro.serial("handles run exceptions", { + runFn: () => { + throw new Error("Test failure"); + }, + expectedErrorMessage: "Test failure", +}); +runInActionsMacro.serial("transforms run exceptions", { + runFn: () => { + throw new Error("Test failure"); + }, + expectedErrorMessage: "Test failure", + expectedTelemetryError: "Transformed failure message", +});