Refactor configuration errors (#2105)

Refactor the existing classes of configuration errors into their own file; consolidate the place we check for configuration errors into `codeql.ts`, where the actual command invocations happen.

Also, rename the `UserError` type to `ConfigurationError` to standardize on a single term.
This commit is contained in:
Angela P Wen 2024-02-08 09:20:03 -08:00 committed by GitHub
parent fc9f9e5ef9
commit 1515e2bb20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 654 additions and 502 deletions

View file

@ -11,7 +11,7 @@ import {
doesDirectoryExist,
getCodeQLDatabasePath,
getRequiredEnvParam,
UserError,
ConfigurationError,
} from "./util";
// eslint-disable-next-line import/no-commonjs
@ -26,7 +26,7 @@ const pkg = require("../package.json") as JSONSchemaForNPMPackageJsonFiles;
export const getRequiredInput = function (name: string): string {
const value = core.getInput(name);
if (!value) {
throw new UserError(`Input required and not supplied: ${name}`);
throw new ConfigurationError(`Input required and not supplied: ${name}`);
}
return value;
};
@ -187,7 +187,7 @@ export async function getRef(): Promise<string> {
const hasShaInput = !!shaInput;
// If one of 'ref' or 'sha' are provided, both are required
if ((hasRefInput || hasShaInput) && !(hasRefInput && hasShaInput)) {
throw new UserError(
throw new ConfigurationError(
"Both 'ref' and 'sha' are required if one of them is provided.",
);
}

View file

@ -207,7 +207,7 @@ async function run() {
}
if (hasBadExpectErrorInput()) {
throw new util.UserError(
throw new util.ConfigurationError(
"`expect-error` input parameter is for internal use only. It should only be set by codeql-action or a fork.",
);
}
@ -285,7 +285,7 @@ async function run() {
actionsUtil.getRequiredInput("checkout_path"),
actionsUtil.getOptionalInput("category"),
logger,
{ considerInvalidRequestUserError: false },
{ considerInvalidRequestConfigError: false },
);
core.setOutput("sarif-id", uploadResult.sarifID);
} else {

209
src/cli-errors.ts Normal file
View file

@ -0,0 +1,209 @@
import { ConfigurationError } from "./util";
/**
* A class of Error that we can classify as an error stemming from a CLI
* invocation, with associated exit code, stderr,etc.
*/
export class CommandInvocationError extends Error {
constructor(
cmd: string,
args: string[],
public exitCode: number,
public stderr: string,
public stdout: string,
) {
const prettyCommand = [cmd, ...args]
.map((x) => (x.includes(" ") ? `'${x}'` : x))
.join(" ");
const fatalErrors = extractFatalErrors(stderr);
const lastLine = stderr.trim().split("\n").pop()?.trim();
let error = fatalErrors
? ` and error was: ${fatalErrors.trim()}`
: lastLine
? ` and last log line was: ${lastLine}`
: "";
if (error[error.length - 1] !== ".") {
error += ".";
}
super(
`Encountered a fatal error while running "${prettyCommand}". ` +
`Exit code was ${exitCode}${error} See the logs for more details.`,
);
}
}
/**
* Provide a better error message from the stderr of a CLI invocation that failed with a fatal
* error.
*
* - If the CLI invocation failed with a fatal error, this returns that fatal error, followed by
* any fatal errors that occurred in plumbing commands.
* - If the CLI invocation did not fail with a fatal error, this returns `undefined`.
*
* ### Example
*
* ```
* Running TRAP import for CodeQL database at /home/runner/work/_temp/codeql_databases/javascript...
* A fatal error occurred: Evaluator heap must be at least 384.00 MiB
* A fatal error occurred: Dataset import for
* /home/runner/work/_temp/codeql_databases/javascript/db-javascript failed with code 2
* ```
*
* becomes
*
* ```
* Encountered a fatal error while running "codeql-for-testing database finalize --finalize-dataset
* --threads=2 --ram=2048 db". Exit code was 32 and error was: A fatal error occurred: Dataset
* import for /home/runner/work/_temp/codeql_databases/javascript/db-javascript failed with code 2.
* Context: A fatal error occurred: Evaluator heap must be at least 384.00 MiB.
* ```
*
* Where possible, this tries to summarize the error into a single line, as this displays better in
* the Actions UI.
*/
function extractFatalErrors(error: string): string | undefined {
const fatalErrorRegex = /.*fatal error occurred:/gi;
let fatalErrors: string[] = [];
let lastFatalErrorIndex: number | undefined;
let match: RegExpMatchArray | null;
while ((match = fatalErrorRegex.exec(error)) !== null) {
if (lastFatalErrorIndex !== undefined) {
fatalErrors.push(error.slice(lastFatalErrorIndex, match.index).trim());
}
lastFatalErrorIndex = match.index;
}
if (lastFatalErrorIndex !== undefined) {
const lastError = error.slice(lastFatalErrorIndex).trim();
if (fatalErrors.length === 0) {
// No other errors
return lastError;
}
const isOneLiner = !fatalErrors.some((e) => e.includes("\n"));
if (isOneLiner) {
fatalErrors = fatalErrors.map(ensureEndsInPeriod);
}
return [
ensureEndsInPeriod(lastError),
"Context:",
...fatalErrors.reverse(),
].join(isOneLiner ? " " : "\n");
}
return undefined;
}
function ensureEndsInPeriod(text: string): string {
return text[text.length - 1] === "." ? text : `${text}.`;
}
/** Error messages from the CLI that we consider configuration errors and handle specially. */
export enum CliConfigErrorCategory {
IncompatibleWithActionVersion = "IncompatibleWithActionVersion",
InitCalledTwice = "InitCalledTwice",
InvalidSourceRoot = "InvalidSourceRoot",
NoJavaScriptTypeScriptCodeFound = "NoJavaScriptTypeScriptCodeFound",
}
type CliErrorConfiguration = {
cliErrorMessageSnippets: string[];
exitCode?: number;
// Error message to prepend for this type of CLI error.
// If undefined, use original CLI error message.
additionalErrorMessageToPrepend?: string;
};
/**
* All of our caught CLI error messages that we handle specially: ie. if we
* would like to categorize an error as a configuration error or not.
*/
export const cliErrorsConfig: Record<
CliConfigErrorCategory,
CliErrorConfiguration
> = {
// Version of CodeQL CLI is incompatible with this version of the CodeQL Action
[CliConfigErrorCategory.IncompatibleWithActionVersion]: {
cliErrorMessageSnippets: ["is not compatible with this CodeQL CLI"],
},
[CliConfigErrorCategory.InitCalledTwice]: {
cliErrorMessageSnippets: [
"Refusing to create databases",
"exists and is not an empty directory",
],
additionalErrorMessageToPrepend: `Is the "init" action called twice in the same job?`,
},
// Expected source location for database creation does not exist
[CliConfigErrorCategory.InvalidSourceRoot]: {
cliErrorMessageSnippets: ["Invalid source root"],
},
/**
* Earlier versions of the JavaScript extractor (pre-CodeQL 2.12.0) extract externs even if no
* source code was found. This means that we don't get the no code found error from
* `codeql database finalize`. To ensure users get a good error message, we detect this manually
* here, and upon detection override the error message.
*
* This can be removed once support for CodeQL 2.11.6 is removed.
*/
[CliConfigErrorCategory.NoJavaScriptTypeScriptCodeFound]: {
exitCode: 32,
cliErrorMessageSnippets: ["No JavaScript or TypeScript code found."],
additionalErrorMessageToPrepend:
"No code found during the build. Please see: " +
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build.",
},
};
// Check if the given CLI error or exit code, if applicable, apply to any known
// CLI errors in the configuration record. If either the CLI error message matches all of
// the error messages in the config record, or the exit codes match, return the error category;
// if not, return undefined.
export function getCliConfigCategoryIfExists(
cliError: CommandInvocationError,
): CliConfigErrorCategory | undefined {
for (const [category, configuration] of Object.entries(cliErrorsConfig)) {
if (
cliError.exitCode !== undefined &&
configuration.exitCode !== undefined &&
cliError.exitCode === configuration.exitCode
) {
return category as CliConfigErrorCategory;
}
let allMessageSnippetsFound: boolean = true;
for (const e of configuration.cliErrorMessageSnippets) {
if (!cliError.message.includes(e) && !cliError.stderr.includes(e)) {
allMessageSnippetsFound = false;
}
}
if (allMessageSnippetsFound === true) {
return category as CliConfigErrorCategory;
}
}
return undefined;
}
/**
* Changes an error received from the CLI to a ConfigurationError with optionally an extra
* error message prepended, if it exists in a known set of configuration errors. Otherwise,
* simply returns the original error.
*/
export function wrapCliConfigurationError(cliError: Error): Error {
if (!(cliError instanceof CommandInvocationError)) {
return cliError;
}
const cliConfigErrorCategory = getCliConfigCategoryIfExists(cliError);
if (cliConfigErrorCategory === undefined) {
return cliError;
}
const errorMessageWrapperIfExists =
cliErrorsConfig[cliConfigErrorCategory].additionalErrorMessageToPrepend;
return errorMessageWrapperIfExists
? new ConfigurationError(
`${errorMessageWrapperIfExists} ${cliError.message}`,
)
: new ConfigurationError(cliError.message);
}

View file

@ -875,9 +875,11 @@ test("database finalize recognises JavaScript no code found error on CodeQL 2.11
await t.throwsAsync(
async () => await codeqlObject.finalizeDatabase("", "", ""),
{
message:
instanceOf: util.ConfigurationError,
message: new RegExp(
"No code found during the build. Please see: " +
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build",
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build.+",
),
},
);
});
@ -892,9 +894,11 @@ test("database finalize overrides no code found error on CodeQL 2.11.6", async (
await t.throwsAsync(
async () => await codeqlObject.finalizeDatabase("", "", ""),
{
message:
instanceOf: util.ConfigurationError,
message: new RegExp(
"No code found during the build. Please see: " +
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build",
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build.+",
),
},
);
});

View file

@ -12,6 +12,10 @@ import {
isAnalyzingDefaultBranch,
} from "./actions-util";
import * as api from "./api-client";
import {
CommandInvocationError,
wrapCliConfigurationError,
} from "./cli-errors";
import type { Config } from "./config-utils";
import { EnvVar } from "./environment";
import {
@ -48,36 +52,6 @@ interface ExtraOptions {
};
}
export class CommandInvocationError extends Error {
constructor(
cmd: string,
args: string[],
public exitCode: number,
public stderr: string,
public stdout: string,
) {
const prettyCommand = [cmd, ...args]
.map((x) => (x.includes(" ") ? `'${x}'` : x))
.join(" ");
const fatalErrors = extractFatalErrors(stderr);
const lastLine = stderr.trim().split("\n").pop()?.trim();
let error = fatalErrors
? ` and error was: ${fatalErrors.trim()}`
: lastLine
? ` and last log line was: ${lastLine}`
: "";
if (error[error.length - 1] !== ".") {
error += ".";
}
super(
`Encountered a fatal error while running "${prettyCommand}". ` +
`Exit code was ${exitCode}${error} See the logs for more details.`,
);
}
}
export interface CodeQL {
/**
* Get the path of the CodeQL executable.
@ -409,7 +383,9 @@ export async function setupCodeQL(
if (process.platform === "win32") {
codeqlCmd += ".exe";
} else if (process.platform !== "linux" && process.platform !== "darwin") {
throw new util.UserError(`Unsupported platform: ${process.platform}`);
throw new util.ConfigurationError(
`Unsupported platform: ${process.platform}`,
);
}
cachedCodeQL = await getCodeQLForCmd(codeqlCmd, checkVersion);
@ -639,20 +615,27 @@ export async function getCodeQLForCmd(
extraArgs.push("--no-sublanguage-file-coverage");
}
await runTool(
cmd,
[
"database",
"init",
"--db-cluster",
config.dbLocation,
`--source-root=${sourceRoot}`,
...(await getLanguageAliasingArguments(this)),
...extraArgs,
...getExtraOptionsFromEnv(["database", "init"]),
],
{ stdin: externalRepositoryToken },
);
try {
await runTool(
cmd,
[
"database",
"init",
"--db-cluster",
config.dbLocation,
`--source-root=${sourceRoot}`,
...(await getLanguageAliasingArguments(this)),
...extraArgs,
...getExtraOptionsFromEnv(["database", "init"]),
],
{ stdin: externalRepositoryToken },
);
} catch (e) {
if (e instanceof Error) {
throw wrapCliConfigurationError(e);
}
throw e;
}
},
async runAutobuild(language: Language) {
const autobuildCmd = path.join(
@ -727,17 +710,13 @@ export async function getCodeQLForCmd(
await runTool(cmd, args);
} catch (e) {
if (
e instanceof CommandInvocationError &&
e instanceof Error &&
!(await util.codeQlVersionAbove(
this,
CODEQL_VERSION_BETTER_NO_CODE_ERROR_MESSAGE,
)) &&
isNoCodeFoundError(e)
))
) {
throw new util.UserError(
"No code found during the build. Please see: " +
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build",
);
throw wrapCliConfigurationError(e);
}
throw e;
}
@ -1128,7 +1107,7 @@ export async function getCodeQLForCmd(
checkVersion &&
!(await util.codeQlVersionAbove(codeql, CODEQL_MINIMUM_VERSION))
) {
throw new util.UserError(
throw new util.ConfigurationError(
`Expected a CodeQL CLI with version at least ${CODEQL_MINIMUM_VERSION} but got version ${
(await codeql.getVersion()).version
}`,
@ -1265,69 +1244,6 @@ async function runTool(
return output;
}
/**
* Provide a better error message from the stderr of a CLI invocation that failed with a fatal
* error.
*
* - If the CLI invocation failed with a fatal error, this returns that fatal error, followed by
* any fatal errors that occurred in plumbing commands.
* - If the CLI invocation did not fail with a fatal error, this returns `undefined`.
*
* ### Example
*
* ```
* Running TRAP import for CodeQL database at /home/runner/work/_temp/codeql_databases/javascript...
* A fatal error occurred: Evaluator heap must be at least 384.00 MiB
* A fatal error occurred: Dataset import for
* /home/runner/work/_temp/codeql_databases/javascript/db-javascript failed with code 2
* ```
*
* becomes
*
* ```
* Encountered a fatal error while running "codeql-for-testing database finalize --finalize-dataset
* --threads=2 --ram=2048 db". Exit code was 32 and error was: A fatal error occurred: Dataset
* import for /home/runner/work/_temp/codeql_databases/javascript/db-javascript failed with code 2.
* Context: A fatal error occurred: Evaluator heap must be at least 384.00 MiB.
* ```
*
* Where possible, this tries to summarize the error into a single line, as this displays better in
* the Actions UI.
*/
function extractFatalErrors(error: string): string | undefined {
const fatalErrorRegex = /.*fatal error occurred:/gi;
let fatalErrors: string[] = [];
let lastFatalErrorIndex: number | undefined;
let match: RegExpMatchArray | null;
while ((match = fatalErrorRegex.exec(error)) !== null) {
if (lastFatalErrorIndex !== undefined) {
fatalErrors.push(error.slice(lastFatalErrorIndex, match.index).trim());
}
lastFatalErrorIndex = match.index;
}
if (lastFatalErrorIndex !== undefined) {
const lastError = error.slice(lastFatalErrorIndex).trim();
if (fatalErrors.length === 0) {
// No other errors
return lastError;
}
const isOneLiner = !fatalErrors.some((e) => e.includes("\n"));
if (isOneLiner) {
fatalErrors = fatalErrors.map(ensureEndsInPeriod);
}
return [
ensureEndsInPeriod(lastError),
"Context:",
...fatalErrors.reverse(),
].join(isOneLiner ? " " : "\n");
}
return undefined;
}
function ensureEndsInPeriod(text: string): string {
return text[text.length - 1] === "." ? text : `${text}.`;
}
/**
* Generates a code scanning configuration that is to be used for a scan.
*
@ -1458,20 +1374,6 @@ export function getGeneratedCodeScanningConfigPath(config: Config): string {
return path.resolve(config.tempDir, "user-config.yaml");
}
function isNoCodeFoundError(e: CommandInvocationError): boolean {
/**
* Earlier versions of the JavaScript extractor (pre-CodeQL 2.12.0) extract externs even if no
* source code was found. This means that we don't get the no code found error from
* `codeql database finalize`. To ensure users get a good error message, we detect this manually
* here, and upon detection override the error message.
*
* This can be removed once support for CodeQL 2.11.6 is removed.
*/
const javascriptNoCodeFoundWarning =
"No JavaScript or TypeScript code found.";
return e.exitCode === 32 || e.stderr.includes(javascriptNoCodeFoundWarning);
}
async function isDiagnosticsExportInvalidSarifFixed(
codeql: CodeQL,
): Promise<boolean> {

View file

@ -26,7 +26,7 @@ import {
GitHubVariant,
GitHubVersion,
prettyPrintPack,
UserError,
ConfigurationError,
withTmpDir,
} from "./util";
@ -219,7 +219,7 @@ test("load input outside of workspace", async (t) => {
} catch (err) {
t.deepEqual(
err,
new UserError(
new ConfigurationError(
configUtils.getConfigFileOutsideWorkspaceErrorMessage(
path.join(tempDir, "../input"),
),
@ -247,7 +247,7 @@ test("load non-local input with invalid repo syntax", async (t) => {
} catch (err) {
t.deepEqual(
err,
new UserError(
new ConfigurationError(
configUtils.getConfigFileRepoFormatInvalidMessage(
"octo-org/codeql-config@main",
),
@ -277,7 +277,7 @@ test("load non-existent input", async (t) => {
} catch (err) {
t.deepEqual(
err,
new UserError(
new ConfigurationError(
configUtils.getConfigFileDoesNotExistErrorMessage(
path.join(tempDir, "input"),
),
@ -517,7 +517,7 @@ test("Remote config handles the case where a directory is provided", async (t) =
} catch (err) {
t.deepEqual(
err,
new UserError(
new ConfigurationError(
configUtils.getConfigFileDirectoryGivenMessage(repoReference),
),
);
@ -546,7 +546,7 @@ test("Invalid format of remote config handled correctly", async (t) => {
} catch (err) {
t.deepEqual(
err,
new UserError(
new ConfigurationError(
configUtils.getConfigFileFormatInvalidMessage(repoReference),
),
);
@ -576,7 +576,10 @@ test("No detected languages", async (t) => {
);
throw new Error("initConfig did not throw error");
} catch (err) {
t.deepEqual(err, new UserError(configUtils.getNoLanguagesError()));
t.deepEqual(
err,
new ConfigurationError(configUtils.getNoLanguagesError()),
);
}
});
});
@ -598,7 +601,7 @@ test("Unknown languages", async (t) => {
} catch (err) {
t.deepEqual(
err,
new UserError(
new ConfigurationError(
configUtils.getUnknownLanguagesError(["rubbish", "english"]),
),
);

View file

@ -15,7 +15,7 @@ import {
codeQlVersionAbove,
GitHubVersion,
prettyPrintPack,
UserError,
ConfigurationError,
} from "./util";
// Property names from the user-supplied config file.
@ -332,7 +332,7 @@ export async function getLanguages(
// If the languages parameter was not given and no languages were
// detected then fail here as this is a workflow configuration error.
if (languages.length === 0) {
throw new UserError(getNoLanguagesError());
throw new ConfigurationError(getNoLanguagesError());
}
// Make sure they are supported
@ -350,7 +350,7 @@ export async function getLanguages(
// Any unknown languages here would have come directly from the input
// since we filter unknown languages coming from the GitHub API.
if (unknownLanguages.length > 0) {
throw new UserError(getUnknownLanguagesError(unknownLanguages));
throw new ConfigurationError(getUnknownLanguagesError(unknownLanguages));
}
return parsedLanguages;
@ -631,7 +631,7 @@ function parseQueriesFromInput(
? rawQueriesInput.trim().slice(1).trim()
: rawQueriesInput?.trim() ?? "";
if (queriesInputCombines && trimmedInput.length === 0) {
throw new UserError(
throw new ConfigurationError(
getConfigFilePropertyError(
undefined,
"queries",
@ -664,11 +664,11 @@ export function parsePacksFromInput(
}
if (languages.length > 1) {
throw new UserError(
throw new ConfigurationError(
"Cannot specify a 'packs' input in a multi-language analysis. Use a codeql-config.yml file instead and specify packs by language.",
);
} else if (languages.length === 0) {
throw new UserError(
throw new ConfigurationError(
"No languages specified. Cannot process the packs input.",
);
}
@ -677,7 +677,7 @@ export function parsePacksFromInput(
if (packsInputCombines) {
rawPacksInput = rawPacksInput.trim().substring(1).trim();
if (!rawPacksInput) {
throw new UserError(
throw new ConfigurationError(
getConfigFilePropertyError(
undefined,
"packs",
@ -715,7 +715,7 @@ export function parsePacksFromInput(
*/
export function parsePacksSpecification(packStr: string): Pack {
if (typeof packStr !== "string") {
throw new UserError(getPacksStrInvalid(packStr));
throw new ConfigurationError(getPacksStrInvalid(packStr));
}
packStr = packStr.trim();
@ -743,14 +743,14 @@ export function parsePacksSpecification(packStr: string): Pack {
: undefined;
if (!PACK_IDENTIFIER_PATTERN.test(packName)) {
throw new UserError(getPacksStrInvalid(packStr));
throw new ConfigurationError(getPacksStrInvalid(packStr));
}
if (version) {
try {
new semver.Range(version);
} catch (e) {
// The range string is invalid. OK to ignore the caught error
throw new UserError(getPacksStrInvalid(packStr));
throw new ConfigurationError(getPacksStrInvalid(packStr));
}
}
@ -764,12 +764,12 @@ export function parsePacksSpecification(packStr: string): Pack {
path.normalize(packPath).split(path.sep).join("/") !==
packPath.split(path.sep).join("/"))
) {
throw new UserError(getPacksStrInvalid(packStr));
throw new ConfigurationError(getPacksStrInvalid(packStr));
}
if (!packPath && pathStart) {
// 0 length path
throw new UserError(getPacksStrInvalid(packStr));
throw new ConfigurationError(getPacksStrInvalid(packStr));
}
return {
@ -852,7 +852,9 @@ function parseRegistries(
? (yaml.load(registriesInput) as RegistryConfigWithCredentials[])
: undefined;
} catch (e) {
throw new UserError("Invalid registries input. Must be a YAML string.");
throw new ConfigurationError(
"Invalid registries input. Must be a YAML string.",
);
}
}
@ -868,12 +870,16 @@ function isLocal(configPath: string): boolean {
function getLocalConfig(configFile: string, workspacePath: string): UserConfig {
// Error if the config file is now outside of the workspace
if (!(configFile + path.sep).startsWith(workspacePath + path.sep)) {
throw new UserError(getConfigFileOutsideWorkspaceErrorMessage(configFile));
throw new ConfigurationError(
getConfigFileOutsideWorkspaceErrorMessage(configFile),
);
}
// Error if the file does not exist
if (!fs.existsSync(configFile)) {
throw new UserError(getConfigFileDoesNotExistErrorMessage(configFile));
throw new ConfigurationError(
getConfigFileDoesNotExistErrorMessage(configFile),
);
}
return yaml.load(fs.readFileSync(configFile, "utf8")) as UserConfig;
@ -890,7 +896,9 @@ async function getRemoteConfig(
const pieces = format.exec(configFile);
// 5 = 4 groups + the whole expression
if (pieces === null || pieces.groups === undefined || pieces.length < 5) {
throw new UserError(getConfigFileRepoFormatInvalidMessage(configFile));
throw new ConfigurationError(
getConfigFileRepoFormatInvalidMessage(configFile),
);
}
const response = await api
@ -906,9 +914,11 @@ async function getRemoteConfig(
if ("content" in response.data && response.data.content !== undefined) {
fileContents = response.data.content;
} else if (Array.isArray(response.data)) {
throw new UserError(getConfigFileDirectoryGivenMessage(configFile));
throw new ConfigurationError(
getConfigFileDirectoryGivenMessage(configFile),
);
} else {
throw new UserError(getConfigFileFormatInvalidMessage(configFile));
throw new ConfigurationError(getConfigFileFormatInvalidMessage(configFile));
}
return yaml.load(
@ -1008,7 +1018,7 @@ function createRegistriesBlock(registries: RegistryConfigWithCredentials[]): {
!Array.isArray(registries) ||
registries.some((r) => !r.url || !r.packages)
) {
throw new UserError(
throw new ConfigurationError(
"Invalid 'registries' input. Must be an array of objects with 'url' and 'packages' properties.",
);
}
@ -1071,7 +1081,7 @@ function validateBuildModeInput(
}
if (!Object.values(BuildMode).includes(buildModeInput as BuildMode)) {
throw new UserError(
throw new ConfigurationError(
`Invalid build mode: '${buildModeInput}'. Supported build modes are: ${Object.values(
BuildMode,
).join(", ")}.`,

View file

@ -107,7 +107,7 @@ async function maybeUploadFailedSarif(
checkoutPath,
category,
logger,
{ considerInvalidRequestUserError: false },
{ considerInvalidRequestConfigError: false },
);
await uploadLib.waitForProcessing(
repositoryNwo,
@ -134,7 +134,7 @@ export async function tryUploadSarifIfRunFailed(
// consider this a configuration error.
core.exportVariable(
EnvVar.JOB_STATUS,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.ConfigurationError,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.ConfigErrorStatus,
);
try {
return await maybeUploadFailedSarif(
@ -152,7 +152,7 @@ export async function tryUploadSarifIfRunFailed(
} else {
core.exportVariable(
EnvVar.JOB_STATUS,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.Success,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.SuccessStatus,
);
return {
upload_failed_run_skipped_because:
@ -314,7 +314,7 @@ export function getFinalJobStatus(): JobStatus {
!jobStatusFromEnvironment ||
!Object.values(JobStatus).includes(jobStatusFromEnvironment as JobStatus)
) {
return JobStatus.Unknown;
return JobStatus.UnknownStatus;
}
return jobStatusFromEnvironment as JobStatus;
}

View file

@ -52,7 +52,7 @@ import {
getThreadsFlagValue,
initializeEnvironment,
isHostedRunner,
UserError,
ConfigurationError,
wrapError,
checkActionVersion,
} from "./util";
@ -317,7 +317,7 @@ async function run() {
await sendStatusReport(
await createStatusReportBase(
"init",
error instanceof UserError ? "user-error" : "aborted",
error instanceof ConfigurationError ? "user-error" : "aborted",
startedAt,
await checkDiskUsage(),
error.message,

View file

@ -64,32 +64,29 @@ export async function runInit(
logger: Logger,
): Promise<TracerConfig | undefined> {
fs.mkdirSync(config.dbLocation, { recursive: true });
try {
const { registriesAuthTokens, qlconfigFile } =
await configUtils.generateRegistries(
registriesInput,
config.tempDir,
logger,
);
await configUtils.wrapEnvironment(
{
GITHUB_TOKEN: apiDetails.auth,
CODEQL_REGISTRIES_AUTH: registriesAuthTokens,
},
// Init a database cluster
async () =>
await codeql.databaseInitCluster(
config,
sourceRoot,
processName,
qlconfigFile,
logger,
),
const { registriesAuthTokens, qlconfigFile } =
await configUtils.generateRegistries(
registriesInput,
config.tempDir,
logger,
);
} catch (e) {
throw processError(e);
}
await configUtils.wrapEnvironment(
{
GITHUB_TOKEN: apiDetails.auth,
CODEQL_REGISTRIES_AUTH: registriesAuthTokens,
},
// Init a database cluster
async () =>
await codeql.databaseInitCluster(
config,
sourceRoot,
processName,
qlconfigFile,
logger,
),
);
return await getCombinedTracerConfig(codeql, config);
}
@ -110,42 +107,6 @@ export function printPathFiltersWarning(
}
}
/**
* Possibly convert this error into a UserError in order to avoid
* counting this error towards our internal error budget.
*
* @param e The error to possibly convert to a UserError.
*
* @returns A UserError if the error is a known error that can be
* attributed to the user, otherwise the original error.
*/
function processError(e: any): Error {
if (!(e instanceof Error)) {
return e;
}
if (
// Init action called twice
e.message?.includes("Refusing to create databases") &&
e.message?.includes("exists and is not an empty directory.")
) {
return new util.UserError(
`Is the "init" action called twice in the same job? ${e.message}`,
);
}
if (
// Version of CodeQL CLI is incompatible with this version of the CodeQL Action
e.message?.includes("is not compatible with this CodeQL CLI") ||
// Expected source location for database creation does not exist
e.message?.includes("Invalid source root")
) {
return new util.UserError(e.message);
}
return e;
}
/**
* If we are running python 3.12+ on windows, we need to switch to python 3.11.
* This check happens in a powershell script.

View file

@ -1,4 +1,4 @@
import { UserError } from "./util";
import { ConfigurationError } from "./util";
// A repository name with owner, parsed into its two parts
export interface RepositoryNwo {
@ -9,7 +9,7 @@ export interface RepositoryNwo {
export function parseRepositoryNwo(input: string): RepositoryNwo {
const parts = input.split("/");
if (parts.length !== 2) {
throw new UserError(`"${input}" is not a valid repository name`);
throw new ConfigurationError(`"${input}" is not a valid repository name`);
}
return {
owner: parts[0],

View file

@ -7,7 +7,7 @@ import {
getTemporaryDirectory,
} from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { CommandInvocationError } from "./codeql";
import { CommandInvocationError } from "./cli-errors";
import * as configUtils from "./config-utils";
import { getActionsLogger } from "./logging";
import { runResolveBuildEnvironment } from "./resolve-environment";

View file

@ -27,7 +27,7 @@ export async function runResolveBuildEnvironment(
) {
const parsedLanguage = parseLanguage(languageInput)?.toString();
if (parsedLanguage === undefined) {
throw new util.UserError(
throw new util.ConfigurationError(
`Did not recognize the language '${languageInput}'.`,
);
}

View file

@ -608,7 +608,7 @@ export async function downloadCodeQL(
export function getCodeQLURLVersion(url: string): string {
const match = url.match(/\/codeql-bundle-(.*)\//);
if (match === null || match.length < 2) {
throw new util.UserError(
throw new util.ConfigurationError(
`Malformed tools url: ${url}. Version could not be inferred`,
);
}

View file

@ -14,7 +14,7 @@ import {
import { getAnalysisKey, getApiClient } from "./api-client";
import { EnvVar } from "./environment";
import {
UserError,
ConfigurationError,
isHTTPError,
getRequiredEnvParam,
getCachedCodeQlVersion,
@ -40,10 +40,10 @@ export type ActionStatus =
/** Overall status of the entire job. String values match the Hydro schema. */
export enum JobStatus {
Unknown = "JOB_STATUS_UNKNOWN",
Success = "JOB_STATUS_SUCCESS",
Failure = "JOB_STATUS_FAILURE",
ConfigurationError = "JOB_STATUS_CONFIGURATION_ERROR",
UnknownStatus = "JOB_STATUS_UNKNOWN",
SuccessStatus = "JOB_STATUS_SUCCESS",
FailureStatus = "JOB_STATUS_FAILURE",
ConfigErrorStatus = "JOB_STATUS_CONFIGURATION_ERROR",
}
export interface StatusReportBase {
@ -135,7 +135,7 @@ export function getActionsStatus(
otherFailureCause?: string,
): ActionStatus {
if (error || otherFailureCause) {
return error instanceof UserError ? "user-error" : "failure";
return error instanceof ConfigurationError ? "user-error" : "failure";
} else {
return "success";
}
@ -150,12 +150,12 @@ function setJobStatusIfUnsuccessful(actionStatus: ActionStatus) {
if (actionStatus === "user-error") {
core.exportVariable(
EnvVar.JOB_STATUS,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.ConfigurationError,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.ConfigErrorStatus,
);
} else if (actionStatus === "failure" || actionStatus === "aborted") {
core.exportVariable(
EnvVar.JOB_STATUS,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.Failure,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.FailureStatus,
);
}
}

View file

@ -14,7 +14,7 @@ import * as fingerprints from "./fingerprints";
import { Logger } from "./logging";
import { parseRepositoryNwo, RepositoryNwo } from "./repository";
import * as util from "./util";
import { SarifFile, UserError, wrapError } from "./util";
import { SarifFile, ConfigurationError, wrapError } from "./util";
// Takes a list of paths to sarif files and combines them together,
// returning the contents of the combined sarif file.
@ -158,7 +158,7 @@ export function findSarifFilesInDir(sarifPath: string): string[] {
* Uploads a single SARIF file or a directory of SARIF files depending on what `sarifPath` refers
* to.
*
* @param considerInvalidRequestUserError Whether an invalid request, for example one with a
* @param considerInvalidRequestConfigError Whether an invalid request, for example one with a
* `sarifPath` that does not exist, should be considered a
* user error.
*/
@ -168,8 +168,8 @@ export async function uploadFromActions(
category: string | undefined,
logger: Logger,
{
considerInvalidRequestUserError,
}: { considerInvalidRequestUserError: boolean },
considerInvalidRequestConfigError: considerInvalidRequestConfigError,
}: { considerInvalidRequestConfigError: boolean },
): Promise<UploadResult> {
try {
return await uploadFiles(
@ -187,8 +187,8 @@ export async function uploadFromActions(
logger,
);
} catch (e) {
if (e instanceof InvalidRequestError && considerInvalidRequestUserError) {
throw new UserError(e.message);
if (e instanceof InvalidRequestError && considerInvalidRequestConfigError) {
throw new ConfigurationError(e.message);
}
throw e;
}
@ -489,8 +489,8 @@ export async function waitForProcessing(
break;
} else if (status === "failed") {
const message = `Code Scanning could not process the submitted SARIF file:\n${response.data.errors}`;
throw shouldConsiderAsUserError(response.data.errors as string[])
? new UserError(message)
throw shouldConsiderConfigurationError(response.data.errors as string[])
? new ConfigurationError(message)
: new InvalidRequestError(message);
} else {
util.assertNever(status);
@ -508,7 +508,7 @@ export async function waitForProcessing(
/**
* Returns whether the provided processing errors should be considered a user error.
*/
function shouldConsiderAsUserError(processingErrors: string[]): boolean {
function shouldConsiderConfigurationError(processingErrors: string[]): boolean {
return (
processingErrors.length === 1 &&
processingErrors[0] ===

View file

@ -69,7 +69,7 @@ async function run() {
actionsUtil.getRequiredInput("checkout_path"),
actionsUtil.getOptionalInput("category"),
logger,
{ considerInvalidRequestUserError: true },
{ considerInvalidRequestConfigError: true },
);
core.setOutput("sarif-id", uploadResult.sarifID);

View file

@ -120,7 +120,7 @@ export function getExtraOptionsEnvParam(): object {
return JSON.parse(raw);
} catch (unwrappedError) {
const error = wrapError(unwrappedError);
throw new UserError(
throw new ConfigurationError(
`${varName} environment variable is set, but does not contain valid JSON: ${error.message}`,
);
}
@ -204,7 +204,9 @@ export function getMemoryFlagValueForPlatform(
if (userInput) {
memoryToUseMegaBytes = Number(userInput);
if (Number.isNaN(memoryToUseMegaBytes) || memoryToUseMegaBytes <= 0) {
throw new UserError(`Invalid RAM setting "${userInput}", specified.`);
throw new ConfigurationError(
`Invalid RAM setting "${userInput}", specified.`,
);
}
} else {
const totalMemoryMegaBytes = totalMemoryBytes / (1024 * 1024);
@ -373,7 +375,9 @@ export function getThreadsFlagValue(
if (userInput) {
numThreads = Number(userInput);
if (Number.isNaN(numThreads)) {
throw new UserError(`Invalid threads setting "${userInput}", specified.`);
throw new ConfigurationError(
`Invalid threads setting "${userInput}", specified.`,
);
}
if (numThreads > maxThreads) {
logger.info(
@ -500,14 +504,14 @@ export function parseGitHubUrl(inputUrl: string): string {
inputUrl = `https://${inputUrl}`;
}
if (!inputUrl.startsWith("http://") && !inputUrl.startsWith("https://")) {
throw new UserError(`"${originalUrl}" is not a http or https URL`);
throw new ConfigurationError(`"${originalUrl}" is not a http or https URL`);
}
let url: URL;
try {
url = new URL(inputUrl);
} catch (e) {
throw new UserError(`"${originalUrl}" is not a valid URL`);
throw new ConfigurationError(`"${originalUrl}" is not a valid URL`);
}
// If we detect this is trying to be to github.com
@ -652,7 +656,7 @@ export class HTTPError extends Error {
* An Error class that indicates an error that occurred due to
* a misconfiguration of the action or the CodeQL CLI.
*/
export class UserError extends Error {
export class ConfigurationError extends Error {
constructor(message: string) {
super(message);
}