Merge pull request #174 from github/nickfyson/error_wrapper
add error-catching wrapper around calls to toolrunner
This commit is contained in:
commit
245c02cf7d
16 changed files with 1339 additions and 19 deletions
10
lib/codeql.js
generated
10
lib/codeql.js
generated
|
|
@ -21,6 +21,8 @@ const globalutil = __importStar(require("util"));
|
||||||
const v4_1 = __importDefault(require("uuid/v4"));
|
const v4_1 = __importDefault(require("uuid/v4"));
|
||||||
const api = __importStar(require("./api-client"));
|
const api = __importStar(require("./api-client"));
|
||||||
const defaults = __importStar(require("./defaults.json")); // Referenced from codeql-action-sync-tool!
|
const defaults = __importStar(require("./defaults.json")); // Referenced from codeql-action-sync-tool!
|
||||||
|
const error_matcher_1 = require("./error-matcher");
|
||||||
|
const toolrunner_error_catcher_1 = require("./toolrunner-error-catcher");
|
||||||
const util = __importStar(require("./util"));
|
const util = __importStar(require("./util"));
|
||||||
/**
|
/**
|
||||||
* Stores the CodeQL object, and is populated by `setupCodeQL` or `getCodeQL`.
|
* Stores the CodeQL object, and is populated by `setupCodeQL` or `getCodeQL`.
|
||||||
|
|
@ -318,22 +320,22 @@ function getCodeQLForCmd(cmd) {
|
||||||
const ext = process.platform === "win32" ? ".cmd" : ".sh";
|
const ext = process.platform === "win32" ? ".cmd" : ".sh";
|
||||||
const traceCommand = path.resolve(JSON.parse(extractorPath), "tools", `autobuild${ext}`);
|
const traceCommand = path.resolve(JSON.parse(extractorPath), "tools", `autobuild${ext}`);
|
||||||
// Run trace command
|
// Run trace command
|
||||||
await new toolrunnner.ToolRunner(cmd, [
|
await toolrunner_error_catcher_1.toolrunnerErrorCatcher(cmd, [
|
||||||
"database",
|
"database",
|
||||||
"trace-command",
|
"trace-command",
|
||||||
...getExtraOptionsFromEnv(["database", "trace-command"]),
|
...getExtraOptionsFromEnv(["database", "trace-command"]),
|
||||||
databasePath,
|
databasePath,
|
||||||
"--",
|
"--",
|
||||||
traceCommand,
|
traceCommand,
|
||||||
]).exec();
|
], error_matcher_1.errorMatchers);
|
||||||
},
|
},
|
||||||
async finalizeDatabase(databasePath) {
|
async finalizeDatabase(databasePath) {
|
||||||
await new toolrunnner.ToolRunner(cmd, [
|
await toolrunner_error_catcher_1.toolrunnerErrorCatcher(cmd, [
|
||||||
"database",
|
"database",
|
||||||
"finalize",
|
"finalize",
|
||||||
...getExtraOptionsFromEnv(["database", "finalize"]),
|
...getExtraOptionsFromEnv(["database", "finalize"]),
|
||||||
databasePath,
|
databasePath,
|
||||||
]).exec();
|
], error_matcher_1.errorMatchers);
|
||||||
},
|
},
|
||||||
async resolveQueries(queries, extraSearchPath) {
|
async resolveQueries(queries, extraSearchPath) {
|
||||||
const codeqlArgs = [
|
const codeqlArgs = [
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
17
lib/error-matcher.js
generated
Normal file
17
lib/error-matcher.js
generated
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
// 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://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/troubleshooting-code-scanning#no-code-found-during-the-build",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// we collapse the matches into an array for use in execErrorCatcher
|
||||||
|
exports.errorMatchers = Object.values(exports.namedMatchersForTesting);
|
||||||
|
//# sourceMappingURL=error-matcher.js.map
|
||||||
1
lib/error-matcher.js.map
Normal file
1
lib/error-matcher.js.map
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"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,yJAAyJ;KAC5J;CACF,CAAC;AAEF,oEAAoE;AACvD,QAAA,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,+BAAuB,CAAC,CAAC"}
|
||||||
29
lib/error-matcher.test.js
generated
Normal file
29
lib/error-matcher.test.js
generated
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
"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.
|
||||||
|
*/
|
||||||
|
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])
|
||||||
|
`));
|
||||||
|
});
|
||||||
|
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
lib/error-matcher.test.js.map
Normal file
1
lib/error-matcher.test.js.map
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"error-matcher.test.js","sourceRoot":"","sources":["../src/error-matcher.test.ts"],"names":[],"mappings":";;;;;AAAA,8CAAuB;AAEvB,mDAA0D;AAE1D;;EAEE;AAEF,aAAI,CAAC,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,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"}
|
||||||
86
lib/toolrunner-error-catcher.js
generated
Normal file
86
lib/toolrunner-error-catcher.js
generated
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
"use strict";
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||||
|
result["default"] = mod;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const toolrunnner = __importStar(require("@actions/exec/lib/toolrunner"));
|
||||||
|
/**
|
||||||
|
* 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 Promise<number> exit code
|
||||||
|
*/
|
||||||
|
async function toolrunnerErrorCatcher(commandLine, args, matchers, options) {
|
||||||
|
var _a, _b, _c;
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
const listeners = {
|
||||||
|
stdout: (data) => {
|
||||||
|
var _a, _b;
|
||||||
|
stdout += data.toString();
|
||||||
|
if (((_b = (_a = options) === null || _a === void 0 ? void 0 : _a.listeners) === null || _b === void 0 ? void 0 : _b.stdout) !== undefined) {
|
||||||
|
options.listeners.stdout(data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// if no stdout listener was originally defined then we match default behavior of Toolrunner
|
||||||
|
process.stdout.write(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stderr: (data) => {
|
||||||
|
var _a, _b;
|
||||||
|
stderr += data.toString();
|
||||||
|
if (((_b = (_a = options) === null || _a === void 0 ? void 0 : _a.listeners) === null || _b === void 0 ? void 0 : _b.stderr) !== undefined) {
|
||||||
|
options.listeners.stderr(data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// if no stderr listener was originally defined then we match default behavior of Toolrunner
|
||||||
|
process.stderr.write(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// we capture the original return code or error so that if no match is found we can duplicate the behavior
|
||||||
|
let returnState;
|
||||||
|
try {
|
||||||
|
returnState = await new toolrunnner.ToolRunner(commandLine, args, {
|
||||||
|
...options,
|
||||||
|
listeners,
|
||||||
|
ignoreReturnCode: true,
|
||||||
|
}).exec();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
returnState = e;
|
||||||
|
}
|
||||||
|
// if there is a zero return code then we do not apply the matchers
|
||||||
|
if (returnState === 0)
|
||||||
|
return returnState;
|
||||||
|
if (matchers) {
|
||||||
|
for (const matcher of matchers) {
|
||||||
|
if (matcher.exitCode === returnState || ((_a = matcher.outputRegex) === null || _a === void 0 ? void 0 : _a.test(stderr)) || ((_b = matcher.outputRegex) === null || _b === void 0 ? void 0 : _b.test(stdout))) {
|
||||||
|
throw new Error(matcher.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof returnState === "number") {
|
||||||
|
// only if we were instructed to ignore the return code do we ever return it non-zero
|
||||||
|
if ((_c = options) === null || _c === void 0 ? void 0 : _c.ignoreReturnCode) {
|
||||||
|
return returnState;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`The process \'${commandLine}\' failed with exit code ${returnState}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw returnState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.toolrunnerErrorCatcher = toolrunnerErrorCatcher;
|
||||||
|
//# sourceMappingURL=toolrunner-error-catcher.js.map
|
||||||
1
lib/toolrunner-error-catcher.js.map
Normal file
1
lib/toolrunner-error-catcher.js.map
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"toolrunner-error-catcher.js","sourceRoot":"","sources":["../src/toolrunner-error-catcher.ts"],"names":[],"mappings":";;;;;;;;;AACA,0EAA4D;AAI5D;;;;;;;;;;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,aAAA,OAAO,0CAAE,SAAS,0CAAE,MAAM,MAAK,SAAS,EAAE;gBAC5C,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;aAChC;iBAAM;gBACL,4FAA4F;gBAC5F,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;aAC5B;QACH,CAAC;QACD,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;;YACvB,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC1B,IAAI,aAAA,OAAO,0CAAE,SAAS,0CAAE,MAAM,MAAK,SAAS,EAAE;gBAC5C,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;aAChC;iBAAM;gBACL,4FAA4F;gBAC5F,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;aAC5B;QACH,CAAC;KACF,CAAC;IAEF,0GAA0G;IAC1G,IAAI,WAA2B,CAAC;IAChC,IAAI;QACF,WAAW,GAAG,MAAM,IAAI,WAAW,CAAC,UAAU,CAAC,WAAW,EAAE,IAAI,EAAE;YAChE,GAAG,OAAO;YACV,SAAS;YACT,gBAAgB,EAAE,IAAI;SACvB,CAAC,CAAC,IAAI,EAAE,CAAC;KACX;IAAC,OAAO,CAAC,EAAE;QACV,WAAW,GAAG,CAAC,CAAC;KACjB;IAED,mEAAmE;IACnE,IAAI,WAAW,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC;IAE1C,IAAI,QAAQ,EAAE;QACZ,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE;YAC9B,IACE,OAAO,CAAC,QAAQ,KAAK,WAAW,WAChC,OAAO,CAAC,WAAW,0CAAE,IAAI,CAAC,MAAM,EAAC,WACjC,OAAO,CAAC,WAAW,0CAAE,IAAI,CAAC,MAAM,EAAC,EACjC;gBACA,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;aAClC;SACF;KACF;IAED,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE;QACnC,qFAAqF;QACrF,UAAI,OAAO,0CAAE,gBAAgB,EAAE;YAC7B,OAAO,WAAW,CAAC;SACpB;aAAM;YACL,MAAM,IAAI,KAAK,CACb,iBAAiB,WAAW,4BAA4B,WAAW,EAAE,CACtE,CAAC;SACH;KACF;SAAM;QACL,MAAM,WAAW,CAAC;KACnB;AACH,CAAC;AArED,wDAqEC"}
|
||||||
145
lib/toolrunner-error-catcher.test.js
generated
Normal file
145
lib/toolrunner-error-catcher.test.js
generated
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
"use strict";
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||||
|
result["default"] = 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");
|
||||||
|
testing_utils_1.setupTests(ava_1.default);
|
||||||
|
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);
|
||||||
|
t.deepEqual(await toolrunner_error_catcher_1.toolrunnerErrorCatcher("node", testArgs, matchers), 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: "The process 'node' failed with exit code 1",
|
||||||
|
});
|
||||||
|
await t.throwsAsync(toolrunner_error_catcher_1.toolrunnerErrorCatcher("node", testArgs, matchers), {
|
||||||
|
instanceOf: Error,
|
||||||
|
message: "🦄",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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: "The process 'node' failed with exit code 1",
|
||||||
|
});
|
||||||
|
await t.throwsAsync(toolrunner_error_catcher_1.toolrunnerErrorCatcher("node", testArgs, matchers), {
|
||||||
|
instanceOf: Error,
|
||||||
|
message: "🦄",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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: "The process 'node' failed with exit code 1",
|
||||||
|
});
|
||||||
|
await t.throwsAsync(toolrunner_error_catcher_1.toolrunnerErrorCatcher("node", testArgs, matchers), {
|
||||||
|
instanceOf: Error,
|
||||||
|
message: "🦄",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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: "The process 'node' failed with exit code 1",
|
||||||
|
});
|
||||||
|
await t.throwsAsync(toolrunner_error_catcher_1.toolrunnerErrorCatcher("node", testArgs, matchers), {
|
||||||
|
instanceOf: Error,
|
||||||
|
message: "🦄",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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: "The process 'node' failed with exit code 123",
|
||||||
|
});
|
||||||
|
await t.throwsAsync(toolrunner_error_catcher_1.toolrunnerErrorCatcher("node", testArgs, matchers), {
|
||||||
|
instanceOf: Error,
|
||||||
|
message: "🦄",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ava_1.default("execErrorCatcher respects the ignoreReturnValue option", async (t) => {
|
||||||
|
const testArgs = buildDummyArgs("standard output", "error output", "", 199);
|
||||||
|
await t.throwsAsync(toolrunner_error_catcher_1.toolrunnerErrorCatcher("node", testArgs, [], { ignoreReturnCode: false }), { instanceOf: Error });
|
||||||
|
t.deepEqual(await toolrunner_error_catcher_1.toolrunnerErrorCatcher("node", testArgs, [], {
|
||||||
|
ignoreReturnCode: true,
|
||||||
|
}), 199);
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
t.deepEqual(await toolrunner_error_catcher_1.toolrunnerErrorCatcher("node", testArgs, [], {
|
||||||
|
listeners,
|
||||||
|
}), 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
|
||||||
1
lib/toolrunner-error-catcher.test.js.map
Normal file
1
lib/toolrunner-error-catcher.test.js.map
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -11,8 +11,10 @@ import uuidV4 from "uuid/v4";
|
||||||
|
|
||||||
import * as api from "./api-client";
|
import * as api from "./api-client";
|
||||||
import * as defaults from "./defaults.json"; // Referenced from codeql-action-sync-tool!
|
import * as defaults from "./defaults.json"; // Referenced from codeql-action-sync-tool!
|
||||||
|
import { errorMatchers } from "./error-matcher";
|
||||||
import { Language } from "./languages";
|
import { Language } from "./languages";
|
||||||
import { Logger } from "./logging";
|
import { Logger } from "./logging";
|
||||||
|
import { toolrunnerErrorCatcher } from "./toolrunner-error-catcher";
|
||||||
import * as util from "./util";
|
import * as util from "./util";
|
||||||
|
|
||||||
type Options = Array<string | number | boolean>;
|
type Options = Array<string | number | boolean>;
|
||||||
|
|
@ -505,22 +507,30 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Run trace command
|
// Run trace command
|
||||||
await new toolrunnner.ToolRunner(cmd, [
|
await toolrunnerErrorCatcher(
|
||||||
"database",
|
cmd,
|
||||||
"trace-command",
|
[
|
||||||
...getExtraOptionsFromEnv(["database", "trace-command"]),
|
"database",
|
||||||
databasePath,
|
"trace-command",
|
||||||
"--",
|
...getExtraOptionsFromEnv(["database", "trace-command"]),
|
||||||
traceCommand,
|
databasePath,
|
||||||
]).exec();
|
"--",
|
||||||
|
traceCommand,
|
||||||
|
],
|
||||||
|
errorMatchers
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async finalizeDatabase(databasePath: string) {
|
async finalizeDatabase(databasePath: string) {
|
||||||
await new toolrunnner.ToolRunner(cmd, [
|
await toolrunnerErrorCatcher(
|
||||||
"database",
|
cmd,
|
||||||
"finalize",
|
[
|
||||||
...getExtraOptionsFromEnv(["database", "finalize"]),
|
"database",
|
||||||
databasePath,
|
"finalize",
|
||||||
]).exec();
|
...getExtraOptionsFromEnv(["database", "finalize"]),
|
||||||
|
databasePath,
|
||||||
|
],
|
||||||
|
errorMatchers
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async resolveQueries(
|
async resolveQueries(
|
||||||
queries: string[],
|
queries: string[],
|
||||||
|
|
|
||||||
676
src/codeql.ts.orig
Normal file
676
src/codeql.ts.orig
Normal file
|
|
@ -0,0 +1,676 @@
|
||||||
|
<<<<<<< HEAD
|
||||||
|
import * as toolrunnner from '@actions/exec/lib/toolrunner';
|
||||||
|
import * as http from '@actions/http-client';
|
||||||
|
import { IHeaders } from '@actions/http-client/interfaces';
|
||||||
|
import * as toolcache from '@actions/tool-cache';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as semver from 'semver';
|
||||||
|
import * as stream from 'stream';
|
||||||
|
import * as globalutil from 'util';
|
||||||
|
import uuidV4 from 'uuid/v4';
|
||||||
|
|
||||||
|
import * as api from './api-client';
|
||||||
|
import * as defaults from './defaults.json'; // Referenced from codeql-action-sync-tool!
|
||||||
|
import { errorMatchers} from './error-matcher';
|
||||||
|
import { Language } from './languages';
|
||||||
|
import { Logger } from './logging';
|
||||||
|
import { toolrunnerErrorCatcher } from './toolrunner-error-catcher';
|
||||||
|
import * as util from './util';
|
||||||
|
|
||||||
|
type Options = (string|number|boolean)[];
|
||||||
|
=======
|
||||||
|
import * as toolrunnner from "@actions/exec/lib/toolrunner";
|
||||||
|
import * as http from "@actions/http-client";
|
||||||
|
import { IHeaders } from "@actions/http-client/interfaces";
|
||||||
|
import * as toolcache from "@actions/tool-cache";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as semver from "semver";
|
||||||
|
import * as stream from "stream";
|
||||||
|
import * as globalutil from "util";
|
||||||
|
import uuidV4 from "uuid/v4";
|
||||||
|
|
||||||
|
import * as api from "./api-client";
|
||||||
|
import * as defaults from "./defaults.json"; // Referenced from codeql-action-sync-tool!
|
||||||
|
import { Language } from "./languages";
|
||||||
|
import { Logger } from "./logging";
|
||||||
|
import * as util from "./util";
|
||||||
|
|
||||||
|
type Options = Array<string | number | boolean>;
|
||||||
|
>>>>>>> main
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra command line options for the codeql commands.
|
||||||
|
*/
|
||||||
|
interface ExtraOptions {
|
||||||
|
"*"?: Options;
|
||||||
|
database?: {
|
||||||
|
"*"?: Options;
|
||||||
|
init?: Options;
|
||||||
|
"trace-command"?: Options;
|
||||||
|
analyze?: Options;
|
||||||
|
finalize?: Options;
|
||||||
|
};
|
||||||
|
resolve?: {
|
||||||
|
"*"?: Options;
|
||||||
|
extractor?: Options;
|
||||||
|
queries?: Options;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeQL {
|
||||||
|
/**
|
||||||
|
* Get the path of the CodeQL executable.
|
||||||
|
*/
|
||||||
|
getPath(): string;
|
||||||
|
/**
|
||||||
|
* Print version information about CodeQL.
|
||||||
|
*/
|
||||||
|
printVersion(): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Run 'codeql database trace-command' on 'tracer-env.js' and parse
|
||||||
|
* the result to get environment variables set by CodeQL.
|
||||||
|
*/
|
||||||
|
getTracerEnv(databasePath: string): Promise<{ [key: string]: string }>;
|
||||||
|
/**
|
||||||
|
* Run 'codeql database init'.
|
||||||
|
*/
|
||||||
|
databaseInit(
|
||||||
|
databasePath: string,
|
||||||
|
language: Language,
|
||||||
|
sourceRoot: string
|
||||||
|
): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Runs the autobuilder for the given language.
|
||||||
|
*/
|
||||||
|
runAutobuild(language: Language): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Extract code for a scanned language using 'codeql database trace-command'
|
||||||
|
* and running the language extracter.
|
||||||
|
*/
|
||||||
|
extractScannedLanguage(database: string, language: Language): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Finalize a database using 'codeql database finalize'.
|
||||||
|
*/
|
||||||
|
finalizeDatabase(databasePath: string): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Run 'codeql resolve queries'.
|
||||||
|
*/
|
||||||
|
resolveQueries(
|
||||||
|
queries: string[],
|
||||||
|
extraSearchPath: string | undefined
|
||||||
|
): Promise<ResolveQueriesOutput>;
|
||||||
|
/**
|
||||||
|
* Run 'codeql database analyze'.
|
||||||
|
*/
|
||||||
|
databaseAnalyze(
|
||||||
|
databasePath: string,
|
||||||
|
sarifFile: string,
|
||||||
|
querySuite: string,
|
||||||
|
memoryFlag: string,
|
||||||
|
addSnippetsFlag: string,
|
||||||
|
threadsFlag: string
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolveQueriesOutput {
|
||||||
|
byLanguage: {
|
||||||
|
[language: string]: {
|
||||||
|
[queryPath: string]: {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
noDeclaredLanguage: {
|
||||||
|
[queryPath: string]: {};
|
||||||
|
};
|
||||||
|
multipleDeclaredLanguages: {
|
||||||
|
[queryPath: string]: {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the CodeQL object, and is populated by `setupCodeQL` or `getCodeQL`.
|
||||||
|
* Can be overridden in tests using `setCodeQL`.
|
||||||
|
*/
|
||||||
|
let cachedCodeQL: CodeQL | undefined = undefined;
|
||||||
|
|
||||||
|
const CODEQL_BUNDLE_VERSION = defaults.bundleVersion;
|
||||||
|
const CODEQL_BUNDLE_NAME = "codeql-bundle.tar.gz";
|
||||||
|
const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action";
|
||||||
|
|
||||||
|
function getCodeQLActionRepository(mode: util.Mode): string {
|
||||||
|
if (mode !== "actions") {
|
||||||
|
return CODEQL_DEFAULT_ACTION_REPOSITORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions do not know their own repository name,
|
||||||
|
// so we currently use this hack to find the name based on where our files are.
|
||||||
|
// This can be removed once the change to the runner in https://github.com/actions/runner/pull/585 is deployed.
|
||||||
|
const runnerTemp = util.getRequiredEnvParam("RUNNER_TEMP");
|
||||||
|
const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions");
|
||||||
|
const relativeScriptPath = path.relative(actionsDirectory, __filename);
|
||||||
|
// This handles the case where the Action does not come from an Action repository,
|
||||||
|
// e.g. our integration tests which use the Action code from the current checkout.
|
||||||
|
if (
|
||||||
|
relativeScriptPath.startsWith("..") ||
|
||||||
|
path.isAbsolute(relativeScriptPath)
|
||||||
|
) {
|
||||||
|
return CODEQL_DEFAULT_ACTION_REPOSITORY;
|
||||||
|
}
|
||||||
|
const relativeScriptPathParts = relativeScriptPath.split(path.sep);
|
||||||
|
return `${relativeScriptPathParts[0]}/${relativeScriptPathParts[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCodeQLBundleDownloadURL(
|
||||||
|
githubAuth: string,
|
||||||
|
githubUrl: string,
|
||||||
|
mode: util.Mode,
|
||||||
|
logger: Logger
|
||||||
|
): Promise<string> {
|
||||||
|
const codeQLActionRepository = getCodeQLActionRepository(mode);
|
||||||
|
const potentialDownloadSources = [
|
||||||
|
// This GitHub instance, and this Action.
|
||||||
|
[githubUrl, codeQLActionRepository],
|
||||||
|
// This GitHub instance, and the canonical Action.
|
||||||
|
[githubUrl, CODEQL_DEFAULT_ACTION_REPOSITORY],
|
||||||
|
// GitHub.com, and the canonical Action.
|
||||||
|
[util.GITHUB_DOTCOM_URL, CODEQL_DEFAULT_ACTION_REPOSITORY],
|
||||||
|
];
|
||||||
|
// We now filter out any duplicates.
|
||||||
|
// Duplicates will happen either because the GitHub instance is GitHub.com, or because the Action is not a fork.
|
||||||
|
const uniqueDownloadSources = potentialDownloadSources.filter(
|
||||||
|
(url, index, self) => index === self.indexOf(url)
|
||||||
|
);
|
||||||
|
for (const downloadSource of uniqueDownloadSources) {
|
||||||
|
const [apiURL, repository] = downloadSource;
|
||||||
|
// If we've reached the final case, short-circuit the API check since we know the bundle exists and is public.
|
||||||
|
if (
|
||||||
|
apiURL === util.GITHUB_DOTCOM_URL &&
|
||||||
|
repository === CODEQL_DEFAULT_ACTION_REPOSITORY
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const [repositoryOwner, repositoryName] = repository.split("/");
|
||||||
|
try {
|
||||||
|
const release = await api
|
||||||
|
.getApiClient(githubAuth, githubUrl)
|
||||||
|
.repos.getReleaseByTag({
|
||||||
|
owner: repositoryOwner,
|
||||||
|
repo: repositoryName,
|
||||||
|
tag: CODEQL_BUNDLE_VERSION,
|
||||||
|
});
|
||||||
|
for (const asset of release.data.assets) {
|
||||||
|
if (asset.name === CODEQL_BUNDLE_NAME) {
|
||||||
|
logger.info(
|
||||||
|
`Found CodeQL bundle in ${downloadSource[1]} on ${downloadSource[0]} with URL ${asset.url}.`
|
||||||
|
);
|
||||||
|
return asset.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.info(
|
||||||
|
`Looked for CodeQL bundle in ${downloadSource[1]} on ${downloadSource[0]} but got error ${e}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${CODEQL_BUNDLE_VERSION}/${CODEQL_BUNDLE_NAME}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to download CodeQL manually because the toolcache doesn't support Accept headers.
|
||||||
|
// This can be removed once https://github.com/actions/toolkit/pull/530 is merged and released.
|
||||||
|
async function toolcacheDownloadTool(
|
||||||
|
url: string,
|
||||||
|
headers: IHeaders | undefined,
|
||||||
|
tempDir: string,
|
||||||
|
logger: Logger
|
||||||
|
): Promise<string> {
|
||||||
|
const client = new http.HttpClient("CodeQL Action");
|
||||||
|
const dest = path.join(tempDir, uuidV4());
|
||||||
|
const response: http.HttpClientResponse = await client.get(url, headers);
|
||||||
|
if (response.message.statusCode !== 200) {
|
||||||
|
logger.info(
|
||||||
|
`Failed to download from "${url}". Code(${response.message.statusCode}) Message(${response.message.statusMessage})`
|
||||||
|
);
|
||||||
|
throw new Error(`Unexpected HTTP response: ${response.message.statusCode}`);
|
||||||
|
}
|
||||||
|
const pipeline = globalutil.promisify(stream.pipeline);
|
||||||
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||||
|
await pipeline(response.message, fs.createWriteStream(dest));
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupCodeQL(
|
||||||
|
codeqlURL: string | undefined,
|
||||||
|
githubAuth: string,
|
||||||
|
githubUrl: string,
|
||||||
|
tempDir: string,
|
||||||
|
toolsDir: string,
|
||||||
|
mode: util.Mode,
|
||||||
|
logger: Logger
|
||||||
|
): Promise<CodeQL> {
|
||||||
|
// Setting these two env vars makes the toolcache code safe to use outside,
|
||||||
|
// of actions but this is obviously not a great thing we're doing and it would
|
||||||
|
// be better to write our own implementation to use outside of actions.
|
||||||
|
process.env["RUNNER_TEMP"] = tempDir;
|
||||||
|
process.env["RUNNER_TOOL_CACHE"] = toolsDir;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const codeqlURLVersion = getCodeQLURLVersion(
|
||||||
|
codeqlURL || `/${CODEQL_BUNDLE_VERSION}/`,
|
||||||
|
logger
|
||||||
|
);
|
||||||
|
|
||||||
|
let codeqlFolder = toolcache.find("CodeQL", codeqlURLVersion);
|
||||||
|
if (codeqlFolder) {
|
||||||
|
logger.debug(`CodeQL found in cache ${codeqlFolder}`);
|
||||||
|
} else {
|
||||||
|
if (!codeqlURL) {
|
||||||
|
codeqlURL = await getCodeQLBundleDownloadURL(
|
||||||
|
githubAuth,
|
||||||
|
githubUrl,
|
||||||
|
mode,
|
||||||
|
logger
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: IHeaders = { accept: "application/octet-stream" };
|
||||||
|
// We only want to provide an authorization header if we are downloading
|
||||||
|
// from the same GitHub instance the Action is running on.
|
||||||
|
// This avoids leaking Enterprise tokens to dotcom.
|
||||||
|
if (codeqlURL.startsWith(`${githubUrl}/`)) {
|
||||||
|
logger.debug("Downloading CodeQL bundle with token.");
|
||||||
|
headers.authorization = `token ${githubAuth}`;
|
||||||
|
} else {
|
||||||
|
logger.debug("Downloading CodeQL bundle without token.");
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
`Downloading CodeQL tools from ${codeqlURL}. This may take a while.`
|
||||||
|
);
|
||||||
|
const codeqlPath = await toolcacheDownloadTool(
|
||||||
|
codeqlURL,
|
||||||
|
headers,
|
||||||
|
tempDir,
|
||||||
|
logger
|
||||||
|
);
|
||||||
|
logger.debug(`CodeQL bundle download to ${codeqlPath} complete.`);
|
||||||
|
|
||||||
|
const codeqlExtracted = await toolcache.extractTar(codeqlPath);
|
||||||
|
codeqlFolder = await toolcache.cacheDir(
|
||||||
|
codeqlExtracted,
|
||||||
|
"CodeQL",
|
||||||
|
codeqlURLVersion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let codeqlCmd = path.join(codeqlFolder, "codeql", "codeql");
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
codeqlCmd += ".exe";
|
||||||
|
} else if (process.platform !== "linux" && process.platform !== "darwin") {
|
||||||
|
throw new Error(`Unsupported platform: ${process.platform}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedCodeQL = getCodeQLForCmd(codeqlCmd);
|
||||||
|
return cachedCodeQL;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
throw new Error("Unable to download and extract CodeQL CLI");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCodeQLURLVersion(url: string, logger: Logger): string {
|
||||||
|
const match = url.match(/\/codeql-bundle-(.*)\//);
|
||||||
|
if (match === null || match.length < 2) {
|
||||||
|
throw new Error(
|
||||||
|
`Malformed tools url: ${url}. Version could not be inferred`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = match[1];
|
||||||
|
|
||||||
|
if (!semver.valid(version)) {
|
||||||
|
logger.debug(
|
||||||
|
`Bundle version ${version} is not in SemVer format. Will treat it as pre-release 0.0.0-${version}.`
|
||||||
|
);
|
||||||
|
version = `0.0.0-${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = semver.clean(version);
|
||||||
|
if (!s) {
|
||||||
|
throw new Error(
|
||||||
|
`Malformed tools url ${url}. Version should be in SemVer format but have ${version} instead`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the CodeQL executable located at the given path.
|
||||||
|
*/
|
||||||
|
export function getCodeQL(cmd: string): CodeQL {
|
||||||
|
if (cachedCodeQL === undefined) {
|
||||||
|
cachedCodeQL = getCodeQLForCmd(cmd);
|
||||||
|
}
|
||||||
|
return cachedCodeQL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFunction<T>(
|
||||||
|
partialCodeql: Partial<CodeQL>,
|
||||||
|
methodName: string,
|
||||||
|
defaultImplementation?: T
|
||||||
|
): T {
|
||||||
|
if (typeof partialCodeql[methodName] !== "function") {
|
||||||
|
if (defaultImplementation !== undefined) {
|
||||||
|
return defaultImplementation;
|
||||||
|
}
|
||||||
|
const dummyMethod = () => {
|
||||||
|
throw new Error(`CodeQL ${methodName} method not correctly defined`);
|
||||||
|
};
|
||||||
|
return dummyMethod as any;
|
||||||
|
}
|
||||||
|
return partialCodeql[methodName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the functionality for CodeQL methods. Only for use in tests.
|
||||||
|
*
|
||||||
|
* Accepts a partial object and any undefined methods will be implemented
|
||||||
|
* to immediately throw an exception indicating which method is missing.
|
||||||
|
*/
|
||||||
|
export function setCodeQL(partialCodeql: Partial<CodeQL>): CodeQL {
|
||||||
|
cachedCodeQL = {
|
||||||
|
getPath: resolveFunction(partialCodeql, "getPath", () => "/tmp/dummy-path"),
|
||||||
|
printVersion: resolveFunction(partialCodeql, "printVersion"),
|
||||||
|
getTracerEnv: resolveFunction(partialCodeql, "getTracerEnv"),
|
||||||
|
databaseInit: resolveFunction(partialCodeql, "databaseInit"),
|
||||||
|
runAutobuild: resolveFunction(partialCodeql, "runAutobuild"),
|
||||||
|
extractScannedLanguage: resolveFunction(
|
||||||
|
partialCodeql,
|
||||||
|
"extractScannedLanguage"
|
||||||
|
),
|
||||||
|
finalizeDatabase: resolveFunction(partialCodeql, "finalizeDatabase"),
|
||||||
|
resolveQueries: resolveFunction(partialCodeql, "resolveQueries"),
|
||||||
|
databaseAnalyze: resolveFunction(partialCodeql, "databaseAnalyze"),
|
||||||
|
};
|
||||||
|
return cachedCodeQL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cached CodeQL object. Should only be used from tests.
|
||||||
|
*
|
||||||
|
* TODO: Work out a good way for tests to get this from the test context
|
||||||
|
* instead of having to have this method.
|
||||||
|
*/
|
||||||
|
export function getCachedCodeQL(): CodeQL {
|
||||||
|
if (cachedCodeQL === undefined) {
|
||||||
|
// Should never happen as setCodeQL is called by testing-utils.setupTests
|
||||||
|
throw new Error("cachedCodeQL undefined");
|
||||||
|
}
|
||||||
|
return cachedCodeQL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCodeQLForCmd(cmd: string): CodeQL {
|
||||||
|
return {
|
||||||
|
getPath() {
|
||||||
|
return cmd;
|
||||||
|
},
|
||||||
|
async printVersion() {
|
||||||
|
await new toolrunnner.ToolRunner(cmd, [
|
||||||
|
"version",
|
||||||
|
"--format=json",
|
||||||
|
]).exec();
|
||||||
|
},
|
||||||
|
async getTracerEnv(databasePath: string) {
|
||||||
|
// Write tracer-env.js to a temp location.
|
||||||
|
const tracerEnvJs = path.resolve(
|
||||||
|
databasePath,
|
||||||
|
"working",
|
||||||
|
"tracer-env.js"
|
||||||
|
);
|
||||||
|
fs.mkdirSync(path.dirname(tracerEnvJs), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
tracerEnvJs,
|
||||||
|
`
|
||||||
|
const fs = require('fs');
|
||||||
|
const env = {};
|
||||||
|
for (let entry of Object.entries(process.env)) {
|
||||||
|
const key = entry[0];
|
||||||
|
const value = entry[1];
|
||||||
|
if (typeof value !== 'undefined' && key !== '_' && !key.startsWith('JAVA_MAIN_CLASS_')) {
|
||||||
|
env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.stdout.write(process.argv[2]);
|
||||||
|
fs.writeFileSync(process.argv[2], JSON.stringify(env), 'utf-8');`
|
||||||
|
);
|
||||||
|
|
||||||
|
const envFile = path.resolve(databasePath, "working", "env.tmp");
|
||||||
|
await new toolrunnner.ToolRunner(cmd, [
|
||||||
|
"database",
|
||||||
|
"trace-command",
|
||||||
|
databasePath,
|
||||||
|
...getExtraOptionsFromEnv(["database", "trace-command"]),
|
||||||
|
process.execPath,
|
||||||
|
tracerEnvJs,
|
||||||
|
envFile,
|
||||||
|
]).exec();
|
||||||
|
return JSON.parse(fs.readFileSync(envFile, "utf-8"));
|
||||||
|
},
|
||||||
|
async databaseInit(
|
||||||
|
databasePath: string,
|
||||||
|
language: Language,
|
||||||
|
sourceRoot: string
|
||||||
|
) {
|
||||||
|
await new toolrunnner.ToolRunner(cmd, [
|
||||||
|
"database",
|
||||||
|
"init",
|
||||||
|
databasePath,
|
||||||
|
`--language=${language}`,
|
||||||
|
`--source-root=${sourceRoot}`,
|
||||||
|
...getExtraOptionsFromEnv(["database", "init"]),
|
||||||
|
]).exec();
|
||||||
|
},
|
||||||
|
async runAutobuild(language: Language) {
|
||||||
|
const cmdName =
|
||||||
|
process.platform === "win32" ? "autobuild.cmd" : "autobuild.sh";
|
||||||
|
const autobuildCmd = path.join(
|
||||||
|
path.dirname(cmd),
|
||||||
|
language,
|
||||||
|
"tools",
|
||||||
|
cmdName
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update JAVA_TOOL_OPTIONS to contain '-Dhttp.keepAlive=false'
|
||||||
|
// This is because of an issue with Azure pipelines timing out connections after 4 minutes
|
||||||
|
// and Maven not properly handling closed connections
|
||||||
|
// Otherwise long build processes will timeout when pulling down Java packages
|
||||||
|
// https://developercommunity.visualstudio.com/content/problem/292284/maven-hosted-agent-connection-timeout.html
|
||||||
|
const javaToolOptions = process.env["JAVA_TOOL_OPTIONS"] || "";
|
||||||
|
process.env["JAVA_TOOL_OPTIONS"] = [
|
||||||
|
...javaToolOptions.split(/\s+/),
|
||||||
|
"-Dhttp.keepAlive=false",
|
||||||
|
"-Dmaven.wagon.http.pool=false",
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
await new toolrunnner.ToolRunner(autobuildCmd).exec();
|
||||||
|
},
|
||||||
|
async extractScannedLanguage(databasePath: string, language: Language) {
|
||||||
|
// Get extractor location
|
||||||
|
let extractorPath = "";
|
||||||
|
await new toolrunnner.ToolRunner(
|
||||||
|
cmd,
|
||||||
|
[
|
||||||
|
"resolve",
|
||||||
|
"extractor",
|
||||||
|
"--format=json",
|
||||||
|
`--language=${language}`,
|
||||||
|
...getExtraOptionsFromEnv(["resolve", "extractor"]),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
silent: true,
|
||||||
|
listeners: {
|
||||||
|
stdout: (data) => {
|
||||||
|
extractorPath += data.toString();
|
||||||
|
},
|
||||||
|
stderr: (data) => {
|
||||||
|
process.stderr.write(data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
).exec();
|
||||||
|
|
||||||
|
// Set trace command
|
||||||
|
const ext = process.platform === "win32" ? ".cmd" : ".sh";
|
||||||
|
const traceCommand = path.resolve(
|
||||||
|
JSON.parse(extractorPath),
|
||||||
|
"tools",
|
||||||
|
`autobuild${ext}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run trace command
|
||||||
|
<<<<<<< HEAD
|
||||||
|
await toolrunnerErrorCatcher(
|
||||||
|
cmd, [
|
||||||
|
'database',
|
||||||
|
'trace-command',
|
||||||
|
...getExtraOptionsFromEnv(['database', 'trace-command']),
|
||||||
|
databasePath,
|
||||||
|
'--',
|
||||||
|
traceCommand
|
||||||
|
],
|
||||||
|
errorMatchers
|
||||||
|
);
|
||||||
|
},
|
||||||
|
finalizeDatabase: async function(databasePath: string) {
|
||||||
|
await toolrunnerErrorCatcher(
|
||||||
|
cmd, [
|
||||||
|
'database',
|
||||||
|
'finalize',
|
||||||
|
...getExtraOptionsFromEnv(['database', 'finalize']),
|
||||||
|
databasePath
|
||||||
|
],
|
||||||
|
errorMatchers);
|
||||||
|
=======
|
||||||
|
await new toolrunnner.ToolRunner(cmd, [
|
||||||
|
"database",
|
||||||
|
"trace-command",
|
||||||
|
...getExtraOptionsFromEnv(["database", "trace-command"]),
|
||||||
|
databasePath,
|
||||||
|
"--",
|
||||||
|
traceCommand,
|
||||||
|
]).exec();
|
||||||
|
},
|
||||||
|
async finalizeDatabase(databasePath: string) {
|
||||||
|
await new toolrunnner.ToolRunner(cmd, [
|
||||||
|
"database",
|
||||||
|
"finalize",
|
||||||
|
...getExtraOptionsFromEnv(["database", "finalize"]),
|
||||||
|
databasePath,
|
||||||
|
]).exec();
|
||||||
|
>>>>>>> main
|
||||||
|
},
|
||||||
|
async resolveQueries(
|
||||||
|
queries: string[],
|
||||||
|
extraSearchPath: string | undefined
|
||||||
|
) {
|
||||||
|
const codeqlArgs = [
|
||||||
|
"resolve",
|
||||||
|
"queries",
|
||||||
|
...queries,
|
||||||
|
"--format=bylanguage",
|
||||||
|
...getExtraOptionsFromEnv(["resolve", "queries"]),
|
||||||
|
];
|
||||||
|
if (extraSearchPath !== undefined) {
|
||||||
|
codeqlArgs.push("--search-path", extraSearchPath);
|
||||||
|
}
|
||||||
|
let output = "";
|
||||||
|
await new toolrunnner.ToolRunner(cmd, codeqlArgs, {
|
||||||
|
listeners: {
|
||||||
|
stdout: (data: Buffer) => {
|
||||||
|
output += data.toString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).exec();
|
||||||
|
|
||||||
|
return JSON.parse(output);
|
||||||
|
},
|
||||||
|
async databaseAnalyze(
|
||||||
|
databasePath: string,
|
||||||
|
sarifFile: string,
|
||||||
|
querySuite: string,
|
||||||
|
memoryFlag: string,
|
||||||
|
addSnippetsFlag: string,
|
||||||
|
threadsFlag: string
|
||||||
|
) {
|
||||||
|
await new toolrunnner.ToolRunner(cmd, [
|
||||||
|
"database",
|
||||||
|
"analyze",
|
||||||
|
memoryFlag,
|
||||||
|
threadsFlag,
|
||||||
|
databasePath,
|
||||||
|
"--format=sarif-latest",
|
||||||
|
`--output=${sarifFile}`,
|
||||||
|
addSnippetsFlag,
|
||||||
|
...getExtraOptionsFromEnv(["database", "analyze"]),
|
||||||
|
querySuite,
|
||||||
|
]).exec();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the options for `path` of `options` as an array of extra option strings.
|
||||||
|
*/
|
||||||
|
function getExtraOptionsFromEnv(path: string[]) {
|
||||||
|
const options: ExtraOptions = util.getExtraOptionsEnvParam();
|
||||||
|
return getExtraOptions(options, path, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the options for `path` of `options` as an array of extra option strings.
|
||||||
|
*
|
||||||
|
* - the special terminal step name '*' in `options` matches all path steps
|
||||||
|
* - throws an exception if this conversion is impossible.
|
||||||
|
*/
|
||||||
|
export /* exported for testing */ function getExtraOptions(
|
||||||
|
options: any,
|
||||||
|
path: string[],
|
||||||
|
pathInfo: string[]
|
||||||
|
): string[] {
|
||||||
|
/**
|
||||||
|
* Gets `options` as an array of extra option strings.
|
||||||
|
*
|
||||||
|
* - throws an exception mentioning `pathInfo` if this conversion is impossible.
|
||||||
|
*/
|
||||||
|
function asExtraOptions(options: any, pathInfo: string[]): string[] {
|
||||||
|
if (options === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
const msg = `The extra options for '${pathInfo.join(
|
||||||
|
"."
|
||||||
|
)}' ('${JSON.stringify(options)}') are not in an array.`;
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
return options.map((o) => {
|
||||||
|
const t = typeof o;
|
||||||
|
if (t !== "string" && t !== "number" && t !== "boolean") {
|
||||||
|
const msg = `The extra option for '${pathInfo.join(
|
||||||
|
"."
|
||||||
|
)}' ('${JSON.stringify(o)}') is not a primitive value.`;
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
return `${o}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const all = asExtraOptions(options?.["*"], pathInfo.concat("*"));
|
||||||
|
const specific =
|
||||||
|
path.length === 0
|
||||||
|
? asExtraOptions(options, pathInfo)
|
||||||
|
: getExtraOptions(
|
||||||
|
options?.[path[0]],
|
||||||
|
path?.slice(1),
|
||||||
|
pathInfo.concat(path[0])
|
||||||
|
);
|
||||||
|
return all.concat(specific);
|
||||||
|
}
|
||||||
32
src/error-matcher.test.ts
Normal file
32
src/error-matcher.test.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
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])
|
||||||
|
`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
24
src/error-matcher.ts
Normal file
24
src/error-matcher.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// 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://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/troubleshooting-code-scanning#no-code-found-during-the-build",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// we collapse the matches into an array for use in execErrorCatcher
|
||||||
|
export const errorMatchers = Object.values(namedMatchersForTesting);
|
||||||
209
src/toolrunner-error-catcher.test.ts
Normal file
209
src/toolrunner-error-catcher.test.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
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);
|
||||||
|
|
||||||
|
t.deepEqual(await toolrunnerErrorCatcher("node", testArgs, matchers), 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: "The process 'node' 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: "The process 'node' 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: "The process 'node' 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: "The process 'node' 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: "The process 'node' 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 }
|
||||||
|
);
|
||||||
|
|
||||||
|
t.deepEqual(
|
||||||
|
await toolrunnerErrorCatcher("node", testArgs, [], {
|
||||||
|
ignoreReturnCode: true,
|
||||||
|
}),
|
||||||
|
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);
|
||||||
|
|
||||||
|
t.deepEqual(
|
||||||
|
await toolrunnerErrorCatcher("node", testArgs, [], {
|
||||||
|
listeners,
|
||||||
|
}),
|
||||||
|
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];
|
||||||
|
}
|
||||||
86
src/toolrunner-error-catcher.ts
Normal file
86
src/toolrunner-error-catcher.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import * as im from "@actions/exec/lib/interfaces";
|
||||||
|
import * as toolrunnner from "@actions/exec/lib/toolrunner";
|
||||||
|
|
||||||
|
import { ErrorMatcher } from "./error-matcher";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Promise<number> exit code
|
||||||
|
*/
|
||||||
|
export async function toolrunnerErrorCatcher(
|
||||||
|
commandLine: string,
|
||||||
|
args?: string[],
|
||||||
|
matchers?: ErrorMatcher[],
|
||||||
|
options?: im.ExecOptions
|
||||||
|
): Promise<number> {
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
const listeners = {
|
||||||
|
stdout: (data: Buffer) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
if (options?.listeners?.stdout !== undefined) {
|
||||||
|
options.listeners.stdout(data);
|
||||||
|
} else {
|
||||||
|
// if no stdout listener was originally defined then we match default behavior of Toolrunner
|
||||||
|
process.stdout.write(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stderr: (data: Buffer) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
if (options?.listeners?.stderr !== undefined) {
|
||||||
|
options.listeners.stderr(data);
|
||||||
|
} else {
|
||||||
|
// if no stderr listener was originally defined then we match default behavior of Toolrunner
|
||||||
|
process.stderr.write(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// we capture the original return code or error so that if no match is found we can duplicate the behavior
|
||||||
|
let returnState: Error | number;
|
||||||
|
try {
|
||||||
|
returnState = await new toolrunnner.ToolRunner(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();
|
||||||
|
} catch (e) {
|
||||||
|
returnState = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a zero return code then we do not apply the matchers
|
||||||
|
if (returnState === 0) return returnState;
|
||||||
|
|
||||||
|
if (matchers) {
|
||||||
|
for (const matcher of matchers) {
|
||||||
|
if (
|
||||||
|
matcher.exitCode === returnState ||
|
||||||
|
matcher.outputRegex?.test(stderr) ||
|
||||||
|
matcher.outputRegex?.test(stdout)
|
||||||
|
) {
|
||||||
|
throw new Error(matcher.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof returnState === "number") {
|
||||||
|
// only if we were instructed to ignore the return code do we ever return it non-zero
|
||||||
|
if (options?.ignoreReturnCode) {
|
||||||
|
return returnState;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`The process \'${commandLine}\' failed with exit code ${returnState}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw returnState;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue