Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,386 changes: 1,191 additions & 1,195 deletions lib/entry-points.js

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions src/action-common.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
25 changes: 9 additions & 16 deletions src/analyze-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -40,7 +41,6 @@ import {
createStatusReportBase,
DatabaseCreationTimings,
getActionsStatus,
sendUnhandledErrorStatusReport,
StatusReportBase,
} from "./status-report";
import {
Expand Down Expand Up @@ -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.

Expand All @@ -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());
Expand Down Expand Up @@ -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();
}
28 changes: 10 additions & 18 deletions src/autobuild-action.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as core from "@actions/core";

import { Action, ActionState, runInActions } from "./action-common";
import {
getActionVersion,
getOptionalInput,
Expand All @@ -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";
Expand All @@ -26,7 +26,6 @@ import {
checkDiskUsage,
checkGitHubVersionInRange,
ConfigurationError,
getErrorMessage,
initializeEnvironment,
wrapError,
} from "./util";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
105 changes: 46 additions & 59 deletions src/config/file.test.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,96 @@
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 = {
[RepositoryPropertyName.CONFIG_FILE]: "/path/from/property",
};

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")
.returns(testInput);

// 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);
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 workflow"));
t.true(
targetWithArgs
.getLogger()
.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,
const target = callee(getConfigFileInput)
.withFeatures([Feature.ConfigFileRepositoryProperty])
.withArgs(repositoryProperties);

await target.passes(async (fn) =>
t.is(await fn(), repositoryProperties[RepositoryPropertyName.CONFIG_FILE]),
);
t.is(result, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);

// Check for the expected log message.
t.true(
logger.hasMessage(
"Using configuration file input from repository property",
),
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.",
),
);
});
Loading
Loading