Merge pull request #1795 from github/henrymercer/use-more-cli-errors
Improve handling of fatal CLI errors
This commit is contained in:
commit
57a11be8e4
19 changed files with 399 additions and 730 deletions
|
|
@ -4,7 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
|
||||||
|
|
||||||
## [UNRELEASED]
|
## [UNRELEASED]
|
||||||
|
|
||||||
No user facing changes.
|
- Improve the handling of fatal errors from the CodeQL CLI. [#1795](https://github.com/github/codeql-action/pull/1795)
|
||||||
|
|
||||||
## 2.21.0 - 19 Jul 2023
|
## 2.21.0 - 19 Jul 2023
|
||||||
|
|
||||||
|
|
|
||||||
119
lib/codeql.js
generated
119
lib/codeql.js
generated
|
|
@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.getGeneratedCodeScanningConfigPath = exports.getTrapCachingExtractorConfigArgsForLang = exports.getTrapCachingExtractorConfigArgs = exports.getExtraOptions = exports.getCodeQLForCmd = exports.getCodeQLForTesting = exports.getCachedCodeQL = exports.setCodeQL = exports.getCodeQL = exports.setupCodeQL = exports.CODEQL_VERSION_RESOLVE_ENVIRONMENT = exports.CODEQL_VERSION_INIT_WITH_QLCONFIG = exports.CODEQL_VERSION_EXPORT_CODE_SCANNING_CONFIG = exports.CODEQL_VERSION_SECURITY_EXPERIMENTAL_SUITE = exports.CODEQL_VERSION_BETTER_RESOLVE_LANGUAGES = exports.CODEQL_VERSION_GHES_PACK_DOWNLOAD = exports.CommandInvocationError = void 0;
|
exports.getGeneratedCodeScanningConfigPath = exports.getTrapCachingExtractorConfigArgsForLang = exports.getTrapCachingExtractorConfigArgs = exports.getExtraOptions = exports.getCodeQLForCmd = exports.getCodeQLForTesting = exports.getCachedCodeQL = exports.setCodeQL = exports.getCodeQL = exports.setupCodeQL = exports.CODEQL_VERSION_RESOLVE_ENVIRONMENT = exports.CODEQL_VERSION_BETTER_NO_CODE_ERROR_MESSAGE = exports.CODEQL_VERSION_INIT_WITH_QLCONFIG = exports.CODEQL_VERSION_EXPORT_CODE_SCANNING_CONFIG = exports.CODEQL_VERSION_SECURITY_EXPERIMENTAL_SUITE = exports.CODEQL_VERSION_BETTER_RESOLVE_LANGUAGES = exports.CODEQL_VERSION_GHES_PACK_DOWNLOAD = exports.CommandInvocationError = void 0;
|
||||||
const fs = __importStar(require("fs"));
|
const fs = __importStar(require("fs"));
|
||||||
const path = __importStar(require("path"));
|
const path = __importStar(require("path"));
|
||||||
const core = __importStar(require("@actions/core"));
|
const core = __importStar(require("@actions/core"));
|
||||||
|
|
@ -31,18 +31,20 @@ 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 {
|
||||||
constructor(cmd, args, exitCode, error, output) {
|
constructor(cmd, args, exitCode, error, output) {
|
||||||
super(`Failure invoking ${cmd} with arguments ${args}.\n
|
const prettyCommand = [cmd, ...args]
|
||||||
Exit code ${exitCode} and error was:\n
|
.map((x) => (x.includes(" ") ? `'${x}'` : x))
|
||||||
${error}`);
|
.join(" ");
|
||||||
|
super(`Encountered a fatal error while running "${prettyCommand}". ` +
|
||||||
|
`Exit code was ${exitCode} and error was: ${error.trim()}`);
|
||||||
|
this.exitCode = exitCode;
|
||||||
|
this.error = error;
|
||||||
this.output = output;
|
this.output = output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +95,11 @@ exports.CODEQL_VERSION_EXPORT_CODE_SCANNING_CONFIG = "2.12.3";
|
||||||
* Versions 2.12.4+ of the CodeQL CLI support the `--qlconfig-file` flag in calls to `database init`.
|
* Versions 2.12.4+ of the CodeQL CLI support the `--qlconfig-file` flag in calls to `database init`.
|
||||||
*/
|
*/
|
||||||
exports.CODEQL_VERSION_INIT_WITH_QLCONFIG = "2.12.4";
|
exports.CODEQL_VERSION_INIT_WITH_QLCONFIG = "2.12.4";
|
||||||
|
/**
|
||||||
|
* Versions 2.12.4+ of the CodeQL CLI provide a better error message when `database finalize`
|
||||||
|
* determines that no code has been found.
|
||||||
|
*/
|
||||||
|
exports.CODEQL_VERSION_BETTER_NO_CODE_ERROR_MESSAGE = "2.12.4";
|
||||||
/**
|
/**
|
||||||
* Versions 2.13.4+ of the CodeQL CLI support the `resolve build-environment` command.
|
* Versions 2.13.4+ of the CodeQL CLI support the `resolve build-environment` command.
|
||||||
*/
|
*/
|
||||||
|
|
@ -311,7 +318,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)),
|
||||||
|
|
@ -319,7 +326,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 = [
|
||||||
|
|
@ -331,7 +338,18 @@ async function getCodeQLForCmd(cmd, checkVersion) {
|
||||||
...getExtraOptionsFromEnv(["database", "finalize"]),
|
...getExtraOptionsFromEnv(["database", "finalize"]),
|
||||||
databasePath,
|
databasePath,
|
||||||
];
|
];
|
||||||
await (0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)(cmd, args, error_matcher_1.errorMatchers);
|
try {
|
||||||
|
await runTool(cmd, args);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (e instanceof CommandInvocationError &&
|
||||||
|
!(await util.codeQlVersionAbove(this, exports.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 e;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async resolveLanguages() {
|
async resolveLanguages() {
|
||||||
const codeqlArgs = [
|
const codeqlArgs = [
|
||||||
|
|
@ -421,7 +439,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);
|
||||||
|
|
@ -468,11 +486,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 = [
|
||||||
|
|
@ -717,10 +735,73 @@ 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;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
const fatalErrorRegex = /.*fatal error occurred:/gi;
|
||||||
|
let fatalErrors = [];
|
||||||
|
let lastFatalErrorIndex;
|
||||||
|
let match;
|
||||||
|
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) {
|
||||||
|
return text[text.length - 1] === "." ? text : `${text}.`;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
|
@ -841,4 +922,16 @@ function getGeneratedCodeScanningConfigPath(config) {
|
||||||
return path.resolve(config.tempDir, "user-config.yaml");
|
return path.resolve(config.tempDir, "user-config.yaml");
|
||||||
}
|
}
|
||||||
exports.getGeneratedCodeScanningConfigPath = getGeneratedCodeScanningConfigPath;
|
exports.getGeneratedCodeScanningConfigPath = getGeneratedCodeScanningConfigPath;
|
||||||
|
function isNoCodeFoundError(e) {
|
||||||
|
/**
|
||||||
|
* 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.error.includes(javascriptNoCodeFoundWarning);
|
||||||
|
}
|
||||||
//# sourceMappingURL=codeql.js.map
|
//# sourceMappingURL=codeql.js.map
|
||||||
File diff suppressed because one or more lines are too long
68
lib/codeql.test.js
generated
68
lib/codeql.test.js
generated
|
|
@ -704,11 +704,73 @@ for (const { featureEnabled, codeqlVersion, flagPassed, negativeFlagPassed, } of
|
||||||
t.is(runnerConstructorStub.firstCall.args[1].includes("--no-new-analysis-summary"), negativeFlagPassed, `--no-new-analysis-summary should${negativeFlagPassed ? "" : "n't"} be passed`);
|
t.is(runnerConstructorStub.firstCall.args[1].includes("--no-new-analysis-summary"), negativeFlagPassed, `--no-new-analysis-summary should${negativeFlagPassed ? "" : "n't"} be passed`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function stubToolRunnerConstructor() {
|
(0, ava_1.default)("database finalize recognises JavaScript no code found error on CodeQL 2.11.6", async (t) => {
|
||||||
|
stubToolRunnerConstructor(1, `2020-09-07T17:39:53.9050522Z [2020-09-07 17:39:53] [build] Done extracting /opt/hostedtoolcache/CodeQL/0.0.0-20200630/x64/codeql/javascript/tools/data/externs/web/ie_vml.js (3 ms)
|
||||||
|
2020-09-07T17:39:53.9051849Z [2020-09-07 17:39:53] [build-err] No JavaScript or TypeScript code found.
|
||||||
|
2020-09-07T17:39:53.9052444Z [2020-09-07 17:39:53] [build-err] No JavaScript or TypeScript code found.
|
||||||
|
2020-09-07T17:39:53.9251124Z [2020-09-07 17:39:53] [ERROR] Spawned process exited abnormally (code 255; tried to run: [/opt/hostedtoolcache/CodeQL/0.0.0-20200630/x64/codeql/javascript/tools/autobuild.sh])`);
|
||||||
|
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||||
|
sinon.stub(codeqlObject, "getVersion").resolves("2.11.6");
|
||||||
|
// safeWhich throws because of the test CodeQL object.
|
||||||
|
sinon.stub(safeWhich, "safeWhich").resolves("");
|
||||||
|
await t.throwsAsync(async () => await codeqlObject.finalizeDatabase("", "", ""), {
|
||||||
|
message: "No code found during the build. Please see: " +
|
||||||
|
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
(0, ava_1.default)("database finalize overrides no code found error on CodeQL 2.11.6", async (t) => {
|
||||||
|
stubToolRunnerConstructor(32);
|
||||||
|
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||||
|
sinon.stub(codeqlObject, "getVersion").resolves("2.11.6");
|
||||||
|
// safeWhich throws because of the test CodeQL object.
|
||||||
|
sinon.stub(safeWhich, "safeWhich").resolves("");
|
||||||
|
await t.throwsAsync(async () => await codeqlObject.finalizeDatabase("", "", ""), {
|
||||||
|
message: "No code found during the build. Please see: " +
|
||||||
|
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
(0, ava_1.default)("database finalize does not override no code found error on CodeQL 2.12.4", async (t) => {
|
||||||
|
const cliMessage = "CodeQL did not detect any code written in languages supported by CodeQL. Review our troubleshooting guide at " +
|
||||||
|
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build.";
|
||||||
|
stubToolRunnerConstructor(32, cliMessage);
|
||||||
|
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||||
|
sinon.stub(codeqlObject, "getVersion").resolves("2.12.4");
|
||||||
|
// safeWhich throws because of the test CodeQL object.
|
||||||
|
sinon.stub(safeWhich, "safeWhich").resolves("");
|
||||||
|
await t.throwsAsync(async () => await codeqlObject.finalizeDatabase("db", "--threads=2", "--ram=2048"), {
|
||||||
|
message: '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: ${cliMessage}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
(0, ava_1.default)("runTool summarizes several fatal errors", async (t) => {
|
||||||
|
const heapError = "A fatal error occurred: Evaluator heap must be at least 384.00 MiB";
|
||||||
|
const datasetImportError = "A fatal error occurred: Dataset import for /home/runner/work/_temp/codeql_databases/javascript/db-javascript failed with code 2";
|
||||||
|
const cliStderr = `Running TRAP import for CodeQL database at /home/runner/work/_temp/codeql_databases/javascript...\n` +
|
||||||
|
`${heapError}\n${datasetImportError}.`;
|
||||||
|
stubToolRunnerConstructor(32, cliStderr);
|
||||||
|
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||||
|
sinon.stub(codeqlObject, "getVersion").resolves("2.12.4");
|
||||||
|
// safeWhich throws because of the test CodeQL object.
|
||||||
|
sinon.stub(safeWhich, "safeWhich").resolves("");
|
||||||
|
await t.throwsAsync(async () => await codeqlObject.finalizeDatabase("db", "--threads=2", "--ram=2048"), {
|
||||||
|
message: '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: ${datasetImportError}. Context: ${heapError}.`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
function stubToolRunnerConstructor(exitCode = 0, stderr) {
|
||||||
const runnerObjectStub = sinon.createStubInstance(toolrunner.ToolRunner);
|
const runnerObjectStub = sinon.createStubInstance(toolrunner.ToolRunner);
|
||||||
runnerObjectStub.exec.resolves(0);
|
|
||||||
const runnerConstructorStub = sinon.stub(toolrunner, "ToolRunner");
|
const runnerConstructorStub = sinon.stub(toolrunner, "ToolRunner");
|
||||||
runnerConstructorStub.returns(runnerObjectStub);
|
let stderrListener = undefined;
|
||||||
|
runnerConstructorStub.callsFake((_cmd, _args, options) => {
|
||||||
|
stderrListener = options.listeners?.stderr;
|
||||||
|
return runnerObjectStub;
|
||||||
|
});
|
||||||
|
runnerObjectStub.exec.callsFake(async () => {
|
||||||
|
if (stderrListener !== undefined && stderr !== undefined) {
|
||||||
|
stderrListener(Buffer.from(stderr));
|
||||||
|
}
|
||||||
|
return exitCode;
|
||||||
|
});
|
||||||
return runnerConstructorStub;
|
return runnerConstructorStub;
|
||||||
}
|
}
|
||||||
exports.stubToolRunnerConstructor = stubToolRunnerConstructor;
|
exports.stubToolRunnerConstructor = stubToolRunnerConstructor;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
22
lib/error-matcher.js
generated
22
lib/error-matcher.js
generated
|
|
@ -1,22 +0,0 @@
|
||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.errorMatchers = exports.namedMatchersForTesting = void 0;
|
|
||||||
// exported only for testing purposes
|
|
||||||
exports.namedMatchersForTesting = {
|
|
||||||
/*
|
|
||||||
In due course it may be possible to remove the regex, if/when javascript also exits with code 32.
|
|
||||||
*/
|
|
||||||
noSourceCodeFound: {
|
|
||||||
exitCode: 32,
|
|
||||||
outputRegex: new RegExp("No JavaScript or TypeScript code found\\."),
|
|
||||||
message: "No code found during the build. Please see:\n" +
|
|
||||||
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build",
|
|
||||||
},
|
|
||||||
fatalError: {
|
|
||||||
outputRegex: new RegExp("A fatal error occurred"),
|
|
||||||
message: "A fatal error occurred.",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// we collapse the matches into an array for use in execErrorCatcher
|
|
||||||
exports.errorMatchers = Object.values(exports.namedMatchersForTesting);
|
|
||||||
//# sourceMappingURL=error-matcher.js.map
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"version":3,"file":"error-matcher.js","sourceRoot":"","sources":["../src/error-matcher.ts"],"names":[],"mappings":";;;AAQA,qCAAqC;AACxB,QAAA,uBAAuB,GAAoC;IACtE;;MAEE;IACF,iBAAiB,EAAE;QACjB,QAAQ,EAAE,EAAE;QACZ,WAAW,EAAE,IAAI,MAAM,CAAC,2CAA2C,CAAC;QACpE,OAAO,EACL,+CAA+C;YAC/C,8EAA8E;KACjF;IACD,UAAU,EAAE;QACV,WAAW,EAAE,IAAI,MAAM,CAAC,wBAAwB,CAAC;QACjD,OAAO,EAAE,yBAAyB;KACnC;CACF,CAAC;AAEF,oEAAoE;AACvD,QAAA,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,+BAAuB,CAAC,CAAC"}
|
|
||||||
32
lib/error-matcher.test.js
generated
32
lib/error-matcher.test.js
generated
|
|
@ -1,32 +0,0 @@
|
||||||
"use strict";
|
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
const ava_1 = __importDefault(require("ava"));
|
|
||||||
const error_matcher_1 = require("./error-matcher");
|
|
||||||
/*
|
|
||||||
NB We test the regexes for all the matchers against example log output snippets.
|
|
||||||
*/
|
|
||||||
(0, ava_1.default)("noSourceCodeFound matches against example javascript output", async (t) => {
|
|
||||||
t.assert(testErrorMatcher("noSourceCodeFound", `
|
|
||||||
2020-09-07T17:39:53.9050522Z [2020-09-07 17:39:53] [build] Done extracting /opt/hostedtoolcache/CodeQL/0.0.0-20200630/x64/codeql/javascript/tools/data/externs/web/ie_vml.js (3 ms)
|
|
||||||
2020-09-07T17:39:53.9051849Z [2020-09-07 17:39:53] [build-err] No JavaScript or TypeScript code found.
|
|
||||||
2020-09-07T17:39:53.9052444Z [2020-09-07 17:39:53] [build-err] No JavaScript or TypeScript code found.
|
|
||||||
2020-09-07T17:39:53.9251124Z [2020-09-07 17:39:53] [ERROR] Spawned process exited abnormally (code 255; tried to run: [/opt/hostedtoolcache/CodeQL/0.0.0-20200630/x64/codeql/javascript/tools/autobuild.sh])
|
|
||||||
`));
|
|
||||||
});
|
|
||||||
(0, ava_1.default)("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, logSample) {
|
|
||||||
if (!(matcherName in error_matcher_1.namedMatchersForTesting)) {
|
|
||||||
throw new Error(`Unknown matcher ${matcherName}`);
|
|
||||||
}
|
|
||||||
const regex = error_matcher_1.namedMatchersForTesting[matcherName].outputRegex;
|
|
||||||
if (regex === undefined) {
|
|
||||||
throw new Error(`Cannot test matcher ${matcherName} with null regex`);
|
|
||||||
}
|
|
||||||
return regex.test(logSample);
|
|
||||||
}
|
|
||||||
//# sourceMappingURL=error-matcher.test.js.map
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"version":3,"file":"error-matcher.test.js","sourceRoot":"","sources":["../src/error-matcher.test.ts"],"names":[],"mappings":";;;;;AAAA,8CAAuB;AAEvB,mDAA0D;AAE1D;;EAEE;AAEF,IAAA,aAAI,EAAC,6DAA6D,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IAC9E,CAAC,CAAC,MAAM,CACN,gBAAgB,CACd,mBAAmB,EACnB;;;;;GAKH,CACE,CACF,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,IAAA,aAAI,EAAC,+CAA+C,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IAChE,CAAC,CAAC,MAAM,CACN,gBAAgB,CACd,YAAY,EACZ,4EAA4E,CAC7E,CACF,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,SAAS,gBAAgB,CAAC,WAAmB,EAAE,SAAiB;IAC9D,IAAI,CAAC,CAAC,WAAW,IAAI,uCAAuB,CAAC,EAAE;QAC7C,MAAM,IAAI,KAAK,CAAC,mBAAmB,WAAW,EAAE,CAAC,CAAC;KACnD;IACD,MAAM,KAAK,GAAG,uCAAuB,CAAC,WAAW,CAAC,CAAC,WAAW,CAAC;IAC/D,IAAI,KAAK,KAAK,SAAS,EAAE;QACvB,MAAM,IAAI,KAAK,CAAC,uBAAuB,WAAW,kBAAkB,CAAC,CAAC;KACvE;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAC/B,CAAC"}
|
|
||||||
91
lib/toolrunner-error-catcher.js
generated
91
lib/toolrunner-error-catcher.js
generated
|
|
@ -1,91 +0,0 @@
|
||||||
"use strict";
|
|
||||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
||||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
||||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
||||||
}
|
|
||||||
Object.defineProperty(o, k2, desc);
|
|
||||||
}) : (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
o[k2] = m[k];
|
|
||||||
}));
|
|
||||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
||||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
||||||
}) : function(o, v) {
|
|
||||||
o["default"] = v;
|
|
||||||
});
|
|
||||||
var __importStar = (this && this.__importStar) || function (mod) {
|
|
||||||
if (mod && mod.__esModule) return mod;
|
|
||||||
var result = {};
|
|
||||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
||||||
__setModuleDefault(result, mod);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.toolrunnerErrorCatcher = void 0;
|
|
||||||
const toolrunner = __importStar(require("@actions/exec/lib/toolrunner"));
|
|
||||||
const safeWhich = __importStar(require("@chrisgavin/safe-which"));
|
|
||||||
const util_1 = require("./util");
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
async function toolrunnerErrorCatcher(commandLine, args, matchers, options) {
|
|
||||||
let stdout = "";
|
|
||||||
let stderr = "";
|
|
||||||
const listeners = {
|
|
||||||
stdout: (data) => {
|
|
||||||
stdout += data.toString();
|
|
||||||
if (options?.listeners?.stdout !== undefined) {
|
|
||||||
options.listeners.stdout(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stderr: (data) => {
|
|
||||||
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;
|
|
||||||
try {
|
|
||||||
exitCode = await new toolrunner.ToolRunner(await safeWhich.safeWhich(commandLine), args, {
|
|
||||||
...options,
|
|
||||||
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 (0, util_1.wrapError)(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.toolrunnerErrorCatcher = toolrunnerErrorCatcher;
|
|
||||||
//# sourceMappingURL=toolrunner-error-catcher.js.map
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"version":3,"file":"toolrunner-error-catcher.js","sourceRoot":"","sources":["../src/toolrunner-error-catcher.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AACA,yEAA2D;AAC3D,kEAAoD;AAGpD,iCAAmC;AAOnC;;;;;;;;;;GAUG;AACI,KAAK,UAAU,sBAAsB,CAC1C,WAAmB,EACnB,IAAe,EACf,QAAyB,EACzB,OAAwB;IAExB,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,MAAM,SAAS,GAAG;QAChB,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YACvB,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC1B,IAAI,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,SAAS,EAAE;gBAC5C,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;aAChC;QACH,CAAC;QACD,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YACvB,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC1B,IAAI,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,SAAS,EAAE;gBAC5C,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;aAChC;QACH,CAAC;KACF,CAAC;IAEF,0GAA0G;IAC1G,IAAI,QAAgB,CAAC;IACrB,IAAI;QACF,QAAQ,GAAG,MAAM,IAAI,UAAU,CAAC,UAAU,CACxC,MAAM,SAAS,CAAC,SAAS,CAAC,WAAW,CAAC,EACtC,IAAI,EACJ;YACE,GAAG,OAAO;YACV,SAAS;YACT,gBAAgB,EAAE,IAAI,EAAE,wDAAwD;SACjF,CACF,CAAC,IAAI,EAAE,CAAC;QAET,mEAAmE;QACnE,IAAI,QAAQ,KAAK,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAEhD,IAAI,QAAQ,EAAE;YACZ,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE;gBAC9B,IACE,OAAO,CAAC,QAAQ,KAAK,QAAQ;oBAC7B,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;oBACjC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,EACjC;oBACA,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;iBAClC;aACF;SACF;QAED,qFAAqF;QACrF,IAAI,OAAO,EAAE,gBAAgB,EAAE;YAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;SAC7B;aAAM;YACL,MAAM,IAAI,KAAK,CACb,gBAAgB,WAAW,2BAA2B,QAAQ,EAAE,CACjE,CAAC;SACH;KACF;IAAC,OAAO,CAAC,EAAE;QACV,MAAM,IAAA,gBAAS,EAAC,CAAC,CAAC,CAAC;KACpB;AACH,CAAC;AA/DD,wDA+DC"}
|
|
||||||
164
lib/toolrunner-error-catcher.test.js
generated
164
lib/toolrunner-error-catcher.test.js
generated
|
|
@ -1,164 +0,0 @@
|
||||||
"use strict";
|
|
||||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
||||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
||||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
||||||
}
|
|
||||||
Object.defineProperty(o, k2, desc);
|
|
||||||
}) : (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
o[k2] = m[k];
|
|
||||||
}));
|
|
||||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
||||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
||||||
}) : function(o, v) {
|
|
||||||
o["default"] = v;
|
|
||||||
});
|
|
||||||
var __importStar = (this && this.__importStar) || function (mod) {
|
|
||||||
if (mod && mod.__esModule) return mod;
|
|
||||||
var result = {};
|
|
||||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
||||||
__setModuleDefault(result, mod);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
const exec = __importStar(require("@actions/exec"));
|
|
||||||
const ava_1 = __importDefault(require("ava"));
|
|
||||||
const testing_utils_1 = require("./testing-utils");
|
|
||||||
const toolrunner_error_catcher_1 = require("./toolrunner-error-catcher");
|
|
||||||
(0, testing_utils_1.setupTests)(ava_1.default);
|
|
||||||
(0, ava_1.default)("matchers are never applied if non-error exit", async (t) => {
|
|
||||||
const testArgs = buildDummyArgs("foo bar\\nblort qux", "foo bar\\nblort qux", "", 0);
|
|
||||||
const matchers = [
|
|
||||||
{ exitCode: 123, outputRegex: new RegExp("foo bar"), message: "error!!!" },
|
|
||||||
];
|
|
||||||
t.deepEqual(await exec.exec("node", testArgs), 0);
|
|
||||||
const returnState = await (0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)("node", testArgs, matchers);
|
|
||||||
t.deepEqual(returnState.exitCode, 0);
|
|
||||||
});
|
|
||||||
(0, ava_1.default)("regex matchers are applied to stdout for non-zero exit code", async (t) => {
|
|
||||||
const testArgs = buildDummyArgs("foo bar\\nblort qux", "", "", 1);
|
|
||||||
const matchers = [
|
|
||||||
{ 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((0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)("node", testArgs, matchers), {
|
|
||||||
instanceOf: Error,
|
|
||||||
message: "🦄",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
(0, ava_1.default)("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 = [
|
|
||||||
{ 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((0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)("node", testArgs, matchers), {
|
|
||||||
instanceOf: Error,
|
|
||||||
message: "🦄",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
(0, ava_1.default)("matcher returns correct error message when multiple matchers defined", async (t) => {
|
|
||||||
const testArgs = buildDummyArgs("non matching string", "foo bar\\nblort qux", "", 1);
|
|
||||||
const matchers = [
|
|
||||||
{ 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((0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)("node", testArgs, matchers), {
|
|
||||||
instanceOf: Error,
|
|
||||||
message: "🦄",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
(0, ava_1.default)("matcher returns first match to regex when multiple matches", async (t) => {
|
|
||||||
const testArgs = buildDummyArgs("non matching string", "foo bar\\nblort qux", "", 1);
|
|
||||||
const matchers = [
|
|
||||||
{ 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((0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)("node", testArgs, matchers), {
|
|
||||||
instanceOf: Error,
|
|
||||||
message: "🦄",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
(0, ava_1.default)("exit code matchers are applied", async (t) => {
|
|
||||||
const testArgs = buildDummyArgs("non matching string", "foo bar\\nblort qux", "", 123);
|
|
||||||
const matchers = [
|
|
||||||
{
|
|
||||||
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((0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)("node", testArgs, matchers), {
|
|
||||||
instanceOf: Error,
|
|
||||||
message: "🦄",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
(0, ava_1.default)("execErrorCatcher respects the ignoreReturnValue option", async (t) => {
|
|
||||||
const testArgs = buildDummyArgs("standard output", "error output", "", 199);
|
|
||||||
await t.throwsAsync((0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)("node", testArgs, [], { ignoreReturnCode: false }), { instanceOf: Error });
|
|
||||||
const returnState = await (0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)("node", testArgs, [], {
|
|
||||||
ignoreReturnCode: true,
|
|
||||||
});
|
|
||||||
t.deepEqual(returnState.exitCode, 199);
|
|
||||||
});
|
|
||||||
(0, ava_1.default)("execErrorCatcher preserves behavior of provided listeners", async (t) => {
|
|
||||||
const stdoutExpected = "standard output";
|
|
||||||
const stderrExpected = "error output";
|
|
||||||
let stdoutActual = "";
|
|
||||||
let stderrActual = "";
|
|
||||||
const listeners = {
|
|
||||||
stdout: (data) => {
|
|
||||||
stdoutActual += data.toString();
|
|
||||||
},
|
|
||||||
stderr: (data) => {
|
|
||||||
stderrActual += data.toString();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const testArgs = buildDummyArgs(stdoutExpected, stderrExpected, "", 0);
|
|
||||||
const returnState = await (0, toolrunner_error_catcher_1.toolrunnerErrorCatcher)("node", testArgs, [], {
|
|
||||||
listeners,
|
|
||||||
});
|
|
||||||
t.deepEqual(returnState.exitCode, 0);
|
|
||||||
t.deepEqual(stdoutActual, `${stdoutExpected}\n`);
|
|
||||||
t.deepEqual(stderrActual, `${stderrExpected}\n`);
|
|
||||||
});
|
|
||||||
function buildDummyArgs(stdoutContents, stderrContents, desiredErrorMessage, desiredExitCode) {
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
//# sourceMappingURL=toolrunner-error-catcher.test.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,7 @@
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
import { ExecOptions } from "@actions/exec";
|
||||||
import * as toolrunner from "@actions/exec/lib/toolrunner";
|
import * as toolrunner from "@actions/exec/lib/toolrunner";
|
||||||
import * as toolcache from "@actions/tool-cache";
|
import * as toolcache from "@actions/tool-cache";
|
||||||
import * as safeWhich from "@chrisgavin/safe-which";
|
import * as safeWhich from "@chrisgavin/safe-which";
|
||||||
|
|
@ -1133,13 +1134,108 @@ for (const {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stubToolRunnerConstructor(): sinon.SinonStub<
|
test("database finalize recognises JavaScript no code found error on CodeQL 2.11.6", async (t) => {
|
||||||
any[],
|
stubToolRunnerConstructor(
|
||||||
toolrunner.ToolRunner
|
1,
|
||||||
> {
|
`2020-09-07T17:39:53.9050522Z [2020-09-07 17:39:53] [build] Done extracting /opt/hostedtoolcache/CodeQL/0.0.0-20200630/x64/codeql/javascript/tools/data/externs/web/ie_vml.js (3 ms)
|
||||||
|
2020-09-07T17:39:53.9051849Z [2020-09-07 17:39:53] [build-err] No JavaScript or TypeScript code found.
|
||||||
|
2020-09-07T17:39:53.9052444Z [2020-09-07 17:39:53] [build-err] No JavaScript or TypeScript code found.
|
||||||
|
2020-09-07T17:39:53.9251124Z [2020-09-07 17:39:53] [ERROR] Spawned process exited abnormally (code 255; tried to run: [/opt/hostedtoolcache/CodeQL/0.0.0-20200630/x64/codeql/javascript/tools/autobuild.sh])`
|
||||||
|
);
|
||||||
|
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||||
|
sinon.stub(codeqlObject, "getVersion").resolves("2.11.6");
|
||||||
|
// safeWhich throws because of the test CodeQL object.
|
||||||
|
sinon.stub(safeWhich, "safeWhich").resolves("");
|
||||||
|
|
||||||
|
await t.throwsAsync(
|
||||||
|
async () => await codeqlObject.finalizeDatabase("", "", ""),
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"No code found during the build. Please see: " +
|
||||||
|
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("database finalize overrides no code found error on CodeQL 2.11.6", async (t) => {
|
||||||
|
stubToolRunnerConstructor(32);
|
||||||
|
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||||
|
sinon.stub(codeqlObject, "getVersion").resolves("2.11.6");
|
||||||
|
// safeWhich throws because of the test CodeQL object.
|
||||||
|
sinon.stub(safeWhich, "safeWhich").resolves("");
|
||||||
|
|
||||||
|
await t.throwsAsync(
|
||||||
|
async () => await codeqlObject.finalizeDatabase("", "", ""),
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"No code found during the build. Please see: " +
|
||||||
|
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("database finalize does not override no code found error on CodeQL 2.12.4", async (t) => {
|
||||||
|
const cliMessage =
|
||||||
|
"CodeQL did not detect any code written in languages supported by CodeQL. Review our troubleshooting guide at " +
|
||||||
|
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build.";
|
||||||
|
stubToolRunnerConstructor(32, cliMessage);
|
||||||
|
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||||
|
sinon.stub(codeqlObject, "getVersion").resolves("2.12.4");
|
||||||
|
// safeWhich throws because of the test CodeQL object.
|
||||||
|
sinon.stub(safeWhich, "safeWhich").resolves("");
|
||||||
|
|
||||||
|
await t.throwsAsync(
|
||||||
|
async () =>
|
||||||
|
await codeqlObject.finalizeDatabase("db", "--threads=2", "--ram=2048"),
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'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: ${cliMessage}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("runTool summarizes several fatal errors", async (t) => {
|
||||||
|
const heapError =
|
||||||
|
"A fatal error occurred: Evaluator heap must be at least 384.00 MiB";
|
||||||
|
const datasetImportError =
|
||||||
|
"A fatal error occurred: Dataset import for /home/runner/work/_temp/codeql_databases/javascript/db-javascript failed with code 2";
|
||||||
|
const cliStderr =
|
||||||
|
`Running TRAP import for CodeQL database at /home/runner/work/_temp/codeql_databases/javascript...\n` +
|
||||||
|
`${heapError}\n${datasetImportError}.`;
|
||||||
|
stubToolRunnerConstructor(32, cliStderr);
|
||||||
|
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||||
|
sinon.stub(codeqlObject, "getVersion").resolves("2.12.4");
|
||||||
|
// safeWhich throws because of the test CodeQL object.
|
||||||
|
sinon.stub(safeWhich, "safeWhich").resolves("");
|
||||||
|
|
||||||
|
await t.throwsAsync(
|
||||||
|
async () =>
|
||||||
|
await codeqlObject.finalizeDatabase("db", "--threads=2", "--ram=2048"),
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'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: ${datasetImportError}. Context: ${heapError}.`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export function stubToolRunnerConstructor(
|
||||||
|
exitCode: number = 0,
|
||||||
|
stderr?: string
|
||||||
|
): sinon.SinonStub<any[], toolrunner.ToolRunner> {
|
||||||
const runnerObjectStub = sinon.createStubInstance(toolrunner.ToolRunner);
|
const runnerObjectStub = sinon.createStubInstance(toolrunner.ToolRunner);
|
||||||
runnerObjectStub.exec.resolves(0);
|
|
||||||
const runnerConstructorStub = sinon.stub(toolrunner, "ToolRunner");
|
const runnerConstructorStub = sinon.stub(toolrunner, "ToolRunner");
|
||||||
runnerConstructorStub.returns(runnerObjectStub);
|
let stderrListener: ((data: Buffer) => void) | undefined = undefined;
|
||||||
|
runnerConstructorStub.callsFake((_cmd, _args, options: ExecOptions) => {
|
||||||
|
stderrListener = options.listeners?.stderr;
|
||||||
|
return runnerObjectStub;
|
||||||
|
});
|
||||||
|
runnerObjectStub.exec.callsFake(async () => {
|
||||||
|
if (stderrListener !== undefined && stderr !== undefined) {
|
||||||
|
stderrListener(Buffer.from(stderr));
|
||||||
|
}
|
||||||
|
return exitCode;
|
||||||
|
});
|
||||||
return runnerConstructorStub;
|
return runnerConstructorStub;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
138
src/codeql.ts
138
src/codeql.ts
|
|
@ -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";
|
||||||
|
|
||||||
|
|
@ -49,14 +47,16 @@ export class CommandInvocationError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
cmd: string,
|
cmd: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
exitCode: number,
|
public exitCode: number,
|
||||||
error: string,
|
public error: string,
|
||||||
public output: string
|
public output: string
|
||||||
) {
|
) {
|
||||||
|
const prettyCommand = [cmd, ...args]
|
||||||
|
.map((x) => (x.includes(" ") ? `'${x}'` : x))
|
||||||
|
.join(" ");
|
||||||
super(
|
super(
|
||||||
`Failure invoking ${cmd} with arguments ${args}.\n
|
`Encountered a fatal error while running "${prettyCommand}". ` +
|
||||||
Exit code ${exitCode} and error was:\n
|
`Exit code was ${exitCode} and error was: ${error.trim()}`
|
||||||
${error}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -307,6 +307,12 @@ export const CODEQL_VERSION_EXPORT_CODE_SCANNING_CONFIG = "2.12.3";
|
||||||
*/
|
*/
|
||||||
export const CODEQL_VERSION_INIT_WITH_QLCONFIG = "2.12.4";
|
export const CODEQL_VERSION_INIT_WITH_QLCONFIG = "2.12.4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versions 2.12.4+ of the CodeQL CLI provide a better error message when `database finalize`
|
||||||
|
* determines that no code has been found.
|
||||||
|
*/
|
||||||
|
export const CODEQL_VERSION_BETTER_NO_CODE_ERROR_MESSAGE = "2.12.4";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Versions 2.13.4+ of the CodeQL CLI support the `resolve build-environment` command.
|
* Versions 2.13.4+ of the CodeQL CLI support the `resolve build-environment` command.
|
||||||
*/
|
*/
|
||||||
|
|
@ -621,9 +627,7 @@ export async function getCodeQLForCmd(
|
||||||
`autobuild${ext}`
|
`autobuild${ext}`
|
||||||
);
|
);
|
||||||
// Run trace command
|
// Run trace command
|
||||||
await toolrunnerErrorCatcher(
|
await runTool(cmd, [
|
||||||
cmd,
|
|
||||||
[
|
|
||||||
"database",
|
"database",
|
||||||
"trace-command",
|
"trace-command",
|
||||||
...(await getTrapCachingExtractorConfigArgsForLang(config, language)),
|
...(await getTrapCachingExtractorConfigArgsForLang(config, language)),
|
||||||
|
|
@ -631,9 +635,7 @@ export async function getCodeQLForCmd(
|
||||||
databasePath,
|
databasePath,
|
||||||
"--",
|
"--",
|
||||||
traceCommand,
|
traceCommand,
|
||||||
],
|
]);
|
||||||
errorMatchers
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
async finalizeDatabase(
|
async finalizeDatabase(
|
||||||
databasePath: string,
|
databasePath: string,
|
||||||
|
|
@ -649,7 +651,24 @@ export async function getCodeQLForCmd(
|
||||||
...getExtraOptionsFromEnv(["database", "finalize"]),
|
...getExtraOptionsFromEnv(["database", "finalize"]),
|
||||||
databasePath,
|
databasePath,
|
||||||
];
|
];
|
||||||
await toolrunnerErrorCatcher(cmd, args, errorMatchers);
|
try {
|
||||||
|
await runTool(cmd, args);
|
||||||
|
} catch (e) {
|
||||||
|
if (
|
||||||
|
e instanceof CommandInvocationError &&
|
||||||
|
!(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 e;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async resolveLanguages() {
|
async resolveLanguages() {
|
||||||
const codeqlArgs = [
|
const codeqlArgs = [
|
||||||
|
|
@ -759,7 +778,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,
|
||||||
|
|
@ -825,17 +844,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 = [
|
||||||
|
|
@ -1138,11 +1153,76 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}.`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
|
@ -1292,3 +1372,17 @@ export async function getTrapCachingExtractorConfigArgsForLang(
|
||||||
export function getGeneratedCodeScanningConfigPath(config: Config): string {
|
export function getGeneratedCodeScanningConfigPath(config: Config): string {
|
||||||
return path.resolve(config.tempDir, "user-config.yaml");
|
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.error.includes(javascriptNoCodeFoundWarning);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +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("noSourceCodeFound matches against example javascript output", async (t) => {
|
|
||||||
t.assert(
|
|
||||||
testErrorMatcher(
|
|
||||||
"noSourceCodeFound",
|
|
||||||
`
|
|
||||||
2020-09-07T17:39:53.9050522Z [2020-09-07 17:39:53] [build] Done extracting /opt/hostedtoolcache/CodeQL/0.0.0-20200630/x64/codeql/javascript/tools/data/externs/web/ie_vml.js (3 ms)
|
|
||||||
2020-09-07T17:39:53.9051849Z [2020-09-07 17:39:53] [build-err] No JavaScript or TypeScript code found.
|
|
||||||
2020-09-07T17:39:53.9052444Z [2020-09-07 17:39:53] [build-err] No JavaScript or TypeScript code found.
|
|
||||||
2020-09-07T17:39:53.9251124Z [2020-09-07 17:39:53] [ERROR] Spawned process exited abnormally (code 255; tried to run: [/opt/hostedtoolcache/CodeQL/0.0.0-20200630/x64/codeql/javascript/tools/autobuild.sh])
|
|
||||||
`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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,28 +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 } = {
|
|
||||||
/*
|
|
||||||
In due course it may be possible to remove the regex, if/when javascript also exits with code 32.
|
|
||||||
*/
|
|
||||||
noSourceCodeFound: {
|
|
||||||
exitCode: 32,
|
|
||||||
outputRegex: new RegExp("No JavaScript or TypeScript code found\\."),
|
|
||||||
message:
|
|
||||||
"No code found during the build. Please see:\n" +
|
|
||||||
"https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build",
|
|
||||||
},
|
|
||||||
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