Skip to content

Commit 403a6f8

Browse files
authored
feat: Add regex support to scope and disallowScopes configuration (#226)
1 parent 0b14f54 commit 403a6f8

File tree

5 files changed

+111
-15
lines changed

5 files changed

+111
-15
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,20 @@ feat(ui): Add `Button` component.
6262
fix
6363
feat
6464
# Configure which scopes are allowed (newline delimited).
65+
# These are regex patterns auto-wrapped in `^ $`.
6566
scopes: |
6667
core
6768
ui
69+
JIRA-\d+
6870
# Configure that a scope must always be provided.
6971
requireScope: true
7072
# Configure which scopes (newline delimited) are disallowed in PR
7173
# titles. For instance by setting # the value below, `chore(release):
7274
# ...` and `ci(e2e,release): ...` will be rejected.
75+
# These are regex patterns auto-wrapped in `^ $`.
7376
disallowScopes: |
7477
release
78+
[A-Z]+
7579
# Configure additional validation for the subject based on a regex.
7680
# This example ensures the subject doesn't start with an uppercase character.
7781
subjectPattern: ^(?![A-Z]).+$

action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ inputs:
1212
description: "Provide custom types (newline delimited) if you don't want the default ones from https://www.conventionalcommits.org."
1313
required: false
1414
scopes:
15-
description: "Configure which scopes are allowed (newline delimited)."
15+
description: "Configure which scopes are allowed (newline delimited). These are regex patterns auto-wrapped in `^ $`."
1616
required: false
1717
requireScope:
1818
description: "Configure that a scope must always be provided."
1919
required: false
2020
disallowScopes:
21-
description: 'Configure which scopes are disallowed in PR titles (newline delimited).'
21+
description: 'Configure which scopes are disallowed in PR titles (newline delimited). These are regex patterns auto-wrapped in ` ^$`.'
2222
required: false
2323
subjectPattern:
2424
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."

src/ConfigParser.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ describe('parseEnum', () => {
99
'four'
1010
]);
1111
});
12+
it('parses newline-delimited lists, including regex, trimming whitespace', () => {
13+
expect(
14+
ConfigParser.parseEnum('one \ntwo \n^[A-Z]+\\n$ \r\nfour')
15+
).toEqual(['one', 'two', '^[A-Z]+\\n$', 'four']);
16+
});
1217
});

src/validatePrTitle.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@ module.exports = async function validatePrTitle(
4545
}
4646

4747
function isUnknownScope(s) {
48-
return scopes && !scopes.includes(s);
48+
return scopes && !scopes.some((scope) => new RegExp(`^${scope}$`).test(s));
4949
}
5050

5151
function isDisallowedScope(s) {
52-
return disallowScopes && disallowScopes.includes(s);
52+
return (
53+
disallowScopes &&
54+
disallowScopes.some((scope) => new RegExp(`^${scope}$`).test(s))
55+
);
5356
}
5457

5558
if (!result.type) {
@@ -73,7 +76,7 @@ module.exports = async function validatePrTitle(
7376
if (requireScope && !result.scope) {
7477
let message = `No scope found in pull request title "${prTitle}".`;
7578
if (scopes) {
76-
message += ` Use one of the available scopes: ${scopes.join(', ')}.`;
79+
message += ` Scope must match one of: ${scopes.join(', ')}.`;
7780
}
7881
raiseError(message);
7982
}
@@ -89,7 +92,7 @@ module.exports = async function validatePrTitle(
8992
unknownScopes.length > 1 ? 'scopes' : 'scope'
9093
} "${unknownScopes.join(
9194
','
92-
)}" found in pull request title "${prTitle}". Use one of the available scopes: ${scopes.join(
95+
)}" found in pull request title "${prTitle}". Scope must match one of: ${scopes.join(
9396
', '
9497
)}.`
9598
);
@@ -102,7 +105,7 @@ module.exports = async function validatePrTitle(
102105
raiseError(
103106
`Disallowed ${
104107
disallowedScopes.length === 1 ? 'scope was' : 'scopes were'
105-
} found: ${disallowScopes.join(', ')}`
108+
} found: ${disallowedScopes.join(', ')}`
106109
);
107110
}
108111

src/validatePrTitle.test.js

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,25 +55,69 @@ describe('defined scopes', () => {
5555
await validatePrTitle('fix(core): Bar', {scopes: ['core']});
5656
});
5757

58+
it('allows a regex matching scope', async () => {
59+
await validatePrTitle('fix(CORE): Bar', {scopes: ['[A-Z]+']});
60+
});
61+
5862
it('allows multiple matching scopes', async () => {
5963
await validatePrTitle('fix(core,e2e): Bar', {
6064
scopes: ['core', 'e2e', 'web']
6165
});
6266
});
6367

68+
it('allows multiple regex matching scopes', async () => {
69+
await validatePrTitle('fix(CORE,WEB): Bar', {
70+
scopes: ['[A-Z]+']
71+
});
72+
});
73+
6474
it('throws when an unknown scope is detected within multiple scopes', async () => {
6575
await expect(
6676
validatePrTitle('fix(core,e2e,foo,bar): Bar', {scopes: ['foo', 'core']})
6777
).rejects.toThrow(
68-
'Unknown scopes "e2e,bar" found in pull request title "fix(core,e2e,foo,bar): Bar". Use one of the available scopes: foo, core.'
78+
'Unknown scopes "e2e,bar" found in pull request title "fix(core,e2e,foo,bar): Bar". Scope must match one of: foo, core.'
79+
);
80+
});
81+
82+
it('throws when an unknown scope is detected within multiple scopes', async () => {
83+
await expect(
84+
validatePrTitle('fix(CORE,e2e,foo,bar): Bar', {
85+
scopes: ['foo', '[A-Z]+']
86+
})
87+
).rejects.toThrow(
88+
'Unknown scopes "e2e,bar" found in pull request title "fix(CORE,e2e,foo,bar): Bar". Scope must match one of: foo, [A-Z]+.'
6989
);
7090
});
7191

7292
it('throws when an unknown scope is detected', async () => {
7393
await expect(
7494
validatePrTitle('fix(core): Bar', {scopes: ['foo']})
7595
).rejects.toThrow(
76-
'Unknown scope "core" found in pull request title "fix(core): Bar". Use one of the available scopes: foo.'
96+
'Unknown scope "core" found in pull request title "fix(core): Bar". Scope must match one of: foo.'
97+
);
98+
});
99+
100+
it('throws when an unknown scope is detected for auto-wrapped regex matching', async () => {
101+
await expect(
102+
validatePrTitle('fix(score): Bar', {scopes: ['core']})
103+
).rejects.toThrow(
104+
'Unknown scope "score" found in pull request title "fix(score): Bar". Scope must match one of: core.'
105+
);
106+
});
107+
108+
it('throws when an unknown scope is detected for auto-wrapped regex matching when input is already wrapped', async () => {
109+
await expect(
110+
validatePrTitle('fix(score): Bar', {scopes: ['^[A-Z]+$']})
111+
).rejects.toThrow(
112+
'Unknown scope "score" found in pull request title "fix(score): Bar". Scope must match one of: ^[A-Z]+$.'
113+
);
114+
});
115+
116+
it('throws when an unknown scope is detected for regex matching', async () => {
117+
await expect(
118+
validatePrTitle('fix(core): Bar', {scopes: ['[A-Z]+']})
119+
).rejects.toThrow(
120+
'Unknown scope "core" found in pull request title "fix(core): Bar". Scope must match one of: [A-Z]+.'
77121
);
78122
});
79123

@@ -93,7 +137,7 @@ describe('defined scopes', () => {
93137
requireScope: true
94138
})
95139
).rejects.toThrow(
96-
'No scope found in pull request title "fix: Bar". Use one of the available scopes: foo, bar.'
140+
'No scope found in pull request title "fix: Bar". Scope must match one of: foo, bar.'
97141
);
98142
});
99143
});
@@ -103,21 +147,31 @@ describe('defined scopes', () => {
103147
await validatePrTitle('fix(core): Bar', {disallowScopes: ['release']});
104148
});
105149

150+
it('passes when a single scope is provided, but not present in disallowScopes with one regex item', async () => {
151+
await validatePrTitle('fix(core): Bar', {disallowScopes: ['[A-Z]+']});
152+
});
153+
106154
it('passes when multiple scopes are provided, but not present in disallowScopes with one item', async () => {
107155
await validatePrTitle('fix(core,e2e,bar): Bar', {
108156
disallowScopes: ['release']
109157
});
110158
});
111159

160+
it('passes when multiple scopes are provided, but not present in disallowScopes with one regex item', async () => {
161+
await validatePrTitle('fix(core,e2e,bar): Bar', {
162+
disallowScopes: ['[A-Z]+']
163+
});
164+
});
165+
112166
it('passes when a single scope is provided, but not present in disallowScopes with multiple items', async () => {
113167
await validatePrTitle('fix(core): Bar', {
114-
disallowScopes: ['release', 'test']
168+
disallowScopes: ['release', 'test', '[A-Z]+']
115169
});
116170
});
117171

118172
it('passes when multiple scopes are provided, but not present in disallowScopes with multiple items', async () => {
119173
await validatePrTitle('fix(core,e2e,bar): Bar', {
120-
disallowScopes: ['release', 'test']
174+
disallowScopes: ['release', 'test', '[A-Z]+']
121175
});
122176
});
123177

@@ -127,6 +181,12 @@ describe('defined scopes', () => {
127181
).rejects.toThrow('Disallowed scope was found: release');
128182
});
129183

184+
it('throws when a single scope is provided and it is present in disallowScopes with one regex item', async () => {
185+
await expect(
186+
validatePrTitle('fix(RELEASE): Bar', {disallowScopes: ['[A-Z]+']})
187+
).rejects.toThrow('Disallowed scope was found: RELEASE');
188+
});
189+
130190
it('throws when a single scope is provided and it is present in disallowScopes with multiple item', async () => {
131191
await expect(
132192
validatePrTitle('fix(release): Bar', {
@@ -135,6 +195,14 @@ describe('defined scopes', () => {
135195
).rejects.toThrow('Disallowed scope was found: release');
136196
});
137197

198+
it('throws when a single scope is provided and it is present in disallowScopes with multiple regex item', async () => {
199+
await expect(
200+
validatePrTitle('fix(RELEASE): Bar', {
201+
disallowScopes: ['[A-Z]+', '^[A-Z].+$']
202+
})
203+
).rejects.toThrow('Disallowed scope was found: RELEASE');
204+
});
205+
138206
it('throws when multiple scopes are provided and one of them is present in disallowScopes with one item ', async () => {
139207
await expect(
140208
validatePrTitle('fix(release,e2e): Bar', {
@@ -143,6 +211,14 @@ describe('defined scopes', () => {
143211
).rejects.toThrow('Disallowed scope was found: release');
144212
});
145213

214+
it('throws when multiple scopes are provided and one of them is present in disallowScopes with one regex item ', async () => {
215+
await expect(
216+
validatePrTitle('fix(RELEASE,e2e): Bar', {
217+
disallowScopes: ['[A-Z]+']
218+
})
219+
).rejects.toThrow('Disallowed scope was found: RELEASE');
220+
});
221+
146222
it('throws when multiple scopes are provided and one of them is present in disallowScopes with multiple items ', async () => {
147223
await expect(
148224
validatePrTitle('fix(release,e2e): Bar', {
@@ -151,12 +227,20 @@ describe('defined scopes', () => {
151227
).rejects.toThrow('Disallowed scope was found: release');
152228
});
153229

230+
it('throws when multiple scopes are provided and one of them is present in disallowScopes with multiple items ', async () => {
231+
await expect(
232+
validatePrTitle('fix(RELEASE,e2e): Bar', {
233+
disallowScopes: ['[A-Z]+', 'test']
234+
})
235+
).rejects.toThrow('Disallowed scope was found: RELEASE');
236+
});
237+
154238
it('throws when multiple scopes are provided and more than one of them are present in disallowScopes', async () => {
155239
await expect(
156-
validatePrTitle('fix(release,test): Bar', {
157-
disallowScopes: ['release', 'test']
240+
validatePrTitle('fix(release,test,CORE): Bar', {
241+
disallowScopes: ['release', 'test', '[A-Z]+']
158242
})
159-
).rejects.toThrow('Disallowed scopes were found: release, test');
243+
).rejects.toThrow('Disallowed scopes were found: release, test, CORE');
160244
});
161245
});
162246

0 commit comments

Comments
 (0)