Memory management and let runLint handle resolving exit code

This commit is contained in:
Michael J Mulligan 2021-04-06 20:38:59 +01:00
parent dd63ff3880
commit 61a0ce84ec
3 changed files with 360 additions and 386 deletions

242
dist/post_run/index.js vendored
View file

@ -6798,8 +6798,9 @@ const parseOutput = (json) => {
} }
if (lintOutput.Issues.length) { if (lintOutput.Issues.length) {
lintOutput.Issues = lintOutput.Issues.filter((issue) => issue.Severity !== `ignore`).map((issue) => { lintOutput.Issues = lintOutput.Issues.filter((issue) => issue.Severity !== `ignore`).map((issue) => {
const Severity = issue.Severity.toLowerCase(); issue.Severity = ((Severity) => {
issue.Severity = severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure`; return severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure`;
})(issue.Severity.toLowerCase());
return issue; return issue;
}); });
} }
@ -6807,21 +6808,30 @@ const parseOutput = (json) => {
}; };
const logLintIssues = (issues) => { const logLintIssues = (issues) => {
issues.forEach((issue) => { issues.forEach((issue) => {
let header = `${ansi_styles_1.default.red.open}${ansi_styles_1.default.bold.open}Lint Error:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.red.close}`; core.info(((issue) => {
if (issue.Severity === `warning`) { switch (issue.Severity) {
header = `${ansi_styles_1.default.yellow.open}${ansi_styles_1.default.bold.open}Lint Warning:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.yellow.close}`; case `warning`:
} return `${ansi_styles_1.default.yellow.open}${ansi_styles_1.default.bold.open}Lint Warning:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.yellow.close}`;
else if (issue.Severity === `notice`) { case `notice`:
header = `${ansi_styles_1.default.cyan.open}${ansi_styles_1.default.bold.open}Lint Notice:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.cyan.close}`; return `${ansi_styles_1.default.cyan.open}${ansi_styles_1.default.bold.open}Lint Notice:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.cyan.close}`;
} default:
let pos = `${issue.Pos.Filename}:${issue.Pos.Line}`; return `${ansi_styles_1.default.red.open}${ansi_styles_1.default.bold.open}Lint Error:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.red.close}`;
if (issue.LineRange !== undefined) { }
pos += `-${issue.LineRange.To}`; })(issue) +
} ` ` +
else if (issue.Pos.Column) { `${issue.Pos.Filename}:${issue.Pos.Line}` +
pos += `:${issue.Pos.Column}`; ((issue) => {
} if (issue.LineRange !== undefined) {
core.info(`${header} ${pos} - ${issue.Text} (${issue.FromLinter})`); return `-${issue.LineRange.To}`;
}
else if (issue.Pos.Column) {
return `:${issue.Pos.Column}`;
}
else {
return ``;
}
})(issue) +
` - ${issue.Text} (${issue.FromLinter})`);
}); });
}; };
function resolveCheckRunId() { function resolveCheckRunId() {
@ -6894,77 +6904,52 @@ function annotateLintIssues(issues, checkRunId) {
}; };
const ctx = github.context; const ctx = github.context;
const octokit = github.getOctokit(core.getInput(`github-token`, { required: true })); const octokit = github.getOctokit(core.getInput(`github-token`, { required: true }));
const githubAnnotations = issues.map((issue) => {
// If/when we transition to comments, we would build the request structure here
const annotation = {
path: issue.Pos.Filename,
start_line: issue.Pos.Line,
end_line: issue.Pos.Line,
title: issue.FromLinter,
message: issue.Text,
annotation_level: issue.Severity,
};
issueCounts[issue.Severity]++;
if (issue.LineRange !== undefined) {
annotation.end_line = issue.LineRange.To;
}
else if (issue.Pos.Column) {
annotation.start_column = issue.Pos.Column;
annotation.end_column = issue.Pos.Column;
}
if (issue.Replacement !== null) {
let replacement = ``;
if (issue.Replacement.Inline) {
replacement =
issue.SourceLines[0].slice(0, issue.Replacement.Inline.StartCol) +
issue.Replacement.Inline.NewString +
issue.SourceLines[0].slice(issue.Replacement.Inline.StartCol + issue.Replacement.Inline.Length);
}
else if (issue.Replacement.NewLines) {
replacement = issue.Replacement.NewLines.join("\n");
}
annotation.raw_details = "```suggestion\n" + replacement + "\n```";
}
return annotation;
});
const title = `GolangCI-Lint`; const title = `GolangCI-Lint`;
const summary = `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.`; for (let i = 0; i < Math.ceil(issues.length / chunkSize); i++) {
Array.from({ length: Math.ceil(githubAnnotations.length / chunkSize) }, (v, i) => githubAnnotations.slice(i * chunkSize, i * chunkSize + chunkSize)).forEach((annotations) => {
octokit.checks octokit.checks
.update(Object.assign(Object.assign({}, ctx.repo), { check_run_id: checkRunId, output: { .update(Object.assign(Object.assign({}, ctx.repo), { check_run_id: checkRunId, output: {
title, title: title,
summary, annotations: issues.slice(i * chunkSize, i * chunkSize + chunkSize).map((issue) => {
annotations, // If/when we transition to comments, we would build the request structure here
const annotation = {
path: issue.Pos.Filename,
start_line: issue.Pos.Line,
end_line: issue.Pos.Line,
title: issue.FromLinter,
message: issue.Text,
annotation_level: issue.Severity,
};
issueCounts[issue.Severity]++;
if (issue.LineRange !== undefined) {
annotation.end_line = issue.LineRange.To;
}
else if (issue.Pos.Column) {
annotation.start_column = issue.Pos.Column;
annotation.end_column = issue.Pos.Column;
}
if (issue.Replacement !== null) {
let replacement = ``;
if (issue.Replacement.Inline) {
replacement =
issue.SourceLines[0].slice(0, issue.Replacement.Inline.StartCol) +
issue.Replacement.Inline.NewString +
issue.SourceLines[0].slice(issue.Replacement.Inline.StartCol + issue.Replacement.Inline.Length);
}
else if (issue.Replacement.NewLines) {
replacement = issue.Replacement.NewLines.join("\n");
}
annotation.raw_details = "```suggestion\n" + replacement + "\n```";
}
return annotation;
}),
summary: `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.`,
} })) } }))
.catch((e) => { .catch((e) => {
throw `Error patching Check Run Data (annotations): ${e}`; throw `Error patching Check Run Data (annotations): ${e}`;
}); });
}); }
}); });
} }
const hasFailingIssues = (issues) => {
// If the user input is not a valid Severity Level, this will be -1, and any issue will fail
const userFailureSeverity = core.getInput(`failure-severity`).toLowerCase();
let failureSeverity = DefaultFailureSeverity;
if (userFailureSeverity) {
failureSeverity = Object.values(LintSeverity).indexOf(userFailureSeverity);
}
if (failureSeverity < 0) {
core.info(`::warning::failure-severity must be one of (${Object.keys(LintSeverity).join(" | ")}). "${userFailureSeverity}" not supported, using default (${LintSeverity[DefaultFailureSeverity]})`);
failureSeverity = DefaultFailureSeverity;
}
if (issues.length) {
if (failureSeverity <= 0) {
return true;
}
for (const issue of issues) {
if (failureSeverity <= LintSeverity[issue.Severity]) {
return true;
}
}
}
return false;
};
const printOutput = (res) => { const printOutput = (res) => {
if (res.stdout) { if (res.stdout) {
core.info(res.stdout); core.info(res.stdout);
@ -6973,56 +6958,39 @@ const printOutput = (res) => {
core.info(res.stderr); core.info(res.stderr);
} }
}; };
function printLintOutput(res, checkRunId) { function processLintOutput(res, checkRunId) {
var _a;
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
let lintOutput; let lintIssues = [];
const exit_code = (_a = res.code) !== null && _a !== void 0 ? _a : 0; if (res.stdout) {
try { try {
if (res.stdout) { // This object contains other information, such as errors and the active linters
try { // TODO: Should we do something with that data?
// This object contains other information, such as errors and the active linters ;
// TODO: Should we do something with that data? ({ Issues: lintIssues } = parseOutput(res.stdout));
lintOutput = parseOutput(res.stdout); if (lintIssues.length) {
if (lintOutput.Issues.length) { logLintIssues(lintIssues);
logLintIssues(lintOutput.Issues); // We can only Annotate (or Comment) on Push or Pull Request
// We can only Annotate (or Comment) on Push or Pull Request switch (github.context.eventName) {
switch (github.context.eventName) { case `pull_request`:
case `pull_request`: // TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here
// TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here /* falls through */
/* falls through */ case `push`:
case `push`: yield annotateLintIssues(lintIssues, checkRunId);
yield annotateLintIssues(lintOutput.Issues, checkRunId); break;
break; default:
default: // At this time, other events are not supported
// At this time, other events are not supported break;
break;
}
} }
} }
catch (e) {
throw `there was an error processing golangci-lint output: ${e}`;
}
} }
if (res.stderr) { catch (e) {
core.info(res.stderr); core.setFailed(`Error processing golangci-lint output: ${e}`);
}
if (exit_code === 1) {
if (!lintOutput) {
throw `unexpected state, golangci-lint exited with 1, but provided no lint output`;
}
if (hasFailingIssues(lintOutput.Issues)) {
throw `issues found`;
}
}
else if (exit_code > 1) {
throw `golangci-lint exit with code ${exit_code}`;
} }
} }
catch (e) { if (res.stderr) {
return core.setFailed(`${e}`); core.info(res.stderr);
} }
return core.info(`golangci-lint found no blocking issues`); return lintIssues;
}); });
} }
function runLint(lintPath, patchPath, checkRunId) { function runLint(lintPath, patchPath, checkRunId) {
@ -7032,6 +7000,17 @@ function runLint(lintPath, patchPath, checkRunId) {
const res = yield execShellCommand(`${lintPath} cache status`); const res = yield execShellCommand(`${lintPath} cache status`);
printOutput(res); printOutput(res);
} }
const failureSeverity = ((userFailureSeverity) => {
if (userFailureSeverity) {
if (Object.values(LintSeverity).indexOf(userFailureSeverity) != -1) {
return Object.values(LintSeverity).indexOf(userFailureSeverity);
}
else {
core.info(`::warning::failure-severity must be one of (${Object.keys(LintSeverity).join(" | ")}). "${userFailureSeverity}" not supported, using default (${LintSeverity[DefaultFailureSeverity]})`);
}
}
return DefaultFailureSeverity;
})(core.getInput(`failure-severity`).toLowerCase());
const userArgs = core.getInput(`args`); const userArgs = core.getInput(`args`);
const addedArgs = []; const addedArgs = [];
const userArgNames = new Set(); const userArgNames = new Set();
@ -7073,14 +7052,29 @@ function runLint(lintPath, patchPath, checkRunId) {
const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimRight(); const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimRight();
core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`); core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`);
const startedAt = Date.now(); const startedAt = Date.now();
let exit_code = 0;
try { try {
const res = yield execShellCommand(cmd, cmdArgs); const res = yield execShellCommand(cmd, cmdArgs);
yield printLintOutput(res, checkRunId); processLintOutput(res, checkRunId);
} }
catch (exc) { catch (exc) {
// This logging passes issues to GitHub annotations but comments can be more convenient for some users. // This logging passes issues to GitHub annotations but comments can be more convenient for some users.
// TODO: support reviewdog or leaving comments by GitHub API. // TODO: support reviewdog or leaving comments by GitHub API.
yield printLintOutput(exc, checkRunId); const issuesPromise = processLintOutput(exc, checkRunId);
if (exc.code !== 1 || (yield issuesPromise).findIndex((issue) => LintSeverity[issue.Severity] >= failureSeverity) != -1) {
exit_code = exc.code;
}
}
finally {
if (exit_code === 0) {
core.info(`golangci-lint found no blocking issues`);
}
else if (exit_code === 1) {
core.setFailed(`issues found`);
}
else {
core.setFailed(`golangci-lint exit with code ${exit_code}`);
}
} }
core.info(`Ran golangci-lint in ${Date.now() - startedAt}ms`); core.info(`Ran golangci-lint in ${Date.now() - startedAt}ms`);
}); });

242
dist/run/index.js vendored
View file

@ -6808,8 +6808,9 @@ const parseOutput = (json) => {
} }
if (lintOutput.Issues.length) { if (lintOutput.Issues.length) {
lintOutput.Issues = lintOutput.Issues.filter((issue) => issue.Severity !== `ignore`).map((issue) => { lintOutput.Issues = lintOutput.Issues.filter((issue) => issue.Severity !== `ignore`).map((issue) => {
const Severity = issue.Severity.toLowerCase(); issue.Severity = ((Severity) => {
issue.Severity = severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure`; return severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure`;
})(issue.Severity.toLowerCase());
return issue; return issue;
}); });
} }
@ -6817,21 +6818,30 @@ const parseOutput = (json) => {
}; };
const logLintIssues = (issues) => { const logLintIssues = (issues) => {
issues.forEach((issue) => { issues.forEach((issue) => {
let header = `${ansi_styles_1.default.red.open}${ansi_styles_1.default.bold.open}Lint Error:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.red.close}`; core.info(((issue) => {
if (issue.Severity === `warning`) { switch (issue.Severity) {
header = `${ansi_styles_1.default.yellow.open}${ansi_styles_1.default.bold.open}Lint Warning:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.yellow.close}`; case `warning`:
} return `${ansi_styles_1.default.yellow.open}${ansi_styles_1.default.bold.open}Lint Warning:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.yellow.close}`;
else if (issue.Severity === `notice`) { case `notice`:
header = `${ansi_styles_1.default.cyan.open}${ansi_styles_1.default.bold.open}Lint Notice:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.cyan.close}`; return `${ansi_styles_1.default.cyan.open}${ansi_styles_1.default.bold.open}Lint Notice:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.cyan.close}`;
} default:
let pos = `${issue.Pos.Filename}:${issue.Pos.Line}`; return `${ansi_styles_1.default.red.open}${ansi_styles_1.default.bold.open}Lint Error:${ansi_styles_1.default.bold.close}${ansi_styles_1.default.red.close}`;
if (issue.LineRange !== undefined) { }
pos += `-${issue.LineRange.To}`; })(issue) +
} ` ` +
else if (issue.Pos.Column) { `${issue.Pos.Filename}:${issue.Pos.Line}` +
pos += `:${issue.Pos.Column}`; ((issue) => {
} if (issue.LineRange !== undefined) {
core.info(`${header} ${pos} - ${issue.Text} (${issue.FromLinter})`); return `-${issue.LineRange.To}`;
}
else if (issue.Pos.Column) {
return `:${issue.Pos.Column}`;
}
else {
return ``;
}
})(issue) +
` - ${issue.Text} (${issue.FromLinter})`);
}); });
}; };
function resolveCheckRunId() { function resolveCheckRunId() {
@ -6904,77 +6914,52 @@ function annotateLintIssues(issues, checkRunId) {
}; };
const ctx = github.context; const ctx = github.context;
const octokit = github.getOctokit(core.getInput(`github-token`, { required: true })); const octokit = github.getOctokit(core.getInput(`github-token`, { required: true }));
const githubAnnotations = issues.map((issue) => {
// If/when we transition to comments, we would build the request structure here
const annotation = {
path: issue.Pos.Filename,
start_line: issue.Pos.Line,
end_line: issue.Pos.Line,
title: issue.FromLinter,
message: issue.Text,
annotation_level: issue.Severity,
};
issueCounts[issue.Severity]++;
if (issue.LineRange !== undefined) {
annotation.end_line = issue.LineRange.To;
}
else if (issue.Pos.Column) {
annotation.start_column = issue.Pos.Column;
annotation.end_column = issue.Pos.Column;
}
if (issue.Replacement !== null) {
let replacement = ``;
if (issue.Replacement.Inline) {
replacement =
issue.SourceLines[0].slice(0, issue.Replacement.Inline.StartCol) +
issue.Replacement.Inline.NewString +
issue.SourceLines[0].slice(issue.Replacement.Inline.StartCol + issue.Replacement.Inline.Length);
}
else if (issue.Replacement.NewLines) {
replacement = issue.Replacement.NewLines.join("\n");
}
annotation.raw_details = "```suggestion\n" + replacement + "\n```";
}
return annotation;
});
const title = `GolangCI-Lint`; const title = `GolangCI-Lint`;
const summary = `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.`; for (let i = 0; i < Math.ceil(issues.length / chunkSize); i++) {
Array.from({ length: Math.ceil(githubAnnotations.length / chunkSize) }, (v, i) => githubAnnotations.slice(i * chunkSize, i * chunkSize + chunkSize)).forEach((annotations) => {
octokit.checks octokit.checks
.update(Object.assign(Object.assign({}, ctx.repo), { check_run_id: checkRunId, output: { .update(Object.assign(Object.assign({}, ctx.repo), { check_run_id: checkRunId, output: {
title, title: title,
summary, annotations: issues.slice(i * chunkSize, i * chunkSize + chunkSize).map((issue) => {
annotations, // If/when we transition to comments, we would build the request structure here
const annotation = {
path: issue.Pos.Filename,
start_line: issue.Pos.Line,
end_line: issue.Pos.Line,
title: issue.FromLinter,
message: issue.Text,
annotation_level: issue.Severity,
};
issueCounts[issue.Severity]++;
if (issue.LineRange !== undefined) {
annotation.end_line = issue.LineRange.To;
}
else if (issue.Pos.Column) {
annotation.start_column = issue.Pos.Column;
annotation.end_column = issue.Pos.Column;
}
if (issue.Replacement !== null) {
let replacement = ``;
if (issue.Replacement.Inline) {
replacement =
issue.SourceLines[0].slice(0, issue.Replacement.Inline.StartCol) +
issue.Replacement.Inline.NewString +
issue.SourceLines[0].slice(issue.Replacement.Inline.StartCol + issue.Replacement.Inline.Length);
}
else if (issue.Replacement.NewLines) {
replacement = issue.Replacement.NewLines.join("\n");
}
annotation.raw_details = "```suggestion\n" + replacement + "\n```";
}
return annotation;
}),
summary: `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.`,
} })) } }))
.catch((e) => { .catch((e) => {
throw `Error patching Check Run Data (annotations): ${e}`; throw `Error patching Check Run Data (annotations): ${e}`;
}); });
}); }
}); });
} }
const hasFailingIssues = (issues) => {
// If the user input is not a valid Severity Level, this will be -1, and any issue will fail
const userFailureSeverity = core.getInput(`failure-severity`).toLowerCase();
let failureSeverity = DefaultFailureSeverity;
if (userFailureSeverity) {
failureSeverity = Object.values(LintSeverity).indexOf(userFailureSeverity);
}
if (failureSeverity < 0) {
core.info(`::warning::failure-severity must be one of (${Object.keys(LintSeverity).join(" | ")}). "${userFailureSeverity}" not supported, using default (${LintSeverity[DefaultFailureSeverity]})`);
failureSeverity = DefaultFailureSeverity;
}
if (issues.length) {
if (failureSeverity <= 0) {
return true;
}
for (const issue of issues) {
if (failureSeverity <= LintSeverity[issue.Severity]) {
return true;
}
}
}
return false;
};
const printOutput = (res) => { const printOutput = (res) => {
if (res.stdout) { if (res.stdout) {
core.info(res.stdout); core.info(res.stdout);
@ -6983,56 +6968,39 @@ const printOutput = (res) => {
core.info(res.stderr); core.info(res.stderr);
} }
}; };
function printLintOutput(res, checkRunId) { function processLintOutput(res, checkRunId) {
var _a;
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
let lintOutput; let lintIssues = [];
const exit_code = (_a = res.code) !== null && _a !== void 0 ? _a : 0; if (res.stdout) {
try { try {
if (res.stdout) { // This object contains other information, such as errors and the active linters
try { // TODO: Should we do something with that data?
// This object contains other information, such as errors and the active linters ;
// TODO: Should we do something with that data? ({ Issues: lintIssues } = parseOutput(res.stdout));
lintOutput = parseOutput(res.stdout); if (lintIssues.length) {
if (lintOutput.Issues.length) { logLintIssues(lintIssues);
logLintIssues(lintOutput.Issues); // We can only Annotate (or Comment) on Push or Pull Request
// We can only Annotate (or Comment) on Push or Pull Request switch (github.context.eventName) {
switch (github.context.eventName) { case `pull_request`:
case `pull_request`: // TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here
// TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here /* falls through */
/* falls through */ case `push`:
case `push`: yield annotateLintIssues(lintIssues, checkRunId);
yield annotateLintIssues(lintOutput.Issues, checkRunId); break;
break; default:
default: // At this time, other events are not supported
// At this time, other events are not supported break;
break;
}
} }
} }
catch (e) {
throw `there was an error processing golangci-lint output: ${e}`;
}
} }
if (res.stderr) { catch (e) {
core.info(res.stderr); core.setFailed(`Error processing golangci-lint output: ${e}`);
}
if (exit_code === 1) {
if (!lintOutput) {
throw `unexpected state, golangci-lint exited with 1, but provided no lint output`;
}
if (hasFailingIssues(lintOutput.Issues)) {
throw `issues found`;
}
}
else if (exit_code > 1) {
throw `golangci-lint exit with code ${exit_code}`;
} }
} }
catch (e) { if (res.stderr) {
return core.setFailed(`${e}`); core.info(res.stderr);
} }
return core.info(`golangci-lint found no blocking issues`); return lintIssues;
}); });
} }
function runLint(lintPath, patchPath, checkRunId) { function runLint(lintPath, patchPath, checkRunId) {
@ -7042,6 +7010,17 @@ function runLint(lintPath, patchPath, checkRunId) {
const res = yield execShellCommand(`${lintPath} cache status`); const res = yield execShellCommand(`${lintPath} cache status`);
printOutput(res); printOutput(res);
} }
const failureSeverity = ((userFailureSeverity) => {
if (userFailureSeverity) {
if (Object.values(LintSeverity).indexOf(userFailureSeverity) != -1) {
return Object.values(LintSeverity).indexOf(userFailureSeverity);
}
else {
core.info(`::warning::failure-severity must be one of (${Object.keys(LintSeverity).join(" | ")}). "${userFailureSeverity}" not supported, using default (${LintSeverity[DefaultFailureSeverity]})`);
}
}
return DefaultFailureSeverity;
})(core.getInput(`failure-severity`).toLowerCase());
const userArgs = core.getInput(`args`); const userArgs = core.getInput(`args`);
const addedArgs = []; const addedArgs = [];
const userArgNames = new Set(); const userArgNames = new Set();
@ -7083,14 +7062,29 @@ function runLint(lintPath, patchPath, checkRunId) {
const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimRight(); const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimRight();
core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`); core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`);
const startedAt = Date.now(); const startedAt = Date.now();
let exit_code = 0;
try { try {
const res = yield execShellCommand(cmd, cmdArgs); const res = yield execShellCommand(cmd, cmdArgs);
yield printLintOutput(res, checkRunId); processLintOutput(res, checkRunId);
} }
catch (exc) { catch (exc) {
// This logging passes issues to GitHub annotations but comments can be more convenient for some users. // This logging passes issues to GitHub annotations but comments can be more convenient for some users.
// TODO: support reviewdog or leaving comments by GitHub API. // TODO: support reviewdog or leaving comments by GitHub API.
yield printLintOutput(exc, checkRunId); const issuesPromise = processLintOutput(exc, checkRunId);
if (exc.code !== 1 || (yield issuesPromise).findIndex((issue) => LintSeverity[issue.Severity] >= failureSeverity) != -1) {
exit_code = exc.code;
}
}
finally {
if (exit_code === 0) {
core.info(`golangci-lint found no blocking issues`);
}
else if (exit_code === 1) {
core.setFailed(`issues found`);
}
else {
core.setFailed(`golangci-lint exit with code ${exit_code}`);
}
} }
core.info(`Ran golangci-lint in ${Date.now() - startedAt}ms`); core.info(`Ran golangci-lint in ${Date.now() - startedAt}ms`);
}); });

View file

@ -107,7 +107,6 @@ async function prepareEnv(): Promise<Env> {
type ExecRes = { type ExecRes = {
stdout: string stdout: string
stderr: string stderr: string
code?: number
} }
enum LintSeverity { enum LintSeverity {
@ -226,8 +225,9 @@ const parseOutput = (json: string): LintOutput => {
if (lintOutput.Issues.length) { if (lintOutput.Issues.length) {
lintOutput.Issues = lintOutput.Issues.filter((issue: UnfilteredLintIssue) => issue.Severity !== `ignore`).map( lintOutput.Issues = lintOutput.Issues.filter((issue: UnfilteredLintIssue) => issue.Severity !== `ignore`).map(
(issue: UnfilteredLintIssue): LintIssue => { (issue: UnfilteredLintIssue): LintIssue => {
const Severity = issue.Severity.toLowerCase() issue.Severity = ((Severity: string): LintSeverityStrings => {
issue.Severity = severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure` return severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure`
})(issue.Severity.toLowerCase())
return issue as LintIssue return issue as LintIssue
} }
) )
@ -237,21 +237,30 @@ const parseOutput = (json: string): LintOutput => {
const logLintIssues = (issues: LintIssue[]): void => { const logLintIssues = (issues: LintIssue[]): void => {
issues.forEach((issue: LintIssue): void => { issues.forEach((issue: LintIssue): void => {
let header = `${style.red.open}${style.bold.open}Lint Error:${style.bold.close}${style.red.close}` core.info(
if (issue.Severity === `warning`) { ((issue: LintIssue): string => {
header = `${style.yellow.open}${style.bold.open}Lint Warning:${style.bold.close}${style.yellow.close}` switch (issue.Severity) {
} else if (issue.Severity === `notice`) { case `warning`:
header = `${style.cyan.open}${style.bold.open}Lint Notice:${style.bold.close}${style.cyan.close}` return `${style.yellow.open}${style.bold.open}Lint Warning:${style.bold.close}${style.yellow.close}`
} case `notice`:
return `${style.cyan.open}${style.bold.open}Lint Notice:${style.bold.close}${style.cyan.close}`
let pos = `${issue.Pos.Filename}:${issue.Pos.Line}` default:
if (issue.LineRange !== undefined) { return `${style.red.open}${style.bold.open}Lint Error:${style.bold.close}${style.red.close}`
pos += `-${issue.LineRange.To}` }
} else if (issue.Pos.Column) { })(issue) +
pos += `:${issue.Pos.Column}` ` ` +
} `${issue.Pos.Filename}:${issue.Pos.Line}` +
((issue: LintIssue): string => {
core.info(`${header} ${pos} - ${issue.Text} (${issue.FromLinter})`) if (issue.LineRange !== undefined) {
return `-${issue.LineRange.To}`
} else if (issue.Pos.Column) {
return `:${issue.Pos.Column}`
} else {
return ``
}
})(issue) +
` - ${issue.Text} (${issue.FromLinter})`
)
}) })
} }
@ -324,101 +333,66 @@ async function annotateLintIssues(issues: LintIssue[], checkRunId: number): Prom
if (checkRunId === -1 || !issues.length) { if (checkRunId === -1 || !issues.length) {
return return
} }
const chunkSize = 50 const chunkSize = 50
const issueCounts = { const issueCounts = {
notice: 0, notice: 0,
warning: 0, warning: 0,
failure: 0, failure: 0,
} }
const ctx = github.context const ctx = github.context
const octokit = github.getOctokit(core.getInput(`github-token`, { required: true })) const octokit = github.getOctokit(core.getInput(`github-token`, { required: true }))
const githubAnnotations: GithubAnnotation[] = issues.map(
(issue: LintIssue): GithubAnnotation => {
// If/when we transition to comments, we would build the request structure here
const annotation: GithubAnnotation = {
path: issue.Pos.Filename,
start_line: issue.Pos.Line,
end_line: issue.Pos.Line,
title: issue.FromLinter,
message: issue.Text,
annotation_level: issue.Severity,
}
issueCounts[issue.Severity]++
if (issue.LineRange !== undefined) {
annotation.end_line = issue.LineRange.To
} else if (issue.Pos.Column) {
annotation.start_column = issue.Pos.Column
annotation.end_column = issue.Pos.Column
}
if (issue.Replacement !== null) {
let replacement = ``
if (issue.Replacement.Inline) {
replacement =
issue.SourceLines[0].slice(0, issue.Replacement.Inline.StartCol) +
issue.Replacement.Inline.NewString +
issue.SourceLines[0].slice(issue.Replacement.Inline.StartCol + issue.Replacement.Inline.Length)
} else if (issue.Replacement.NewLines) {
replacement = issue.Replacement.NewLines.join("\n")
}
annotation.raw_details = "```suggestion\n" + replacement + "\n```"
}
return annotation as GithubAnnotation
}
)
const title = `GolangCI-Lint` const title = `GolangCI-Lint`
const summary = `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.` for (let i = 0; i < Math.ceil(issues.length / chunkSize); i++) {
Array.from({ length: Math.ceil(githubAnnotations.length / chunkSize) }, (v, i) =>
githubAnnotations.slice(i * chunkSize, i * chunkSize + chunkSize)
).forEach((annotations: GithubAnnotation[]): void => {
octokit.checks octokit.checks
.update({ .update({
...ctx.repo, ...ctx.repo,
check_run_id: checkRunId, check_run_id: checkRunId,
output: { output: {
title, title: title,
summary, annotations: issues.slice(i * chunkSize, i * chunkSize + chunkSize).map(
annotations, (issue: LintIssue): GithubAnnotation => {
// If/when we transition to comments, we would build the request structure here
const annotation: GithubAnnotation = {
path: issue.Pos.Filename,
start_line: issue.Pos.Line,
end_line: issue.Pos.Line,
title: issue.FromLinter,
message: issue.Text,
annotation_level: issue.Severity,
}
issueCounts[issue.Severity]++
if (issue.LineRange !== undefined) {
annotation.end_line = issue.LineRange.To
} else if (issue.Pos.Column) {
annotation.start_column = issue.Pos.Column
annotation.end_column = issue.Pos.Column
}
if (issue.Replacement !== null) {
let replacement = ``
if (issue.Replacement.Inline) {
replacement =
issue.SourceLines[0].slice(0, issue.Replacement.Inline.StartCol) +
issue.Replacement.Inline.NewString +
issue.SourceLines[0].slice(issue.Replacement.Inline.StartCol + issue.Replacement.Inline.Length)
} else if (issue.Replacement.NewLines) {
replacement = issue.Replacement.NewLines.join("\n")
}
annotation.raw_details = "```suggestion\n" + replacement + "\n```"
}
return annotation as GithubAnnotation
}
),
summary: `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.`,
}, },
}) })
.catch((e) => { .catch((e) => {
throw `Error patching Check Run Data (annotations): ${e}` throw `Error patching Check Run Data (annotations): ${e}`
}) })
})
}
const hasFailingIssues = (issues: LintIssue[]): boolean => {
// If the user input is not a valid Severity Level, this will be -1, and any issue will fail
const userFailureSeverity = core.getInput(`failure-severity`).toLowerCase()
let failureSeverity = DefaultFailureSeverity
if (userFailureSeverity) {
failureSeverity = Object.values(LintSeverity).indexOf(userFailureSeverity)
} }
if (failureSeverity < 0) {
core.info(
`::warning::failure-severity must be one of (${Object.keys(LintSeverity).join(
" | "
)}). "${userFailureSeverity}" not supported, using default (${LintSeverity[DefaultFailureSeverity]})`
)
failureSeverity = DefaultFailureSeverity
}
if (issues.length) {
if (failureSeverity <= 0) {
return true
}
for (const issue of issues) {
if (failureSeverity <= LintSeverity[issue.Severity]) {
return true
}
}
}
return false
} }
const printOutput = (res: ExecRes): void => { const printOutput = (res: ExecRes): void => {
@ -430,55 +404,40 @@ const printOutput = (res: ExecRes): void => {
} }
} }
async function printLintOutput(res: ExecRes, checkRunId: number): Promise<void> { async function processLintOutput(res: ExecRes, checkRunId: number): Promise<LintIssue[]> {
let lintOutput: LintOutput | undefined let lintIssues: LintIssue[] = []
const exit_code = res.code ?? 0 if (res.stdout) {
try { try {
if (res.stdout) { // This object contains other information, such as errors and the active linters
try { // TODO: Should we do something with that data?
// This object contains other information, such as errors and the active linters ;({ Issues: lintIssues } = parseOutput(res.stdout))
// TODO: Should we do something with that data?
lintOutput = parseOutput(res.stdout)
if (lintOutput.Issues.length) { if (lintIssues.length) {
logLintIssues(lintOutput.Issues) logLintIssues(lintIssues)
// We can only Annotate (or Comment) on Push or Pull Request // We can only Annotate (or Comment) on Push or Pull Request
switch (github.context.eventName) { switch (github.context.eventName) {
case `pull_request`: case `pull_request`:
// TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here // TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here
/* falls through */ /* falls through */
case `push`: case `push`:
await annotateLintIssues(lintOutput.Issues, checkRunId) await annotateLintIssues(lintIssues, checkRunId)
break break
default: default:
// At this time, other events are not supported // At this time, other events are not supported
break break
}
} }
} catch (e) {
throw `there was an error processing golangci-lint output: ${e}`
} }
} catch (e) {
core.setFailed(`Error processing golangci-lint output: ${e}`)
} }
if (res.stderr) {
core.info(res.stderr)
}
if (exit_code === 1) {
if (!lintOutput) {
throw `unexpected state, golangci-lint exited with 1, but provided no lint output`
}
if (hasFailingIssues(lintOutput.Issues)) {
throw `issues found`
}
} else if (exit_code > 1) {
throw `golangci-lint exit with code ${exit_code}`
}
} catch (e) {
return <void>core.setFailed(`${e}`)
} }
return <void>core.info(`golangci-lint found no blocking issues`)
if (res.stderr) {
core.info(res.stderr)
}
return lintIssues
} }
async function runLint(lintPath: string, patchPath: string, checkRunId: number): Promise<void> { async function runLint(lintPath: string, patchPath: string, checkRunId: number): Promise<void> {
@ -488,6 +447,21 @@ async function runLint(lintPath: string, patchPath: string, checkRunId: number):
printOutput(res) printOutput(res)
} }
const failureSeverity = ((userFailureSeverity: string): LintSeverity => {
if (userFailureSeverity) {
if (Object.values(LintSeverity).indexOf(userFailureSeverity) != -1) {
return Object.values(LintSeverity).indexOf(userFailureSeverity)
} else {
core.info(
`::warning::failure-severity must be one of (${Object.keys(LintSeverity).join(
" | "
)}). "${userFailureSeverity}" not supported, using default (${LintSeverity[DefaultFailureSeverity]})`
)
}
}
return DefaultFailureSeverity
})(core.getInput(`failure-severity`).toLowerCase())
const userArgs = core.getInput(`args`) const userArgs = core.getInput(`args`)
const addedArgs: string[] = [] const addedArgs: string[] = []
@ -535,13 +509,25 @@ async function runLint(lintPath: string, patchPath: string, checkRunId: number):
const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimRight() const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimRight()
core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`) core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`)
const startedAt = Date.now() const startedAt = Date.now()
let exit_code = 0
try { try {
const res = await execShellCommand(cmd, cmdArgs) const res = await execShellCommand(cmd, cmdArgs)
await printLintOutput(res, checkRunId) processLintOutput(res, checkRunId)
} catch (exc) { } catch (exc) {
// This logging passes issues to GitHub annotations but comments can be more convenient for some users. // This logging passes issues to GitHub annotations but comments can be more convenient for some users.
// TODO: support reviewdog or leaving comments by GitHub API. // TODO: support reviewdog or leaving comments by GitHub API.
await printLintOutput(exc, checkRunId) const issuesPromise = processLintOutput(exc, checkRunId)
if (exc.code !== 1 || (await issuesPromise).findIndex((issue: LintIssue) => LintSeverity[issue.Severity] >= failureSeverity) != -1) {
exit_code = exc.code
}
} finally {
if (exit_code === 0) {
core.info(`golangci-lint found no blocking issues`)
} else if (exit_code === 1) {
core.setFailed(`issues found`)
} else {
core.setFailed(`golangci-lint exit with code ${exit_code}`)
}
} }
core.info(`Ran golangci-lint in ${Date.now() - startedAt}ms`) core.info(`Ran golangci-lint in ${Date.now() - startedAt}ms`)