mirror of
https://github.com/github/codeql-action.git
synced 2026-10-03 17:41:28 +00:00
Merge pull request #3990 from github/mbg/action-state
Add `ActionState` type and make some initial use of it
This commit is contained in:
2386
lib/entry-points.js
generated
2386
lib/entry-points.js
generated
File diff suppressed because it is too large
Load Diff
95
src/action-common.ts
Normal file
95
src/action-common.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { performance } from "perf_hooks";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import {
|
||||
@@ -40,7 +41,6 @@ import {
|
||||
createStatusReportBase,
|
||||
DatabaseCreationTimings,
|
||||
getActionsStatus,
|
||||
sendUnhandledErrorStatusReport,
|
||||
StatusReportBase,
|
||||
} from "./status-report";
|
||||
import {
|
||||
@@ -212,7 +212,7 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: 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
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
@@ -228,7 +228,6 @@ async function run(startedAt: Date) {
|
||||
let didUploadTrapCaches = false;
|
||||
let dependencyCacheResults: DependencyCacheUploadStatusReport | undefined;
|
||||
let databaseUploadResults: DatabaseUploadResult[] = [];
|
||||
const logger = getActionsLogger();
|
||||
|
||||
try {
|
||||
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() {
|
||||
const startedAt = new Date();
|
||||
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 runInActions(analyze);
|
||||
await util.checkForTimeout();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
getActionVersion,
|
||||
getOptionalInput,
|
||||
@@ -11,13 +12,12 @@ import { getCodeQL } from "./codeql";
|
||||
import { Config, getConfig } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { Language } from "./languages";
|
||||
import { Logger, getActionsLogger } from "./logging";
|
||||
import { Logger } from "./logging";
|
||||
import {
|
||||
StatusReportBase,
|
||||
getActionsStatus,
|
||||
createStatusReportBase,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
ActionName,
|
||||
} from "./status-report";
|
||||
import { endTracingForCluster } from "./tracer-config";
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
checkDiskUsage,
|
||||
checkGitHubVersionInRange,
|
||||
ConfigurationError,
|
||||
getErrorMessage,
|
||||
initializeEnvironment,
|
||||
wrapError,
|
||||
} 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
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
let config: Config | undefined;
|
||||
let currentLanguage: Language | undefined;
|
||||
let languages: Language[] | undefined;
|
||||
@@ -142,18 +140,12 @@ async function run(startedAt: Date) {
|
||||
await sendCompletedStatusReport(config, logger, startedAt, languages ?? []);
|
||||
}
|
||||
|
||||
/** Defines the `autobuild` Action. */
|
||||
const autobuild: Action = {
|
||||
name: ActionName.Autobuild,
|
||||
run,
|
||||
};
|
||||
|
||||
export async function runWrapper() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`autobuild action failed. ${getErrorMessage(error)}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.Autobuild,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(autobuild);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import test from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { Feature } from "../feature-flags";
|
||||
import { RepositoryPropertyName } from "../feature-flags/properties";
|
||||
import {
|
||||
getTestActionsEnv,
|
||||
RecordingLogger,
|
||||
setupTests,
|
||||
} from "../testing-utils";
|
||||
import { callee, setupTests } from "../testing-utils";
|
||||
|
||||
import { getConfigFileInput } from "./file";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test("getConfigFileInput returns undefined by default", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
const result = getConfigFileInput(logger, actionsEnv, {}, true);
|
||||
t.is(result, undefined);
|
||||
await callee(getConfigFileInput)
|
||||
.withArgs({})
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.passes(async (fn) => t.is(await fn(), undefined));
|
||||
});
|
||||
|
||||
const repositoryProperties = {
|
||||
@@ -24,9 +21,12 @@ const repositoryProperties = {
|
||||
};
|
||||
|
||||
test("getConfigFileInput returns input value", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
const testInput = "/some/path";
|
||||
const target = callee(getConfigFileInput).withFeatures([
|
||||
Feature.ConfigFileRepositoryProperty,
|
||||
]);
|
||||
|
||||
const actionsEnv = target.getState().actions;
|
||||
sinon
|
||||
.stub(actionsEnv, "getOptionalInput")
|
||||
.withArgs("config-file")
|
||||
@@ -34,76 +34,63 @@ test("getConfigFileInput returns input value", async (t) => {
|
||||
|
||||
// Even though both an input and repository property are configured,
|
||||
// we prefer the direct input to the Action.
|
||||
const result = getConfigFileInput(
|
||||
logger,
|
||||
actionsEnv,
|
||||
repositoryProperties,
|
||||
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]);
|
||||
const targetWithArgs = target
|
||||
.withActions(actionsEnv)
|
||||
.withArgs(repositoryProperties);
|
||||
await targetWithArgs.passes(async (fn) => t.is(await fn(), testInput));
|
||||
|
||||
// Check for the expected log message.
|
||||
t.true(
|
||||
logger.hasMessage(
|
||||
"Using configuration file input from repository property",
|
||||
),
|
||||
targetWithArgs
|
||||
.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) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
|
||||
// Since the repository property value is an empty/whitespace string, we should ignore it.
|
||||
const result = getConfigFileInput(
|
||||
logger,
|
||||
actionsEnv,
|
||||
{
|
||||
[RepositoryPropertyName.CONFIG_FILE]: " ",
|
||||
},
|
||||
true,
|
||||
);
|
||||
t.is(result, undefined);
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withArgs({ [RepositoryPropertyName.CONFIG_FILE]: " " })
|
||||
.passes(async (fn) => t.is(await fn(), undefined));
|
||||
});
|
||||
|
||||
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.
|
||||
const result = getConfigFileInput(
|
||||
logger,
|
||||
actionsEnv,
|
||||
repositoryProperties,
|
||||
false,
|
||||
);
|
||||
t.is(result, undefined);
|
||||
const target = callee(getConfigFileInput)
|
||||
.withFeatures([])
|
||||
.withArgs(repositoryProperties);
|
||||
|
||||
await target.passes(async (fn) => t.is(await fn(), undefined));
|
||||
|
||||
t.false(
|
||||
logger.hasMessage(
|
||||
"Using configuration file input from repository property",
|
||||
),
|
||||
target
|
||||
.getLogger()
|
||||
.hasMessage("Using configuration file input from repository property"),
|
||||
);
|
||||
t.true(
|
||||
logger.hasMessage(
|
||||
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
|
||||
),
|
||||
target
|
||||
.getLogger()
|
||||
.hasMessage(
|
||||
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { ActionsEnv } from "../actions-util";
|
||||
import { ActionState } from "../action-common";
|
||||
import { Feature } from "../feature-flags";
|
||||
import {
|
||||
RepositoryProperties,
|
||||
RepositoryPropertyName,
|
||||
} from "../feature-flags/properties";
|
||||
import { Logger } from "../logging";
|
||||
|
||||
/**
|
||||
* Gets the value that is configured for the configuration file, if any.
|
||||
*/
|
||||
export function getConfigFileInput(
|
||||
logger: Logger,
|
||||
actions: ActionsEnv,
|
||||
export async function getConfigFileInput(
|
||||
{
|
||||
logger,
|
||||
actions,
|
||||
features,
|
||||
}: ActionState<["Logger", "Actions", "FeatureFlags"]>,
|
||||
repositoryProperties: Partial<RepositoryProperties>,
|
||||
useRepositoryProperty: boolean,
|
||||
): string | undefined {
|
||||
): Promise<string | undefined> {
|
||||
const input = actions.getOptionalInput("config-file");
|
||||
|
||||
if (input !== undefined) {
|
||||
@@ -26,6 +28,10 @@ export function getConfigFileInput(
|
||||
|
||||
if (propertyValue !== undefined && propertyValue.trim().length > 0) {
|
||||
// Only use the repository property value if the FF is enabled.
|
||||
const useRepositoryProperty = await features.getValue(
|
||||
Feature.ConfigFileRepositoryProperty,
|
||||
);
|
||||
|
||||
if (useRepositoryProperty) {
|
||||
logger.info(
|
||||
`Using configuration file input from repository property: ${propertyValue}`,
|
||||
|
||||
@@ -7,9 +7,9 @@ import * as io from "@actions/io";
|
||||
import * as semver from "semver";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
FileCmdNotFoundError,
|
||||
getActionsEnv,
|
||||
getActionVersion,
|
||||
getFileType,
|
||||
getOptionalInput,
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
runDatabaseInitCluster,
|
||||
} from "./init";
|
||||
import { JavaEnvVars, BuiltInLanguage } from "./languages";
|
||||
import { getActionsLogger, Logger, withGroupAsync } from "./logging";
|
||||
import { Logger, withGroupAsync } from "./logging";
|
||||
import {
|
||||
downloadOverlayBaseDatabaseFromCache,
|
||||
OverlayBaseDatabaseDownloadStats,
|
||||
@@ -73,7 +73,6 @@ import {
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
} from "./status-report";
|
||||
import { ZstdAvailability } from "./tar";
|
||||
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
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
const actionsEnv = getActionsEnv();
|
||||
const startedAt = actionState.startedAt;
|
||||
const logger = actionState.logger;
|
||||
|
||||
let apiDetails: GitHubApiCombinedDetails;
|
||||
let config: configUtils.Config | undefined;
|
||||
@@ -263,14 +262,9 @@ async function run(startedAt: Date) {
|
||||
|
||||
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
|
||||
|
||||
const useConfigFileProperty = await features.getValue(
|
||||
Feature.ConfigFileRepositoryProperty,
|
||||
);
|
||||
configFile = getConfigFileInput(
|
||||
logger,
|
||||
actionsEnv,
|
||||
configFile = await getConfigFileInput(
|
||||
{ ...actionState, features },
|
||||
repositoryProperties,
|
||||
useConfigFileProperty,
|
||||
);
|
||||
|
||||
// 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() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`init action failed: ${getErrorMessage(error)}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.Init,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(init);
|
||||
await checkForTimeout();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as core from "@actions/core";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
getActionVersion,
|
||||
getOptionalInput,
|
||||
@@ -14,7 +15,7 @@ import { getRawLanguagesNoAutodetect } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { initFeatures } from "./feature-flags";
|
||||
import { initCodeQL } from "./init";
|
||||
import { getActionsLogger, Logger } from "./logging";
|
||||
import { Logger } from "./logging";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import { ToolsSource } from "./setup-codeql";
|
||||
import {
|
||||
@@ -24,7 +25,6 @@ import {
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
} from "./status-report";
|
||||
import { ToolsDownloadStatusReport } from "./tools-download";
|
||||
import {
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
ConfigurationError,
|
||||
wrapError,
|
||||
checkActionVersion,
|
||||
getErrorMessage,
|
||||
} from "./util";
|
||||
|
||||
/**
|
||||
@@ -88,12 +87,13 @@ async function sendCompletedStatusReport(
|
||||
}
|
||||
|
||||
/** 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
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
|
||||
let codeql: CodeQL;
|
||||
let toolsDownloadStatusReport: ToolsDownloadStatusReport | 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. */
|
||||
export async function runWrapper(): Promise<void> {
|
||||
const startedAt = new Date();
|
||||
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 runInActions(setupCodeQL);
|
||||
await checkForTimeout();
|
||||
}
|
||||
|
||||
@@ -46,6 +46,17 @@ export enum ActionName {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -10,6 +10,7 @@ import test, {
|
||||
import nock from "nock";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import { ActionState, StateFeature } from "./action-common";
|
||||
import { ActionsEnv, ActionsEnvVars, getActionVersion } from "./actions-util";
|
||||
import { AnalysisKind } from "./analyses";
|
||||
import * as apiClient from "./api-client";
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
} from "./feature-flags";
|
||||
import { Logger } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import { ActionName } from "./status-report";
|
||||
import {
|
||||
DEFAULT_DEBUG_ARTIFACT_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
|
||||
* environment. Tests can override individual variables by passing them in the
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { getActionVersion, getTemporaryDirectory } from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import { getGitHubVersion } from "./api-client";
|
||||
import { initFeatures } from "./feature-flags";
|
||||
import { Logger, getActionsLogger } from "./logging";
|
||||
import { Logger } from "./logging";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import { InvalidSarifUploadError } from "./sarif";
|
||||
import {
|
||||
createStatusReportBase,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
StatusReportBase,
|
||||
getActionsStatus,
|
||||
ActionName,
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
ConfigurationError,
|
||||
checkActionVersion,
|
||||
checkDiskUsage,
|
||||
getErrorMessage,
|
||||
initializeEnvironment,
|
||||
shouldSkipSarifUpload,
|
||||
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
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
|
||||
try {
|
||||
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() {
|
||||
const startedAt = new Date();
|
||||
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,
|
||||
);
|
||||
}
|
||||
await runInActions(uploadSarif);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user