Capture the details of fatal errors
This commit is contained in:
parent
76b2afaa4a
commit
21c926745f
7 changed files with 63 additions and 371 deletions
34
lib/codeql.js
generated
34
lib/codeql.js
generated
|
|
@ -31,11 +31,9 @@ const toolrunner = __importStar(require("@actions/exec/lib/toolrunner"));
|
|||
const yaml = __importStar(require("js-yaml"));
|
||||
const actions_util_1 = require("./actions-util");
|
||||
const environment_1 = require("./environment");
|
||||
const error_matcher_1 = require("./error-matcher");
|
||||
const feature_flags_1 = require("./feature-flags");
|
||||
const languages_1 = require("./languages");
|
||||
const setupCodeql = __importStar(require("./setup-codeql"));
|
||||
const toolrunner_error_catcher_1 = require("./toolrunner-error-catcher");
|
||||
const util = __importStar(require("./util"));
|
||||
const util_1 = require("./util");
|
||||
class CommandInvocationError extends Error {
|
||||
|
|
@ -318,7 +316,7 @@ async function getCodeQLForCmd(cmd, checkVersion) {
|
|||
const ext = process.platform === "win32" ? ".cmd" : ".sh";
|
||||
const traceCommand = path.resolve(await this.resolveExtractor(language), "tools", `autobuild${ext}`);
|
||||
// Run trace command
|
||||
await (0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)(cmd, [
|
||||
await runTool(cmd, [
|
||||
"database",
|
||||
"trace-command",
|
||||
...(await getTrapCachingExtractorConfigArgsForLang(config, language)),
|
||||
|
|
@ -326,7 +324,7 @@ async function getCodeQLForCmd(cmd, checkVersion) {
|
|||
databasePath,
|
||||
"--",
|
||||
traceCommand,
|
||||
], error_matcher_1.errorMatchers);
|
||||
]);
|
||||
},
|
||||
async finalizeDatabase(databasePath, threadsFlag, memoryFlag) {
|
||||
const args = [
|
||||
|
|
@ -439,7 +437,7 @@ async function getCodeQLForCmd(cmd, checkVersion) {
|
|||
if (querySuitePath) {
|
||||
codeqlArgs.push(querySuitePath);
|
||||
}
|
||||
await (0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)(cmd, codeqlArgs, error_matcher_1.errorMatchers);
|
||||
await runTool(cmd, codeqlArgs);
|
||||
},
|
||||
async databaseInterpretResults(databasePath, querySuitePaths, sarifFile, addSnippetsFlag, threadsFlag, verbosityFlag, automationDetailsId, config, features, logger) {
|
||||
const shouldExportDiagnostics = await features.getValue(feature_flags_1.Feature.ExportDiagnosticsEnabled, this);
|
||||
|
|
@ -486,11 +484,11 @@ async function getCodeQLForCmd(cmd, checkVersion) {
|
|||
codeqlArgs.push(...querySuitePaths);
|
||||
}
|
||||
// capture stdout, which contains analysis summaries
|
||||
const returnState = await (0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)(cmd, codeqlArgs, error_matcher_1.errorMatchers);
|
||||
const returnState = await runTool(cmd, codeqlArgs);
|
||||
if (shouldWorkaroundInvalidNotifications) {
|
||||
util.fixInvalidNotificationsInFile(codeqlOutputFile, sarifFile, logger);
|
||||
}
|
||||
return returnState.stdout;
|
||||
return returnState;
|
||||
},
|
||||
async databasePrintBaseline(databasePath) {
|
||||
const codeqlArgs = [
|
||||
|
|
@ -735,10 +733,30 @@ async function runTool(cmd, args = [], opts = {}) {
|
|||
ignoreReturnCode: true,
|
||||
...(opts.stdin ? { input: Buffer.from(opts.stdin || "") } : {}),
|
||||
}).exec();
|
||||
if (exitCode !== 0)
|
||||
if (exitCode !== 0) {
|
||||
error = extractFatalErrors(error) || error;
|
||||
throw new CommandInvocationError(cmd, args, exitCode, error, output);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
function extractFatalErrors(error) {
|
||||
const fatalErrors = [];
|
||||
const fatalErrorRegex = /.*fatal error occurred:/gi;
|
||||
let lastFatalErrorIndex;
|
||||
let match;
|
||||
while ((match = fatalErrorRegex.exec(error)) !== null) {
|
||||
if (lastFatalErrorIndex !== undefined) {
|
||||
fatalErrors.push(error.slice(lastFatalErrorIndex, match.index));
|
||||
}
|
||||
lastFatalErrorIndex = match.index;
|
||||
}
|
||||
if (lastFatalErrorIndex !== undefined) {
|
||||
const lastError = error.slice(lastFatalErrorIndex);
|
||||
return (lastError +
|
||||
(fatalErrors.length > 0 ? `\nContext:\n${fatalErrors.join("\n")}` : ""));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
/**
|
||||
* If appropriate, generates a code scanning configuration that is to be used for a scan.
|
||||
* If the configuration is not to be generated, returns undefined.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -9,7 +9,6 @@ import { getOptionalInput, isAnalyzingDefaultBranch } from "./actions-util";
|
|||
import * as api from "./api-client";
|
||||
import type { Config } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { errorMatchers } from "./error-matcher";
|
||||
import {
|
||||
CODEQL_VERSION_NEW_ANALYSIS_SUMMARY,
|
||||
CodeQLDefaultVersionInfo,
|
||||
|
|
@ -20,7 +19,6 @@ import {
|
|||
import { isTracedLanguage, Language } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import * as setupCodeql from "./setup-codeql";
|
||||
import { toolrunnerErrorCatcher } from "./toolrunner-error-catcher";
|
||||
import * as util from "./util";
|
||||
import { wrapError } from "./util";
|
||||
|
||||
|
|
@ -627,19 +625,15 @@ export async function getCodeQLForCmd(
|
|||
`autobuild${ext}`
|
||||
);
|
||||
// Run trace command
|
||||
await toolrunnerErrorCatcher(
|
||||
cmd,
|
||||
[
|
||||
"database",
|
||||
"trace-command",
|
||||
...(await getTrapCachingExtractorConfigArgsForLang(config, language)),
|
||||
...getExtraOptionsFromEnv(["database", "trace-command"]),
|
||||
databasePath,
|
||||
"--",
|
||||
traceCommand,
|
||||
],
|
||||
errorMatchers
|
||||
);
|
||||
await runTool(cmd, [
|
||||
"database",
|
||||
"trace-command",
|
||||
...(await getTrapCachingExtractorConfigArgsForLang(config, language)),
|
||||
...getExtraOptionsFromEnv(["database", "trace-command"]),
|
||||
databasePath,
|
||||
"--",
|
||||
traceCommand,
|
||||
]);
|
||||
},
|
||||
async finalizeDatabase(
|
||||
databasePath: string,
|
||||
|
|
@ -782,7 +776,7 @@ export async function getCodeQLForCmd(
|
|||
if (querySuitePath) {
|
||||
codeqlArgs.push(querySuitePath);
|
||||
}
|
||||
await toolrunnerErrorCatcher(cmd, codeqlArgs, errorMatchers);
|
||||
await runTool(cmd, codeqlArgs);
|
||||
},
|
||||
async databaseInterpretResults(
|
||||
databasePath: string,
|
||||
|
|
@ -848,17 +842,13 @@ export async function getCodeQLForCmd(
|
|||
codeqlArgs.push(...querySuitePaths);
|
||||
}
|
||||
// capture stdout, which contains analysis summaries
|
||||
const returnState = await toolrunnerErrorCatcher(
|
||||
cmd,
|
||||
codeqlArgs,
|
||||
errorMatchers
|
||||
);
|
||||
const returnState = await runTool(cmd, codeqlArgs);
|
||||
|
||||
if (shouldWorkaroundInvalidNotifications) {
|
||||
util.fixInvalidNotificationsInFile(codeqlOutputFile, sarifFile, logger);
|
||||
}
|
||||
|
||||
return returnState.stdout;
|
||||
return returnState;
|
||||
},
|
||||
async databasePrintBaseline(databasePath: string): Promise<string> {
|
||||
const codeqlArgs = [
|
||||
|
|
@ -1161,11 +1151,34 @@ async function runTool(
|
|||
ignoreReturnCode: true,
|
||||
...(opts.stdin ? { input: Buffer.from(opts.stdin || "") } : {}),
|
||||
}).exec();
|
||||
if (exitCode !== 0)
|
||||
if (exitCode !== 0) {
|
||||
error = extractFatalErrors(error) || error;
|
||||
throw new CommandInvocationError(cmd, args, exitCode, error, output);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function extractFatalErrors(error: string): string | undefined {
|
||||
const fatalErrors: string[] = [];
|
||||
const fatalErrorRegex = /.*fatal error occurred:/gi;
|
||||
let lastFatalErrorIndex: number | undefined;
|
||||
let match: RegExpMatchArray | null;
|
||||
while ((match = fatalErrorRegex.exec(error)) !== null) {
|
||||
if (lastFatalErrorIndex !== undefined) {
|
||||
fatalErrors.push(error.slice(lastFatalErrorIndex, match.index));
|
||||
}
|
||||
lastFatalErrorIndex = match.index;
|
||||
}
|
||||
if (lastFatalErrorIndex !== undefined) {
|
||||
const lastError = error.slice(lastFatalErrorIndex);
|
||||
return (
|
||||
lastError +
|
||||
(fatalErrors.length > 0 ? `\nContext:\n${fatalErrors.join("\n")}` : "")
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* If appropriate, generates a code scanning configuration that is to be used for a scan.
|
||||
* If the configuration is not to be generated, returns undefined.
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import test from "ava";
|
||||
|
||||
import { namedMatchersForTesting } from "./error-matcher";
|
||||
|
||||
/*
|
||||
NB We test the regexes for all the matchers against example log output snippets.
|
||||
*/
|
||||
|
||||
test("fatalError matches against example log output", async (t) => {
|
||||
t.assert(
|
||||
testErrorMatcher(
|
||||
"fatalError",
|
||||
"A fatal error occurred: Could not process query metadata for test-query.ql"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
function testErrorMatcher(matcherName: string, logSample: string): boolean {
|
||||
if (!(matcherName in namedMatchersForTesting)) {
|
||||
throw new Error(`Unknown matcher ${matcherName}`);
|
||||
}
|
||||
const regex = namedMatchersForTesting[matcherName].outputRegex;
|
||||
if (regex === undefined) {
|
||||
throw new Error(`Cannot test matcher ${matcherName} with null regex`);
|
||||
}
|
||||
return regex.test(logSample);
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
// defines properties to match against the result of executed commands,
|
||||
// and a custom error to return when a match is found
|
||||
export interface ErrorMatcher {
|
||||
exitCode?: number; // exit code of the run process
|
||||
outputRegex?: RegExp; // pattern to match against either stdout or stderr
|
||||
message: string; // the error message that will be thrown for a matching process
|
||||
}
|
||||
|
||||
// exported only for testing purposes
|
||||
export const namedMatchersForTesting: { [key: string]: ErrorMatcher } = {
|
||||
fatalError: {
|
||||
outputRegex: new RegExp("A fatal error occurred"),
|
||||
message: "A fatal error occurred.",
|
||||
},
|
||||
};
|
||||
|
||||
// we collapse the matches into an array for use in execErrorCatcher
|
||||
export const errorMatchers = Object.values(namedMatchersForTesting);
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
import * as exec from "@actions/exec";
|
||||
import test from "ava";
|
||||
|
||||
import { ErrorMatcher } from "./error-matcher";
|
||||
import { setupTests } from "./testing-utils";
|
||||
import { toolrunnerErrorCatcher } from "./toolrunner-error-catcher";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test("matchers are never applied if non-error exit", async (t) => {
|
||||
const testArgs = buildDummyArgs(
|
||||
"foo bar\\nblort qux",
|
||||
"foo bar\\nblort qux",
|
||||
"",
|
||||
0
|
||||
);
|
||||
|
||||
const matchers: ErrorMatcher[] = [
|
||||
{ exitCode: 123, outputRegex: new RegExp("foo bar"), message: "error!!!" },
|
||||
];
|
||||
|
||||
t.deepEqual(await exec.exec("node", testArgs), 0);
|
||||
|
||||
const returnState = await toolrunnerErrorCatcher("node", testArgs, matchers);
|
||||
t.deepEqual(returnState.exitCode, 0);
|
||||
});
|
||||
|
||||
test("regex matchers are applied to stdout for non-zero exit code", async (t) => {
|
||||
const testArgs = buildDummyArgs("foo bar\\nblort qux", "", "", 1);
|
||||
|
||||
const matchers: ErrorMatcher[] = [
|
||||
{ exitCode: 123, outputRegex: new RegExp("foo bar"), message: "🦄" },
|
||||
];
|
||||
|
||||
await t.throwsAsync(exec.exec("node", testArgs), {
|
||||
instanceOf: Error,
|
||||
message: /failed with exit code 1/,
|
||||
});
|
||||
|
||||
await t.throwsAsync(toolrunnerErrorCatcher("node", testArgs, matchers), {
|
||||
instanceOf: Error,
|
||||
message: "🦄",
|
||||
});
|
||||
});
|
||||
|
||||
test("regex matchers are applied to stderr for non-zero exit code", async (t) => {
|
||||
const testArgs = buildDummyArgs(
|
||||
"non matching string",
|
||||
"foo bar\\nblort qux",
|
||||
"",
|
||||
1
|
||||
);
|
||||
|
||||
const matchers: ErrorMatcher[] = [
|
||||
{ exitCode: 123, outputRegex: new RegExp("foo bar"), message: "🦄" },
|
||||
];
|
||||
|
||||
await t.throwsAsync(exec.exec("node", testArgs), {
|
||||
instanceOf: Error,
|
||||
message: /failed with exit code 1/,
|
||||
});
|
||||
|
||||
await t.throwsAsync(toolrunnerErrorCatcher("node", testArgs, matchers), {
|
||||
instanceOf: Error,
|
||||
message: "🦄",
|
||||
});
|
||||
});
|
||||
|
||||
test("matcher returns correct error message when multiple matchers defined", async (t) => {
|
||||
const testArgs = buildDummyArgs(
|
||||
"non matching string",
|
||||
"foo bar\\nblort qux",
|
||||
"",
|
||||
1
|
||||
);
|
||||
|
||||
const matchers: ErrorMatcher[] = [
|
||||
{ exitCode: 456, outputRegex: new RegExp("lorem ipsum"), message: "😩" },
|
||||
{ exitCode: 123, outputRegex: new RegExp("foo bar"), message: "🦄" },
|
||||
{ exitCode: 789, outputRegex: new RegExp("blah blah"), message: "🤦♂️" },
|
||||
];
|
||||
|
||||
await t.throwsAsync(exec.exec("node", testArgs), {
|
||||
instanceOf: Error,
|
||||
message: /failed with exit code 1/,
|
||||
});
|
||||
|
||||
await t.throwsAsync(toolrunnerErrorCatcher("node", testArgs, matchers), {
|
||||
instanceOf: Error,
|
||||
message: "🦄",
|
||||
});
|
||||
});
|
||||
|
||||
test("matcher returns first match to regex when multiple matches", async (t) => {
|
||||
const testArgs = buildDummyArgs(
|
||||
"non matching string",
|
||||
"foo bar\\nblort qux",
|
||||
"",
|
||||
1
|
||||
);
|
||||
|
||||
const matchers: ErrorMatcher[] = [
|
||||
{ exitCode: 123, outputRegex: new RegExp("foo bar"), message: "🦄" },
|
||||
{ exitCode: 789, outputRegex: new RegExp("blah blah"), message: "🤦♂️" },
|
||||
{ exitCode: 987, outputRegex: new RegExp("foo bar"), message: "🚫" },
|
||||
];
|
||||
|
||||
await t.throwsAsync(exec.exec("node", testArgs), {
|
||||
instanceOf: Error,
|
||||
message: /failed with exit code 1/,
|
||||
});
|
||||
|
||||
await t.throwsAsync(toolrunnerErrorCatcher("node", testArgs, matchers), {
|
||||
instanceOf: Error,
|
||||
message: "🦄",
|
||||
});
|
||||
});
|
||||
|
||||
test("exit code matchers are applied", async (t) => {
|
||||
const testArgs = buildDummyArgs(
|
||||
"non matching string",
|
||||
"foo bar\\nblort qux",
|
||||
"",
|
||||
123
|
||||
);
|
||||
|
||||
const matchers: ErrorMatcher[] = [
|
||||
{
|
||||
exitCode: 123,
|
||||
outputRegex: new RegExp("this will not match"),
|
||||
message: "🦄",
|
||||
},
|
||||
];
|
||||
|
||||
await t.throwsAsync(exec.exec("node", testArgs), {
|
||||
instanceOf: Error,
|
||||
message: /failed with exit code 123/,
|
||||
});
|
||||
|
||||
await t.throwsAsync(toolrunnerErrorCatcher("node", testArgs, matchers), {
|
||||
instanceOf: Error,
|
||||
message: "🦄",
|
||||
});
|
||||
});
|
||||
|
||||
test("execErrorCatcher respects the ignoreReturnValue option", async (t) => {
|
||||
const testArgs = buildDummyArgs("standard output", "error output", "", 199);
|
||||
|
||||
await t.throwsAsync(
|
||||
toolrunnerErrorCatcher("node", testArgs, [], { ignoreReturnCode: false }),
|
||||
{ instanceOf: Error }
|
||||
);
|
||||
|
||||
const returnState = await toolrunnerErrorCatcher("node", testArgs, [], {
|
||||
ignoreReturnCode: true,
|
||||
});
|
||||
|
||||
t.deepEqual(returnState.exitCode, 199);
|
||||
});
|
||||
|
||||
test("execErrorCatcher preserves behavior of provided listeners", async (t) => {
|
||||
const stdoutExpected = "standard output";
|
||||
const stderrExpected = "error output";
|
||||
|
||||
let stdoutActual = "";
|
||||
let stderrActual = "";
|
||||
|
||||
const listeners = {
|
||||
stdout: (data: Buffer) => {
|
||||
stdoutActual += data.toString();
|
||||
},
|
||||
stderr: (data: Buffer) => {
|
||||
stderrActual += data.toString();
|
||||
},
|
||||
};
|
||||
|
||||
const testArgs = buildDummyArgs(stdoutExpected, stderrExpected, "", 0);
|
||||
|
||||
const returnState = await toolrunnerErrorCatcher("node", testArgs, [], {
|
||||
listeners,
|
||||
});
|
||||
t.deepEqual(returnState.exitCode, 0);
|
||||
|
||||
t.deepEqual(stdoutActual, `${stdoutExpected}\n`);
|
||||
t.deepEqual(stderrActual, `${stderrExpected}\n`);
|
||||
});
|
||||
|
||||
function buildDummyArgs(
|
||||
stdoutContents: string,
|
||||
stderrContents: string,
|
||||
desiredErrorMessage?: string,
|
||||
desiredExitCode?: number
|
||||
): string[] {
|
||||
let command = "";
|
||||
|
||||
if (stdoutContents) command += `console.log("${stdoutContents}");`;
|
||||
if (stderrContents) command += `console.error("${stderrContents}");`;
|
||||
|
||||
if (command.length === 0)
|
||||
throw new Error("Must provide contents for either stdout or stderr");
|
||||
|
||||
if (desiredErrorMessage)
|
||||
command += `throw new Error("${desiredErrorMessage}");`;
|
||||
if (desiredExitCode) command += `process.exitCode = ${desiredExitCode};`;
|
||||
|
||||
return ["-e", command];
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import * as im from "@actions/exec/lib/interfaces";
|
||||
import * as toolrunner from "@actions/exec/lib/toolrunner";
|
||||
import * as safeWhich from "@chrisgavin/safe-which";
|
||||
|
||||
import { ErrorMatcher } from "./error-matcher";
|
||||
import { wrapError } from "./util";
|
||||
|
||||
export interface ReturnState {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for toolrunner.Toolrunner which checks for specific return code and/or regex matches in console output.
|
||||
* Output will be streamed to the live console as well as captured for subsequent processing.
|
||||
* Returns promise with return code
|
||||
*
|
||||
* @param commandLine command to execute
|
||||
* @param args optional arguments for tool. Escaping is handled by the lib.
|
||||
* @param matchers defines specific codes and/or regexes that should lead to return of a custom error
|
||||
* @param options optional exec options. See ExecOptions
|
||||
* @returns ReturnState exit code and stdout output, if applicable
|
||||
*/
|
||||
export async function toolrunnerErrorCatcher(
|
||||
commandLine: string,
|
||||
args?: string[],
|
||||
matchers?: ErrorMatcher[],
|
||||
options?: im.ExecOptions
|
||||
): Promise<ReturnState> {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
const listeners = {
|
||||
stdout: (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
if (options?.listeners?.stdout !== undefined) {
|
||||
options.listeners.stdout(data);
|
||||
}
|
||||
},
|
||||
stderr: (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
if (options?.listeners?.stderr !== undefined) {
|
||||
options.listeners.stderr(data);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// we capture the original return code or error so that if no match is found we can duplicate the behavior
|
||||
let exitCode: number;
|
||||
try {
|
||||
exitCode = await new toolrunner.ToolRunner(
|
||||
await safeWhich.safeWhich(commandLine),
|
||||
args,
|
||||
{
|
||||
...options, // we want to override the original options, so include them first
|
||||
listeners,
|
||||
ignoreReturnCode: true, // so we can check for specific codes using the matchers
|
||||
}
|
||||
).exec();
|
||||
|
||||
// if there is a zero return code then we do not apply the matchers
|
||||
if (exitCode === 0) return { exitCode, stdout };
|
||||
|
||||
if (matchers) {
|
||||
for (const matcher of matchers) {
|
||||
if (
|
||||
matcher.exitCode === exitCode ||
|
||||
matcher.outputRegex?.test(stderr) ||
|
||||
matcher.outputRegex?.test(stdout)
|
||||
) {
|
||||
throw new Error(matcher.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only if we were instructed to ignore the return code do we ever return it non-zero
|
||||
if (options?.ignoreReturnCode) {
|
||||
return { exitCode, stdout };
|
||||
} else {
|
||||
throw new Error(
|
||||
`The process '${commandLine}' failed with exit code ${exitCode}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw wrapError(e);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue