From 61a0ce84ec58e12ceb69decc3bd2ff8476336548 Mon Sep 17 00:00:00 2001 From: Michael J Mulligan Date: Tue, 6 Apr 2021 20:38:59 +0100 Subject: [PATCH] Memory management and let runLint handle resolving exit code --- dist/post_run/index.js | 242 +++++++++++++++++++------------------ dist/run/index.js | 242 +++++++++++++++++++------------------ src/run.ts | 262 +++++++++++++++++++---------------------- 3 files changed, 360 insertions(+), 386 deletions(-) diff --git a/dist/post_run/index.js b/dist/post_run/index.js index 4109431..87b3425 100644 --- a/dist/post_run/index.js +++ b/dist/post_run/index.js @@ -6798,8 +6798,9 @@ const parseOutput = (json) => { } if (lintOutput.Issues.length) { lintOutput.Issues = lintOutput.Issues.filter((issue) => issue.Severity !== `ignore`).map((issue) => { - const Severity = issue.Severity.toLowerCase(); - issue.Severity = severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure`; + issue.Severity = ((Severity) => { + return severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure`; + })(issue.Severity.toLowerCase()); return issue; }); } @@ -6807,21 +6808,30 @@ const parseOutput = (json) => { }; const logLintIssues = (issues) => { 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}`; - if (issue.Severity === `warning`) { - 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}`; - } - else if (issue.Severity === `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}`; - } - let pos = `${issue.Pos.Filename}:${issue.Pos.Line}`; - if (issue.LineRange !== undefined) { - pos += `-${issue.LineRange.To}`; - } - else if (issue.Pos.Column) { - pos += `:${issue.Pos.Column}`; - } - core.info(`${header} ${pos} - ${issue.Text} (${issue.FromLinter})`); + core.info(((issue) => { + switch (issue.Severity) { + 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}`; + case `notice`: + 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: + 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}`; + } + })(issue) + + ` ` + + `${issue.Pos.Filename}:${issue.Pos.Line}` + + ((issue) => { + if (issue.LineRange !== undefined) { + return `-${issue.LineRange.To}`; + } + else if (issue.Pos.Column) { + return `:${issue.Pos.Column}`; + } + else { + return ``; + } + })(issue) + + ` - ${issue.Text} (${issue.FromLinter})`); }); }; function resolveCheckRunId() { @@ -6894,77 +6904,52 @@ function annotateLintIssues(issues, checkRunId) { }; const ctx = github.context; 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 summary = `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.`; - Array.from({ length: Math.ceil(githubAnnotations.length / chunkSize) }, (v, i) => githubAnnotations.slice(i * chunkSize, i * chunkSize + chunkSize)).forEach((annotations) => { + for (let i = 0; i < Math.ceil(issues.length / chunkSize); i++) { octokit.checks .update(Object.assign(Object.assign({}, ctx.repo), { check_run_id: checkRunId, output: { - title, - summary, - annotations, + title: title, + annotations: issues.slice(i * chunkSize, i * chunkSize + chunkSize).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; + }), + summary: `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.`, } })) .catch((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) => { if (res.stdout) { core.info(res.stdout); @@ -6973,56 +6958,39 @@ const printOutput = (res) => { core.info(res.stderr); } }; -function printLintOutput(res, checkRunId) { - var _a; +function processLintOutput(res, checkRunId) { return __awaiter(this, void 0, void 0, function* () { - let lintOutput; - const exit_code = (_a = res.code) !== null && _a !== void 0 ? _a : 0; - try { - if (res.stdout) { - try { - // This object contains other information, such as errors and the active linters - // TODO: Should we do something with that data? - lintOutput = parseOutput(res.stdout); - if (lintOutput.Issues.length) { - logLintIssues(lintOutput.Issues); - // We can only Annotate (or Comment) on Push or Pull Request - switch (github.context.eventName) { - case `pull_request`: - // TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here - /* falls through */ - case `push`: - yield annotateLintIssues(lintOutput.Issues, checkRunId); - break; - default: - // At this time, other events are not supported - break; - } + let lintIssues = []; + if (res.stdout) { + try { + // 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)); + if (lintIssues.length) { + logLintIssues(lintIssues); + // We can only Annotate (or Comment) on Push or Pull Request + switch (github.context.eventName) { + case `pull_request`: + // TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here + /* falls through */ + case `push`: + yield annotateLintIssues(lintIssues, checkRunId); + break; + default: + // At this time, other events are not supported + break; } } - catch (e) { - throw `there was an 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) { + core.setFailed(`Error processing golangci-lint output: ${e}`); } } - catch (e) { - return core.setFailed(`${e}`); + if (res.stderr) { + core.info(res.stderr); } - return core.info(`golangci-lint found no blocking issues`); + return lintIssues; }); } function runLint(lintPath, patchPath, checkRunId) { @@ -7032,6 +7000,17 @@ function runLint(lintPath, patchPath, checkRunId) { const res = yield execShellCommand(`${lintPath} cache status`); 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 addedArgs = []; const userArgNames = new Set(); @@ -7073,14 +7052,29 @@ function runLint(lintPath, patchPath, checkRunId) { const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimRight(); core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`); const startedAt = Date.now(); + let exit_code = 0; try { const res = yield execShellCommand(cmd, cmdArgs); - yield printLintOutput(res, checkRunId); + processLintOutput(res, checkRunId); } catch (exc) { // 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. - 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`); }); diff --git a/dist/run/index.js b/dist/run/index.js index 461a8d9..2ec3e37 100644 --- a/dist/run/index.js +++ b/dist/run/index.js @@ -6808,8 +6808,9 @@ const parseOutput = (json) => { } if (lintOutput.Issues.length) { lintOutput.Issues = lintOutput.Issues.filter((issue) => issue.Severity !== `ignore`).map((issue) => { - const Severity = issue.Severity.toLowerCase(); - issue.Severity = severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure`; + issue.Severity = ((Severity) => { + return severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure`; + })(issue.Severity.toLowerCase()); return issue; }); } @@ -6817,21 +6818,30 @@ const parseOutput = (json) => { }; const logLintIssues = (issues) => { 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}`; - if (issue.Severity === `warning`) { - 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}`; - } - else if (issue.Severity === `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}`; - } - let pos = `${issue.Pos.Filename}:${issue.Pos.Line}`; - if (issue.LineRange !== undefined) { - pos += `-${issue.LineRange.To}`; - } - else if (issue.Pos.Column) { - pos += `:${issue.Pos.Column}`; - } - core.info(`${header} ${pos} - ${issue.Text} (${issue.FromLinter})`); + core.info(((issue) => { + switch (issue.Severity) { + 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}`; + case `notice`: + 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: + 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}`; + } + })(issue) + + ` ` + + `${issue.Pos.Filename}:${issue.Pos.Line}` + + ((issue) => { + if (issue.LineRange !== undefined) { + return `-${issue.LineRange.To}`; + } + else if (issue.Pos.Column) { + return `:${issue.Pos.Column}`; + } + else { + return ``; + } + })(issue) + + ` - ${issue.Text} (${issue.FromLinter})`); }); }; function resolveCheckRunId() { @@ -6904,77 +6914,52 @@ function annotateLintIssues(issues, checkRunId) { }; const ctx = github.context; 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 summary = `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.`; - Array.from({ length: Math.ceil(githubAnnotations.length / chunkSize) }, (v, i) => githubAnnotations.slice(i * chunkSize, i * chunkSize + chunkSize)).forEach((annotations) => { + for (let i = 0; i < Math.ceil(issues.length / chunkSize); i++) { octokit.checks .update(Object.assign(Object.assign({}, ctx.repo), { check_run_id: checkRunId, output: { - title, - summary, - annotations, + title: title, + annotations: issues.slice(i * chunkSize, i * chunkSize + chunkSize).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; + }), + summary: `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.`, } })) .catch((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) => { if (res.stdout) { core.info(res.stdout); @@ -6983,56 +6968,39 @@ const printOutput = (res) => { core.info(res.stderr); } }; -function printLintOutput(res, checkRunId) { - var _a; +function processLintOutput(res, checkRunId) { return __awaiter(this, void 0, void 0, function* () { - let lintOutput; - const exit_code = (_a = res.code) !== null && _a !== void 0 ? _a : 0; - try { - if (res.stdout) { - try { - // This object contains other information, such as errors and the active linters - // TODO: Should we do something with that data? - lintOutput = parseOutput(res.stdout); - if (lintOutput.Issues.length) { - logLintIssues(lintOutput.Issues); - // We can only Annotate (or Comment) on Push or Pull Request - switch (github.context.eventName) { - case `pull_request`: - // TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here - /* falls through */ - case `push`: - yield annotateLintIssues(lintOutput.Issues, checkRunId); - break; - default: - // At this time, other events are not supported - break; - } + let lintIssues = []; + if (res.stdout) { + try { + // 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)); + if (lintIssues.length) { + logLintIssues(lintIssues); + // We can only Annotate (or Comment) on Push or Pull Request + switch (github.context.eventName) { + case `pull_request`: + // TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here + /* falls through */ + case `push`: + yield annotateLintIssues(lintIssues, checkRunId); + break; + default: + // At this time, other events are not supported + break; } } - catch (e) { - throw `there was an 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) { + core.setFailed(`Error processing golangci-lint output: ${e}`); } } - catch (e) { - return core.setFailed(`${e}`); + if (res.stderr) { + core.info(res.stderr); } - return core.info(`golangci-lint found no blocking issues`); + return lintIssues; }); } function runLint(lintPath, patchPath, checkRunId) { @@ -7042,6 +7010,17 @@ function runLint(lintPath, patchPath, checkRunId) { const res = yield execShellCommand(`${lintPath} cache status`); 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 addedArgs = []; const userArgNames = new Set(); @@ -7083,14 +7062,29 @@ function runLint(lintPath, patchPath, checkRunId) { const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimRight(); core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`); const startedAt = Date.now(); + let exit_code = 0; try { const res = yield execShellCommand(cmd, cmdArgs); - yield printLintOutput(res, checkRunId); + processLintOutput(res, checkRunId); } catch (exc) { // 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. - 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`); }); diff --git a/src/run.ts b/src/run.ts index 7fa065c..5875638 100644 --- a/src/run.ts +++ b/src/run.ts @@ -107,7 +107,6 @@ async function prepareEnv(): Promise { type ExecRes = { stdout: string stderr: string - code?: number } enum LintSeverity { @@ -226,8 +225,9 @@ const parseOutput = (json: string): LintOutput => { if (lintOutput.Issues.length) { lintOutput.Issues = lintOutput.Issues.filter((issue: UnfilteredLintIssue) => issue.Severity !== `ignore`).map( (issue: UnfilteredLintIssue): LintIssue => { - const Severity = issue.Severity.toLowerCase() - issue.Severity = severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure` + issue.Severity = ((Severity: string): LintSeverityStrings => { + return severityMap[`${Severity}`] ? severityMap[`${Severity}`] : `failure` + })(issue.Severity.toLowerCase()) return issue as LintIssue } ) @@ -237,21 +237,30 @@ const parseOutput = (json: string): LintOutput => { const logLintIssues = (issues: LintIssue[]): void => { issues.forEach((issue: LintIssue): void => { - let header = `${style.red.open}${style.bold.open}Lint Error:${style.bold.close}${style.red.close}` - if (issue.Severity === `warning`) { - header = `${style.yellow.open}${style.bold.open}Lint Warning:${style.bold.close}${style.yellow.close}` - } else if (issue.Severity === `notice`) { - header = `${style.cyan.open}${style.bold.open}Lint Notice:${style.bold.close}${style.cyan.close}` - } - - let pos = `${issue.Pos.Filename}:${issue.Pos.Line}` - if (issue.LineRange !== undefined) { - pos += `-${issue.LineRange.To}` - } else if (issue.Pos.Column) { - pos += `:${issue.Pos.Column}` - } - - core.info(`${header} ${pos} - ${issue.Text} (${issue.FromLinter})`) + core.info( + ((issue: LintIssue): string => { + switch (issue.Severity) { + case `warning`: + 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}` + default: + return `${style.red.open}${style.bold.open}Lint Error:${style.bold.close}${style.red.close}` + } + })(issue) + + ` ` + + `${issue.Pos.Filename}:${issue.Pos.Line}` + + ((issue: LintIssue): string => { + 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) { return } - const chunkSize = 50 const issueCounts = { notice: 0, warning: 0, failure: 0, } - const ctx = github.context 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 summary = `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.` - Array.from({ length: Math.ceil(githubAnnotations.length / chunkSize) }, (v, i) => - githubAnnotations.slice(i * chunkSize, i * chunkSize + chunkSize) - ).forEach((annotations: GithubAnnotation[]): void => { + for (let i = 0; i < Math.ceil(issues.length / chunkSize); i++) { octokit.checks .update({ ...ctx.repo, check_run_id: checkRunId, output: { - title, - summary, - annotations, + title: title, + annotations: issues.slice(i * chunkSize, i * chunkSize + chunkSize).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 + } + ), + summary: `There are {issueCounts.failure} failures, {issueCounts.wairning} warnings, and {issueCounts.notice} notices.`, }, }) .catch((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 => { @@ -430,55 +404,40 @@ const printOutput = (res: ExecRes): void => { } } -async function printLintOutput(res: ExecRes, checkRunId: number): Promise { - let lintOutput: LintOutput | undefined - const exit_code = res.code ?? 0 - try { - if (res.stdout) { - try { - // This object contains other information, such as errors and the active linters - // TODO: Should we do something with that data? - lintOutput = parseOutput(res.stdout) +async function processLintOutput(res: ExecRes, checkRunId: number): Promise { + let lintIssues: LintIssue[] = [] + if (res.stdout) { + try { + // 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)) - if (lintOutput.Issues.length) { - logLintIssues(lintOutput.Issues) + if (lintIssues.length) { + logLintIssues(lintIssues) - // We can only Annotate (or Comment) on Push or Pull Request - switch (github.context.eventName) { - case `pull_request`: - // TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here - /* falls through */ - case `push`: - await annotateLintIssues(lintOutput.Issues, checkRunId) - break - default: - // At this time, other events are not supported - break - } + // We can only Annotate (or Comment) on Push or Pull Request + switch (github.context.eventName) { + case `pull_request`: + // TODO: When we are ready to handle these as Comments, instead of Annotations, we would place that logic here + /* falls through */ + case `push`: + await annotateLintIssues(lintIssues, checkRunId) + break + default: + // At this time, other events are not supported + 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 core.setFailed(`${e}`) } - return 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 { @@ -488,6 +447,21 @@ async function runLint(lintPath: string, patchPath: string, checkRunId: number): 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 addedArgs: string[] = [] @@ -535,13 +509,25 @@ async function runLint(lintPath: string, patchPath: string, checkRunId: number): const cmd = `${lintPath} run ${addedArgs.join(` `)} ${userArgs}`.trimRight() core.info(`Running [${cmd}] in [${cmdArgs.cwd || ``}] ...`) const startedAt = Date.now() + let exit_code = 0 try { const res = await execShellCommand(cmd, cmdArgs) - await printLintOutput(res, checkRunId) + processLintOutput(res, checkRunId) } catch (exc) { // 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. - 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`)