Compare commits

...
11 changed files with 657 additions and 565 deletions

No files matched your search

+460 -448
View File
File diff suppressed because it is too large. Load diff
+17 -15
View File
@@ -19,24 +19,26 @@ import { getErrorMessage } from "./util";
setupTests(test);
for (const [key, displayName] of Object.entries(
statusReport.ACTION_DISPLAY_NAMES,
)) {
test(`getDisplayActionName - '${displayName}' instead of '${key}'`, (t) => {
const expectedActionDisplayNames: Record<ActionName, string> = {
[ActionName.Analyze]: "analyze action",
[ActionName.AnalyzePost]: "analyze post-action",
[ActionName.Autobuild]: "autobuild action",
[ActionName.Init]: "init action",
[ActionName.InitPost]: "init post-action",
[ActionName.ResolveEnvironment]: "resolve-environment action",
[ActionName.SetupCodeQL]: "setup-codeql action",
[ActionName.StartProxy]: "start-proxy action",
[ActionName.StartProxyPost]: "start-proxy post-action",
[ActionName.UploadSarif]: "upload-sarif action",
[ActionName.UploadSarifPost]: "upload-sarif post-action",
};
for (const [key, displayName] of Object.entries(expectedActionDisplayNames)) {
test(`getDisplayActionName - '${displayName}' for '${key}'`, (t) => {
t.is(statusReport.getDisplayActionName(key as ActionName), displayName);
});
}
test("getDisplayActionName - returns action name", (t) => {
for (const actionName of Object.values(ActionName)) {
if (statusReport.ACTION_DISPLAY_NAMES[actionName]) {
continue;
}
t.is(statusReport.getDisplayActionName(actionName), actionName);
}
});
interface RunInActionsTestOpts {
runFn?: () => Promise<any>;
expectedErrorMessage?: string;
@@ -109,7 +111,7 @@ const runInActionsMacro = makeMacro({
if (expectFailure) {
t.true(
setFailed.calledOnceWithExactly(
`${statusReport.getDisplayActionName(name)} action failed: ${opts?.expectedErrorMessage}`,
`${statusReport.getDisplayActionName(name)} failed: ${opts?.expectedErrorMessage}`,
),
);
t.true(
+10 -2
View File
@@ -1,6 +1,6 @@
import * as core from "@actions/core";
import { ActionsEnv, getActionsEnv } from "./actions-util";
import { ActionsEnv, getActionsEnv, restoreInputs } from "./actions-util";
import type { ApiClient } from "./api-client";
import { Env, ReadOnlyEnv } from "./environment";
import type { FeatureEnablement } from "./feature-flags";
@@ -9,10 +9,13 @@ import {
ActionName,
getDisplayActionName,
getJobUUID,
isPostAction,
sendUnhandledErrorStatusReport,
} from "./status-report";
import { getEnv, getErrorMessage, wrapError } from "./util";
export { ActionName } from "./status-report";
/** Base state that is available to an Action on startup. */
export interface BaseState {
/** The name of the Action. */
@@ -178,13 +181,18 @@ export async function runInActions(action: Action) {
actions: actionsEnv,
};
// If this is a post-action, restore inputs from the main action.
if (isPostAction(action.name)) {
restoreInputs(actionState);
}
// Create a unique identifier for this run.
getJobUUID(actionState);
await action.run(actionState);
} catch (error) {
core.setFailed(
`${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error)}`,
`${getDisplayActionName(action.name)} failed: ${getErrorMessage(error)}`,
);
const statusReportError =
+20 -6
View File
@@ -6,6 +6,7 @@ import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as github from "@actions/github";
import * as io from "@actions/io";
import type { ActionState } from "./action-common";
import { hasConfigKey, type Config } from "./config/action-config";
import { Env, EnvVar, ActionsEnvVars, ReadOnlyEnv } from "./environment";
import { Logger } from "./logging";
@@ -30,6 +31,8 @@ declare const __CODEQL_ACTION_VERSION__: string;
export interface ActionsEnv {
getRequiredInput: (name: string) => string;
getOptionalInput: (name: string) => string | undefined;
setFailed: (message: string) => void;
getState: (name: string) => string;
}
/**
@@ -39,6 +42,8 @@ export function getActionsEnv(): ActionsEnv {
return {
getRequiredInput,
getOptionalInput,
setFailed: core.setFailed,
getState: core.getState,
};
}
@@ -403,6 +408,7 @@ export async function runTool(
return stdout;
}
type PersistedInputs = Array<[string, string | undefined]>;
const persistedInputsKey = "persisted_inputs";
/**
@@ -412,7 +418,7 @@ const persistedInputsKey = "persisted_inputs";
*/
export const persistInputs = function (env: Env = getEnv()) {
const entries = env.entries();
const inputEnvironmentVariables = entries.filter(([name]) =>
const inputEnvironmentVariables: PersistedInputs = entries.filter(([name]) =>
name.startsWith("INPUT_"),
);
core.saveState(persistedInputsKey, JSON.stringify(inputEnvironmentVariables));
@@ -421,18 +427,26 @@ export const persistInputs = function (env: Env = getEnv()) {
/**
* Restores all inputs to the action from the persisted state.
*/
export function restoreInputs(logger: Logger) {
export function restoreInputs(
action: ActionState<["Env", "Logger", "Actions"]>,
) {
try {
const persistedInputsValue = core.getState(persistedInputsKey);
const persistedInputsValue = action.actions.getState(persistedInputsKey);
if (persistedInputsValue) {
const persistedInputs = JSON.parse(persistedInputsValue);
const persistedInputs = JSON.parse(
persistedInputsValue,
) as PersistedInputs;
if (!Array.isArray(persistedInputs)) {
throw new Error("Restored value is not an array.");
}
for (const [name, value] of persistedInputs) {
process.env[name] = value;
action.env.set(name, value);
}
}
} catch (err) {
logger.error(`Unable to restore inputs: ${getErrorMessage(err)}`);
action.logger.error(`Unable to restore inputs: ${getErrorMessage(err)}`);
throw new Error(
"Failed to restore inputs from the state set by this action's main execution.",
);
+25 -8
View File
@@ -5,8 +5,12 @@
*/
import * as fs from "fs";
import * as core from "@actions/core";
import {
ActionName,
runInActions,
type Action,
type ActionState,
} from "./action-common";
import * as actionsUtil from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { getConfig } from "./config-utils";
@@ -16,22 +20,24 @@ import {
getJavaTempDependencyDir,
} from "./dependency-caching";
import { EnvVar } from "./environment";
import { getActionsLogger } from "./logging";
import { checkGitHubVersionInRange, getErrorMessage } from "./util";
export async function runWrapper() {
/**
* The main functionality of the `analyze` post-action is implemented by this function.
* This is called once by {@link runInActions} after it is called in {@link runWrapper}.
*/
export async function run(action: ActionState<["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 = action.logger;
try {
const logger = getActionsLogger();
actionsUtil.restoreInputs(logger);
const gitHubVersion = await getGitHubVersion();
checkGitHubVersionInRange(gitHubVersion, logger);
// Upload SARIF artifacts if we determine that this is a first-party analysis run.
// For third-party runs, this artifact will be uploaded in the `upload-sarif-post` step.
if (process.env[EnvVar.INIT_ACTION_HAS_RUN] === "true") {
if (action.env.getOptional(EnvVar.INIT_ACTION_HAS_RUN) === "true") {
const config = await getConfig(
actionsUtil.getTemporaryDirectory(),
logger,
@@ -63,8 +69,19 @@ export async function runWrapper() {
}
}
} catch (error) {
core.setFailed(
action.actions.setFailed(
`analyze post-action step failed: ${getErrorMessage(error)}`,
);
}
}
/** Defines the `analyze` post-action. */
const analyzePostAction: Action = {
name: ActionName.AnalyzePost,
run,
};
/** The main entry point for the `analyze` post-action. */
export async function runWrapper() {
await runInActions(analyzePostAction);
}
+9 -15
View File
@@ -4,15 +4,8 @@
* other `post:` hooks.
*/
import * as core from "@actions/core";
import { runInActions, type Action, type ActionState } from "./action-common";
import {
restoreInputs,
getOptionalInput,
getTemporaryDirectory,
printDebugLogs,
} from "./actions-util";
import { getTemporaryDirectory, printDebugLogs } from "./actions-util";
import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
import { CachingKind } from "./caching-utils";
import { getCodeQL } from "./codeql";
@@ -49,7 +42,11 @@ interface InitPostStatusReport
initActionPostHelper.JobStatusReport,
initActionPostHelper.DependencyCachingUsageReport {}
async function run(action: ActionState<["Base", "Logger", "Env"]>) {
/**
* The main functionality of the `init` post-action is implemented by this function.
* This is called once by {@link runInActions} after it is called in {@link runWrapper}.
*/
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 = action.logger;
@@ -64,10 +61,7 @@ async function run(action: ActionState<["Base", "Logger", "Env"]>) {
// Read the job status before restoring inputs, since it is provided by the Actions runtime
// environment for this step and would otherwise be overwritten by the value that the `init`
// Action saw, which is always a success.
const jobStatus = getOptionalInput("job-status");
// Restore inputs from `init` Action.
restoreInputs(logger);
const jobStatus = action.actions.getOptionalInput("job-status");
const tmpDir = getTemporaryDirectory();
config = await getConfig(tmpDir, logger);
@@ -89,7 +83,7 @@ async function run(action: ActionState<["Base", "Logger", "Env"]>) {
const features = initFeatures(
gitHubVersion,
repositoryNwo,
getTemporaryDirectory(),
config?.tempDir ?? tmpDir,
logger,
);
@@ -129,7 +123,7 @@ async function run(action: ActionState<["Base", "Logger", "Env"]>) {
}
} catch (unwrappedError) {
const error = wrapApiConfigurationError(wrapError(unwrappedError));
core.setFailed(error.message);
action.actions.setFailed(error.message);
const statusReportBase = await createStatusReportBase(
action.name,
+26 -37
View File
@@ -1,19 +1,13 @@
import * as core from "@actions/core";
import {
getActionVersion,
getOptionalInput,
getRequiredInput,
getTemporaryDirectory,
} from "./actions-util";
import { runInActions, type Action, type ActionState } from "./action-common";
import { getActionVersion, getTemporaryDirectory } from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { CliError } from "./cli-errors";
import { Config, getConfig } from "./config-utils";
import { getActionsLogger } from "./logging";
import { runResolveBuildEnvironment } from "./resolve-environment";
import {
sendStatusReport,
sendUnhandledErrorStatusReport,
createStatusReportBase,
getActionsStatus,
ActionName,
@@ -24,25 +18,28 @@ import {
checkForTimeout,
checkGitHubVersionInRange,
ConfigurationError,
getErrorMessage,
wrapError,
} from "./util";
const ENVIRONMENT_OUTPUT_NAME = "environment";
async function run(startedAt: Date) {
/**
* The main functionality of the `resolve-environment` action is implemented by this function.
* This is called once by {@link runInActions} after it is called in {@link runWrapper}.
*/
async function run(action: ActionState<["Base", "Logger", "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 logger = action.logger;
let config: Config | undefined;
try {
const statusReportBase = await createStatusReportBase(
ActionName.ResolveEnvironment,
action.name,
"starting",
startedAt,
action.startedAt,
config,
await checkDiskUsage(logger),
logger,
@@ -62,12 +59,13 @@ async function run(startedAt: Date) {
);
}
const workingDirectory = getOptionalInput("working-directory");
const workingDirectory =
action.actions.getOptionalInput("working-directory");
const result = await runResolveBuildEnvironment(
config.codeQLCmd,
logger,
workingDirectory,
getRequiredInput("language"),
action.actions.getRequiredInput("language"),
);
core.setOutput(ENVIRONMENT_OUTPUT_NAME, result);
} catch (unwrappedError) {
@@ -82,14 +80,14 @@ async function run(startedAt: Date) {
);
} else {
// For any other error types, something has more seriously gone wrong and we fail.
core.setFailed(
action.actions.setFailed(
`Failed to resolve a build environment suitable for automatically building your code. ${error.message}`,
);
const statusReportBase = await createStatusReportBase(
ActionName.ResolveEnvironment,
action.name,
getActionsStatus(error),
startedAt,
action.startedAt,
config,
await checkDiskUsage(logger),
logger,
@@ -105,9 +103,9 @@ async function run(startedAt: Date) {
}
const statusReportBase = await createStatusReportBase(
ActionName.ResolveEnvironment,
action.name,
"success",
startedAt,
action.startedAt,
config,
await checkDiskUsage(logger),
logger,
@@ -117,23 +115,14 @@ async function run(startedAt: Date) {
}
}
/** Defines the `resolve-environment` action. */
const resolveEnvironmentAction: Action = {
name: ActionName.ResolveEnvironment,
run,
};
/** The main entry point for the `resolve-environment` action. */
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(
`${ActionName.ResolveEnvironment} action failed: ${getErrorMessage(
error,
)}`,
);
await sendUnhandledErrorStatusReport(
ActionName.ResolveEnvironment,
startedAt,
error,
logger,
);
}
await runInActions(resolveEnvironmentAction);
await checkForTimeout();
}
+31 -16
View File
@@ -3,38 +3,41 @@
* It will run after the all steps in this job, in reverse order in relation to
* other `post:` hooks.
*/
import * as core from "@actions/core";
import {
ActionName,
runInActions,
type Action,
type ActionState,
} from "./action-common";
import * as actionsUtil from "./actions-util";
import { getGitHubVersion } from "./api-client";
import * as configUtils from "./config-utils";
import { uploadArtifacts } from "./debug-artifacts";
import { getActionsLogger } from "./logging";
import { getSafeErrorMessage } from "./start-proxy/status-report";
import { checkGitHubVersionInRange, getErrorMessage } from "./util";
export async function runWrapper() {
/**
* The main functionality of the `start-proxy` post-action is implemented by this function.
* This is called once by {@link runInActions} after it is called in {@link runWrapper}.
*/
export async function run(action: ActionState<["Logger", "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 logger = action.logger;
try {
// Restore inputs from `start-proxy` Action.
actionsUtil.restoreInputs(logger);
// Kill the running proxy
const pid = core.getState("proxy-process-pid");
const pid = action.actions.getState("proxy-process-pid");
if (pid) {
process.kill(Number(pid));
}
const config = await configUtils.getConfig(
actionsUtil.getTemporaryDirectory(),
logger,
);
const tempDir = actionsUtil.getTemporaryDirectory();
const config = await configUtils.getConfig(tempDir, logger);
if (config?.debugMode || core.isDebug()) {
const logFilePath = core.getState("proxy-log-file");
if (config?.debugMode || logger.isDebug()) {
const logFilePath = action.actions.getState("proxy-log-file");
logger.info(
"Debug mode is on. Uploading proxy log as Actions debugging artifact...",
);
@@ -44,7 +47,7 @@ export async function runWrapper() {
await uploadArtifacts(
logger,
[logFilePath],
actionsUtil.getTemporaryDirectory(),
tempDir,
"proxy-log-file",
gitHubVersion.type,
);
@@ -56,3 +59,15 @@ export async function runWrapper() {
);
}
}
/** Defines the `start-proxy` post-action. */
const startProxyPostAction: Action = {
name: ActionName.StartProxyPost,
run,
transformTelemetryError: getSafeErrorMessage,
};
/** The main entry point for the `start-proxy` post-action. */
export async function runWrapper() {
await runInActions(startProxyPostAction);
}
+14 -5
View File
@@ -52,32 +52,41 @@ import {
export enum ActionName {
Analyze = "finish",
AnalyzePost = "analyze-post",
Autobuild = "autobuild",
Init = "init",
InitPost = "init-post",
ResolveEnvironment = "resolve-environment",
SetupCodeQL = "setup-codeql",
StartProxy = "start-proxy",
StartProxyPost = "start-proxy-post",
UploadSarif = "upload-sarif",
UploadSarifPost = "upload-sarif-post",
}
/** A partial mapping from `ActionName` to names for actions whose display name is not the `ActionName` value. */
export const ACTION_DISPLAY_NAMES: Partial<Record<ActionName, string>> = {
[ActionName.Analyze]: "analyze",
[ActionName.InitPost]: "init post",
[ActionName.AnalyzePost]: "analyze",
[ActionName.InitPost]: "init",
[ActionName.StartProxyPost]: "start-proxy",
[ActionName.UploadSarifPost]: "upload-sarif",
};
/** Decides if {@link name} refers to a post-action. */
export function isPostAction(name: ActionName) {
return name.endsWith("-post");
}
/**
* Maps an {@link ActionName} to its display name. Usually that is the same, except
* for actions which have a different display name in {@link ACTION_DISPLAY_NAMES}.
*/
export function getDisplayActionName(actionName: ActionName): string {
const suffix = isPostAction(actionName) ? "post-action" : "action";
const displayName = ACTION_DISPLAY_NAMES[actionName];
if (displayName !== undefined) {
return displayName;
}
return actionName;
return `${displayName ?? actionName} ${suffix}`;
}
/**
+18 -1
View File
@@ -227,8 +227,13 @@ export function getTestEnv(testEnv: NodeJS.ProcessEnv = {}): Env {
return getEnv(testEnv);
}
/** An implementation of `ActionsEnv` for use in tests. */
/**
* An implementation of {@link ActionsEnv} for use in tests.
* The methods of instances of this class are intended to be stubbed.
*/
class TestActionsEnv implements ActionsEnv {
private failed: boolean = false;
constructor(private readonly env: Env) {}
public clone(env: Env): this {
@@ -242,6 +247,18 @@ class TestActionsEnv implements ActionsEnv {
public getOptionalInput(_name: string): string | undefined {
return undefined;
}
public setFailed(_message: string) {
this.failed = true;
}
get hasFailed() {
return this.failed;
}
public getState(name: string): string {
throw new Error(`State '${name}' requested but not set.`);
}
}
/**
+27 -12
View File
@@ -3,32 +3,36 @@
* It will run after the all steps in this job, in reverse order in relation to
* other `post:` hooks.
*/
import * as core from "@actions/core";
import * as actionsUtil from "./actions-util";
import {
ActionName,
runInActions,
type Action,
type ActionState,
} from "./action-common";
import { getGitHubVersion } from "./api-client";
import * as debugArtifacts from "./debug-artifacts";
import { EnvVar } from "./environment";
import { getActionsLogger, withGroup } from "./logging";
import { withGroup } from "./logging";
import { checkGitHubVersionInRange, getErrorMessage } from "./util";
export async function runWrapper() {
/**
* The main functionality of the `upload-sarif` post-action is implemented by this function.
* This is called once by {@link runInActions} after it is called in {@link runWrapper}.
*/
export async function run(action: ActionState<["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 = action.logger;
try {
const logger = getActionsLogger();
// Restore inputs from `upload-sarif` Action.
actionsUtil.restoreInputs(logger);
const gitHubVersion = await getGitHubVersion();
checkGitHubVersionInRange(gitHubVersion, logger);
// Upload SARIF artifacts if we determine that this is a third-party analysis run.
// For first-party runs, this artifact will be uploaded in the `analyze-post` step.
if (process.env[EnvVar.INIT_ACTION_HAS_RUN] !== "true") {
if (action.env.getOptional(EnvVar.INIT_ACTION_HAS_RUN) !== "true") {
if (gitHubVersion.type === undefined) {
core.warning(
logger.warning(
`Did not upload debug artifacts because cannot determine the GitHub variant running.`,
);
return;
@@ -38,8 +42,19 @@ export async function runWrapper() {
);
}
} catch (error) {
core.setFailed(
action.actions.setFailed(
`upload-sarif post-action step failed: ${getErrorMessage(error)}`,
);
}
}
/** Defines the `upload-sarif` post-action. */
const uploadSarifPostAction: Action = {
name: ActionName.UploadSarifPost,
run,
};
/** The main entry point for the `upload-sarif` post-action. */
export async function runWrapper() {
await runInActions(uploadSarifPostAction);
}