Skip to content

Commit

Permalink
feat: Add regex support to scope and disallowScopes configuration (
Browse files Browse the repository at this point in the history
  • Loading branch information
Kretolus committed Feb 10, 2023
1 parent 0b14f54 commit 403a6f8
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 15 deletions.
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -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]).+$
Expand Down
4 changes: 2 additions & 2 deletions action.yml
Expand Up @@ -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."
Expand Down
5 changes: 5 additions & 0 deletions src/ConfigParser.test.js
Expand Up @@ -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']);
});
});
13 changes: 8 additions & 5 deletions src/validatePrTitle.js
Expand Up @@ -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) {
Expand All @@ -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);
}
Expand All @@ -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(
', '
)}.`
);
Expand All @@ -102,7 +105,7 @@ module.exports = async function validatePrTitle(
raiseError(
`Disallowed ${
disallowedScopes.length === 1 ? 'scope was' : 'scopes were'
} found: ${disallowScopes.join(', ')}`
} found: ${disallowedScopes.join(', ')}`
);
}

Expand Down
100 changes: 92 additions & 8 deletions src/validatePrTitle.test.js
Expand Up @@ -55,25 +55,69 @@ 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]+.'
);
});

it('throws when an unknown scope is detected', async () => {
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]+.'
);
});

Expand All @@ -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.'
);
});
});
Expand All @@ -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]+']
});
});

Expand All @@ -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', {
Expand All @@ -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', {
Expand All @@ -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', {
Expand All @@ -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');
});
});

Expand Down

0 comments on commit 403a6f8

Please sign in to comment.