diff --git a/lib/entry-points.js b/lib/entry-points.js index 21c0ccd15f..373cdedc01 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -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 diff --git a/src/action-common.test.ts b/src/action-common.test.ts index fc2e0a9aaa..449300a8d2 100644 --- a/src/action-common.test.ts +++ b/src/action-common.test.ts @@ -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; expectedErrorMessage?: string; diff --git a/src/actions-util.test.ts b/src/actions-util.test.ts index 3940cf7551..17cf96a4a5 100644 --- a/src/actions-util.test.ts +++ b/src/actions-util.test.ts @@ -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( { diff --git a/src/actions-util.ts b/src/actions-util.ts index ea9a119ed4..62ab42440c 100644 --- a/src/actions-util.ts +++ b/src/actions-util.ts @@ -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) { + 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)); } diff --git a/src/analyze.ts b/src/analyze.ts index 4eeb447e18..4d5fd53543 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -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, ) { diff --git a/src/codeql.ts b/src/codeql.ts index 8913224fab..68273d27b5 100644 --- a/src/codeql.ts +++ b/src/codeql.ts @@ -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; /** Get the location of an extractor for the specified language. */ resolveExtractor(language: Language): Promise; @@ -913,7 +914,7 @@ async function getCodeQLForCmd( async diagnosticsExport( sarifFile: string, automationDetailsId: string | undefined, - config: Config, + config: ActionConfig<"tempDir">, ): Promise { 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"); } diff --git a/src/config-utils.ts b/src/config-utils.ts index f1ec30b2c8..e0b005db78 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -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, +) { 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 { - 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 { + 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); } diff --git a/src/config/action-config.ts b/src/config/action-config.ts index de6882e77e..10f315028f 100644 --- a/src/config/action-config.ts +++ b/src/config/action-config.ts @@ -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 & Pick; + +/** Helper function to check whether `config` has `key` to narrow the type of `config`. */ +export function hasConfigKey( + config: Partial, + key: Key, +): config is ActionConfig { + 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" +>; diff --git a/src/debug-artifacts.ts b/src/debug-artifacts.ts index 70c9a0ebd4..56497f14e8 100644 --- a/src/debug-artifacts.ts +++ b/src/debug-artifacts.ts @@ -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 { @@ -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 { 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 { const databaseBundlePath = await bundleDb( diff --git a/src/init-action-post-helper.ts b/src/init-action-post-helper.ts index 93ee391aaa..b6aeb5bd34 100644 --- a/src/init-action-post-helper.ts +++ b/src/init-action-post-helper.ts @@ -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> { 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 { @@ -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, - printDebugLogs: (config: Config) => Promise, + printDebugLogs: (logger: Logger, config: Partial) => Promise, 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, 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 diff --git a/src/init-action-post.ts b/src/init-action-post.ts index a81c27810a..689cfd4659 100644 --- a/src/init-action-post.ts +++ b/src/init-action-post.ts @@ -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 | 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); } diff --git a/src/init-action.ts b/src/init-action.ts index 8723044da6..5376763b52 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -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( diff --git a/src/status-report.ts b/src/status-report.ts index ae21beb307..fc45e82d67 100644 --- a/src/status-report.ts +++ b/src/status-report.ts @@ -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> = { + [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; } diff --git a/src/util.ts b/src/util.ts index 674e2a2eef..566386de62 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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,