Merge pull request #4225 from github/mbg/reduce-config-requirements

Store and restore "failed" configuration if `init` step aborts
This commit is contained in:
Michael B. Gale
2026-10-13 09:34:24 +00:00
committed by GitHub
14 changed files with 463 additions and 150 deletions

191
lib/entry-points.js generated
View File

@@ -21656,8 +21656,8 @@ var require_core = __commonJS({
exports2.warning = warning14;
exports2.notice = notice;
exports2.info = info8;
exports2.startGroup = startGroup4;
exports2.endGroup = endGroup4;
exports2.startGroup = startGroup3;
exports2.endGroup = endGroup3;
exports2.group = group;
exports2.saveState = saveState3;
exports2.getState = getState3;
@@ -21755,20 +21755,20 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
function info8(message) {
process.stdout.write(message + os7.EOL);
}
function startGroup4(name) {
function startGroup3(name) {
(0, command_1.issue)("group", name);
}
function endGroup4() {
function endGroup3() {
(0, command_1.issue)("endgroup");
}
function group(name, fn) {
return __awaiter2(this, void 0, void 0, function* () {
startGroup4(name);
startGroup3(name);
let result;
try {
result = yield fn();
} finally {
endGroup4();
endGroup3();
}
return result;
});
@@ -145686,22 +145686,22 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
process.stdout.write(message + os7.EOL);
}
exports2.info = info8;
function startGroup4(name) {
function startGroup3(name) {
(0, command_1.issue)("group", name);
}
exports2.startGroup = startGroup4;
function endGroup4() {
exports2.startGroup = startGroup3;
function endGroup3() {
(0, command_1.issue)("endgroup");
}
exports2.endGroup = endGroup4;
exports2.endGroup = endGroup3;
function group(name, fn) {
return __awaiter2(this, void 0, void 0, function* () {
startGroup4(name);
startGroup3(name);
let result;
try {
result = yield fn();
} finally {
endGroup4();
endGroup3();
}
return result;
});
@@ -166098,6 +166098,11 @@ var toolrunner = __toESM(require_toolrunner());
var github = __toESM(require_github());
var io2 = __toESM(require_io());
// src/config/action-config.ts
function hasConfigKey(config, key) {
return config[key] !== void 0;
}
// src/environment.ts
var core = __toESM(require_core());
function getRequiredEnvVar(env, paramName) {
@@ -170207,27 +170212,33 @@ function getWorkflowEvent(env = getEnv()) {
);
}
}
async function printDebugLogs(config) {
async function printDebugLogs(logger2, config) {
if (!hasConfigKey(config, "languages") || !hasConfigKey(config, "dbLocation")) {
logger2.debug(
`Not printing debug logs, because the configuration does not include the 'languages' or 'dbLocation' fields.`
);
return;
}
for (const language of config.languages) {
const databaseDirectory = getCodeQLDatabasePath(config, language);
const logsDirectory = path2.join(databaseDirectory, "log");
if (!doesDirectoryExist(logsDirectory)) {
core4.info(`Directory ${logsDirectory} does not exist.`);
logger2.info(`Directory ${logsDirectory} does not exist.`);
continue;
}
const walkLogFiles = (dir) => {
const entries = fs2.readdirSync(dir, { withFileTypes: true });
if (entries.length === 0) {
core4.info(`No debug logs found at directory ${logsDirectory}.`);
logger2.info(`No debug logs found at directory ${logsDirectory}.`);
}
for (const entry of entries) {
if (entry.isFile()) {
const absolutePath = path2.resolve(dir, entry.name);
core4.startGroup(
logger2.startGroup(
`CodeQL Debug Logs - ${language} - ${entry.name} from file at path ${absolutePath}`
);
process.stdout.write(fs2.readFileSync(absolutePath));
core4.endGroup();
logger2.info(fs2.readFileSync(absolutePath, "utf-8"));
logger2.endGroup();
} else if (entry.isDirectory()) {
walkLogFiles(path2.resolve(dir, entry.name));
}
@@ -171401,9 +171412,14 @@ function getAddressString(address) {
}
// src/status-report.ts
var ACTION_DISPLAY_NAMES = {
["finish" /* Analyze */]: "analyze",
["init-post" /* InitPost */]: "init post"
};
function getDisplayActionName(actionName) {
if (actionName === "finish" /* Analyze */) {
return "analyze";
const displayName = ACTION_DISPLAY_NAMES[actionName];
if (displayName !== void 0) {
return displayName;
}
return actionName;
}
@@ -175053,20 +175069,47 @@ function getLocalConfig(logger2, configFile, validateConfig) {
function getPathToParsedConfigFile(tempDir) {
return path11.join(tempDir, "config");
}
async function saveConfig(config, logger2) {
function getPathToFailedConfigFile(tempDir) {
return path11.join(tempDir, "failed-config");
}
function saveConfigFile(logger2, configFilePath, config) {
const configString = JSON.stringify(config);
const configFile = getPathToParsedConfigFile(config.tempDir);
fs10.mkdirSync(path11.dirname(configFile), { recursive: true });
fs10.writeFileSync(configFile, configString, "utf8");
fs10.mkdirSync(path11.dirname(configFilePath), { recursive: true });
fs10.writeFileSync(configFilePath, configString, "utf8");
logger2.debug("Saved config:");
logger2.debug(configString);
}
async function getConfig(tempDir, logger2) {
const configFile = getPathToParsedConfigFile(tempDir);
if (!fs10.existsSync(configFile)) {
async function saveConfig(config, logger2) {
const configFile = getPathToParsedConfigFile(config.tempDir);
saveConfigFile(logger2, configFile, config);
}
function saveAbortedInitConfig(action, config) {
if (action.name !== "init" /* Init */) {
throw new Error(
`Internal error: 'saveAbortedInitConfig' should only be called by '${"init" /* Init */}', but was called by '${action.name}'.`
);
}
const failedConfig = {
version: config.version,
tempDir: config.tempDir,
codeQLCmd: config.codeQLCmd,
debugMode: config.debugMode,
gitHubVersion: config.gitHubVersion,
languages: config.languages,
analysisKinds: config.analysisKinds
};
action.logger.debug(`Saving config for failed init step.`);
saveConfigFile(
action.logger,
getPathToFailedConfigFile(config.tempDir),
failedConfig
);
}
function readConfigFile(logger2, configFilePath) {
if (!fs10.existsSync(configFilePath)) {
return void 0;
}
const configString = fs10.readFileSync(configFile, "utf8");
const configString = fs10.readFileSync(configFilePath, "utf8");
logger2.debug("Loaded config:");
logger2.debug(configString);
const config = JSON.parse(configString);
@@ -175087,6 +175130,18 @@ async function getConfig(tempDir, logger2) {
}
return config;
}
async function getConfig(tempDir, logger2) {
const configFile = getPathToParsedConfigFile(tempDir);
return readConfigFile(logger2, configFile);
}
function getAbortedInitConfig(action, tempDir) {
if (action.name !== "init-post" /* InitPost */) {
throw new Error(
`Internal error: 'getAbortedInitConfig' should only be called by '${"init-post" /* InitPost */}', but was called by '${action.name}'.`
);
}
return readConfigFile(action.logger, getPathToFailedConfigFile(tempDir));
}
async function generateRegistries(registriesInput, tempDir, logger2) {
const registries = parseRegistries(registriesInput);
let registriesAuthTokens;
@@ -186495,6 +186550,15 @@ async function run3(actionState) {
}
await checkInstallPython311(config.languages, codeql);
} catch (unwrappedError) {
if (config !== void 0) {
try {
saveAbortedInitConfig(actionState, config);
} catch (configSaveError) {
logger2.warning(
`Unable to save configuration for '${"init-post" /* InitPost */}' step: ${getErrorMessage(configSaveError)}`
);
}
}
const error3 = wrapError(unwrappedError);
core19.setFailed(error3.message);
const statusReportBase = await createStatusReportBase(
@@ -186923,14 +186987,14 @@ async function uploadFailureInfo(uploadAllAvailableDebugArtifacts, printDebugLog
);
}
}
if (config.debugMode) {
if (config.debugMode && hasConfigKey(config, "dbLocation")) {
logger2.info(
"Debug mode is on. Uploading available database bundles and logs as Actions debugging artifacts..."
);
await uploadAllAvailableDebugArtifacts(codeql, config, logger2);
await printDebugLogs2(config);
await printDebugLogs2(logger2, config);
}
if (isSelfHostedRunner()) {
if (isSelfHostedRunner() && config.dbLocation) {
try {
fs30.rmSync(config.dbLocation, {
recursive: true,
@@ -186945,6 +187009,10 @@ async function uploadFailureInfo(uploadAllAvailableDebugArtifacts, printDebugLog
`Failed to clean up database cluster directory ${config.dbLocation}. Details: ${e}`
);
}
} else if (config.dbLocation === void 0) {
logger2.debug(
"Skipping cleanup of database cluster directory since the configuration does not include a database path."
);
} else {
logger2.debug(
"Skipping cleanup of database cluster directory since we are running on a GitHub-hosted runner which will be automatically cleaned up."
@@ -186968,6 +187036,11 @@ function isConclusiveJobStatus(jobStatus) {
async function recordOverlayStatus(codeql, config, features, jobStatus, env, logger2) {
if (config.overlayDatabaseMode !== "overlay-base" /* OverlayBase */ || env.getOptional("CODEQL_ACTION_ANALYZE_DID_COMPLETE_SUCCESSFULLY" /* ANALYZE_DID_COMPLETE_SUCCESSFULLY */) === "true" || !await features.getValue("overlay_analysis_status_save" /* OverlayAnalysisStatusSave */)) {
return;
} else if (config.languages === void 0) {
logger2.debug(
"Not recording overlay status, because the configuration contains no 'languages'."
);
return;
}
if (!isConclusiveJobStatus(jobStatus) && !didCodeQlReportError(env)) {
logger2.info(
@@ -187070,15 +187143,19 @@ async function removeUploadedSarif(uploadFailedSarifResult, logger2) {
}
// src/init-action-post.ts
async function run4(startedAt) {
const logger2 = getActionsLogger();
async function run4(action) {
const logger2 = action.logger;
let config;
let uploadFailedSarifResult;
let dependencyCachingUsage;
try {
const jobStatus2 = getOptionalInput("job-status");
restoreInputs(logger2);
config = await getConfig(getTemporaryDirectory(), logger2);
const tmpDir = getTemporaryDirectory();
config = await getConfig(tmpDir, logger2);
if (config === void 0) {
config = getAbortedInitConfig(action, tmpDir);
}
const gitHubVersion = config?.gitHubVersion ?? await getGitHubVersion();
checkGitHubVersionInRange(gitHubVersion, logger2);
const repositoryNwo = getRepositoryNwo();
@@ -187102,10 +187179,10 @@ async function run4(startedAt) {
repositoryNwo,
features,
jobStatus2,
getEnv(),
action.env,
logger2
);
if (await isAnalyzingDefaultBranch() && config.dependencyCachingEnabled !== "none" /* None */) {
if (await isAnalyzingDefaultBranch() && hasConfigKey(config, "dependencyCachingEnabled") && config.dependencyCachingEnabled !== "none" /* None */) {
dependencyCachingUsage = await getDependencyCacheUsage(logger2);
}
}
@@ -187113,9 +187190,9 @@ async function run4(startedAt) {
const error3 = wrapApiConfigurationError(wrapError(unwrappedError));
core20.setFailed(error3.message);
const statusReportBase2 = await createStatusReportBase(
"init-post" /* InitPost */,
action.name,
getActionsStatus(error3),
startedAt,
action.startedAt,
config,
await checkDiskUsage(logger2),
logger2,
@@ -187127,12 +187204,12 @@ async function run4(startedAt) {
}
return;
}
const jobStatus = getFinalJobStatus(config);
const jobStatus = getFinalJobStatus(action, config);
logger2.info(`CodeQL job status was ${getJobStatusDisplayName(jobStatus)}.`);
const statusReportBase = await createStatusReportBase(
"init-post" /* InitPost */,
action.name,
"success",
startedAt,
action.startedAt,
config,
await checkDiskUsage(logger2),
logger2
@@ -187149,25 +187226,25 @@ async function run4(startedAt) {
logger2.info("Status report sent for init-post step.");
}
}
function getFinalJobStatus(config) {
const existingJobStatus = getJobStatusFromEnvironment();
function getFinalJobStatus(action, config) {
const existingJobStatus = getJobStatusFromEnvironment(action.env);
if (existingJobStatus !== void 0) {
return existingJobStatus;
}
let jobStatus;
if (process.env["CODEQL_ACTION_ANALYZE_DID_COMPLETE_SUCCESSFULLY" /* ANALYZE_DID_COMPLETE_SUCCESSFULLY */] === "true") {
exportEnvVar("CODEQL_ACTION_JOB_STATUS" /* JOB_STATUS */, "JOB_STATUS_SUCCESS" /* SuccessStatus */);
if (action.env.getOptional("CODEQL_ACTION_ANALYZE_DID_COMPLETE_SUCCESSFULLY" /* ANALYZE_DID_COMPLETE_SUCCESSFULLY */) === "true") {
action.env.export("CODEQL_ACTION_JOB_STATUS" /* JOB_STATUS */, "JOB_STATUS_SUCCESS" /* SuccessStatus */);
jobStatus = "JOB_STATUS_SUCCESS" /* SuccessStatus */;
} else if (config !== void 0) {
jobStatus = "JOB_STATUS_CONFIGURATION_ERROR" /* ConfigErrorStatus */;
} else {
jobStatus = "JOB_STATUS_UNKNOWN" /* UnknownStatus */;
}
exportEnvVar("CODEQL_ACTION_JOB_STATUS" /* JOB_STATUS */, jobStatus);
action.env.export("CODEQL_ACTION_JOB_STATUS" /* JOB_STATUS */, jobStatus);
return jobStatus;
}
function getJobStatusFromEnvironment() {
const jobStatusFromEnvironment = process.env["CODEQL_ACTION_JOB_STATUS" /* JOB_STATUS */];
function getJobStatusFromEnvironment(env) {
const jobStatusFromEnvironment = env.getOptional("CODEQL_ACTION_JOB_STATUS" /* JOB_STATUS */);
if (jobStatusFromEnvironment !== void 0) {
if (Object.values(JobStatus).includes(jobStatusFromEnvironment)) {
return jobStatusFromEnvironment;
@@ -187176,20 +187253,12 @@ function getJobStatusFromEnvironment() {
}
return void 0;
}
var initPost = {
name: "init-post" /* InitPost */,
run: run4
};
async function runWrapper5() {
const startedAt = /* @__PURE__ */ new Date();
const logger2 = getActionsLogger();
try {
await run4(startedAt);
} catch (error3) {
core20.setFailed(`init post action failed: ${wrapError(error3).message}`);
await sendUnhandledErrorStatusReport(
"init-post" /* InitPost */,
startedAt,
error3,
logger2
);
}
return await runInActions(initPost);
}
// src/resolve-environment-action.ts

View File

@@ -19,6 +19,24 @@ import { getErrorMessage } from "./util";
setupTests(test);
for (const [key, displayName] of Object.entries(
statusReport.ACTION_DISPLAY_NAMES,
)) {
test(`getDisplayActionName - '${displayName}' instead of '${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;

View File

@@ -7,12 +7,13 @@ import {
isAnalyzingPullRequest,
isDefaultSetup,
isDynamicWorkflow,
printDebugLogs,
} from "./actions-util";
import { computeAutomationID } from "./api-client";
import { EnvVar } from "./environment";
import { getRunnerLogger } from "./logging";
import { setupTests } from "./testing-utils";
import { initializeEnvironment } from "./util";
import { RecordingLogger, setupTests } from "./testing-utils";
import { initializeEnvironment, withTmpDir } from "./util";
setupTests(test);
@@ -100,6 +101,21 @@ test("computeAutomationID()", async (t) => {
);
});
test("printDebugLogs - doesn't throw when config doesn't have needed properties", async (t) => {
const logger = new RecordingLogger();
await t.notThrowsAsync(printDebugLogs(logger, {}));
await t.notThrowsAsync(printDebugLogs(logger, { languages: [] }));
await t.notThrowsAsync(printDebugLogs(logger, { dbLocation: "nowhere" }));
});
test("printDebugLogs - doesn't throw when language db doesn't exist", async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
await t.notThrowsAsync(
printDebugLogs(logger, { dbLocation: tmpDir, languages: ["javascript"] }),
);
}));
test.serial("getPullRequestBranches() with pull request context", (t) => {
withMockedContext(
{

View File

@@ -6,7 +6,7 @@ import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as github from "@actions/github";
import * as io from "@actions/io";
import type { Config } from "./config-utils";
import { hasConfigKey, type Config } from "./config/action-config";
import { Env, EnvVar, ActionsEnvVars, ReadOnlyEnv } from "./environment";
import { Logger } from "./logging";
import {
@@ -131,28 +131,38 @@ export function getWorkflowEvent(env: Env = getEnv()): any {
}
}
export async function printDebugLogs(config: Config) {
export async function printDebugLogs(logger: Logger, config: Partial<Config>) {
if (
!hasConfigKey(config, "languages") ||
!hasConfigKey(config, "dbLocation")
) {
logger.debug(
`Not printing debug logs, because the configuration does not include the 'languages' or 'dbLocation' fields.`,
);
return;
}
for (const language of config.languages) {
const databaseDirectory = getCodeQLDatabasePath(config, language);
const logsDirectory = path.join(databaseDirectory, "log");
if (!doesDirectoryExist(logsDirectory)) {
core.info(`Directory ${logsDirectory} does not exist.`);
logger.info(`Directory ${logsDirectory} does not exist.`);
continue; // Skip this language database.
}
const walkLogFiles = (dir: string) => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
if (entries.length === 0) {
core.info(`No debug logs found at directory ${logsDirectory}.`);
logger.info(`No debug logs found at directory ${logsDirectory}.`);
}
for (const entry of entries) {
if (entry.isFile()) {
const absolutePath = path.resolve(dir, entry.name);
core.startGroup(
logger.startGroup(
`CodeQL Debug Logs - ${language} - ${entry.name} from file at path ${absolutePath}`,
);
process.stdout.write(fs.readFileSync(absolutePath));
core.endGroup();
logger.info(fs.readFileSync(absolutePath, "utf-8"));
logger.endGroup();
} else if (entry.isDirectory()) {
walkLogFiles(path.resolve(dir, entry.name));
}

View File

@@ -9,6 +9,7 @@ import { getTemporaryDirectory } from "./actions-util";
import * as analyses from "./analyses";
import { setupCppAutobuild } from "./autobuild";
import { type CodeQL } from "./codeql";
import type { ActionConfig } from "./config/action-config";
import { defaultSuites } from "./config/db-config";
import * as configUtils from "./config-utils";
import {
@@ -171,7 +172,7 @@ async function shouldExtractLanguage(
}
export function dbIsFinalized(
config: configUtils.Config,
config: ActionConfig<"dbLocation">,
language: Language,
logger: Logger,
) {

View File

@@ -15,6 +15,7 @@ import * as api from "./api-client";
import * as outputCache from "./cli/output-cache";
import type { VersionInfo } from "./cli/types";
import { CliError, wrapCliConfigurationError } from "./cli-errors";
import type { ActionConfig } from "./config/action-config";
import { appendExtraQueryExclusions, type Config } from "./config-utils";
import { DocUrl } from "./doc-url";
import { EnvVar, getEnv, exportEnvVar } from "./environment";
@@ -197,7 +198,7 @@ export interface CodeQL {
diagnosticsExport(
sarifFile: string,
automationDetailsId: string | undefined,
config: Config,
config: ActionConfig<"tempDir">,
): Promise<void>;
/** Get the location of an extractor for the specified language. */
resolveExtractor(language: Language): Promise<string>;
@@ -913,7 +914,7 @@ async function getCodeQLForCmd(
async diagnosticsExport(
sarifFile: string,
automationDetailsId: string | undefined,
config: Config,
config: ActionConfig<"tempDir">,
): Promise<void> {
const args = [
"diagnostics",
@@ -1237,7 +1238,9 @@ export async function getTrapCachingExtractorConfigArgsForLang(
*
* This will not exist if the configuration is being parsed in the Action.
*/
function getGeneratedCodeScanningConfigPath(config: Config): string {
function getGeneratedCodeScanningConfigPath(
config: ActionConfig<"tempDir">,
): string {
return path.resolve(config.tempDir, "user-config.yaml");
}

View File

@@ -21,7 +21,11 @@ import {
import * as api from "./api-client";
import { getCachingKind } from "./caching-utils";
import { type CodeQL } from "./codeql";
import { type Config } from "./config/action-config";
import {
type AbortedInitConfig,
type ActionConfig,
type Config,
} from "./config/action-config";
import {
calculateAugmentation,
ExcludeQueryFilter,
@@ -71,6 +75,7 @@ import {
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import { shouldSkipOverlayAnalysis } from "./overlay/status";
import { RepositoryNwo } from "./repository";
import { ActionName } from "./status-report";
import { ToolsFeature } from "./tools-features";
import { downloadTrapCaches } from "./trap-caching";
import {
@@ -1379,31 +1384,86 @@ export function getPathToParsedConfigFile(tempDir: string): string {
return path.join(tempDir, "config");
}
function getPathToFailedConfigFile(tempDir: string): string {
return path.join(tempDir, "failed-config");
}
/**
* Store the given config to the path returned from getPathToParsedConfigFile.
* Writes {@link config} to {@link configFilePath}.
*
* This function is not intended to be called, other than by
* {@link saveConfig} and {@link saveAbortedInitConfig} below.
*/
export async function saveConfig(config: Config, logger: Logger) {
function saveConfigFile(
logger: Logger,
configFilePath: string,
config: Partial<Config>,
) {
const configString = JSON.stringify(config);
const configFile = getPathToParsedConfigFile(config.tempDir);
fs.mkdirSync(path.dirname(configFile), { recursive: true });
fs.writeFileSync(configFile, configString, "utf8");
fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
fs.writeFileSync(configFilePath, configString, "utf8");
logger.debug("Saved config:");
logger.debug(configString);
}
/**
* Get the config that has been saved to the given temp dir.
* If the config could not be found then returns undefined.
* Store the given config to the path returned from getPathToParsedConfigFile.
*/
export async function getConfig(
tempDir: string,
export async function saveConfig(config: Config, logger: Logger) {
const configFile = getPathToParsedConfigFile(config.tempDir);
saveConfigFile(logger, configFile, config);
}
/**
* Store parts of {@link config} as a failed configuration file.
*
* The file will only contain parts of {@link config} that are relevant to
* the `init-post` step in the workflow.
*/
export function saveAbortedInitConfig(
action: ActionState<["Base", "Logger"]>,
config: Config,
) {
// Should only be called by init.
if (action.name !== ActionName.Init) {
throw new Error(
`Internal error: 'saveAbortedInitConfig' should only be called by '${ActionName.Init}', but was called by '${action.name}'.`,
);
}
// Create a copy of the input `Config` that only contains the keys we want.
const failedConfig: AbortedInitConfig = {
version: config.version,
tempDir: config.tempDir,
codeQLCmd: config.codeQLCmd,
debugMode: config.debugMode,
gitHubVersion: config.gitHubVersion,
languages: config.languages,
analysisKinds: config.analysisKinds,
};
action.logger.debug(`Saving config for failed init step.`);
saveConfigFile(
action.logger,
getPathToFailedConfigFile(config.tempDir),
failedConfig,
);
}
/**
* Attempts to read a {@link Config} from {@link configFilePath}.
*
* This function is not intended to be called, other than by
* {@link getConfig} and {@link getAbortedInitConfig} below.
*/
function readConfigFile(
logger: Logger,
): Promise<Config | undefined> {
const configFile = getPathToParsedConfigFile(tempDir);
if (!fs.existsSync(configFile)) {
configFilePath: string,
): Config | undefined {
if (!fs.existsSync(configFilePath)) {
return undefined;
}
const configString = fs.readFileSync(configFile, "utf8");
const configString = fs.readFileSync(configFilePath, "utf8");
logger.debug("Loaded config:");
logger.debug(configString);
@@ -1428,6 +1488,38 @@ export async function getConfig(
return config as Config;
}
/**
* Get the config that has been saved to the given temp dir.
* If the config could not be found then returns undefined.
*/
export async function getConfig(
tempDir: string,
logger: Logger,
): Promise<Config | undefined> {
const configFile = getPathToParsedConfigFile(tempDir);
return readConfigFile(logger, configFile);
}
/**
* Loads a {@link AbortedInitConfig} from a failed `init` step if it exists.
*
* The result will only contain parts of a {@link Config} that are relevant to
* the `init-post` step in the workflow.
*/
export function getAbortedInitConfig(
action: ActionState<["Base", "Logger"]>,
tempDir: string,
): AbortedInitConfig | undefined {
// Only intended to be called by the `init-post` action.
if (action.name !== ActionName.InitPost) {
throw new Error(
`Internal error: 'getAbortedInitConfig' should only be called by '${ActionName.InitPost}', but was called by '${action.name}'.`,
);
}
return readConfigFile(action.logger, getPathToFailedConfigFile(tempDir));
}
/**
* Generate a `qlconfig.yml` file from the `registries` input.
* This file is used by the CodeQL CLI to list the registries to use for each
@@ -1618,21 +1710,27 @@ export function appendExtraQueryExclusions(
/**
* Returns `true` if Code Scanning analysis is enabled, or `false` if not.
*/
export function isCodeScanningEnabled(config: Config): boolean {
export function isCodeScanningEnabled(
config: ActionConfig<"analysisKinds">,
): boolean {
return config.analysisKinds.includes(AnalysisKind.CodeScanning);
}
/**
* Returns `true` if Code Quality analysis is enabled, or `false` if not.
*/
export function isCodeQualityEnabled(config: Config): boolean {
export function isCodeQualityEnabled(
config: ActionConfig<"analysisKinds">,
): boolean {
return config.analysisKinds.includes(AnalysisKind.CodeQuality);
}
/**
* Returns `true` if Code Scanning Risk Assessment analysis is enabled, or `false` if not.
*/
export function isRiskAssessmentEnabled(config: Config): boolean {
export function isRiskAssessmentEnabled(
config: ActionConfig<"analysisKinds">,
): boolean {
return config.analysisKinds.includes(AnalysisKind.RiskAssessment);
}

View File

@@ -126,3 +126,38 @@ export interface Config {
*/
enableFileCoverageInformation: boolean;
}
/** Properties available in `Config`. */
export type ActionConfigProperties = keyof Config;
/**
* The CodeQL Action configuration state that is persisted
* between steps of the CodeQL Action in a CodeQL workflow.
*
* By default, this is equivalent to `Config`. By explicitly
* providing required property names for `T` this can be restricted
* to requiring only those. For example, in `ActionConfig<"codeQLCmd">`
* all properties except `codeqlCmd` are optional.
*/
export type ActionConfig<
T extends ActionConfigProperties = ActionConfigProperties,
> = Partial<Config> & Pick<Config, T>;
/** Helper function to check whether `config` has `key` to narrow the type of `config`. */
export function hasConfigKey<Key extends ActionConfigProperties>(
config: Partial<Config>,
key: Key,
): config is ActionConfig<Key> {
return config[key] !== undefined;
}
/** A minimal configuration that we can make available even when `init` aborts. */
export type AbortedInitConfig = ActionConfig<
| "version"
| "tempDir"
| "gitHubVersion"
| "debugMode"
| "languages"
| "analysisKinds"
| "codeQLCmd"
>;

View File

@@ -10,7 +10,7 @@ import { getOptionalInput, getTemporaryDirectory } from "./actions-util";
import { dbIsFinalized } from "./analyze";
import { scanArtifactsForTokens } from "./artifact-scanner";
import { type CodeQL } from "./codeql";
import { Config } from "./config-utils";
import type { ActionConfig } from "./config/action-config";
import { EnvVar, exportEnvVar } from "./environment";
import * as json from "./json";
import { Language } from "./languages";
@@ -89,7 +89,7 @@ export async function uploadCombinedSarifArtifacts(
* @return The path to that debug artifact, or undefined if an error occurs.
*/
function tryPrepareSarifDebugArtifact(
config: Config,
config: ActionConfig<"dbLocation">,
language: Language,
logger: Logger,
): string | undefined {
@@ -131,7 +131,7 @@ function tryPrepareSarifDebugArtifact(
*/
async function tryBundleDatabase(
codeql: CodeQL,
config: Config,
config: ActionConfig<"dbLocation">,
language: Language,
logger: Logger,
): Promise<string | undefined> {
@@ -157,6 +157,11 @@ async function tryBundleDatabase(
}
}
/** The minimal configuration needed for `tryUploadAllAvailableDebugArtifacts`. */
export type UploadDebugArtifactsConfig = ActionConfig<
"languages" | "gitHubVersion" | "dbLocation" | "debugArtifactName"
>;
/**
* Attempt to upload all available debug artifacts.
*
@@ -164,7 +169,7 @@ async function tryBundleDatabase(
*/
export async function tryUploadAllAvailableDebugArtifacts(
codeql: CodeQL,
config: Config,
config: UploadDebugArtifactsConfig,
logger: Logger,
) {
const filesToUpload: string[] = [];
@@ -353,7 +358,7 @@ export async function getArtifactUploaderClient(
* all files in the database folder and return the path.
*/
async function createPartialDatabaseBundle(
config: Config,
config: ActionConfig<"dbLocation">,
language: Language,
): Promise<string> {
const databasePath = getCodeQLDatabasePath(config, language);
@@ -394,7 +399,7 @@ async function createPartialDatabaseBundle(
*/
async function createDatabaseBundleCli(
codeql: CodeQL,
config: Config,
config: ActionConfig<"dbLocation">,
language: Language,
): Promise<string> {
const databaseBundlePath = await bundleDb(

View File

@@ -7,6 +7,11 @@ import * as actionsUtil from "./actions-util";
import { CodeScanning, RiskAssessment } from "./analyses";
import { getApiClient, getGitHubVersion } from "./api-client";
import { CodeQL, getCodeQL } from "./codeql";
import {
hasConfigKey,
type AbortedInitConfig,
type ActionConfig,
} from "./config/action-config";
import {
Config,
isCodeScanningEnabled,
@@ -16,6 +21,7 @@ import {
getArtifactSuffix,
getArtifactUploaderClient,
sanitizeArtifactName,
type UploadDebugArtifactsConfig,
} from "./debug-artifacts";
import * as dependencyCaching from "./dependency-caching";
import { EnvVar, ReadOnlyEnv } from "./environment";
@@ -86,6 +92,11 @@ interface FailedSarifInfo {
checkoutPath: string;
}
/** The minimal Action configuration required to prepare and upload a failed SARIF. */
export type FailedSarifConfig = ActionConfig<
"codeQLCmd" | "tempDir" | "analysisKinds" | "languages"
>;
/**
* Tries to prepare a SARIF file that contains information about a failed analysis.
*
@@ -94,7 +105,7 @@ interface FailedSarifInfo {
async function prepareFailedSarif(
logger: Logger,
features: FeatureEnablement,
config: Config,
config: FailedSarifConfig,
): Promise<Result<FailedSarifInfo, UploadFailedSarifResult>> {
if (!config.codeQLCmd) {
return new Failure({
@@ -160,7 +171,7 @@ async function prepareFailedSarif(
async function generateFailedSarif(
logger: Logger,
features: FeatureEnablement,
config: Config,
config: ActionConfig<"codeQLCmd" | "tempDir">,
category: string | undefined,
checkoutPath: string,
sarifFile?: string,
@@ -192,7 +203,7 @@ async function generateFailedSarif(
* category for the workflow.
*/
async function maybeUploadFailedSarif(
config: Config,
config: FailedSarifConfig,
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
logger: Logger,
@@ -227,7 +238,7 @@ async function maybeUploadFailedSarif(
/** Uploads a failed SARIF file as workflow artifact, if it can be generated. */
async function maybeUploadFailedSarifArtifact(
config: Config,
config: FailedSarifConfig,
features: FeatureEnablement,
logger: Logger,
): Promise<UploadFailedSarifResult> {
@@ -267,7 +278,7 @@ async function maybeUploadFailedSarifArtifact(
* @returns The results of uploading the SARIF file for the failure.
*/
export async function tryUploadSarifIfRunFailed(
config: Config,
config: FailedSarifConfig,
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
logger: Logger,
@@ -324,12 +335,12 @@ export async function tryUploadSarifIfRunFailed(
export async function uploadFailureInfo(
uploadAllAvailableDebugArtifacts: (
codeql: CodeQL,
config: Config,
config: UploadDebugArtifactsConfig,
logger: Logger,
) => Promise<void>,
printDebugLogs: (config: Config) => Promise<void>,
printDebugLogs: (logger: Logger, config: Partial<Config>) => Promise<void>,
codeql: CodeQL,
config: Config,
config: Config | AbortedInitConfig,
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
jobStatus: string | undefined,
@@ -376,15 +387,15 @@ export async function uploadFailureInfo(
}
// Upload appropriate Actions artifacts for debugging
if (config.debugMode) {
if (config.debugMode && hasConfigKey(config, "dbLocation")) {
logger.info(
"Debug mode is on. Uploading available database bundles and logs as Actions debugging artifacts...",
);
await uploadAllAvailableDebugArtifacts(codeql, config, logger);
await printDebugLogs(config);
await printDebugLogs(logger, config);
}
if (actionsUtil.isSelfHostedRunner()) {
if (actionsUtil.isSelfHostedRunner() && config.dbLocation) {
try {
fs.rmSync(config.dbLocation, {
recursive: true,
@@ -399,6 +410,10 @@ export async function uploadFailureInfo(
`Failed to clean up database cluster directory ${config.dbLocation}. Details: ${e}`,
);
}
} else if (config.dbLocation === undefined) {
logger.debug(
"Skipping cleanup of database cluster directory since the configuration does not include a database path.",
);
} else {
logger.debug(
"Skipping cleanup of database cluster directory since we are running on a GitHub-hosted " +
@@ -447,7 +462,7 @@ function isConclusiveJobStatus(jobStatus: string | undefined): boolean {
*/
async function recordOverlayStatus(
codeql: CodeQL,
config: Config,
config: Partial<Config>,
features: FeatureEnablement,
jobStatus: string | undefined,
env: ReadOnlyEnv,
@@ -459,6 +474,11 @@ async function recordOverlayStatus(
!(await features.getValue(Feature.OverlayAnalysisStatusSave))
) {
return;
} else if (config.languages === undefined) {
logger.debug(
"Not recording overlay status, because the configuration contains no 'languages'.",
);
return;
}
// Only record a failure when the job outcome tells us something about the analysis. A cancelled

View File

@@ -6,6 +6,7 @@
import * as core from "@actions/core";
import { runInActions, type Action, type ActionState } from "./action-common";
import {
restoreInputs,
getOptionalInput,
@@ -15,22 +16,25 @@ import {
import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
import { CachingKind } from "./caching-utils";
import { getCodeQL } from "./codeql";
import { type Config, getConfig } from "./config-utils";
import {
hasConfigKey,
type AbortedInitConfig,
type Config,
} from "./config/action-config";
import { getAbortedInitConfig, getConfig } from "./config-utils";
import * as debugArtifacts from "./debug-artifacts";
import {
DependencyCachingUsageReport,
getDependencyCacheUsage,
} from "./dependency-caching";
import { EnvVar, getEnv, exportEnvVar } from "./environment";
import { EnvVar, type ReadOnlyEnv } from "./environment";
import { initFeatures } from "./feature-flags";
import * as gitUtils from "./git-utils";
import * as initActionPostHelper from "./init-action-post-helper";
import { getActionsLogger } from "./logging";
import { getRepositoryNwo } from "./repository";
import {
StatusReportBase,
sendStatusReport,
sendUnhandledErrorStatusReport,
createStatusReportBase,
getActionsStatus,
ActionName,
@@ -45,16 +49,17 @@ interface InitPostStatusReport
initActionPostHelper.JobStatusReport,
initActionPostHelper.DependencyCachingUsageReport {}
async function run(startedAt: Date) {
async function run(action: ActionState<["Base", "Logger", "Env"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
const logger = action.logger;
const logger = getActionsLogger();
let config: Config | undefined;
let config: Config | AbortedInitConfig | undefined;
let uploadFailedSarifResult:
| initActionPostHelper.UploadFailedSarifResult
| undefined;
let dependencyCachingUsage: DependencyCachingUsageReport | undefined;
try {
// 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`
@@ -64,7 +69,15 @@ async function run(startedAt: Date) {
// Restore inputs from `init` Action.
restoreInputs(logger);
config = await getConfig(getTemporaryDirectory(), logger);
const tmpDir = getTemporaryDirectory();
config = await getConfig(tmpDir, logger);
// If we weren't able to read the normal configuration file, the `init` step failed before it could write it.
// Try to load the "failed" configuration file instead which may have been written by the `init` step
// with a minimal configuration for this post-action.
if (config === undefined) {
config = getAbortedInitConfig(action, tmpDir);
}
const gitHubVersion = config?.gitHubVersion ?? (await getGitHubVersion());
checkGitHubVersionInRange(gitHubVersion, logger);
@@ -92,7 +105,7 @@ async function run(startedAt: Date) {
repositoryNwo,
features,
jobStatus,
getEnv(),
action.env,
logger,
);
@@ -102,6 +115,7 @@ async function run(startedAt: Date) {
// and where caching may not be enabled.
if (
(await gitUtils.isAnalyzingDefaultBranch()) &&
hasConfigKey(config, "dependencyCachingEnabled") &&
config.dependencyCachingEnabled !== CachingKind.None
) {
dependencyCachingUsage = await getDependencyCacheUsage(logger);
@@ -112,9 +126,9 @@ async function run(startedAt: Date) {
core.setFailed(error.message);
const statusReportBase = await createStatusReportBase(
ActionName.InitPost,
action.name,
getActionsStatus(error),
startedAt,
action.startedAt,
config,
await checkDiskUsage(logger),
logger,
@@ -126,13 +140,13 @@ async function run(startedAt: Date) {
}
return;
}
const jobStatus = getFinalJobStatus(config);
const jobStatus = getFinalJobStatus(action, config);
logger.info(`CodeQL job status was ${getJobStatusDisplayName(jobStatus)}.`);
const statusReportBase = await createStatusReportBase(
ActionName.InitPost,
action.name,
"success",
startedAt,
action.startedAt,
config,
await checkDiskUsage(logger),
logger,
@@ -157,16 +171,21 @@ async function run(startedAt: Date) {
* Otherwise, we determine the job status based on whether the analyze step
* completed successfully and whether we have a valid CodeQL config.
*/
function getFinalJobStatus(config: Config | undefined): JobStatus {
const existingJobStatus = getJobStatusFromEnvironment();
function getFinalJobStatus(
action: ActionState<["Env"]>,
config: Partial<Config> | undefined,
): JobStatus {
const existingJobStatus = getJobStatusFromEnvironment(action.env);
if (existingJobStatus !== undefined) {
return existingJobStatus;
}
let jobStatus: JobStatus;
if (process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true") {
exportEnvVar(EnvVar.JOB_STATUS, JobStatus.SuccessStatus);
if (
action.env.getOptional(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY) === "true"
) {
action.env.export(EnvVar.JOB_STATUS, JobStatus.SuccessStatus);
jobStatus = JobStatus.SuccessStatus;
} else if (config !== undefined) {
// - We have computed a CodeQL config
@@ -191,7 +210,7 @@ function getFinalJobStatus(config: Config | undefined): JobStatus {
// This shouldn't be necessary, but in the odd case that we run more than one
// `init` post step, ensure the job status is consistent between them.
exportEnvVar(EnvVar.JOB_STATUS, jobStatus);
action.env.export(EnvVar.JOB_STATUS, jobStatus);
return jobStatus;
}
@@ -200,8 +219,8 @@ function getFinalJobStatus(config: Config | undefined): JobStatus {
*
* If the job status is invalid, return `UnknownStatus`.
*/
function getJobStatusFromEnvironment(): JobStatus | undefined {
const jobStatusFromEnvironment = process.env[EnvVar.JOB_STATUS];
function getJobStatusFromEnvironment(env: ReadOnlyEnv): JobStatus | undefined {
const jobStatusFromEnvironment = env.getOptional(EnvVar.JOB_STATUS);
if (jobStatusFromEnvironment !== undefined) {
// Validate the job status from the environment. If it is invalid, return unknown.
@@ -216,18 +235,11 @@ function getJobStatusFromEnvironment(): JobStatus | undefined {
return undefined;
}
const initPost: Action = {
name: ActionName.InitPost,
run,
};
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(`init post action failed: ${wrapError(error).message}`);
await sendUnhandledErrorStatusReport(
ActionName.InitPost,
startedAt,
error,
logger,
);
}
return await runInActions(initPost);
}

View File

@@ -457,6 +457,18 @@ async function run(
await checkInstallPython311(config.languages, codeql);
} catch (unwrappedError) {
// If we have initialised the Action state, then try to save a subset of it for use by the `init-post` action
// so that it can try to create and upload additional diagnostic information.
if (config !== undefined) {
try {
configUtils.saveAbortedInitConfig(actionState, config);
} catch (configSaveError) {
logger.warning(
`Unable to save configuration for '${ActionName.InitPost}' step: ${getErrorMessage(configSaveError)}`,
);
}
}
const error = wrapError(unwrappedError);
core.setFailed(error.message);
const statusReportBase = await createStatusReportBase(

View File

@@ -61,13 +61,21 @@ export enum ActionName {
UploadSarif = "upload-sarif",
}
/** 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",
};
/**
* Maps an `ActionName` to its display name. Usually that is the same, except
* for `ActionName.Analyze` where it is `"analyze"` instead of `"finish"`.
* 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 {
if (actionName === ActionName.Analyze) {
return "analyze";
const displayName = ACTION_DISPLAY_NAMES[actionName];
if (displayName !== undefined) {
return displayName;
}
return actionName;
}

View File

@@ -12,6 +12,7 @@ import * as semver from "semver";
import * as apiCompatibility from "./api-compatibility.json";
import type { CodeQL } from "./codeql";
import type { ActionConfig } from "./config/action-config";
import type { Pack } from "./config/db-config";
import type { Config } from "./config-utils";
import {
@@ -445,7 +446,10 @@ export function getThreadsFlag(
/**
* Get the path where the CodeQL database for the given language lives.
*/
export function getCodeQLDatabasePath(config: Config, language: Language) {
export function getCodeQLDatabasePath(
config: ActionConfig<"dbLocation">,
language: Language,
) {
return path.resolve(config.dbLocation, language);
}
@@ -639,7 +643,9 @@ export async function codeQlVersionAtLeast(
return semver.gte((await codeql.getVersion()).version, requiredVersion);
}
export function getBaseDatabaseOidsFilePath(config: Config): string {
export function getBaseDatabaseOidsFilePath(
config: ActionConfig<"dbLocation">,
): string {
return path.join(config.dbLocation, BASE_DATABASE_OIDS_FILE_NAME);
}
@@ -651,7 +657,7 @@ export function getBaseDatabaseOidsFilePath(config: Config): string {
* given `includeDiagnostics` value.
*/
export async function bundleDb(
config: Config,
config: ActionConfig<"dbLocation">,
language: Language,
codeql: CodeQL,
dbName: string,