diff --git a/README.md b/README.md index ace883c62..408a3e57c 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,20 @@ feat(ui): Add `Button` component. fix feat # Configure which scopes are allowed (newline delimited). + # These are regex patterns auto-wrapped in `^ $`. scopes: | core ui + JIRA-\d+ # Configure that a scope must always be provided. requireScope: true # Configure which scopes (newline delimited) are disallowed in PR # titles. For instance by setting # the value below, `chore(release): # ...` and `ci(e2e,release): ...` will be rejected. + # These are regex patterns auto-wrapped in `^ $`. disallowScopes: | release + [A-Z]+ # 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]).+$ diff --git a/action.yml b/action.yml index 20c1ea047..1a29a560a 100644 --- a/action.yml +++ b/action.yml @@ -12,13 +12,13 @@ inputs: description: "Provide custom types (newline delimited) if you don't want the default ones from https://www.conventionalcommits.org." required: false scopes: - description: "Configure which scopes are allowed (newline delimited)." + description: "Configure which scopes are allowed (newline delimited). These are regex patterns auto-wrapped in `^ $`." required: false requireScope: description: "Configure that a scope must always be provided." required: false disallowScopes: - description: 'Configure which scopes are disallowed in PR titles (newline delimited).' + description: 'Configure which scopes are disallowed in PR titles (newline delimited). These are regex patterns auto-wrapped in ` ^$`.' required: false 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." diff --git a/src/ConfigParser.test.js b/src/ConfigParser.test.js index 8eb28cc67..0f79c5d38 100644 --- a/src/ConfigParser.test.js +++ b/src/ConfigParser.test.js @@ -9,4 +9,9 @@ describe('parseEnum', () => { 'four' ]); }); + it('parses newline-delimited lists, including regex, trimming whitespace', () => { + expect( + ConfigParser.parseEnum('one \ntwo \n^[A-Z]+\\n$ \r\nfour') + ).toEqual(['one', 'two', '^[A-Z]+\\n$', 'four']); + }); }); diff --git a/src/validatePrTitle.js b/src/validatePrTitle.js index 14630b3c5..6dff4ac79 100644 --- a/src/validatePrTitle.js +++ b/src/validatePrTitle.js @@ -45,11 +45,14 @@ module.exports = async function validatePrTitle( } function isUnknownScope(s) { - return scopes && !scopes.includes(s); + return scopes && !scopes.some((scope) => new RegExp(`^${scope}$`).test(s)); } function isDisallowedScope(s) { - return disallowScopes && disallowScopes.includes(s); + return ( + disallowScopes && + disallowScopes.some((scope) => new RegExp(`^${scope}$`).test(s)) + ); } if (!result.type) { @@ -73,7 +76,7 @@ module.exports = async function validatePrTitle( if (requireScope && !result.scope) { let message = `No scope found in pull request title "${prTitle}".`; if (scopes) { - message += ` Use one of the available scopes: ${scopes.join(', ')}.`; + message += ` Scope must match one of: ${scopes.join(', ')}.`; } raiseError(message); } @@ -89,7 +92,7 @@ module.exports = async function validatePrTitle( unknownScopes.length > 1 ? 'scopes' : 'scope' } "${unknownScopes.join( ',' - )}" found in pull request title "${prTitle}". Use one of the available scopes: ${scopes.join( + )}" found in pull request title "${prTitle}". Scope must match one of: ${scopes.join( ', ' )}.` ); @@ -102,7 +105,7 @@ module.exports = async function validatePrTitle( raiseError( `Disallowed ${ disallowedScopes.length === 1 ? 'scope was' : 'scopes were' - } found: ${disallowScopes.join(', ')}` + } found: ${disallowedScopes.join(', ')}` ); } diff --git a/src/validatePrTitle.test.js b/src/validatePrTitle.test.js index 207eae04a..6575b287c 100644 --- a/src/validatePrTitle.test.js +++ b/src/validatePrTitle.test.js @@ -55,17 +55,37 @@ describe('defined scopes', () => { await validatePrTitle('fix(core): Bar', {scopes: ['core']}); }); + it('allows a regex matching scope', async () => { + await validatePrTitle('fix(CORE): Bar', {scopes: ['[A-Z]+']}); + }); + it('allows multiple matching scopes', async () => { await validatePrTitle('fix(core,e2e): Bar', { scopes: ['core', 'e2e', 'web'] }); }); + it('allows multiple regex matching scopes', async () => { + await validatePrTitle('fix(CORE,WEB): Bar', { + scopes: ['[A-Z]+'] + }); + }); + it('throws when an unknown scope is detected within multiple scopes', async () => { await expect( validatePrTitle('fix(core,e2e,foo,bar): Bar', {scopes: ['foo', 'core']}) ).rejects.toThrow( - 'Unknown scopes "e2e,bar" found in pull request title "fix(core,e2e,foo,bar): Bar". Use one of the available scopes: foo, core.' + 'Unknown scopes "e2e,bar" found in pull request title "fix(core,e2e,foo,bar): Bar". Scope must match one of: foo, core.' + ); + }); + + it('throws when an unknown scope is detected within multiple scopes', async () => { + await expect( + validatePrTitle('fix(CORE,e2e,foo,bar): Bar', { + scopes: ['foo', '[A-Z]+'] + }) + ).rejects.toThrow( + 'Unknown scopes "e2e,bar" found in pull request title "fix(CORE,e2e,foo,bar): Bar". Scope must match one of: foo, [A-Z]+.' ); }); @@ -73,7 +93,31 @@ describe('defined scopes', () => { await expect( validatePrTitle('fix(core): Bar', {scopes: ['foo']}) ).rejects.toThrow( - 'Unknown scope "core" found in pull request title "fix(core): Bar". Use one of the available scopes: foo.' + 'Unknown scope "core" found in pull request title "fix(core): Bar". Scope must match one of: foo.' + ); + }); + + it('throws when an unknown scope is detected for auto-wrapped regex matching', async () => { + await expect( + validatePrTitle('fix(score): Bar', {scopes: ['core']}) + ).rejects.toThrow( + 'Unknown scope "score" found in pull request title "fix(score): Bar". Scope must match one of: core.' + ); + }); + + it('throws when an unknown scope is detected for auto-wrapped regex matching when input is already wrapped', async () => { + await expect( + validatePrTitle('fix(score): Bar', {scopes: ['^[A-Z]+$']}) + ).rejects.toThrow( + 'Unknown scope "score" found in pull request title "fix(score): Bar". Scope must match one of: ^[A-Z]+$.' + ); + }); + + it('throws when an unknown scope is detected for regex matching', async () => { + await expect( + validatePrTitle('fix(core): Bar', {scopes: ['[A-Z]+']}) + ).rejects.toThrow( + 'Unknown scope "core" found in pull request title "fix(core): Bar". Scope must match one of: [A-Z]+.' ); }); @@ -93,7 +137,7 @@ describe('defined scopes', () => { requireScope: true }) ).rejects.toThrow( - 'No scope found in pull request title "fix: Bar". Use one of the available scopes: foo, bar.' + 'No scope found in pull request title "fix: Bar". Scope must match one of: foo, bar.' ); }); }); @@ -103,21 +147,31 @@ describe('defined scopes', () => { await validatePrTitle('fix(core): Bar', {disallowScopes: ['release']}); }); + it('passes when a single scope is provided, but not present in disallowScopes with one regex item', async () => { + await validatePrTitle('fix(core): Bar', {disallowScopes: ['[A-Z]+']}); + }); + it('passes when multiple scopes are provided, but not present in disallowScopes with one item', async () => { await validatePrTitle('fix(core,e2e,bar): Bar', { disallowScopes: ['release'] }); }); + it('passes when multiple scopes are provided, but not present in disallowScopes with one regex item', async () => { + await validatePrTitle('fix(core,e2e,bar): Bar', { + disallowScopes: ['[A-Z]+'] + }); + }); + it('passes when a single scope is provided, but not present in disallowScopes with multiple items', async () => { await validatePrTitle('fix(core): Bar', { - disallowScopes: ['release', 'test'] + disallowScopes: ['release', 'test', '[A-Z]+'] }); }); it('passes when multiple scopes are provided, but not present in disallowScopes with multiple items', async () => { await validatePrTitle('fix(core,e2e,bar): Bar', { - disallowScopes: ['release', 'test'] + disallowScopes: ['release', 'test', '[A-Z]+'] }); }); @@ -127,6 +181,12 @@ describe('defined scopes', () => { ).rejects.toThrow('Disallowed scope was found: release'); }); + it('throws when a single scope is provided and it is present in disallowScopes with one regex item', async () => { + await expect( + validatePrTitle('fix(RELEASE): Bar', {disallowScopes: ['[A-Z]+']}) + ).rejects.toThrow('Disallowed scope was found: RELEASE'); + }); + it('throws when a single scope is provided and it is present in disallowScopes with multiple item', async () => { await expect( validatePrTitle('fix(release): Bar', { @@ -135,6 +195,14 @@ describe('defined scopes', () => { ).rejects.toThrow('Disallowed scope was found: release'); }); + it('throws when a single scope is provided and it is present in disallowScopes with multiple regex item', async () => { + await expect( + validatePrTitle('fix(RELEASE): Bar', { + disallowScopes: ['[A-Z]+', '^[A-Z].+$'] + }) + ).rejects.toThrow('Disallowed scope was found: RELEASE'); + }); + it('throws when multiple scopes are provided and one of them is present in disallowScopes with one item ', async () => { await expect( validatePrTitle('fix(release,e2e): Bar', { @@ -143,6 +211,14 @@ describe('defined scopes', () => { ).rejects.toThrow('Disallowed scope was found: release'); }); + it('throws when multiple scopes are provided and one of them is present in disallowScopes with one regex item ', async () => { + await expect( + validatePrTitle('fix(RELEASE,e2e): Bar', { + disallowScopes: ['[A-Z]+'] + }) + ).rejects.toThrow('Disallowed scope was found: RELEASE'); + }); + it('throws when multiple scopes are provided and one of them is present in disallowScopes with multiple items ', async () => { await expect( validatePrTitle('fix(release,e2e): Bar', { @@ -151,12 +227,20 @@ describe('defined scopes', () => { ).rejects.toThrow('Disallowed scope was found: release'); }); + it('throws when multiple scopes are provided and one of them is present in disallowScopes with multiple items ', async () => { + await expect( + validatePrTitle('fix(RELEASE,e2e): Bar', { + disallowScopes: ['[A-Z]+', 'test'] + }) + ).rejects.toThrow('Disallowed scope was found: RELEASE'); + }); + it('throws when multiple scopes are provided and more than one of them are present in disallowScopes', async () => { await expect( - validatePrTitle('fix(release,test): Bar', { - disallowScopes: ['release', 'test'] + validatePrTitle('fix(release,test,CORE): Bar', { + disallowScopes: ['release', 'test', '[A-Z]+'] }) - ).rejects.toThrow('Disallowed scopes were found: release, test'); + ).rejects.toThrow('Disallowed scopes were found: release, test, CORE'); }); });