Commit
add error-catching wrapper around calls to toolrunner
- v3.22.12
- v3.22.11
- v3
- v2.22.12
- v2.22.11
- v2.22.10
- v2.22.9
- v2.22.8
- v2.22.7
- v2.22.6
- v2.22.5
- v2.22.4
- v2.22.3
- v2.22.2
- v2.22.1
- v2.22.0
- v2.21.9
- v2.21.8
- v2.21.7
- v2.21.6
- v2.21.5
- v2.21.4
- v2.21.3
- v2.21.2
- v2.21.1
- v2.21.0
- v2.20.4
- v2.20.3
- v2.20.2
- v2.20.1
- v2.20.0
- v2.3.6
- v2.3.5
- v2.3.4
- v2.3.3
- v2.3.2
- v2.3.1
- v2.3.0
- v2.2.12
- v2.2.11
- v2.2.10
- v2.2.9
- v2.2.8
- v2.2.7
- v2.2.6
- v2.2.5
- v2.2.4
- v2.2.3
- v2.2.2
- v2.2.1
- v2.2.0
- v2.1.39
- v2.1.38
- v2.1.37
- v2.1.36
- v2.1.35
- v2.1.34
- v2.1.33
- v2.1.32
- v2.1.31
- v2.1.30
- v2.1.29
- v2.1.28
- v2.1.27
- v2.1.26
- v2.1.25
- v2.1.24
- v2.1.23
- v2.1.22
- v2.1.21
- v2.1.20
- v2.1.19
- v2.1.18
- v2.1.17
- v2.1.16
- v2.1.15
- v2.1.14
- v2.1.13
- v2.1.12
- v2.1.11
- v2.1.10
- v2.1.9
- v2.1.8
- v2.1.7
- v2.1.6
- v2
- v1.1.39
- v1.1.38
- v1.1.37
- v1.1.36
- v1.1.35
- v1.1.34
- v1.1.33
- v1.1.32
- v1.1.31
- v1.1.30
- v1.1.29
- v1.1.28
- v1.1.27
- v1.1.26
- v1.1.25
- v1.1.24
- v1.1.23
- v1.1.22
- v1.1.21
- v1.1.20
- v1.1.19
- v1.1.18
- v1.1.17
- v1.1.16
- v1.1.15
- v1.1.14
- v1.1.13
- v1.1.12
- v1.1.11
- v1.1.10
- v1.1.9
- v1.1.8
- v1.1.7
- v1.1.6
- v1.1.5
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.32
- v1.0.31
- v1.0.30
- v1.0.29
- v1.0.28
- v1.0.27
- v1.0.26
- v1.0.25
- v1.0.24
- v1.0.23
- v1.0.22
- v1.0.21
- v1.0.20
- v1.0.19
- v1.0.18
- v1.0.17
- v1.0.16
- v1.0.15
- v1.0.14
- v1.0.13
- v1.0.12
- v1.0.11
- v1.0.10
- v1.0.9
- v1.0.8
- v1.0.7
- v1.0.6
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- v1
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |