Merge remote-tracking branch 'origin/main' into mbg/status/registries

This commit is contained in:
Michael B. Gale
2026-07-29 16:52:25 +01:00
17 changed files with 457 additions and 176 deletions

View File

@@ -71,8 +71,8 @@ jobs:
run: | run: |
cd "$RUNNER_TEMP/results" cd "$RUNNER_TEMP/results"
actual=$(jq -r '.runs[0].properties.jobRunUuid' javascript.sarif) actual=$(jq -r '.runs[0].properties.jobRunUuid' javascript.sarif)
if [[ "$actual" != "$JOB_RUN_UUID" ]]; then if [[ "$actual" != "$CODEQL_ACTION_JOB_RUN_UUID" ]]; then
echo "Expected SARIF output to contain job run UUID '$JOB_RUN_UUID', but found '$actual'." echo "Expected SARIF output to contain job run UUID '$CODEQL_ACTION_JOB_RUN_UUID', but found '$actual'."
exit 1 exit 1
else else
echo "Found job run UUID '$actual'." echo "Found job run UUID '$actual'."

View File

@@ -5,6 +5,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
## [UNRELEASED] ## [UNRELEASED]
- This version of the CodeQL Action adds support for the `tools` input for the `codeql-action/init` step to be specified using a `github-codeql-tools` [repository property](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization). This feature will gradually be rolled out following the release of this version. Once rolled out, this allows for the CodeQL CLI version that is used in GitHub-managed workflows, such as Default Setup, to be set to a custom value. For example, customers who run into issues with rate limits when a new CodeQL CLI version is released can set the value to `toolcache` to always use the CodeQL CLI version that is available in the runner toolcache. For Advanced Setup workflows, the value provided for `tools` in the workflow definition always takes precedence unless the value of the repository property starts with `!`. [#4037](https://github.com/github/codeql-action/pull/4037) - This version of the CodeQL Action adds support for the `tools` input for the `codeql-action/init` step to be specified using a `github-codeql-tools` [repository property](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization). This feature will gradually be rolled out following the release of this version. Once rolled out, this allows for the CodeQL CLI version that is used in GitHub-managed workflows, such as Default Setup, to be set to a custom value. For example, customers who run into issues with rate limits when a new CodeQL CLI version is released can set the value to `toolcache` to always use the CodeQL CLI version that is available in the runner toolcache. For Advanced Setup workflows, the value provided for `tools` in the workflow definition always takes precedence unless the value of the repository property starts with `!`. [#4037](https://github.com/github/codeql-action/pull/4037)
- Update default CodeQL bundle version to [2.26.2](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.2). [#4051](https://github.com/github/codeql-action/pull/4051)
## 4.37.3 - 22 Jul 2026 ## 4.37.3 - 22 Jul 2026

View File

@@ -1,6 +1,6 @@
{ {
"bundleVersion": "codeql-bundle-v2.26.1", "bundleVersion": "codeql-bundle-v2.26.2",
"cliVersion": "2.26.1", "cliVersion": "2.26.2",
"priorBundleVersion": "codeql-bundle-v2.26.0", "priorBundleVersion": "codeql-bundle-v2.26.1",
"priorCliVersion": "2.26.0" "priorCliVersion": "2.26.1"
} }

202
lib/entry-points.js generated
View File

@@ -21559,7 +21559,7 @@ var require_core = __commonJS({
}; };
Object.defineProperty(exports2, "__esModule", { value: true }); Object.defineProperty(exports2, "__esModule", { value: true });
exports2.platform = exports2.toPlatformPath = exports2.toWin32Path = exports2.toPosixPath = exports2.markdownSummary = exports2.summary = exports2.ExitCode = void 0; 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.setSecret = setSecret2;
exports2.addPath = addPath2; exports2.addPath = addPath2;
exports2.getInput = getInput2; exports2.getInput = getInput2;
@@ -21567,7 +21567,7 @@ var require_core = __commonJS({
exports2.getBooleanInput = getBooleanInput; exports2.getBooleanInput = getBooleanInput;
exports2.setOutput = setOutput7; exports2.setOutput = setOutput7;
exports2.setCommandEcho = setCommandEcho; exports2.setCommandEcho = setCommandEcho;
exports2.setFailed = setFailed13; exports2.setFailed = setFailed12;
exports2.isDebug = isDebug5; exports2.isDebug = isDebug5;
exports2.debug = debug6; exports2.debug = debug6;
exports2.error = error3; exports2.error = error3;
@@ -21591,7 +21591,7 @@ var require_core = __commonJS({
ExitCode2[ExitCode2["Success"] = 0] = "Success"; ExitCode2[ExitCode2["Success"] = 0] = "Success";
ExitCode2[ExitCode2["Failure"] = 1] = "Failure"; ExitCode2[ExitCode2["Failure"] = 1] = "Failure";
})(ExitCode || (exports2.ExitCode = ExitCode = {})); })(ExitCode || (exports2.ExitCode = ExitCode = {}));
function exportVariable15(name, val) { function exportVariable16(name, val) {
const convertedVal = (0, utils_1.toCommandValue)(val); const convertedVal = (0, utils_1.toCommandValue)(val);
process.env[name] = convertedVal; process.env[name] = convertedVal;
const filePath = process.env["GITHUB_ENV"] || ""; const filePath = process.env["GITHUB_ENV"] || "";
@@ -21651,7 +21651,7 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
function setCommandEcho(enabled) { function setCommandEcho(enabled) {
(0, command_1.issue)("echo", enabled ? "on" : "off"); (0, command_1.issue)("echo", enabled ? "on" : "off");
} }
function setFailed13(message) { function setFailed12(message) {
process.exitCode = ExitCode.Failure; process.exitCode = ExitCode.Failure;
error3(message); error3(message);
} }
@@ -30477,7 +30477,7 @@ var require_validator = __commonJS({
Validator3.prototype.getSchema = function getSchema(urn) { Validator3.prototype.getSchema = function getSchema(urn) {
return this.schemas[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) { if (typeof schema !== "boolean" && typeof schema !== "object" || schema === null) {
throw new SchemaError("Expected `schema` to be an object or boolean"); throw new SchemaError("Expected `schema` to be an object or boolean");
} }
@@ -121026,7 +121026,7 @@ var require_core3 = __commonJS({
ExitCode2[ExitCode2["Success"] = 0] = "Success"; ExitCode2[ExitCode2["Success"] = 0] = "Success";
ExitCode2[ExitCode2["Failure"] = 1] = "Failure"; ExitCode2[ExitCode2["Failure"] = 1] = "Failure";
})(ExitCode || (exports2.ExitCode = ExitCode = {})); })(ExitCode || (exports2.ExitCode = ExitCode = {}));
function exportVariable15(name, val) { function exportVariable16(name, val) {
const convertedVal = (0, utils_1.toCommandValue)(val); const convertedVal = (0, utils_1.toCommandValue)(val);
process.env[name] = convertedVal; process.env[name] = convertedVal;
const filePath = process.env["GITHUB_ENV"] || ""; const filePath = process.env["GITHUB_ENV"] || "";
@@ -121035,7 +121035,7 @@ var require_core3 = __commonJS({
} }
(0, command_1.issueCommand)("set-env", { name }, convertedVal); (0, command_1.issueCommand)("set-env", { name }, convertedVal);
} }
exports2.exportVariable = exportVariable15; exports2.exportVariable = exportVariable16;
function setSecret2(secret) { function setSecret2(secret) {
(0, command_1.issueCommand)("add-mask", {}, secret); (0, command_1.issueCommand)("add-mask", {}, secret);
} }
@@ -121094,11 +121094,11 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
(0, command_1.issue)("echo", enabled ? "on" : "off"); (0, command_1.issue)("echo", enabled ? "on" : "off");
} }
exports2.setCommandEcho = setCommandEcho; exports2.setCommandEcho = setCommandEcho;
function setFailed13(message) { function setFailed12(message) {
process.exitCode = ExitCode.Failure; process.exitCode = ExitCode.Failure;
error3(message); error3(message);
} }
exports2.setFailed = setFailed13; exports2.setFailed = setFailed12;
function isDebug5() { function isDebug5() {
return process.env["RUNNER_DEBUG"] === "1"; return process.env["RUNNER_DEBUG"] === "1";
} }
@@ -141565,6 +141565,10 @@ var ReadOnlyEnv = class {
clone() { clone() {
return Object.create(this, { vars: { value: { ...this.vars } } }); 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. */ /** Tries to get the value for `name` and throws if there isn't one. */
getRequired(name) { getRequired(name) {
return getRequiredEnvVar(this.vars, name); return getRequiredEnvVar(this.vars, name);
@@ -144598,13 +144602,13 @@ function isBoolean(value) {
function isStringOrUndefined(value) { function isStringOrUndefined(value) {
return value === void 0 || isString(value); return value === void 0 || isString(value);
} }
function defaultCheck(validate) { function defaultCheck(validate2) {
return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) }); return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate2(arg) });
} }
function makeValidator(validate) { function makeValidator(validate2) {
return { return {
validate, validate: validate2,
check: defaultCheck(validate), check: defaultCheck(validate2),
required: true required: true
}; };
} }
@@ -144612,11 +144616,11 @@ var string = makeValidator(isString);
var number = makeValidator(isNumber); var number = makeValidator(isNumber);
var boolean = makeValidator(isBoolean); var boolean = makeValidator(isBoolean);
function array(validator) { function array(validator) {
const validate = (val) => { const validate2 = (val) => {
return isArray(val) && val.every((e) => validator.validate(e)); return isArray(val) && val.every((e) => validator.validate(e));
}; };
return { return {
validate, validate: validate2,
check: (val, opts, path29) => { check: (val, opts, path29) => {
const result = successfulCheckSchema(); const result = successfulCheckSchema();
if (!isArray(val)) { if (!isArray(val)) {
@@ -145391,7 +145395,11 @@ var Failure = class {
// src/actions-util.ts // src/actions-util.ts
function getActionsEnv() { function getActionsEnv() {
return { getOptionalInput }; return {
getRequiredInput,
getOptionalInput,
exportVariable: core3.exportVariable
};
} }
var getRequiredInput = function(name) { var getRequiredInput = function(name) {
const value = core3.getInput(name); const value = core3.getInput(name);
@@ -145736,6 +145744,59 @@ function formatDuration(durationMs) {
var os3 = __toESM(require("os")); var os3 = __toESM(require("os"));
var core7 = __toESM(require_core()); 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) {
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 // src/api-client.ts
var core5 = __toESM(require_core()); var core5 = __toESM(require_core());
var githubUtils = __toESM(require_utils4()); var githubUtils = __toESM(require_utils4());
@@ -146496,6 +146557,17 @@ function getDisplayActionName(actionName) {
} }
return actionName; return actionName;
} }
function getJobUUID(action) {
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("CODEQL_ACTION_JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid);
return jobRunUuid;
}
function isFirstPartyAnalysis(actionName) { function isFirstPartyAnalysis(actionName) {
if (actionName !== "upload-sarif" /* UploadSarif */) { if (actionName !== "upload-sarif" /* UploadSarif */) {
return true; return true;
@@ -146578,7 +146650,7 @@ async function createStatusReportBase(actionName, status, actionStartedAt, confi
try { try {
const commitOid = getOptionalInput("sha") || process.env["GITHUB_SHA"] || ""; const commitOid = getOptionalInput("sha") || process.env["GITHUB_SHA"] || "";
const ref = await getRef(); 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 workflowRunID = getWorkflowRunID();
const workflowRunAttempt = getWorkflowRunAttempt(); const workflowRunAttempt = getWorkflowRunAttempt();
const workflowName = process.env["GITHUB_WORKFLOW"] || ""; const workflowName = process.env["GITHUB_WORKFLOW"] || "";
@@ -146807,18 +146879,26 @@ async function runInActions(action) {
const env = getEnv(); const env = getEnv();
const actionsEnv = getActionsEnv(); const actionsEnv = getActionsEnv();
try { try {
await action.run({ const actionState = {
name: action.name, name: action.name,
startedAt, startedAt,
logger, logger,
env, env,
actions: actionsEnv actions: actionsEnv
}); };
getJobUUID(actionState);
await action.run(actionState);
} catch (error3) { } catch (error3) {
core8.setFailed( core8.setFailed(
`${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error3)}` `${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
);
} }
} }
@@ -146828,8 +146908,8 @@ var path5 = __toESM(require("path"));
var semver4 = __toESM(require_semver2()); var semver4 = __toESM(require_semver2());
// src/defaults.json // src/defaults.json
var bundleVersion = "codeql-bundle-v2.26.1"; var bundleVersion = "codeql-bundle-v2.26.2";
var cliVersion = "2.26.1"; var cliVersion = "2.26.2";
// src/overlay/index.ts // src/overlay/index.ts
var fs4 = __toESM(require("fs")); var fs4 = __toESM(require("fs"));
@@ -150247,50 +150327,6 @@ var toolcache3 = __toESM(require_tool_cache());
var import_fast_deep_equal = __toESM(require_fast_deep_equal()); var import_fast_deep_equal = __toESM(require_fast_deep_equal());
var semver9 = __toESM(require_semver2()); 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 // src/overlay/caching.ts
var fs10 = __toESM(require("fs")); var fs10 = __toESM(require("fs"));
var actionsCache3 = __toESM(require_cache4()); var actionsCache3 = __toESM(require_cache4());
@@ -152163,7 +152199,7 @@ function applyAutobuildAzurePipelinesTimeoutFix() {
].join(" "); ].join(" ");
} }
async function getJobRunUuidSarifOptions() { 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}`] : []; return jobRunUuid ? [`--sarif-run-property=jobRunUuid=${jobRunUuid}`] : [];
} }
@@ -160930,9 +160966,6 @@ async function run3(actionState) {
logger logger
); );
const repositoryProperties = repositoryPropertiesResult.orElse({}); const repositoryProperties = repositoryPropertiesResult.orElse({});
const jobRunUuid = v4_default();
logger.info(`Job run UUID is ${jobRunUuid}.`);
core21.exportVariable("JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid);
core21.exportVariable("CODEQL_ACTION_INIT_HAS_RUN" /* INIT_ACTION_HAS_RUN */, "true"); core21.exportVariable("CODEQL_ACTION_INIT_HAS_RUN" /* INIT_ACTION_HAS_RUN */, "true");
sourceRoot = path24.resolve( sourceRoot = path24.resolve(
getRequiredEnvParam("GITHUB_WORKSPACE"), getRequiredEnvParam("GITHUB_WORKSPACE"),
@@ -161930,9 +161963,6 @@ async function run6(actionState) {
); );
const repositoryProperties = repositoryPropertiesResult.orElse({}); const repositoryProperties = repositoryPropertiesResult.orElse({});
const actionStateWithFeatures = { ...actionState, features }; const actionStateWithFeatures = { ...actionState, features };
const jobRunUuid = v4_default();
logger.info(`Job run UUID is ${jobRunUuid}.`);
core24.exportVariable("JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid);
const statusReportBase = await createStatusReportBase( const statusReportBase = await createStatusReportBase(
"setup-codeql" /* SetupCodeQL */, "setup-codeql" /* SetupCodeQL */,
"starting", "starting",
@@ -162667,8 +162697,9 @@ async function checkConnections(logger, proxy, backend) {
} }
// src/start-proxy-action.ts // src/start-proxy-action.ts
async function run7(startedAt) { async function run7(action) {
const logger = getActionsLogger(); const startedAt = action.startedAt;
const logger = action.logger;
let features; let features;
let language; let language;
try { try {
@@ -162734,20 +162765,13 @@ async function run7(startedAt) {
await sendFailedStatusReport(logger, startedAt, language, unwrappedError); await sendFailedStatusReport(logger, startedAt, language, unwrappedError);
} }
} }
var startProxyAction = {
name: "start-proxy" /* StartProxy */,
run: run7,
transformTelemetryError: getSafeErrorMessage
};
async function runWrapper8() { async function runWrapper8() {
const startedAt = /* @__PURE__ */ new Date(); await runInActions(startProxyAction);
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
);
}
} }
async function startProxy(binPath, config, logFilePath, logger) { async function startProxy(binPath, config, logFilePath, logger) {
const host = "127.0.0.1"; const host = "127.0.0.1";

View File

@@ -21,8 +21,8 @@ steps:
run: | run: |
cd "$RUNNER_TEMP/results" cd "$RUNNER_TEMP/results"
actual=$(jq -r '.runs[0].properties.jobRunUuid' javascript.sarif) actual=$(jq -r '.runs[0].properties.jobRunUuid' javascript.sarif)
if [[ "$actual" != "$JOB_RUN_UUID" ]]; then if [[ "$actual" != "$CODEQL_ACTION_JOB_RUN_UUID" ]]; then
echo "Expected SARIF output to contain job run UUID '$JOB_RUN_UUID', but found '$actual'." echo "Expected SARIF output to contain job run UUID '$CODEQL_ACTION_JOB_RUN_UUID', but found '$actual'."
exit 1 exit 1
else else
echo "Found job run UUID '$actual'." echo "Found job run UUID '$actual'."

123
src/action-common.test.ts Normal file
View File

@@ -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<any>;
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",
});

View File

@@ -8,9 +8,10 @@ import { getActionsLogger, Logger } from "./logging";
import { import {
ActionName, ActionName,
getDisplayActionName, getDisplayActionName,
getJobUUID,
sendUnhandledErrorStatusReport, sendUnhandledErrorStatusReport,
} from "./status-report"; } from "./status-report";
import { getEnv, getErrorMessage } from "./util"; import { getEnv, getErrorMessage, wrapError } from "./util";
/** Base state that is available to an Action on startup. */ /** Base state that is available to an Action on startup. */
export interface BaseState { export interface BaseState {
@@ -78,6 +79,12 @@ export interface Action {
name: ActionName; name: ActionName;
/** The entry point for the Action. */ /** The entry point for the Action. */
run: ActionMain; 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. */ /** A generic entry point that sets up the basic environment for the `action` and runs it. */
@@ -88,17 +95,32 @@ export async function runInActions(action: Action) {
const actionsEnv = getActionsEnv(); const actionsEnv = getActionsEnv();
try { try {
await action.run({ const actionState = {
name: action.name, name: action.name,
startedAt, startedAt,
logger, logger,
env, env,
actions: actionsEnv, actions: actionsEnv,
}); };
// Create a unique identifier for this run.
getJobUUID(actionState);
await action.run(actionState);
} catch (error) { } catch (error) {
core.setFailed( core.setFailed(
`${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error)}`, `${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,
);
} }
} }

View File

@@ -27,14 +27,20 @@ declare const __CODEQL_ACTION_VERSION__: string;
* global functions in tests. * global functions in tests.
*/ */
export interface ActionsEnv { export interface ActionsEnv {
getRequiredInput: (name: string) => string;
getOptionalInput: (name: string) => string | undefined; getOptionalInput: (name: string) => string | undefined;
exportVariable: (name: string, value: string) => void;
} }
/** /**
* Gets the real `ActionsEnv` used by production code. * Gets the real `ActionsEnv` used by production code.
*/ */
export function getActionsEnv(): ActionsEnv { export function getActionsEnv(): ActionsEnv {
return { getOptionalInput }; return {
getRequiredInput,
getOptionalInput,
exportVariable: core.exportVariable,
};
} }
/** /**

View File

@@ -1,7 +1,7 @@
import test from "ava"; import test from "ava";
import sinon from "sinon"; import sinon from "sinon";
import { getActionsEnv } from "../actions-util"; import { ActionsEnv } from "../actions-util";
import { Feature } from "../feature-flags"; import { Feature } from "../feature-flags";
import { RepositoryPropertyName } from "../feature-flags/properties"; import { RepositoryPropertyName } from "../feature-flags/properties";
import { callee } from "../testing-utils"; import { callee } from "../testing-utils";
@@ -22,32 +22,26 @@ const expectedRepositoryPropertyResult: ComputedInput = {
value: "repo-property-input-value", value: "repo-property-input-value",
}; };
function stubGetToolsInput() { function stubGetToolsInput(actions: ActionsEnv) {
const actions = getActionsEnv();
sinon sinon
.stub(actions, "getOptionalInput") .stub(actions, "getOptionalInput")
.withArgs(InputName.Tools) .withArgs(InputName.Tools)
.returns(expectedWorkflowResult.value); .returns(expectedWorkflowResult.value);
return actions;
} }
const workflowLogMessage = `Using ${InputName.Tools} input from workflow:`; const workflowLogMessage = `Using ${InputName.Tools} input from workflow:`;
test("getToolsInput - returns workflow input if available", async (t) => { test("getToolsInput - returns workflow input if available", async (t) => {
const actions = stubGetToolsInput();
await callee(getToolsInput) await callee(getToolsInput)
.withActions(actions) .withActions(stubGetToolsInput)
.withArgs({}) .withArgs({})
.logs(t, workflowLogMessage) .logs(t, workflowLogMessage)
.passes(t.deepEqual, expectedWorkflowResult); .passes(t.deepEqual, expectedWorkflowResult);
}); });
test("getToolsInput - returns repository property value if enforced", async (t) => { test("getToolsInput - returns repository property value if enforced", async (t) => {
const actions = stubGetToolsInput();
const target = callee(getToolsInput) const target = callee(getToolsInput)
.withActions(actions) .withActions(stubGetToolsInput)
.withArgs({ .withArgs({
[RepositoryPropertyName.TOOLS]: `!${expectedRepositoryPropertyResult.value}`, [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) => { test("getToolsInput - prefers workflow input", async (t) => {
const actions = stubGetToolsInput();
const target = callee(getToolsInput) const target = callee(getToolsInput)
.withActions(actions) .withActions(stubGetToolsInput)
.withArgs({ .withArgs({
[RepositoryPropertyName.TOOLS]: expectedRepositoryPropertyResult.value, [RepositoryPropertyName.TOOLS]: expectedRepositoryPropertyResult.value,
}); });

View File

@@ -1,6 +1,6 @@
{ {
"bundleVersion": "codeql-bundle-v2.26.1", "bundleVersion": "codeql-bundle-v2.26.2",
"cliVersion": "2.26.1", "cliVersion": "2.26.2",
"priorBundleVersion": "codeql-bundle-v2.26.0", "priorBundleVersion": "codeql-bundle-v2.26.1",
"priorCliVersion": "2.26.0" "priorCliVersion": "2.26.1"
} }

View File

@@ -88,7 +88,7 @@ export enum EnvVar {
LOG_VERSION_DEPRECATION = "CODEQL_ACTION_DID_LOG_VERSION_DEPRECATION", LOG_VERSION_DEPRECATION = "CODEQL_ACTION_DID_LOG_VERSION_DEPRECATION",
/** UUID representing the current job run. */ /** 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` */ /** Status for the entire job, submitted to the status report in `init-post` */
JOB_STATUS = "CODEQL_ACTION_JOB_STATUS", JOB_STATUS = "CODEQL_ACTION_JOB_STATUS",
@@ -270,6 +270,11 @@ export class ReadOnlyEnv<T extends string | undefined = string | undefined> {
return Object.create(this, { vars: { value: { ...this.vars } } }) as this; return Object.create(this, { vars: { value: { ...this.vars } } }) as this;
} }
/** Gets a copy of the underlying environment. */
public get(): Record<string, T> {
return { ...this.vars };
}
/** Tries to get the value for `name` and throws if there isn't one. */ /** Tries to get the value for `name` and throws if there isn't one. */
public getRequired(name: string): string { public getRequired(name: string): string {
return getRequiredEnvVar(this.vars, name); return getRequiredEnvVar(this.vars, name);

View File

@@ -4,7 +4,6 @@ import * as path from "path";
import * as core from "@actions/core"; import * as core from "@actions/core";
import * as io from "@actions/io"; import * as io from "@actions/io";
import * as semver from "semver"; import * as semver from "semver";
import { v4 as uuidV4 } from "uuid";
import { Action, ActionState, runInActions } from "./action-common"; import { Action, ActionState, runInActions } from "./action-common";
import { import {
@@ -255,11 +254,6 @@ async function run(
); );
const repositoryProperties = repositoryPropertiesResult.orElse({}); 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);
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true"); core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
// path.resolve() respects the intended semantics of source-root. If // path.resolve() respects the intended semantics of source-root. If

View File

@@ -1,5 +1,4 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import { v4 as uuidV4 } from "uuid";
import { Action, ActionState, runInActions } from "./action-common"; import { Action, ActionState, runInActions } from "./action-common";
import { import {
@@ -95,7 +94,7 @@ async function sendCompletedStatusReport(
/** The main behaviour of this action. */ /** The main behaviour of this action. */
async function run( async function run(
actionState: ActionState<["Base", "Logger", "Actions"]>, actionState: ActionState<["Base", "Logger", "Env", "Actions"]>,
): Promise<void> { ): Promise<void> {
// To capture errors appropriately, keep as much code within the try-catch as // To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside. // possible, and only use safe functions outside.
@@ -140,10 +139,6 @@ async function run(
const actionStateWithFeatures = { ...actionState, features }; const actionStateWithFeatures = { ...actionState, features };
const jobRunUuid = uuidV4();
logger.info(`Job run UUID is ${jobRunUuid}.`);
core.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid);
const statusReportBase = await createStatusReportBase( const statusReportBase = await createStatusReportBase(
ActionName.SetupCodeQL, ActionName.SetupCodeQL,
"starting", "starting",

View File

@@ -3,11 +3,12 @@ import * as path from "path";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { Action, ActionState, runInActions } from "./action-common";
import * as actionsUtil from "./actions-util"; import * as actionsUtil from "./actions-util";
import { getGitHubVersion } from "./api-client"; import { getGitHubVersion } from "./api-client";
import { FeatureEnablement, initFeatures } from "./feature-flags"; import { FeatureEnablement, initFeatures } from "./feature-flags";
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages"; import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
import { getActionsLogger, Logger } from "./logging"; import { Logger } from "./logging";
import { getRepositoryNwo } from "./repository"; import { getRepositoryNwo } from "./repository";
import { import {
credentialToStr, credentialToStr,
@@ -23,14 +24,14 @@ import {
import { generateCertificateAuthority } from "./start-proxy/ca"; import { generateCertificateAuthority } from "./start-proxy/ca";
import { checkProxyEnvironment } from "./start-proxy/environment"; import { checkProxyEnvironment } from "./start-proxy/environment";
import { checkConnections } from "./start-proxy/reachability"; import { checkConnections } from "./start-proxy/reachability";
import { ActionName, sendUnhandledErrorStatusReport } from "./status-report"; import { ActionName } from "./status-report";
import * as util from "./util"; 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 // To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside. // possible, and only use safe functions outside.
const startedAt = action.startedAt;
const logger = getActionsLogger(); const logger = action.logger;
let features: FeatureEnablement | undefined; let features: FeatureEnablement | undefined;
let language: BuiltInLanguage | undefined; let language: BuiltInLanguage | undefined;
@@ -122,21 +123,15 @@ async function run(startedAt: Date) {
} }
} }
export async function runWrapper() { /** Defines the `start-proxy` Action. */
const startedAt = new Date(); const startProxyAction: Action = {
const logger = getActionsLogger(); name: ActionName.StartProxy,
run,
transformTelemetryError: getSafeErrorMessage,
};
try { export async function runWrapper() {
await run(startedAt); await runInActions(startProxyAction);
} catch (error) {
core.setFailed(`start-proxy action failed: ${util.getErrorMessage(error)}`);
await sendUnhandledErrorStatusReport(
ActionName.StartProxy,
startedAt,
getSafeErrorMessage(util.wrapError(error)),
logger,
);
}
} }
async function startProxy( async function startProxy(

View File

@@ -1,5 +1,6 @@
import test from "ava"; import test from "ava";
import * as sinon from "sinon"; import * as sinon from "sinon";
import * as uuid from "uuid";
import * as actionsUtil from "./actions-util"; import * as actionsUtil from "./actions-util";
import { Config } from "./config-utils"; import { Config } from "./config-utils";
@@ -14,6 +15,7 @@ import {
createStatusReportBase, createStatusReportBase,
getActionsStatus, getActionsStatus,
getRegistryTypesFromEnv, getRegistryTypesFromEnv,
getJobUUID,
InitStatusReport, InitStatusReport,
InitWithConfigStatusReport, InitWithConfigStatusReport,
} from "./status-report"; } from "./status-report";
@@ -24,6 +26,7 @@ import {
makeMacro, makeMacro,
getTestEnv, getTestEnv,
RecordingLogger, RecordingLogger,
callee,
} from "./testing-utils"; } from "./testing-utils";
import { BuildMode, ConfigurationError, withTmpDir, wrapError } from "./util"; import { BuildMode, ConfigurationError, withTmpDir, wrapError } from "./util";
@@ -84,6 +87,43 @@ test("getRegistryTypesFromEnv - returns undefined if the env var is unexpected J
); );
}); });
test("getJobUUID - generates valid UUIDs", async (t) => {
await callee(getJobUUID)
.withArgs()
.logs(t, "Job run UUID is ")
.hasEnv(t, (val) => {
return {
[EnvVar.JOB_RUN_UUID]: val,
};
})
.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);
});
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) { function setupEnvironmentAndStub(tmpDir: string) {
setupActionsVars(tmpDir, tmpDir, { setupActionsVars(tmpDir, tmpDir, {
GITHUB_EVENT_NAME: "dynamic", GITHUB_EVENT_NAME: "dynamic",

View File

@@ -1,7 +1,9 @@
import * as os from "os"; import * as os from "os";
import * as core from "@actions/core"; import * as core from "@actions/core";
import * as uuid from "uuid";
import type { ActionState } from "./action-common";
import { import {
getWorkflowEventName, getWorkflowEventName,
getOptionalInput, getOptionalInput,
@@ -61,6 +63,30 @@ export function getDisplayActionName(actionName: ActionName): string {
return actionName; return actionName;
} }
/**
* 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", "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);
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}.`);
action.actions.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid);
return jobRunUuid;
}
/** /**
* @returns a boolean indicating whether the analysis is considered to be first party. * @returns a boolean indicating whether the analysis is considered to be first party.
* *

View File

@@ -34,11 +34,14 @@ import { ActionName } from "./status-report";
import { import {
DEFAULT_DEBUG_ARTIFACT_NAME, DEFAULT_DEBUG_ARTIFACT_NAME,
DEFAULT_DEBUG_DATABASE_NAME, DEFAULT_DEBUG_DATABASE_NAME,
Failure,
getEnv, getEnv,
GitHubVariant, GitHubVariant,
GitHubVersion, GitHubVersion,
HTTPError, HTTPError,
resetCachedCodeQlVersion, resetCachedCodeQlVersion,
Result,
Success,
} from "./util"; } from "./util";
export const SAMPLE_DOTCOM_API_DETAILS = { export const SAMPLE_DOTCOM_API_DETAILS = {
@@ -182,13 +185,32 @@ export function getTestEnv(testEnv: NodeJS.ProcessEnv = {}): Env {
return getEnv(testEnv); 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. * Gets an `ActionsEnv` instance for use in tests.
*/ */
export function getTestActionsEnv(): ActionsEnv { export function getTestActionsEnv(env: Env): TestActionsEnv {
return { return new TestActionsEnv(env);
getOptionalInput: () => undefined,
};
} }
/** For testing purposes, we make all available state features accessible in `TestEnv`. */ /** For testing purposes, we make all available state features accessible in `TestEnv`. */
@@ -206,12 +228,13 @@ type AllState = [
export function initAllState( export function initAllState(
overrides?: Partial<ActionState<AllState>>, overrides?: Partial<ActionState<AllState>>,
): ActionState<AllState> { ): ActionState<AllState> {
const env = getTestEnv();
return { return {
name: ActionName.Init, name: ActionName.Init,
startedAt: new Date(), startedAt: new Date(),
logger: new RecordingLogger(), logger: new RecordingLogger(),
env: getTestEnv(), env,
actions: getTestActionsEnv(), actions: getTestActionsEnv(env),
apiClient: github.getOctokit("123"), apiClient: github.getOctokit("123"),
features: createFeatures([]), features: createFeatures([]),
...overrides, ...overrides,
@@ -222,9 +245,13 @@ type DelayedCheck<
Args extends readonly any[], Args extends readonly any[],
R, R,
Fs extends ReadonlyArray<AllState[number]>, Fs extends ReadonlyArray<AllState[number]>,
> = (env: Readonly<BaseEnvBuilder<Args, R, Fs>>) => Promise<any>; > = (
env: Readonly<BaseEnvBuilder<Args, R, Fs>>,
result: Result<Awaited<R>, ThrownError<ErrorConstructor | Error>>,
) => Promise<any>;
export type ValueOrMutation<T> = T | ((val: T) => void); export type Mutation<T> = (val: T) => void;
export type ValueOrMutation<T> = T | Mutation<T>;
/** /**
* Wraps a function that accepts an `ActionState` for testing in different environments. * Wraps a function that accepts an `ActionState` for testing in different environments.
@@ -236,6 +263,7 @@ abstract class BaseEnvBuilder<
> { > {
protected readonly fn: (state: ActionState<Fs>, ...args: Args) => R; protected readonly fn: (state: ActionState<Fs>, ...args: Args) => R;
private logger: RecordingLogger; private logger: RecordingLogger;
private actions: TestActionsEnv;
protected state: ActionState<AllState>; protected state: ActionState<AllState>;
protected checks: Array<DelayedCheck<Args, R, Fs>>; protected checks: Array<DelayedCheck<Args, R, Fs>>;
@@ -245,15 +273,26 @@ abstract class BaseEnvBuilder<
) { ) {
this.fn = fn; this.fn = fn;
this.logger = new RecordingLogger(); this.logger = new RecordingLogger();
this.state =
cloneFrom !== undefined if (cloneFrom !== undefined) {
? ({ const env = cloneFrom.state.env.clone();
...cloneFrom.state, this.actions = cloneFrom.actions.clone(env);
env: cloneFrom.state.env.clone(), this.state = {
actions: Object.create(cloneFrom.state.actions), ...cloneFrom.state,
logger: this.logger, env,
} satisfies ActionState<AllState>) actions: this.actions,
: initAllState({ logger: this.logger }); logger: this.logger,
} satisfies ActionState<AllState>;
} else {
const env = getTestEnv();
this.actions = getTestActionsEnv(env);
this.state = initAllState({
logger: this.logger,
env,
actions: this.actions,
});
}
this.checks = [...(cloneFrom?.checks ?? [])]; this.checks = [...(cloneFrom?.checks ?? [])];
} }
@@ -320,13 +359,10 @@ abstract class BaseEnvBuilder<
return result; return result;
} }
public withActions(arg: ValueOrMutation<ActionsEnv>): this { /** Applies `fn` to the `ActionsEnv`. */
public withActions(fn: Mutation<ActionsEnv>): this {
const result = this.clone(); const result = this.clone();
if (typeof arg === "function") { fn(result.state.actions);
arg(result.state.actions);
} else {
result.state.actions = arg;
}
return result; return result;
} }
@@ -342,6 +378,28 @@ abstract class BaseEnvBuilder<
return result; 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<unknown>,
fn: (
value: Awaited<R> | undefined,
error: ThrownError<ErrorConstructor | Error> | undefined,
) => Record<string, string | undefined>,
): 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 * Adds a delayed check that `messages` are not logged. The check will be
* performed after the main assertion passes. * performed after the main assertion passes.
@@ -439,7 +497,7 @@ class CallableEnvBuilder<
// Run other delayed checks. // Run other delayed checks.
for (const delayedCheck of this.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. // Return the results of the function call and the main assertion.
@@ -465,7 +523,7 @@ class CallableEnvBuilder<
// Run other delayed checks. // Run other delayed checks.
for (const delayedCheck of this.checks) { for (const delayedCheck of this.checks) {
await delayedCheck(this); await delayedCheck(this, new Failure(error));
} }
// Return the error. // Return the error.