diff --git a/README.md b/README.md index fd10a27f3..b1341d15d 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ jobs: # Configure additional validation for the subject based on a regex. # This example ensures the subject doesn't start with an uppercase character. subjectPattern: ^(?![A-Z]).+$ + # If `subjectPattern` is configured, you can use this property to override + # the default error message that is shown when the pattern doesn't match. + # The variables `subject` and `title` can be used within the message. + subjectPatternError: | + The subject "{subject}" found in the pull request title "{title}" didn't match the configured pattern. + Please ensure that the subject doesn't start with an uppercase character. # For work-in-progress PRs you can typically use draft pull requests # from Github. However, private repositories on the free plan don't have # this option and therefore this action allows you to opt-in to using the diff --git a/action.yml b/action.yml index edf2ba625..ea202d904 100644 --- a/action.yml +++ b/action.yml @@ -20,6 +20,9 @@ inputs: subjectPattern: description: "Configure additional validation for the subject based on a regex. E.g. '^(?![A-Z]).+$' ensures the subject doesn't start with an uppercase character." required: false + subjectPatternError: + description: "If `subjectPattern` is configured, you can use this property to override the default error message that is shown when the pattern doesn't match. The variables `subject` and `title` can be used within the message." + required: false wip: description: "For work-in-progress PRs you can typically use draft pull requests from Github. However, private repositories on the free plan don't have this option and therefore this action allows you to opt-in to using the special '[WIP]' prefix to indicate this state. This will avoid the validation of the PR title and the pull request checks remain pending. Note that a second check will be reported if this is enabled." required: false diff --git a/src/formatMessage.js b/src/formatMessage.js new file mode 100644 index 000000000..4bf99a8db --- /dev/null +++ b/src/formatMessage.js @@ -0,0 +1,9 @@ +module.exports = function formatMessage(message, values) { + let formatted = message; + if (values) { + Object.entries(values).forEach(([key, value]) => { + formatted = formatted.replace(new RegExp(`{${key}}`, 'g'), value); + }); + } + return formatted; +}; diff --git a/src/formatMessage.test.js b/src/formatMessage.test.js new file mode 100644 index 000000000..2f0c70063 --- /dev/null +++ b/src/formatMessage.test.js @@ -0,0 +1,30 @@ +const formatMessage = require('./formatMessage'); + +it('handles a string without variables', () => { + const message = 'this is test'; + expect(formatMessage(message)).toEqual(message); +}); + +it('replaces a variable', () => { + expect( + formatMessage('this is {subject} test', {subject: 'my subject'}) + ).toEqual('this is my subject test'); +}); + +it('replaces multiple variables', () => { + expect( + formatMessage('this {title} is {subject} test', { + subject: 'my subject', + title: 'my title' + }) + ).toEqual('this my title is my subject test'); +}); + +it('replaces multiple instances of a variable', () => { + expect( + formatMessage( + '99 bottles of {beverage} on the wall, 99 bottles of {beverage}.', + {beverage: 'beer'} + ) + ).toEqual('99 bottles of beer on the wall, 99 bottles of beer.'); +}); diff --git a/src/index.js b/src/index.js index 89cce2b91..8c58fa77f 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,14 @@ const validatePrTitle = require('./validatePrTitle'); module.exports = async function run() { try { const client = github.getOctokit(process.env.GITHUB_TOKEN); - const {types, scopes, requireScope, wip, subjectPattern} = parseConfig(); + const { + types, + scopes, + requireScope, + wip, + subjectPattern, + subjectPatternError + } = parseConfig(); const contextPullRequest = github.context.payload.pull_request; if (!contextPullRequest) { @@ -38,7 +45,8 @@ module.exports = async function run() { types, scopes, requireScope, - subjectPattern + subjectPattern, + subjectPatternError }); } catch (error) { validationError = error; diff --git a/src/parseConfig.js b/src/parseConfig.js index 193d18c43..6c228e084 100644 --- a/src/parseConfig.js +++ b/src/parseConfig.js @@ -21,10 +21,24 @@ module.exports = function parseConfig() { subjectPattern = ConfigParser.parseString(process.env.INPUT_SUBJECTPATTERN); } + let subjectPatternError; + if (process.env.INPUT_SUBJECTPATTERNERROR) { + subjectPatternError = ConfigParser.parseString( + process.env.INPUT_SUBJECTPATTERNERROR + ); + } + let wip; if (process.env.INPUT_WIP) { wip = ConfigParser.parseBoolean(process.env.INPUT_WIP); } - return {types, scopes, requireScope, wip, subjectPattern}; + return { + types, + scopes, + requireScope, + wip, + subjectPattern, + subjectPatternError + }; }; diff --git a/src/validatePrTitle.js b/src/validatePrTitle.js index eee8d6aa5..1674e99ac 100644 --- a/src/validatePrTitle.js +++ b/src/validatePrTitle.js @@ -1,12 +1,13 @@ const conventionalCommitsConfig = require('conventional-changelog-conventionalcommits'); const conventionalCommitTypes = require('conventional-commit-types'); const parser = require('conventional-commits-parser').sync; +const formatMessage = require('./formatMessage'); const defaultTypes = Object.keys(conventionalCommitTypes.types); module.exports = async function validatePrTitle( prTitle, - {types, scopes, requireScope, subjectPattern} = {} + {types, scopes, requireScope, subjectPattern, subjectPatternError} = {} ) { if (!types) types = defaultTypes; @@ -66,6 +67,15 @@ module.exports = async function validatePrTitle( if (subjectPattern) { const match = result.subject.match(new RegExp(subjectPattern)); + if (subjectPatternError) { + throw new Error( + formatMessage(subjectPatternError, { + subject: result.subject, + title: prTitle + }) + ); + } + if (!match) { throw new Error( `The subject "${result.subject}" found in pull request title "${prTitle}" doesn't match the configured pattern "${subjectPattern}".` diff --git a/src/validatePrTitle.test.js b/src/validatePrTitle.test.js index ec5f5bdee..93fc96fc1 100644 --- a/src/validatePrTitle.test.js +++ b/src/validatePrTitle.test.js @@ -108,6 +108,29 @@ describe('description validation', () => { await validatePrTitle('fix: sK!"ยง4123'); }); + it('uses the `subjectPatternError` if available when the `subjectPattern` does not match', async () => { + const customError = + 'The subject found in the pull request title cannot start with an uppercase character.'; + await expect( + validatePrTitle('fix: Foobar', { + subjectPattern: '^(?![A-Z]).+$', + subjectPatternError: customError + }) + ).rejects.toThrow(customError); + }); + + it('interpolates variables into `subjectPatternError`', async () => { + await expect( + validatePrTitle('fix: Foobar', { + subjectPattern: '^(?![A-Z]).+$', + subjectPatternError: + 'The subject "{subject}" found in the pull request title "{title}" cannot start with an uppercase character.' + }) + ).rejects.toThrow( + 'The subject "Foobar" found in the pull request title "fix: Foobar" cannot start with an uppercase character.' + ); + }); + it('throws for invalid subjects', async () => { await expect( validatePrTitle('fix: Foobar', {