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

View File

@@ -19,6 +19,24 @@ import { getErrorMessage } from "./util";
setupTests(test); 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 { interface RunInActionsTestOpts {
runFn?: () => Promise<any>; runFn?: () => Promise<any>;
expectedErrorMessage?: string; expectedErrorMessage?: string;

View File

@@ -7,12 +7,13 @@ import {
isAnalyzingPullRequest, isAnalyzingPullRequest,
isDefaultSetup, isDefaultSetup,
isDynamicWorkflow, isDynamicWorkflow,
printDebugLogs,
} from "./actions-util"; } from "./actions-util";
import { computeAutomationID } from "./api-client"; import { computeAutomationID } from "./api-client";
import { EnvVar } from "./environment"; import { EnvVar } from "./environment";
import { getRunnerLogger } from "./logging"; import { getRunnerLogger } from "./logging";
import { setupTests } from "./testing-utils"; import { RecordingLogger, setupTests } from "./testing-utils";
import { initializeEnvironment } from "./util"; import { initializeEnvironment, withTmpDir } from "./util";
setupTests(test); 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) => { test.serial("getPullRequestBranches() with pull request context", (t) => {
withMockedContext( withMockedContext(
{ {

View File

@@ -6,7 +6,7 @@ import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as github from "@actions/github"; import * as github from "@actions/github";
import * as io from "@actions/io"; 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 { Env, EnvVar, ActionsEnvVars, ReadOnlyEnv } from "./environment";
import { Logger } from "./logging"; import { Logger } from "./logging";
import { 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) { for (const language of config.languages) {
const databaseDirectory = getCodeQLDatabasePath(config, language); const databaseDirectory = getCodeQLDatabasePath(config, language);
const logsDirectory = path.join(databaseDirectory, "log"); const logsDirectory = path.join(databaseDirectory, "log");
if (!doesDirectoryExist(logsDirectory)) { if (!doesDirectoryExist(logsDirectory)) {
core.info(`Directory ${logsDirectory} does not exist.`); logger.info(`Directory ${logsDirectory} does not exist.`);
continue; // Skip this language database. continue; // Skip this language database.
} }
const walkLogFiles = (dir: string) => { const walkLogFiles = (dir: string) => {
const entries = fs.readdirSync(dir, { withFileTypes: true }); const entries = fs.readdirSync(dir, { withFileTypes: true });
if (entries.length === 0) { 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) { for (const entry of entries) {
if (entry.isFile()) { if (entry.isFile()) {
const absolutePath = path.resolve(dir, entry.name); const absolutePath = path.resolve(dir, entry.name);
core.startGroup( logger.startGroup(
`CodeQL Debug Logs - ${language} - ${entry.name} from file at path ${absolutePath}`, `CodeQL Debug Logs - ${language} - ${entry.name} from file at path ${absolutePath}`,
); );
process.stdout.write(fs.readFileSync(absolutePath)); logger.info(fs.readFileSync(absolutePath, "utf-8"));
core.endGroup(); logger.endGroup();
} else if (entry.isDirectory()) { } else if (entry.isDirectory()) {
walkLogFiles(path.resolve(dir, entry.name)); walkLogFiles(path.resolve(dir, entry.name));
} }

View File

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

View File

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

View File

@@ -21,7 +21,11 @@ import {
import * as api from "./api-client"; import * as api from "./api-client";
import { getCachingKind } from "./caching-utils"; import { getCachingKind } from "./caching-utils";
import { type CodeQL } from "./codeql"; import { type CodeQL } from "./codeql";
import { type Config } from "./config/action-config"; import {
type AbortedInitConfig,
type ActionConfig,
type Config,
} from "./config/action-config";
import { import {
calculateAugmentation, calculateAugmentation,
ExcludeQueryFilter, ExcludeQueryFilter,
@@ -71,6 +75,7 @@ import {
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import { shouldSkipOverlayAnalysis } from "./overlay/status"; import { shouldSkipOverlayAnalysis } from "./overlay/status";
import { RepositoryNwo } from "./repository"; import { RepositoryNwo } from "./repository";
import { ActionName } from "./status-report";
import { ToolsFeature } from "./tools-features"; import { ToolsFeature } from "./tools-features";
import { downloadTrapCaches } from "./trap-caching"; import { downloadTrapCaches } from "./trap-caching";
import { import {
@@ -1379,31 +1384,86 @@ export function getPathToParsedConfigFile(tempDir: string): string {
return path.join(tempDir, "config"); 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 configString = JSON.stringify(config);
const configFile = getPathToParsedConfigFile(config.tempDir); fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
fs.mkdirSync(path.dirname(configFile), { recursive: true }); fs.writeFileSync(configFilePath, configString, "utf8");
fs.writeFileSync(configFile, configString, "utf8");
logger.debug("Saved config:"); logger.debug("Saved config:");
logger.debug(configString); logger.debug(configString);
} }
/** /**
* Get the config that has been saved to the given temp dir. * Store the given config to the path returned from getPathToParsedConfigFile.
* If the config could not be found then returns undefined.
*/ */
export async function getConfig( export async function saveConfig(config: Config, logger: Logger) {
tempDir: string, 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, logger: Logger,
): Promise<Config | undefined> { configFilePath: string,
const configFile = getPathToParsedConfigFile(tempDir); ): Config | undefined {
if (!fs.existsSync(configFile)) { if (!fs.existsSync(configFilePath)) {
return undefined; return undefined;
} }
const configString = fs.readFileSync(configFile, "utf8"); const configString = fs.readFileSync(configFilePath, "utf8");
logger.debug("Loaded config:"); logger.debug("Loaded config:");
logger.debug(configString); logger.debug(configString);
@@ -1428,6 +1488,38 @@ export async function getConfig(
return config as Config; 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. * 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 * 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. * 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); return config.analysisKinds.includes(AnalysisKind.CodeScanning);
} }
/** /**
* Returns `true` if Code Quality analysis is enabled, or `false` if not. * 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); return config.analysisKinds.includes(AnalysisKind.CodeQuality);
} }
/** /**
* Returns `true` if Code Scanning Risk Assessment analysis is enabled, or `false` if not. * 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); return config.analysisKinds.includes(AnalysisKind.RiskAssessment);
} }

View File

@@ -126,3 +126,38 @@ export interface Config {
*/ */
enableFileCoverageInformation: boolean; 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 { dbIsFinalized } from "./analyze";
import { scanArtifactsForTokens } from "./artifact-scanner"; import { scanArtifactsForTokens } from "./artifact-scanner";
import { type CodeQL } from "./codeql"; import { type CodeQL } from "./codeql";
import { Config } from "./config-utils"; import type { ActionConfig } from "./config/action-config";
import { EnvVar, exportEnvVar } from "./environment"; import { EnvVar, exportEnvVar } from "./environment";
import * as json from "./json"; import * as json from "./json";
import { Language } from "./languages"; 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. * @return The path to that debug artifact, or undefined if an error occurs.
*/ */
function tryPrepareSarifDebugArtifact( function tryPrepareSarifDebugArtifact(
config: Config, config: ActionConfig<"dbLocation">,
language: Language, language: Language,
logger: Logger, logger: Logger,
): string | undefined { ): string | undefined {
@@ -131,7 +131,7 @@ function tryPrepareSarifDebugArtifact(
*/ */
async function tryBundleDatabase( async function tryBundleDatabase(
codeql: CodeQL, codeql: CodeQL,
config: Config, config: ActionConfig<"dbLocation">,
language: Language, language: Language,
logger: Logger, logger: Logger,
): Promise<string | undefined> { ): 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. * Attempt to upload all available debug artifacts.
* *
@@ -164,7 +169,7 @@ async function tryBundleDatabase(
*/ */
export async function tryUploadAllAvailableDebugArtifacts( export async function tryUploadAllAvailableDebugArtifacts(
codeql: CodeQL, codeql: CodeQL,
config: Config, config: UploadDebugArtifactsConfig,
logger: Logger, logger: Logger,
) { ) {
const filesToUpload: string[] = []; const filesToUpload: string[] = [];
@@ -353,7 +358,7 @@ export async function getArtifactUploaderClient(
* all files in the database folder and return the path. * all files in the database folder and return the path.
*/ */
async function createPartialDatabaseBundle( async function createPartialDatabaseBundle(
config: Config, config: ActionConfig<"dbLocation">,
language: Language, language: Language,
): Promise<string> { ): Promise<string> {
const databasePath = getCodeQLDatabasePath(config, language); const databasePath = getCodeQLDatabasePath(config, language);
@@ -394,7 +399,7 @@ async function createPartialDatabaseBundle(
*/ */
async function createDatabaseBundleCli( async function createDatabaseBundleCli(
codeql: CodeQL, codeql: CodeQL,
config: Config, config: ActionConfig<"dbLocation">,
language: Language, language: Language,
): Promise<string> { ): Promise<string> {
const databaseBundlePath = await bundleDb( const databaseBundlePath = await bundleDb(

View File

@@ -7,6 +7,11 @@ import * as actionsUtil from "./actions-util";
import { CodeScanning, RiskAssessment } from "./analyses"; import { CodeScanning, RiskAssessment } from "./analyses";
import { getApiClient, getGitHubVersion } from "./api-client"; import { getApiClient, getGitHubVersion } from "./api-client";
import { CodeQL, getCodeQL } from "./codeql"; import { CodeQL, getCodeQL } from "./codeql";
import {
hasConfigKey,
type AbortedInitConfig,
type ActionConfig,
} from "./config/action-config";
import { import {
Config, Config,
isCodeScanningEnabled, isCodeScanningEnabled,
@@ -16,6 +21,7 @@ import {
getArtifactSuffix, getArtifactSuffix,
getArtifactUploaderClient, getArtifactUploaderClient,
sanitizeArtifactName, sanitizeArtifactName,
type UploadDebugArtifactsConfig,
} from "./debug-artifacts"; } from "./debug-artifacts";
import * as dependencyCaching from "./dependency-caching"; import * as dependencyCaching from "./dependency-caching";
import { EnvVar, ReadOnlyEnv } from "./environment"; import { EnvVar, ReadOnlyEnv } from "./environment";
@@ -86,6 +92,11 @@ interface FailedSarifInfo {
checkoutPath: string; 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. * Tries to prepare a SARIF file that contains information about a failed analysis.
* *
@@ -94,7 +105,7 @@ interface FailedSarifInfo {
async function prepareFailedSarif( async function prepareFailedSarif(
logger: Logger, logger: Logger,
features: FeatureEnablement, features: FeatureEnablement,
config: Config, config: FailedSarifConfig,
): Promise<Result<FailedSarifInfo, UploadFailedSarifResult>> { ): Promise<Result<FailedSarifInfo, UploadFailedSarifResult>> {
if (!config.codeQLCmd) { if (!config.codeQLCmd) {
return new Failure({ return new Failure({
@@ -160,7 +171,7 @@ async function prepareFailedSarif(
async function generateFailedSarif( async function generateFailedSarif(
logger: Logger, logger: Logger,
features: FeatureEnablement, features: FeatureEnablement,
config: Config, config: ActionConfig<"codeQLCmd" | "tempDir">,
category: string | undefined, category: string | undefined,
checkoutPath: string, checkoutPath: string,
sarifFile?: string, sarifFile?: string,
@@ -192,7 +203,7 @@ async function generateFailedSarif(
* category for the workflow. * category for the workflow.
*/ */
async function maybeUploadFailedSarif( async function maybeUploadFailedSarif(
config: Config, config: FailedSarifConfig,
repositoryNwo: RepositoryNwo, repositoryNwo: RepositoryNwo,
features: FeatureEnablement, features: FeatureEnablement,
logger: Logger, logger: Logger,
@@ -227,7 +238,7 @@ async function maybeUploadFailedSarif(
/** Uploads a failed SARIF file as workflow artifact, if it can be generated. */ /** Uploads a failed SARIF file as workflow artifact, if it can be generated. */
async function maybeUploadFailedSarifArtifact( async function maybeUploadFailedSarifArtifact(
config: Config, config: FailedSarifConfig,
features: FeatureEnablement, features: FeatureEnablement,
logger: Logger, logger: Logger,
): Promise<UploadFailedSarifResult> { ): Promise<UploadFailedSarifResult> {
@@ -267,7 +278,7 @@ async function maybeUploadFailedSarifArtifact(
* @returns The results of uploading the SARIF file for the failure. * @returns The results of uploading the SARIF file for the failure.
*/ */
export async function tryUploadSarifIfRunFailed( export async function tryUploadSarifIfRunFailed(
config: Config, config: FailedSarifConfig,
repositoryNwo: RepositoryNwo, repositoryNwo: RepositoryNwo,
features: FeatureEnablement, features: FeatureEnablement,
logger: Logger, logger: Logger,
@@ -324,12 +335,12 @@ export async function tryUploadSarifIfRunFailed(
export async function uploadFailureInfo( export async function uploadFailureInfo(
uploadAllAvailableDebugArtifacts: ( uploadAllAvailableDebugArtifacts: (
codeql: CodeQL, codeql: CodeQL,
config: Config, config: UploadDebugArtifactsConfig,
logger: Logger, logger: Logger,
) => Promise<void>, ) => Promise<void>,
printDebugLogs: (config: Config) => Promise<void>, printDebugLogs: (logger: Logger, config: Partial<Config>) => Promise<void>,
codeql: CodeQL, codeql: CodeQL,
config: Config, config: Config | AbortedInitConfig,
repositoryNwo: RepositoryNwo, repositoryNwo: RepositoryNwo,
features: FeatureEnablement, features: FeatureEnablement,
jobStatus: string | undefined, jobStatus: string | undefined,
@@ -376,15 +387,15 @@ export async function uploadFailureInfo(
} }
// Upload appropriate Actions artifacts for debugging // Upload appropriate Actions artifacts for debugging
if (config.debugMode) { if (config.debugMode && hasConfigKey(config, "dbLocation")) {
logger.info( logger.info(
"Debug mode is on. Uploading available database bundles and logs as Actions debugging artifacts...", "Debug mode is on. Uploading available database bundles and logs as Actions debugging artifacts...",
); );
await uploadAllAvailableDebugArtifacts(codeql, config, logger); await uploadAllAvailableDebugArtifacts(codeql, config, logger);
await printDebugLogs(config); await printDebugLogs(logger, config);
} }
if (actionsUtil.isSelfHostedRunner()) { if (actionsUtil.isSelfHostedRunner() && config.dbLocation) {
try { try {
fs.rmSync(config.dbLocation, { fs.rmSync(config.dbLocation, {
recursive: true, recursive: true,
@@ -399,6 +410,10 @@ export async function uploadFailureInfo(
`Failed to clean up database cluster directory ${config.dbLocation}. Details: ${e}`, `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 { } else {
logger.debug( logger.debug(
"Skipping cleanup of database cluster directory since we are running on a GitHub-hosted " + "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( async function recordOverlayStatus(
codeql: CodeQL, codeql: CodeQL,
config: Config, config: Partial<Config>,
features: FeatureEnablement, features: FeatureEnablement,
jobStatus: string | undefined, jobStatus: string | undefined,
env: ReadOnlyEnv, env: ReadOnlyEnv,
@@ -459,6 +474,11 @@ async function recordOverlayStatus(
!(await features.getValue(Feature.OverlayAnalysisStatusSave)) !(await features.getValue(Feature.OverlayAnalysisStatusSave))
) { ) {
return; 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 // 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 * as core from "@actions/core";
import { runInActions, type Action, type ActionState } from "./action-common";
import { import {
restoreInputs, restoreInputs,
getOptionalInput, getOptionalInput,
@@ -15,22 +16,25 @@ import {
import { getGitHubVersion, wrapApiConfigurationError } from "./api-client"; import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
import { CachingKind } from "./caching-utils"; import { CachingKind } from "./caching-utils";
import { getCodeQL } from "./codeql"; 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 * as debugArtifacts from "./debug-artifacts";
import { import {
DependencyCachingUsageReport, DependencyCachingUsageReport,
getDependencyCacheUsage, getDependencyCacheUsage,
} from "./dependency-caching"; } from "./dependency-caching";
import { EnvVar, getEnv, exportEnvVar } from "./environment"; import { EnvVar, type ReadOnlyEnv } from "./environment";
import { initFeatures } from "./feature-flags"; import { initFeatures } from "./feature-flags";
import * as gitUtils from "./git-utils"; import * as gitUtils from "./git-utils";
import * as initActionPostHelper from "./init-action-post-helper"; import * as initActionPostHelper from "./init-action-post-helper";
import { getActionsLogger } from "./logging";
import { getRepositoryNwo } from "./repository"; import { getRepositoryNwo } from "./repository";
import { import {
StatusReportBase, StatusReportBase,
sendStatusReport, sendStatusReport,
sendUnhandledErrorStatusReport,
createStatusReportBase, createStatusReportBase,
getActionsStatus, getActionsStatus,
ActionName, ActionName,
@@ -45,16 +49,17 @@ interface InitPostStatusReport
initActionPostHelper.JobStatusReport, initActionPostHelper.JobStatusReport,
initActionPostHelper.DependencyCachingUsageReport {} 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 // 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 = action.logger;
const logger = getActionsLogger(); let config: Config | AbortedInitConfig | undefined;
let config: Config | undefined;
let uploadFailedSarifResult: let uploadFailedSarifResult:
| initActionPostHelper.UploadFailedSarifResult | initActionPostHelper.UploadFailedSarifResult
| undefined; | undefined;
let dependencyCachingUsage: DependencyCachingUsageReport | undefined; let dependencyCachingUsage: DependencyCachingUsageReport | undefined;
try { try {
// Read the job status before restoring inputs, since it is provided by the Actions runtime // 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` // 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. // Restore inputs from `init` Action.
restoreInputs(logger); 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()); const gitHubVersion = config?.gitHubVersion ?? (await getGitHubVersion());
checkGitHubVersionInRange(gitHubVersion, logger); checkGitHubVersionInRange(gitHubVersion, logger);
@@ -92,7 +105,7 @@ async function run(startedAt: Date) {
repositoryNwo, repositoryNwo,
features, features,
jobStatus, jobStatus,
getEnv(), action.env,
logger, logger,
); );
@@ -102,6 +115,7 @@ async function run(startedAt: Date) {
// and where caching may not be enabled. // and where caching may not be enabled.
if ( if (
(await gitUtils.isAnalyzingDefaultBranch()) && (await gitUtils.isAnalyzingDefaultBranch()) &&
hasConfigKey(config, "dependencyCachingEnabled") &&
config.dependencyCachingEnabled !== CachingKind.None config.dependencyCachingEnabled !== CachingKind.None
) { ) {
dependencyCachingUsage = await getDependencyCacheUsage(logger); dependencyCachingUsage = await getDependencyCacheUsage(logger);
@@ -112,9 +126,9 @@ async function run(startedAt: Date) {
core.setFailed(error.message); core.setFailed(error.message);
const statusReportBase = await createStatusReportBase( const statusReportBase = await createStatusReportBase(
ActionName.InitPost, action.name,
getActionsStatus(error), getActionsStatus(error),
startedAt, action.startedAt,
config, config,
await checkDiskUsage(logger), await checkDiskUsage(logger),
logger, logger,
@@ -126,13 +140,13 @@ async function run(startedAt: Date) {
} }
return; return;
} }
const jobStatus = getFinalJobStatus(config); const jobStatus = getFinalJobStatus(action, config);
logger.info(`CodeQL job status was ${getJobStatusDisplayName(jobStatus)}.`); logger.info(`CodeQL job status was ${getJobStatusDisplayName(jobStatus)}.`);
const statusReportBase = await createStatusReportBase( const statusReportBase = await createStatusReportBase(
ActionName.InitPost, action.name,
"success", "success",
startedAt, action.startedAt,
config, config,
await checkDiskUsage(logger), await checkDiskUsage(logger),
logger, logger,
@@ -157,16 +171,21 @@ async function run(startedAt: Date) {
* Otherwise, we determine the job status based on whether the analyze step * Otherwise, we determine the job status based on whether the analyze step
* completed successfully and whether we have a valid CodeQL config. * completed successfully and whether we have a valid CodeQL config.
*/ */
function getFinalJobStatus(config: Config | undefined): JobStatus { function getFinalJobStatus(
const existingJobStatus = getJobStatusFromEnvironment(); action: ActionState<["Env"]>,
config: Partial<Config> | undefined,
): JobStatus {
const existingJobStatus = getJobStatusFromEnvironment(action.env);
if (existingJobStatus !== undefined) { if (existingJobStatus !== undefined) {
return existingJobStatus; return existingJobStatus;
} }
let jobStatus: JobStatus; let jobStatus: JobStatus;
if (process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true") { if (
exportEnvVar(EnvVar.JOB_STATUS, JobStatus.SuccessStatus); action.env.getOptional(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY) === "true"
) {
action.env.export(EnvVar.JOB_STATUS, JobStatus.SuccessStatus);
jobStatus = JobStatus.SuccessStatus; jobStatus = JobStatus.SuccessStatus;
} else if (config !== undefined) { } else if (config !== undefined) {
// - We have computed a CodeQL config // - 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 // 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. // `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; return jobStatus;
} }
@@ -200,8 +219,8 @@ function getFinalJobStatus(config: Config | undefined): JobStatus {
* *
* If the job status is invalid, return `UnknownStatus`. * If the job status is invalid, return `UnknownStatus`.
*/ */
function getJobStatusFromEnvironment(): JobStatus | undefined { function getJobStatusFromEnvironment(env: ReadOnlyEnv): JobStatus | undefined {
const jobStatusFromEnvironment = process.env[EnvVar.JOB_STATUS]; const jobStatusFromEnvironment = env.getOptional(EnvVar.JOB_STATUS);
if (jobStatusFromEnvironment !== undefined) { if (jobStatusFromEnvironment !== undefined) {
// Validate the job status from the environment. If it is invalid, return unknown. // Validate the job status from the environment. If it is invalid, return unknown.
@@ -216,18 +235,11 @@ function getJobStatusFromEnvironment(): JobStatus | undefined {
return undefined; return undefined;
} }
const initPost: Action = {
name: ActionName.InitPost,
run,
};
export async function runWrapper() { export async function runWrapper() {
const startedAt = new Date(); return await runInActions(initPost);
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,
);
}
} }

View File

@@ -457,6 +457,18 @@ async function run(
await checkInstallPython311(config.languages, codeql); await checkInstallPython311(config.languages, codeql);
} catch (unwrappedError) { } 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); const error = wrapError(unwrappedError);
core.setFailed(error.message); core.setFailed(error.message);
const statusReportBase = await createStatusReportBase( const statusReportBase = await createStatusReportBase(

View File

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

View File

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