diff --git a/.github/workflows/lint-pr-title-preview.yml b/.github/workflows/lint-pr-title-preview.yml index e61a4f73a..c09563d2c 100644 --- a/.github/workflows/lint-pr-title-preview.yml +++ b/.github/workflows/lint-pr-title-preview.yml @@ -1,4 +1,4 @@ -name: 'Lint PR title preview (current branch)' +name: "Lint PR title preview (current branch)" on: pull_request: types: diff --git a/README.md b/README.md index 320df8739..d775cdc95 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,10 @@ The action works without configuration, however you can provide options for cust ui # Configure that a scope must always be provided. requireScope: true + # Configure which scopes are disallowed in PR titles. For instance, by setting + # the value below, `chore(release): ...` or `ci(e2e,release): ...` will be rejected. + disallowScopes: | + release # 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 8f9d8a130..ab3323ca4 100644 --- a/action.yml +++ b/action.yml @@ -17,6 +17,9 @@ inputs: requireScope: description: "Configure that a scope must always be provided." required: false + disallowScopes: + description: 'Configure which scopes are disallowed in PR titles.' + 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." required: false diff --git a/src/index.js b/src/index.js index 763bce938..4fb796a82 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ module.exports = async function run() { types, scopes, requireScope, + disallowScopes, wip, subjectPattern, subjectPatternError, @@ -67,6 +68,7 @@ module.exports = async function run() { types, scopes, requireScope, + disallowScopes, subjectPattern, subjectPatternError, headerPattern, @@ -108,6 +110,7 @@ module.exports = async function run() { types, scopes, requireScope, + disallowScopes, subjectPattern, subjectPatternError, headerPattern, diff --git a/src/parseConfig.js b/src/parseConfig.js index a3f8f93f0..bbfa85aa4 100644 --- a/src/parseConfig.js +++ b/src/parseConfig.js @@ -16,6 +16,11 @@ module.exports = function parseConfig() { requireScope = ConfigParser.parseBoolean(process.env.INPUT_REQUIRESCOPE); } + let disallowScopes; + if (process.env.INPUT_DISALLOWSCOPES) { + disallowScopes = ConfigParser.parseEnum(process.env.INPUT_DISALLOWSCOPES); + } + let subjectPattern; if (process.env.INPUT_SUBJECTPATTERN) { subjectPattern = ConfigParser.parseString(process.env.INPUT_SUBJECTPATTERN); @@ -73,6 +78,7 @@ module.exports = function parseConfig() { types, scopes, requireScope, + disallowScopes, wip, subjectPattern, subjectPatternError, diff --git a/src/validatePrTitle.js b/src/validatePrTitle.js index b87f28dfd..f3369134b 100644 --- a/src/validatePrTitle.js +++ b/src/validatePrTitle.js @@ -11,6 +11,7 @@ module.exports = async function validatePrTitle( types, scopes, requireScope, + disallowScopes, subjectPattern, subjectPatternError, headerPattern, @@ -46,6 +47,10 @@ module.exports = async function validatePrTitle( return scopes && !scopes.includes(s); } + function isDisallowedScope(s) { + return disallowScopes && disallowScopes.includes(s); + } + if (!result.type) { throw new Error( `No release type found in pull request title "${prTitle}". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/\n\n${printAvailableTypes()}` @@ -76,6 +81,7 @@ module.exports = async function validatePrTitle( const givenScopes = result.scope ? result.scope.split(',').map((scope) => scope.trim()) : undefined; + const unknownScopes = givenScopes ? givenScopes.filter(isUnknownScope) : []; if (scopes && unknownScopes.length > 0) { throw new Error( @@ -89,6 +95,17 @@ module.exports = async function validatePrTitle( ); } + const disallowedScopes = givenScopes + ? givenScopes.filter(isDisallowedScope) + : []; + if (disallowScopes && disallowedScopes.length > 0) { + throw new Error( + `Disallowed ${ + disallowedScopes.length === 1 ? 'scope was' : 'scopes were' + } found: ${disallowScopes.join(', ')}` + ); + } + function throwSubjectPatternError(message) { if (subjectPatternError) { message = formatMessage(subjectPatternError, { diff --git a/src/validatePrTitle.test.js b/src/validatePrTitle.test.js index 073a85ef3..207eae04a 100644 --- a/src/validatePrTitle.test.js +++ b/src/validatePrTitle.test.js @@ -98,6 +98,68 @@ describe('defined scopes', () => { }); }); + describe('disallow scopes', () => { + it('passes when a single scope is provided, but not present in disallowScopes with one item', async () => { + await validatePrTitle('fix(core): Bar', {disallowScopes: ['release']}); + }); + + 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 a single scope is provided, but not present in disallowScopes with multiple items', async () => { + await validatePrTitle('fix(core): Bar', { + disallowScopes: ['release', 'test'] + }); + }); + + 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'] + }); + }); + + it('throws when a single scope is provided and it is present in disallowScopes with one item', async () => { + await expect( + validatePrTitle('fix(release): Bar', {disallowScopes: ['release']}) + ).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', { + disallowScopes: ['release', 'test'] + }) + ).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', { + disallowScopes: ['release'] + }) + ).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: ['release', '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'] + }) + ).rejects.toThrow('Disallowed scopes were found: release, test'); + }); + }); + describe('scope allowlist not defined', () => { it('passes when a scope is provided', async () => { await validatePrTitle('fix(core): Bar', {