Compare commits

...

18 Commits

Author SHA1 Message Date
Michael B. Gale
eb94de7c5c Merge pull request #4225 from github/mbg/reduce-config-requirements
Store and restore "failed" configuration if `init` step aborts
2026-10-13 09:34:24 +00:00
Michael B. Gale
9645acc2ff Revert config === undefined condition 2026-10-12 22:54:47 +01:00
Michael B. Gale
6a9f7e7fc3 Log if returning early from recordOverlayStatus due to no languages 2026-10-12 22:50:11 +01:00
Michael B. Gale
67f44bc916 Use Logger for printDebugLogs and log when returning early 2026-10-12 22:48:21 +01:00
Michael B. Gale
26c6f91e6b Throw if aborted config functions are called incorrectly 2026-10-12 22:42:38 +01:00
Michael B. Gale
e3aeabd8f1 Fix example in comment 2026-10-12 22:40:25 +01:00
Michael B. Gale
be78e02f2d Fix comment 2026-10-12 22:39:34 +01:00
Michael B. Gale
bed4cc0475 Add log message for !config.dbLocation case 2026-10-12 22:39:08 +01:00
Michael B. Gale
ac8654a8f2 Add printDebugLogs tests 2026-10-12 13:08:08 +01:00
Michael B. Gale
ecfc96e0a9 Add getDisplayActionName tests 2026-10-12 13:08:01 +01:00
Michael B. Gale
409be01136 Replace deprecated functions in init-action.post.ts 2026-10-12 13:07:55 +01:00
Michael B. Gale
f4a15a13fd Try to load failed config if normal config is unavailable
Adapt relevant functions by narrowing their types or skipping steps that would otherwise have been skipped as well
2026-10-12 13:07:48 +01:00
Michael B. Gale
4fdbb71b54 Use runInActions for init-post 2026-10-12 13:07:38 +01:00
Michael B. Gale
5136039854 Store partial configuration when init is aborted 2026-10-12 13:07:36 +01:00
Michael B. Gale
19445ddfed Reduce Config requirements for tryUploadSarifIfRunFailed 2026-10-12 13:07:13 +01:00
Michael B. Gale
317f264e1f Change diagnosticsExport to not require full Config 2026-10-12 13:07:06 +01:00
Michael B. Gale
309f6af89b Change getGeneratedCodeScanningConfigPath to not require full Config 2026-10-12 13:06:59 +01:00
Michael B. Gale
cf8f7b9787 Add ActionConfig type 2026-10-12 13:06:43 +01:00
14 changed files with 463 additions and 150 deletions

191
lib/entry-points.js generated
View File

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

View File

@@ -19,6 +19,24 @@ import { getErrorMessage } from "./util";
setupTests(test);
for (const [key, displayName] of Object.entries(
statusReport.ACTION_DISPLAY_NAMES,
)) {
test(`getDisplayActionName - '${displayName}' instead of '${key}'`, (t) => {
t.is(statusReport.getDisplayActionName(key as ActionName), displayName);
});
}
test("getDisplayActionName - returns action name", (t) => {
for (const actionName of Object.values(ActionName)) {
if (statusReport.ACTION_DISPLAY_NAMES[actionName]) {
continue;
}
t.is(statusReport.getDisplayActionName(actionName), actionName);
}
});
interface RunInActionsTestOpts {
runFn?: () => Promise<any>;
expectedErrorMessage?: string;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,13 +61,21 @@ export enum ActionName {
UploadSarif = "upload-sarif",
}
/** A partial mapping from `ActionName` to names for actions whose display name is not the `ActionName` value. */
export const ACTION_DISPLAY_NAMES: Partial<Record<ActionName, string>> = {
[ActionName.Analyze]: "analyze",
[ActionName.InitPost]: "init post",
};
/**
* Maps an `ActionName` to its display name. Usually that is the same, except
* for `ActionName.Analyze` where it is `"analyze"` instead of `"finish"`.
* Maps an {@link ActionName} to its display name. Usually that is the same, except
* for actions which have a different display name in {@link ACTION_DISPLAY_NAMES}.
*/
export function getDisplayActionName(actionName: ActionName): string {
if (actionName === ActionName.Analyze) {
return "analyze";
const displayName = ACTION_DISPLAY_NAMES[actionName];
if (displayName !== undefined) {
return displayName;
}
return actionName;
}

View File

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