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 yaml = __importStar(require("js-yaml"));
|
||||||
const actions_util_1 = require("./actions-util");
|
const actions_util_1 = require("./actions-util");
|
||||||
const environment_1 = require("./environment");
|
const environment_1 = require("./environment");
|
||||||
const error_matcher_1 = require("./error-matcher");
|
|
||||||
const feature_flags_1 = require("./feature-flags");
|
const feature_flags_1 = require("./feature-flags");
|
||||||
const languages_1 = require("./languages");
|
const languages_1 = require("./languages");
|
||||||
const setupCodeql = __importStar(require("./setup-codeql"));
|
const setupCodeql = __importStar(require("./setup-codeql"));
|
||||||
const toolrunner_error_catcher_1 = require("./toolrunner-error-catcher");
|
|
||||||
const util = __importStar(require("./util"));
|
const util = __importStar(require("./util"));
|
||||||
const util_1 = require("./util");
|
const util_1 = require("./util");
|
||||||
class CommandInvocationError extends Error {
|
class CommandInvocationError extends Error {
|
||||||
|
|
@ -318,7 +316,7 @@ async function getCodeQLForCmd(cmd, checkVersion) {
|
||||||
const ext = process.platform === "win32" ? ".cmd" : ".sh";
|
const ext = process.platform === "win32" ? ".cmd" : ".sh";
|
||||||
const traceCommand = path.resolve(await this.resolveExtractor(language), "tools", `autobuild${ext}`);
|
const traceCommand = path.resolve(await this.resolveExtractor(language), "tools", `autobuild${ext}`);
|
||||||
// Run trace command
|
// Run trace command
|
||||||
await (0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)(cmd, [
|
await runTool(cmd, [
|
||||||
"database",
|
"database",
|
||||||
"trace-command",
|
"trace-command",
|
||||||
...(await getTrapCachingExtractorConfigArgsForLang(config, language)),
|
...(await getTrapCachingExtractorConfigArgsForLang(config, language)),
|
||||||
|
|
@ -326,7 +324,7 @@ async function getCodeQLForCmd(cmd, checkVersion) {
|
||||||
databasePath,
|
databasePath,
|
||||||
"--",
|
"--",
|
||||||
traceCommand,
|
traceCommand,
|
||||||
], error_matcher_1.errorMatchers);
|
]);
|
||||||
},
|
},
|
||||||
async finalizeDatabase(databasePath, threadsFlag, memoryFlag) {
|
async finalizeDatabase(databasePath, threadsFlag, memoryFlag) {
|
||||||
const args = [
|
const args = [
|
||||||
|
|
@ -439,7 +437,7 @@ async function getCodeQLForCmd(cmd, checkVersion) {
|
||||||
if (querySuitePath) {
|
if (querySuitePath) {
|
||||||
codeqlArgs.push(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) {
|
async databaseInterpretResults(databasePath, querySuitePaths, sarifFile, addSnippetsFlag, threadsFlag, verbosityFlag, automationDetailsId, config, features, logger) {
|
||||||
const shouldExportDiagnostics = await features.getValue(feature_flags_1.Feature.ExportDiagnosticsEnabled, this);
|
const shouldExportDiagnostics = await features.getValue(feature_flags_1.Feature.ExportDiagnosticsEnabled, this);
|
||||||
|
|
@ -486,11 +484,11 @@ async function getCodeQLForCmd(cmd, checkVersion) {
|
||||||
codeqlArgs.push(...querySuitePaths);
|
codeqlArgs.push(...querySuitePaths);
|
||||||
}
|
}
|
||||||
// capture stdout, which contains analysis summaries
|
// 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) {
|
if (shouldWorkaroundInvalidNotifications) {
|
||||||
util.fixInvalidNotificationsInFile(codeqlOutputFile, sarifFile, logger);
|
util.fixInvalidNotificationsInFile(codeqlOutputFile, sarifFile, logger);
|
||||||
}
|
}
|
||||||
return returnState.stdout;
|
return returnState;
|
||||||
},
|
},
|
||||||
async databasePrintBaseline(databasePath) {
|
async databasePrintBaseline(databasePath) {
|
||||||
const codeqlArgs = [
|
const codeqlArgs = [
|
||||||
|
|
@ -735,10 +733,30 @@ async function runTool(cmd, args = [], opts = {}) {
|
||||||
ignoreReturnCode: true,
|
ignoreReturnCode: true,
|
||||||
...(opts.stdin ? { input: Buffer.from(opts.stdin || "") } : {}),
|
...(opts.stdin ? { input: Buffer.from(opts.stdin || "") } : {}),
|
||||||
}).exec();
|
}).exec();
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0) {
|
||||||
|
error = extractFatalErrors(error) || error;
|
||||||
throw new CommandInvocationError(cmd, args, exitCode, error, output);
|
throw new CommandInvocationError(cmd, args, exitCode, error, output);
|
||||||
|
}
|
||||||
return 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 appropriate, generates a code scanning configuration that is to be used for a scan.
|
||||||
* If the configuration is not to be generated, returns undefined.
|
* 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 * as api from "./api-client";
|
||||||
import type { Config } from "./config-utils";
|
import type { Config } from "./config-utils";
|
||||||
import { EnvVar } from "./environment";
|
import { EnvVar } from "./environment";
|
||||||
import { errorMatchers } from "./error-matcher";
|
|
||||||
import {
|
import {
|
||||||
CODEQL_VERSION_NEW_ANALYSIS_SUMMARY,
|
CODEQL_VERSION_NEW_ANALYSIS_SUMMARY,
|
||||||
CodeQLDefaultVersionInfo,
|
CodeQLDefaultVersionInfo,
|
||||||
|
|
@ -20,7 +19,6 @@ import {
|
||||||
import { isTracedLanguage, Language } from "./languages";
|
import { isTracedLanguage, Language } from "./languages";
|
||||||
import { Logger } from "./logging";
|
import { Logger } from "./logging";
|
||||||
import * as setupCodeql from "./setup-codeql";
|
import * as setupCodeql from "./setup-codeql";
|
||||||
import { toolrunnerErrorCatcher } from "./toolrunner-error-catcher";
|
|
||||||
import * as util from "./util";
|
import * as util from "./util";
|
||||||
import { wrapError } from "./util";
|
import { wrapError } from "./util";
|
||||||
|
|
||||||
|
|
@ -627,19 +625,15 @@ export async function getCodeQLForCmd(
|
||||||
`autobuild${ext}`
|
`autobuild${ext}`
|
||||||
);
|
);
|
||||||
// Run trace command
|
// Run trace command
|
||||||
await toolrunnerErrorCatcher(
|
await runTool(cmd, [
|
||||||
cmd,
|
"database",
|
||||||
[
|
"trace-command",
|
||||||
"database",
|
...(await getTrapCachingExtractorConfigArgsForLang(config, language)),
|
||||||
"trace-command",
|
...getExtraOptionsFromEnv(["database", "trace-command"]),
|
||||||
...(await getTrapCachingExtractorConfigArgsForLang(config, language)),
|
databasePath,
|
||||||
...getExtraOptionsFromEnv(["database", "trace-command"]),
|
"--",
|
||||||
databasePath,
|
traceCommand,
|
||||||
"--",
|
]);
|
||||||
traceCommand,
|
|
||||||
],
|
|
||||||
errorMatchers
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
async finalizeDatabase(
|
async finalizeDatabase(
|
||||||
databasePath: string,
|
databasePath: string,
|
||||||
|
|
@ -782,7 +776,7 @@ export async function getCodeQLForCmd(
|
||||||
if (querySuitePath) {
|
if (querySuitePath) {
|
||||||
codeqlArgs.push(querySuitePath);
|
codeqlArgs.push(querySuitePath);
|
||||||
}
|
}
|
||||||
await toolrunnerErrorCatcher(cmd, codeqlArgs, errorMatchers);
|
await runTool(cmd, codeqlArgs);
|
||||||
},
|
},
|
||||||
async databaseInterpretResults(
|
async databaseInterpretResults(
|
||||||
databasePath: string,
|
databasePath: string,
|
||||||
|
|
@ -848,17 +842,13 @@ export async function getCodeQLForCmd(
|
||||||
codeqlArgs.push(...querySuitePaths);
|
codeqlArgs.push(...querySuitePaths);
|
||||||
}
|
}
|
||||||
// capture stdout, which contains analysis summaries
|
// capture stdout, which contains analysis summaries
|
||||||
const returnState = await toolrunnerErrorCatcher(
|
const returnState = await runTool(cmd, codeqlArgs);
|
||||||
cmd,
|
|
||||||
codeqlArgs,
|
|
||||||
errorMatchers
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldWorkaroundInvalidNotifications) {
|
if (shouldWorkaroundInvalidNotifications) {
|
||||||
util.fixInvalidNotificationsInFile(codeqlOutputFile, sarifFile, logger);
|
util.fixInvalidNotificationsInFile(codeqlOutputFile, sarifFile, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnState.stdout;
|
return returnState;
|
||||||
},
|
},
|
||||||
async databasePrintBaseline(databasePath: string): Promise<string> {
|
async databasePrintBaseline(databasePath: string): Promise<string> {
|
||||||
const codeqlArgs = [
|
const codeqlArgs = [
|
||||||
|
|
@ -1161,11 +1151,34 @@ async function runTool(
|
||||||
ignoreReturnCode: true,
|
ignoreReturnCode: true,
|
||||||
...(opts.stdin ? { input: Buffer.from(opts.stdin || "") } : {}),
|
...(opts.stdin ? { input: Buffer.from(opts.stdin || "") } : {}),
|
||||||
}).exec();
|
}).exec();
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0) {
|
||||||
|
error = extractFatalErrors(error) || error;
|
||||||
throw new CommandInvocationError(cmd, args, exitCode, error, output);
|
throw new CommandInvocationError(cmd, args, exitCode, error, output);
|
||||||
|
}
|
||||||
return 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 appropriate, generates a code scanning configuration that is to be used for a scan.
|
||||||
* If the configuration is not to be generated, returns undefined.
|
* 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