diff --git a/README.md b/README.md index 2ea7aa89..3213ff87 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,14 @@ You can see more info about GitHub's default token [here](https://docs.github.co default: `${{ github.token }}` +## Outputs + +### `results` + +The error and warning messages for each one of the analyzed commits. This is useful if you want to use the commitlint results in a JSON format in other jobs. See [the documentation](https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#fromjson) on how to read JSON information from outputs. + +Below you can see an example text output together with its corresponding JSON output: + ``` You have commit messages with errors diff --git a/src/action.js b/src/action.js index e8dcaa81..226633e3 100644 --- a/src/action.js +++ b/src/action.js @@ -7,6 +7,7 @@ const lint = require('@commitlint/lint').default const { format } = require('@commitlint/format') const load = require('@commitlint/load').default const gitCommits = require('./gitCommits') +const generateOutputs = require('./generateOutputs') const pullRequestEvent = 'pull_request' @@ -108,9 +109,7 @@ const hasOnlyWarnings = lintedCommits => lintedCommits.some(({ lintResult }) => lintResult.warnings.length) const setFailed = formattedResults => { - process.exitCode = 1 - - console.error(`You have commit messages with errors\n\n${formattedResults}`) + core.setFailed(`You have commit messages with errors\n\n${formattedResults}`) } const handleOnlyWarnings = formattedResults => { @@ -135,9 +134,7 @@ const showLintResults = async ([from, to]) => { ) const formattedResults = formatErrors(lintedCommits) - // disable workflow commands - const token = uuidv4() - console.log(`::stop-commands::${token}`) + generateOutputs(lintedCommits) if (hasOnlyWarnings(lintedCommits)) { handleOnlyWarnings(formattedResults) @@ -146,15 +143,10 @@ const showLintResults = async ([from, to]) => { } else { console.log('Lint free! 🎉') } - - // enable workflow commands - console.log(`::${token}::`) } const exitWithMessage = message => error => { - process.exitCode = 1 - - console.error(`${message}\n${error.message}\n${error.stack}`) + core.setFailed(`${message}\n${error.message}\n${error.stack}`) } const commitLinterAction = () => diff --git a/src/action.test.js b/src/action.test.js index 22ed7aa4..7810dd70 100644 --- a/src/action.test.js +++ b/src/action.test.js @@ -11,6 +11,8 @@ const { updatePullRequestEnvVars, } = require('./testUtils') +const resultsOutputId = 'results' + const { matchers: { contains }, } = td @@ -42,8 +44,8 @@ describe('Commit Linter action', () => { beforeEach(() => { core = require('@actions/core') td.replace(core, 'getInput') - td.replace(console, 'log') - td.replace(console, 'error') + td.replace(core, 'setFailed') + td.replace(core, 'setOutput') td.when(core.getInput('configFile')).thenReturn('./commitlint.config.js') td.when(core.getInput('firstParent')).thenReturn('true') td.when(core.getInput('failOnWarnings')).thenReturn('false') @@ -69,8 +71,7 @@ describe('Commit Linter action', () => { await runAction() - td.verify(console.error(contains('You have commit messages with errors'))) - expect(process.exitCode).toBe(1) + td.verify(core.setFailed(contains('You have commit messages with errors'))) }) it('should fail for single push with incorrect message', async () => { @@ -83,7 +84,7 @@ describe('Commit Linter action', () => { await runAction() - td.verify(console.error(contains('You have commit messages with errors'))) + td.verify(core.setFailed(contains('You have commit messages with errors'))) }) it('should fail for push range with wrong messages', async () => { @@ -98,8 +99,8 @@ describe('Commit Linter action', () => { await runAction() - td.verify(console.error(contains('wrong message 1'))) - td.verify(console.error(contains('wrong message 2'))) + td.verify(core.setFailed(contains('wrong message 1'))) + td.verify(core.setFailed(contains('wrong message 2'))) }) it('should pass for push range with correct messages', async () => { @@ -115,7 +116,7 @@ describe('Commit Linter action', () => { await runAction() - td.verify(console.error(), { times: 0, ignoreExtraArgs: true }) + td.verify(core.setFailed(), { times: 0, ignoreExtraArgs: true }) td.verify(console.log('Lint free! 🎉')) }) @@ -137,8 +138,8 @@ describe('Commit Linter action', () => { 'Commit was forced, checking only the latest commit from push instead of a range of commit messages', ), ) - td.verify(console.error(contains('wrong message 1')), { times: 0 }) - td.verify(console.error(contains('wrong message 2'))) + td.verify(core.setFailed(contains('wrong message 1')), { times: 0 }) + td.verify(core.setFailed(contains('wrong message 2'))) }) it('should lint only last commit when "before" field is an empty sha', async () => { @@ -154,8 +155,8 @@ describe('Commit Linter action', () => { await runAction() - td.verify(console.error(contains('wrong message 1')), { times: 0 }) - td.verify(console.error(contains('chore(WRONG): message 2'))) + td.verify(core.setFailed(contains('wrong message 1')), { times: 0 }) + td.verify(core.setFailed(contains('chore(WRONG): message 2'))) }) it('should fail for commit with scope that is not a lerna package', async () => { @@ -170,7 +171,7 @@ describe('Commit Linter action', () => { await runAction() td.verify( - console.error(contains('chore(wrong): not including package scope')), + core.setFailed(contains('chore(wrong): not including package scope')), ) }) @@ -200,21 +201,23 @@ describe('Commit Linter action', () => { await runAction() - td.verify(console.error(contains('ib-21212121212121: without jira ticket'))) td.verify( - console.error( + core.setFailed(contains('ib-21212121212121: without jira ticket')), + ) + td.verify( + core.setFailed( contains( 'ib-21212121212121 taskId must not be loonger than 9 characters', ), ), ) td.verify( - console.error( + core.setFailed( contains('ib-21212121212121 taskId must be uppercase case'), ), ) td.verify( - console.error( + core.setFailed( contains('ib-21212121212121 commitStatus must be uppercase case'), ), ) @@ -255,10 +258,11 @@ describe('Commit Linter action', () => { await runAction() - td.verify(console.error(contains('wrong commit from another branch'))) + td.verify(core.setFailed(contains('wrong commit from another branch'))) }) describe('when there are multiple commits failing in the pull request', () => { + let expectedResultsOutput const firstMessage = 'wrong message 1' const secondMessage = 'wrong message 2' @@ -281,12 +285,29 @@ describe('Commit Linter action', () => { data: [first, to].map(sha => ({ sha })), }) td.replace(process, 'cwd', () => cwd) + + expectedResultsOutput = [ + { + hash: to, + message: secondMessage, + valid: false, + errors: ['subject may not be empty', 'type may not be empty'], + warnings: [], + }, + { + hash: first, + message: firstMessage, + valid: false, + errors: ['subject may not be empty', 'type may not be empty'], + warnings: [], + }, + ] }) it('should NOT show errors for a message from before the push', async () => { await runAction() - td.verify(console.error(contains('message from before push')), { + td.verify(core.setFailed(contains('message from before push')), { times: 0, }) }) @@ -294,13 +315,19 @@ describe('Commit Linter action', () => { it('should show errors for the first wrong message', async () => { await runAction() - td.verify(console.error(contains(firstMessage))) + td.verify(core.setFailed(contains(firstMessage))) }) it('should show errors for the second wrong message', async () => { await runAction() - td.verify(console.error(contains(secondMessage))) + td.verify(core.setFailed(contains(secondMessage))) + }) + + it('should generate a JSON output of the errors', async () => { + await runAction() + + td.verify(core.setOutput(resultsOutputId, expectedResultsOutput)) }) }) @@ -325,9 +352,8 @@ describe('Commit Linter action', () => { it('should show an error message', async () => { await runAction() - expect(process.exitCode).toBe(1) td.verify( - console.error( + core.setFailed( contains("error trying to get list of pull request's commits"), ), ) @@ -336,7 +362,7 @@ describe('Commit Linter action', () => { it('should show the original error message', async () => { await runAction() - td.verify(console.error(contains('HttpError: Bad credentials'))) + td.verify(core.setFailed(contains('HttpError: Bad credentials'))) }) }) @@ -357,7 +383,7 @@ describe('Commit Linter action', () => { it('should pass', async () => { await runAction() - td.verify(console.error(), { times: 0, ignoreExtraArgs: true }) + td.verify(core.setFailed(), { times: 0, ignoreExtraArgs: true }) }) it('should show success message', async () => { @@ -365,9 +391,27 @@ describe('Commit Linter action', () => { td.verify(console.log('Lint free! 🎉')) }) + + it('should generate a JSON output of the messages', async () => { + const expectedResultsOutput = [ + { + hash: commitHash, + message: 'chore: correct message', + valid: true, + errors: [], + warnings: [], + }, + ] + + await runAction() + + td.verify(core.setOutput(resultsOutputId, expectedResultsOutput)) + }) }) describe('when all errors are just warnings', () => { + let expectedResultsOutput + beforeEach(async () => { cwd = await git.bootstrap('fixtures/conventional') await gitEmptyCommit(cwd, 'chore: previous commit') @@ -381,15 +425,39 @@ describe('Commit Linter action', () => { updatePushEnvVars(cwd, to) td.replace(process, 'cwd', () => cwd) td.replace(console, 'log') + + expectedResultsOutput = [ + { + hash: to, + message: + 'chore: correct message\n\nsome context without leading blank line', + valid: true, + errors: [], + warnings: ['body must have leading blank line'], + }, + { + hash: from, + message: 'chore: correct message with no warnings', + valid: true, + errors: [], + warnings: [], + }, + ] }) it('should pass and show that warnings exist', async () => { await runAction() - td.verify(console.error(), { times: 0, ignoreExtraArgs: true }) + td.verify(core.setFailed(), { times: 0, ignoreExtraArgs: true }) td.verify(console.log(contains('You have commit messages with warnings'))) }) + it('should show the results in an output', async () => { + await runAction() + + td.verify(core.setOutput(resultsOutputId, expectedResultsOutput)) + }) + describe('and failOnWarnings is set to true', () => { beforeEach(() => { td.when(core.getInput('failOnWarnings')).thenReturn('true') @@ -399,9 +467,15 @@ describe('Commit Linter action', () => { await runAction() td.verify( - console.error(contains('You have commit messages with errors')), + core.setFailed(contains('You have commit messages with errors')), ) }) + + it('should show the results in an output', async () => { + await runAction() + + td.verify(core.setOutput(resultsOutputId, expectedResultsOutput)) + }) }) }) @@ -429,7 +503,33 @@ describe('Commit Linter action', () => { it('should fail', async () => { await runAction() - td.verify(console.error(contains('You have commit messages with errors'))) + td.verify( + core.setFailed(contains('You have commit messages with errors')), + ) + }) + + it('should show the results in an output', async () => { + const expectedResultsOutput = [ + { + hash: secondHash, + message: 'wrong message', + valid: false, + errors: ['subject may not be empty', 'type may not be empty'], + warnings: [], + }, + { + hash: firstHash, + message: + 'chore: correct message\n\nsome context without leading blank line', + valid: true, + errors: [], + warnings: ['body must have leading blank line'], + }, + ] + + await runAction() + + td.verify(core.setOutput(resultsOutputId, expectedResultsOutput)) }) describe('and failOnWarnings is set to true', () => { @@ -441,7 +541,7 @@ describe('Commit Linter action', () => { await runAction() td.verify( - console.error(contains('You have commit messages with errors')), + core.setFailed(contains('You have commit messages with errors')), ) }) }) diff --git a/src/generateOutputs.js b/src/generateOutputs.js new file mode 100644 index 00000000..9fc8f0a3 --- /dev/null +++ b/src/generateOutputs.js @@ -0,0 +1,24 @@ +const core = require('@actions/core') + +const resultsOutputId = 'results' + +const mapMessageValidation = item => item.message + +const mapResultOutput = ({ + hash, + lintResult: { valid, errors, warnings, input }, +}) => ({ + hash, + message: input, + valid, + errors: errors.map(mapMessageValidation), + warnings: warnings.map(mapMessageValidation), +}) + +const generateOutputs = lintedCommits => { + const resultsOutput = lintedCommits.map(mapResultOutput) + + core.setOutput(resultsOutputId, resultsOutput) +} + +module.exports = generateOutputs