Merge pull request #3990 from github/mbg/action-state

Add `ActionState` type and make some initial use of it
This commit is contained in:
Michael B. Gale
2026-07-06 13:47:46 +00:00
committed by GitHub
11 changed files with 1523 additions and 1369 deletions

2386
lib/entry-points.js generated

File diff suppressed because it is too large Load Diff

95
src/action-common.ts Normal file
View File

@@ -0,0 +1,95 @@
import * as core from "@actions/core";
import { ActionsEnv, getActionsEnv } from "./actions-util";
import { Env } from "./environment";
import { FeatureEnablement } from "./feature-flags";
import { getActionsLogger, Logger } from "./logging";
import {
ActionName,
getDisplayActionName,
sendUnhandledErrorStatusReport,
} from "./status-report";
import { getEnv, getErrorMessage } from "./util";
/** Common state that is always available in `ActionState`. */
export interface BaseState {
/** The name of the Action. */
name: ActionName;
/** When the Action was started. */
startedAt: Date;
}
/** Describes different state features that an Action may have. */
export interface FeatureState {
Logger: {
/** The logger that is in use. */
logger: Logger;
};
Env: {
/** Information about environment variables. */
env: Env;
};
Actions: {
/** Access to Actions-related functionality. */
actions: ActionsEnv;
};
FeatureFlags: {
/** Information about enabled feature flags. */
features: FeatureEnablement;
};
}
/** Identifies a type of state an Action may have. */
export type StateFeature = keyof FeatureState;
/** Constructs the intersection of all state types identifies by `Fs`. */
export type FieldsOf<Fs extends readonly StateFeature[]> = Fs extends []
? BaseState
: Fs extends [
infer Head extends StateFeature,
...infer Tail extends readonly StateFeature[],
]
? FeatureState[Head] & FieldsOf<Tail>
: never;
/** Describes the state of an Action that has access to the state corresponding to `Fs`. */
export type ActionState<Fs extends readonly StateFeature[]> = FieldsOf<Fs>;
/** The type of an Action's main entry point. This is a function that is provided
* with a basic `ActionState` object with features that are always available.
* Each Action can then augment the `state` further if additional features are required.
*/
export type ActionMain = (
state: ActionState<["Logger", "Env", "Actions"]>,
) => Promise<void>;
/** A specification for a CodeQL Action step. */
export interface Action {
/** The name of the Action. */
name: ActionName;
/** The entry point for the Action. */
run: ActionMain;
}
/** A generic entry point that sets up the basic environment for the `action` and runs it. */
export async function runInActions(action: Action) {
const startedAt = new Date();
const logger = getActionsLogger();
const env = getEnv();
const actionsEnv = getActionsEnv();
try {
await action.run({
name: action.name,
startedAt,
logger,
env,
actions: actionsEnv,
});
} catch (error) {
core.setFailed(
`${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error)}`,
);
await sendUnhandledErrorStatusReport(action.name, startedAt, error, logger);
}
}

View File

@@ -4,6 +4,7 @@ import { performance } from "perf_hooks";
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 * as analyses from "./analyses"; import * as analyses from "./analyses";
import { import {
@@ -40,7 +41,6 @@ import {
createStatusReportBase, createStatusReportBase,
DatabaseCreationTimings, DatabaseCreationTimings,
getActionsStatus, getActionsStatus,
sendUnhandledErrorStatusReport,
StatusReportBase, StatusReportBase,
} from "./status-report"; } from "./status-report";
import { import {
@@ -212,7 +212,7 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
await runAutobuild(config, BuiltInLanguage.go, logger); await runAutobuild(config, BuiltInLanguage.go, logger);
} }
async function run(startedAt: Date) { async function run({ startedAt, logger }: ActionState<["Logger"]>) {
// 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.
@@ -228,7 +228,6 @@ async function run(startedAt: Date) {
let didUploadTrapCaches = false; let didUploadTrapCaches = false;
let dependencyCacheResults: DependencyCacheUploadStatusReport | undefined; let dependencyCacheResults: DependencyCacheUploadStatusReport | undefined;
let databaseUploadResults: DatabaseUploadResult[] = []; let databaseUploadResults: DatabaseUploadResult[] = [];
const logger = getActionsLogger();
try { try {
util.initializeEnvironment(actionsUtil.getActionVersion()); util.initializeEnvironment(actionsUtil.getActionVersion());
@@ -523,19 +522,13 @@ async function run(startedAt: Date) {
} }
} }
/** Defines the `analyze` Action. */
const analyze: Action = {
name: ActionName.Analyze,
run,
};
export async function runWrapper() { export async function runWrapper() {
const startedAt = new Date(); await runInActions(analyze);
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(`analyze action failed: ${util.getErrorMessage(error)}`);
await sendUnhandledErrorStatusReport(
ActionName.Analyze,
startedAt,
error,
logger,
);
}
await util.checkForTimeout(); await util.checkForTimeout();
} }

View File

@@ -1,5 +1,6 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import { Action, ActionState, runInActions } from "./action-common";
import { import {
getActionVersion, getActionVersion,
getOptionalInput, getOptionalInput,
@@ -11,13 +12,12 @@ import { getCodeQL } from "./codeql";
import { Config, getConfig } from "./config-utils"; import { Config, getConfig } from "./config-utils";
import { EnvVar } from "./environment"; import { EnvVar } from "./environment";
import { Language } from "./languages"; import { Language } from "./languages";
import { Logger, getActionsLogger } from "./logging"; import { Logger } from "./logging";
import { import {
StatusReportBase, StatusReportBase,
getActionsStatus, getActionsStatus,
createStatusReportBase, createStatusReportBase,
sendStatusReport, sendStatusReport,
sendUnhandledErrorStatusReport,
ActionName, ActionName,
} from "./status-report"; } from "./status-report";
import { endTracingForCluster } from "./tracer-config"; import { endTracingForCluster } from "./tracer-config";
@@ -26,7 +26,6 @@ import {
checkDiskUsage, checkDiskUsage,
checkGitHubVersionInRange, checkGitHubVersionInRange,
ConfigurationError, ConfigurationError,
getErrorMessage,
initializeEnvironment, initializeEnvironment,
wrapError, wrapError,
} from "./util"; } from "./util";
@@ -69,11 +68,10 @@ async function sendCompletedStatusReport(
} }
} }
async function run(startedAt: Date) { async function run({ startedAt, logger }: ActionState<["Logger"]>) {
// 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 logger = getActionsLogger();
let config: Config | undefined; let config: Config | undefined;
let currentLanguage: Language | undefined; let currentLanguage: Language | undefined;
let languages: Language[] | undefined; let languages: Language[] | undefined;
@@ -142,18 +140,12 @@ async function run(startedAt: Date) {
await sendCompletedStatusReport(config, logger, startedAt, languages ?? []); await sendCompletedStatusReport(config, logger, startedAt, languages ?? []);
} }
/** Defines the `autobuild` Action. */
const autobuild: Action = {
name: ActionName.Autobuild,
run,
};
export async function runWrapper() { export async function runWrapper() {
const startedAt = new Date(); await runInActions(autobuild);
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(`autobuild action failed. ${getErrorMessage(error)}`);
await sendUnhandledErrorStatusReport(
ActionName.Autobuild,
startedAt,
error,
logger,
);
}
} }

View File

@@ -1,22 +1,19 @@
import test from "ava"; import test from "ava";
import sinon from "sinon"; import sinon from "sinon";
import { Feature } from "../feature-flags";
import { RepositoryPropertyName } from "../feature-flags/properties"; import { RepositoryPropertyName } from "../feature-flags/properties";
import { import { callee, setupTests } from "../testing-utils";
getTestActionsEnv,
RecordingLogger,
setupTests,
} from "../testing-utils";
import { getConfigFileInput } from "./file"; import { getConfigFileInput } from "./file";
setupTests(test); setupTests(test);
test("getConfigFileInput returns undefined by default", async (t) => { test("getConfigFileInput returns undefined by default", async (t) => {
const logger = new RecordingLogger(); await callee(getConfigFileInput)
const actionsEnv = getTestActionsEnv(); .withArgs({})
const result = getConfigFileInput(logger, actionsEnv, {}, true); .withFeatures([Feature.ConfigFileRepositoryProperty])
t.is(result, undefined); .passes(async (fn) => t.is(await fn(), undefined));
}); });
const repositoryProperties = { const repositoryProperties = {
@@ -24,9 +21,12 @@ const repositoryProperties = {
}; };
test("getConfigFileInput returns input value", async (t) => { test("getConfigFileInput returns input value", async (t) => {
const logger = new RecordingLogger();
const actionsEnv = getTestActionsEnv();
const testInput = "/some/path"; const testInput = "/some/path";
const target = callee(getConfigFileInput).withFeatures([
Feature.ConfigFileRepositoryProperty,
]);
const actionsEnv = target.getState().actions;
sinon sinon
.stub(actionsEnv, "getOptionalInput") .stub(actionsEnv, "getOptionalInput")
.withArgs("config-file") .withArgs("config-file")
@@ -34,76 +34,63 @@ test("getConfigFileInput returns input value", async (t) => {
// Even though both an input and repository property are configured, // Even though both an input and repository property are configured,
// we prefer the direct input to the Action. // we prefer the direct input to the Action.
const result = getConfigFileInput( const targetWithArgs = target
logger, .withActions(actionsEnv)
actionsEnv, .withArgs(repositoryProperties);
repositoryProperties, await targetWithArgs.passes(async (fn) => t.is(await fn(), testInput));
true,
);
t.is(result, testInput);
// Check for the expected log message.
t.true(logger.hasMessage("Using configuration file input from workflow"));
});
test("getConfigFileInput returns repository property value", async (t) => {
const logger = new RecordingLogger();
const actionsEnv = getTestActionsEnv();
// Since there is no direct input, we should use the repository property.
const result = getConfigFileInput(
logger,
actionsEnv,
repositoryProperties,
true,
);
t.is(result, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
// Check for the expected log message. // Check for the expected log message.
t.true( t.true(
logger.hasMessage( targetWithArgs
"Using configuration file input from repository property", .getLogger()
), .hasMessage("Using configuration file input from workflow"),
);
});
test("getConfigFileInput returns repository property value", async (t) => {
// Since there is no direct input, we should use the repository property.
const target = callee(getConfigFileInput)
.withFeatures([Feature.ConfigFileRepositoryProperty])
.withArgs(repositoryProperties);
await target.passes(async (fn) =>
t.is(await fn(), repositoryProperties[RepositoryPropertyName.CONFIG_FILE]),
);
// Check for the expected log message.
t.true(
target
.getLogger()
.hasMessage("Using configuration file input from repository property"),
); );
}); });
test("getConfigFileInput ignores empty repository property value", async (t) => { test("getConfigFileInput ignores empty repository property value", async (t) => {
const logger = new RecordingLogger();
const actionsEnv = getTestActionsEnv();
// Since the repository property value is an empty/whitespace string, we should ignore it. // Since the repository property value is an empty/whitespace string, we should ignore it.
const result = getConfigFileInput( await callee(getConfigFileInput)
logger, .withFeatures([Feature.ConfigFileRepositoryProperty])
actionsEnv, .withArgs({ [RepositoryPropertyName.CONFIG_FILE]: " " })
{ .passes(async (fn) => t.is(await fn(), undefined));
[RepositoryPropertyName.CONFIG_FILE]: " ",
},
true,
);
t.is(result, undefined);
}); });
test("getConfigFileInput ignores repository property value when FF is off", async (t) => { test("getConfigFileInput ignores repository property value when FF is off", async (t) => {
const logger = new RecordingLogger();
const actionsEnv = getTestActionsEnv();
// Since the FF is off, we should ignore the repository property value. // Since the FF is off, we should ignore the repository property value.
const result = getConfigFileInput( const target = callee(getConfigFileInput)
logger, .withFeatures([])
actionsEnv, .withArgs(repositoryProperties);
repositoryProperties,
false, await target.passes(async (fn) => t.is(await fn(), undefined));
);
t.is(result, undefined);
t.false( t.false(
logger.hasMessage( target
"Using configuration file input from repository property", .getLogger()
), .hasMessage("Using configuration file input from repository property"),
); );
t.true( t.true(
logger.hasMessage( target
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.", .getLogger()
), .hasMessage(
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
),
); );
}); });

View File

@@ -1,19 +1,21 @@
import { ActionsEnv } from "../actions-util"; import { ActionState } from "../action-common";
import { Feature } from "../feature-flags";
import { import {
RepositoryProperties, RepositoryProperties,
RepositoryPropertyName, RepositoryPropertyName,
} from "../feature-flags/properties"; } from "../feature-flags/properties";
import { Logger } from "../logging";
/** /**
* Gets the value that is configured for the configuration file, if any. * Gets the value that is configured for the configuration file, if any.
*/ */
export function getConfigFileInput( export async function getConfigFileInput(
logger: Logger, {
actions: ActionsEnv, logger,
actions,
features,
}: ActionState<["Logger", "Actions", "FeatureFlags"]>,
repositoryProperties: Partial<RepositoryProperties>, repositoryProperties: Partial<RepositoryProperties>,
useRepositoryProperty: boolean, ): Promise<string | undefined> {
): string | undefined {
const input = actions.getOptionalInput("config-file"); const input = actions.getOptionalInput("config-file");
if (input !== undefined) { if (input !== undefined) {
@@ -26,6 +28,10 @@ export function getConfigFileInput(
if (propertyValue !== undefined && propertyValue.trim().length > 0) { if (propertyValue !== undefined && propertyValue.trim().length > 0) {
// Only use the repository property value if the FF is enabled. // Only use the repository property value if the FF is enabled.
const useRepositoryProperty = await features.getValue(
Feature.ConfigFileRepositoryProperty,
);
if (useRepositoryProperty) { if (useRepositoryProperty) {
logger.info( logger.info(
`Using configuration file input from repository property: ${propertyValue}`, `Using configuration file input from repository property: ${propertyValue}`,

View File

@@ -7,9 +7,9 @@ import * as io from "@actions/io";
import * as semver from "semver"; import * as semver from "semver";
import { v4 as uuidV4 } from "uuid"; import { v4 as uuidV4 } from "uuid";
import { Action, ActionState, runInActions } from "./action-common";
import { import {
FileCmdNotFoundError, FileCmdNotFoundError,
getActionsEnv,
getActionVersion, getActionVersion,
getFileType, getFileType,
getOptionalInput, getOptionalInput,
@@ -56,7 +56,7 @@ import {
runDatabaseInitCluster, runDatabaseInitCluster,
} from "./init"; } from "./init";
import { JavaEnvVars, BuiltInLanguage } from "./languages"; import { JavaEnvVars, BuiltInLanguage } from "./languages";
import { getActionsLogger, Logger, withGroupAsync } from "./logging"; import { Logger, withGroupAsync } from "./logging";
import { import {
downloadOverlayBaseDatabaseFromCache, downloadOverlayBaseDatabaseFromCache,
OverlayBaseDatabaseDownloadStats, OverlayBaseDatabaseDownloadStats,
@@ -73,7 +73,6 @@ import {
createStatusReportBase, createStatusReportBase,
getActionsStatus, getActionsStatus,
sendStatusReport, sendStatusReport,
sendUnhandledErrorStatusReport,
} from "./status-report"; } from "./status-report";
import { ZstdAvailability } from "./tar"; import { ZstdAvailability } from "./tar";
import { ToolsDownloadStatusReport } from "./tools-download"; import { ToolsDownloadStatusReport } from "./tools-download";
@@ -204,12 +203,12 @@ async function sendCompletedStatusReport(
} }
} }
async function run(startedAt: Date) { async function run(actionState: ActionState<["Logger", "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 logger = getActionsLogger(); const startedAt = actionState.startedAt;
const actionsEnv = getActionsEnv(); const logger = actionState.logger;
let apiDetails: GitHubApiCombinedDetails; let apiDetails: GitHubApiCombinedDetails;
let config: configUtils.Config | undefined; let config: configUtils.Config | undefined;
@@ -263,14 +262,9 @@ async function run(startedAt: Date) {
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true"); core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
const useConfigFileProperty = await features.getValue( configFile = await getConfigFileInput(
Feature.ConfigFileRepositoryProperty, { ...actionState, features },
);
configFile = getConfigFileInput(
logger,
actionsEnv,
repositoryProperties, repositoryProperties,
useConfigFileProperty,
); );
// path.resolve() respects the intended semantics of source-root. If // path.resolve() respects the intended semantics of source-root. If
@@ -854,19 +848,13 @@ async function recordZstdAvailability(
); );
} }
/** Defines the `init` Action. */
const init: Action = {
name: ActionName.Init,
run,
};
export async function runWrapper() { export async function runWrapper() {
const startedAt = new Date(); await runInActions(init);
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(`init action failed: ${getErrorMessage(error)}`);
await sendUnhandledErrorStatusReport(
ActionName.Init,
startedAt,
error,
logger,
);
}
await checkForTimeout(); await checkForTimeout();
} }

View File

@@ -1,6 +1,7 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import { v4 as uuidV4 } from "uuid"; import { v4 as uuidV4 } from "uuid";
import { Action, ActionState, runInActions } from "./action-common";
import { import {
getActionVersion, getActionVersion,
getOptionalInput, getOptionalInput,
@@ -14,7 +15,7 @@ import { getRawLanguagesNoAutodetect } from "./config-utils";
import { EnvVar } from "./environment"; import { EnvVar } from "./environment";
import { initFeatures } from "./feature-flags"; import { initFeatures } from "./feature-flags";
import { initCodeQL } from "./init"; import { initCodeQL } from "./init";
import { getActionsLogger, Logger } from "./logging"; import { Logger } from "./logging";
import { getRepositoryNwo } from "./repository"; import { getRepositoryNwo } from "./repository";
import { ToolsSource } from "./setup-codeql"; import { ToolsSource } from "./setup-codeql";
import { import {
@@ -24,7 +25,6 @@ import {
createStatusReportBase, createStatusReportBase,
getActionsStatus, getActionsStatus,
sendStatusReport, sendStatusReport,
sendUnhandledErrorStatusReport,
} from "./status-report"; } from "./status-report";
import { ToolsDownloadStatusReport } from "./tools-download"; import { ToolsDownloadStatusReport } from "./tools-download";
import { import {
@@ -36,7 +36,6 @@ import {
ConfigurationError, ConfigurationError,
wrapError, wrapError,
checkActionVersion, checkActionVersion,
getErrorMessage,
} from "./util"; } from "./util";
/** /**
@@ -88,12 +87,13 @@ async function sendCompletedStatusReport(
} }
/** The main behaviour of this action. */ /** The main behaviour of this action. */
async function run(startedAt: Date): Promise<void> { async function run({
startedAt,
logger,
}: ActionState<["Logger"]>): 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.
const logger = getActionsLogger();
let codeql: CodeQL; let codeql: CodeQL;
let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined; let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined;
let toolsFeatureFlagsValid: boolean | undefined; let toolsFeatureFlagsValid: boolean | undefined;
@@ -195,20 +195,14 @@ async function run(startedAt: Date): Promise<void> {
); );
} }
/** Defines the `setup-codeql` Action. */
const setupCodeQL: Action = {
name: ActionName.SetupCodeQL,
run,
};
/** Run the action and catch any unhandled errors. */ /** Run the action and catch any unhandled errors. */
export async function runWrapper(): Promise<void> { export async function runWrapper(): Promise<void> {
const startedAt = new Date(); await runInActions(setupCodeQL);
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(`setup-codeql action failed: ${getErrorMessage(error)}`);
await sendUnhandledErrorStatusReport(
ActionName.SetupCodeQL,
startedAt,
error,
logger,
);
}
await checkForTimeout(); await checkForTimeout();
} }

View File

@@ -46,6 +46,17 @@ export enum ActionName {
UploadSarif = "upload-sarif", UploadSarif = "upload-sarif",
} }
/**
* Maps an `ActionName` to its display name. Usually that is the same, except
* for `ActionName.Analyze` where it is `"analyze"` instead of `"finish"`.
*/
export function getDisplayActionName(actionName: ActionName): string {
if (actionName === ActionName.Analyze) {
return "analyze";
}
return actionName;
}
/** /**
* @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

@@ -10,6 +10,7 @@ import test, {
import nock from "nock"; import nock from "nock";
import * as sinon from "sinon"; import * as sinon from "sinon";
import { ActionState, StateFeature } from "./action-common";
import { ActionsEnv, ActionsEnvVars, getActionVersion } from "./actions-util"; import { ActionsEnv, ActionsEnvVars, getActionVersion } from "./actions-util";
import { AnalysisKind } from "./analyses"; import { AnalysisKind } from "./analyses";
import * as apiClient from "./api-client"; import * as apiClient from "./api-client";
@@ -27,6 +28,7 @@ import {
} from "./feature-flags"; } from "./feature-flags";
import { Logger } from "./logging"; import { Logger } from "./logging";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import { ActionName } from "./status-report";
import { import {
DEFAULT_DEBUG_ARTIFACT_NAME, DEFAULT_DEBUG_ARTIFACT_NAME,
DEFAULT_DEBUG_DATABASE_NAME, DEFAULT_DEBUG_DATABASE_NAME,
@@ -188,6 +190,108 @@ export function getTestActionsEnv(): ActionsEnv {
}; };
} }
/** For testing purposes, we make all available state features accessible in `TestEnv`. */
type AllState = ["Logger", "Env", "Actions", "FeatureFlags"];
/**
* Wraps a function that accepts an `ActionState` for testing in different environments.
*/
export class TestEnv<
Args extends readonly any[],
R,
Fs extends ReadonlyArray<AllState[number]>,
> {
private readonly fn: (state: ActionState<Fs>, ...args: Args) => R;
private args?: Args;
private logger: RecordingLogger;
private state: ActionState<AllState>;
constructor(
fn: (state: ActionState<Fs>, ...args: Args) => R,
cloneFrom?: TestEnv<Args, R, Fs>,
) {
this.fn = fn;
this.args = cloneFrom?.args;
this.logger = new RecordingLogger();
this.state =
cloneFrom !== undefined
? { ...cloneFrom.state, logger: this.logger }
: {
name: ActionName.Init,
startedAt: new Date(),
logger: this.logger,
env: getTestEnv(),
actions: getTestActionsEnv(),
features: createFeatures([]),
};
}
private clone(): TestEnv<Args, R, Fs> {
return new TestEnv(this.fn, this);
}
public getLogger(): RecordingLogger {
return this.logger;
}
public getState(): ActionState<AllState> {
return this.state;
}
public getArgs(): Args | undefined {
return this.args;
}
public withArgs(...args: Args) {
const result = this.clone();
result.args = args;
return result;
}
public withFeatures(enabled: Feature[]): TestEnv<Args, R, Fs> {
const result = this.clone();
result.state.features = createFeatures(enabled);
return result;
}
public withEnv(env: Env): TestEnv<Args, R, Fs> {
const result = this.clone();
result.state.env = env;
return result;
}
public withActions(actions: ActionsEnv): TestEnv<Args, R, Fs> {
const result = this.clone();
result.state.actions = actions;
return result;
}
call(): R {
if (!this.args) {
throw new Error("Trying to call function in TestEnv without arguments.");
}
return this.fn(this.state as unknown as ActionState<Fs>, ...this.args);
}
public passes<T>(
assertion: (makeCall: () => R) => T | Promise<T>,
): T | Promise<T> {
return assertion(() => {
const result = this.call();
return result;
});
}
}
/** Utility function to construct a `TestEnv`. */
export function callee<
Args extends readonly any[],
R,
Fs extends readonly StateFeature[],
>(fn: (state: ActionState<Fs>, ...args: Args) => R): TestEnv<Args, R, Fs> {
return new TestEnv(fn);
}
/** /**
* Default values for environment variables typically set in an Actions * Default values for environment variables typically set in an Actions
* environment. Tests can override individual variables by passing them in the * environment. Tests can override individual variables by passing them in the

View File

@@ -1,17 +1,17 @@
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 { getActionVersion, getTemporaryDirectory } from "./actions-util"; import { getActionVersion, getTemporaryDirectory } from "./actions-util";
import * as analyses from "./analyses"; import * as analyses from "./analyses";
import { getGitHubVersion } from "./api-client"; import { getGitHubVersion } from "./api-client";
import { initFeatures } from "./feature-flags"; import { initFeatures } from "./feature-flags";
import { Logger, getActionsLogger } from "./logging"; import { Logger } from "./logging";
import { getRepositoryNwo } from "./repository"; import { getRepositoryNwo } from "./repository";
import { InvalidSarifUploadError } from "./sarif"; import { InvalidSarifUploadError } from "./sarif";
import { import {
createStatusReportBase, createStatusReportBase,
sendStatusReport, sendStatusReport,
sendUnhandledErrorStatusReport,
StatusReportBase, StatusReportBase,
getActionsStatus, getActionsStatus,
ActionName, ActionName,
@@ -23,7 +23,6 @@ import {
ConfigurationError, ConfigurationError,
checkActionVersion, checkActionVersion,
checkDiskUsage, checkDiskUsage,
getErrorMessage,
initializeEnvironment, initializeEnvironment,
shouldSkipSarifUpload, shouldSkipSarifUpload,
wrapError, wrapError,
@@ -55,12 +54,9 @@ async function sendSuccessStatusReport(
} }
} }
async function run(startedAt: Date) { async function run({ startedAt, logger }: ActionState<["Logger"]>) {
// 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 logger = getActionsLogger();
try { try {
initializeEnvironment(getActionVersion()); initializeEnvironment(getActionVersion());
@@ -165,20 +161,12 @@ async function run(startedAt: Date) {
} }
} }
/** Defines the `upload-sarif` Action. */
const uploadSarif: Action = {
name: ActionName.UploadSarif,
run,
};
export async function runWrapper() { export async function runWrapper() {
const startedAt = new Date(); await runInActions(uploadSarif);
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(
`codeql/upload-sarif action failed: ${getErrorMessage(error)}`,
);
await sendUnhandledErrorStatusReport(
ActionName.UploadSarif,
startedAt,
error,
logger,
);
}
} }