Skip to content

Commit 95b7031

Browse files
authored
feat: Add ability to constrain scopes (#66)
* feat: Constrain scopes * Adapt action config * Fix NPE * Add logging * Change env param name * Remove logging * Optional scope * Remove config
1 parent 153d429 commit 95b7031

File tree

8 files changed

+153
-15
lines changed

8 files changed

+153
-15
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Examples for valid PR titles:
1010
- fix: Correct typo.
1111
- feat: Add support for Node 12.
1212
- refactor!: Drop support for Node 6.
13+
- feat(ui): Add `Button` component.
1314

1415
Note that since PR titles only have a single line, you have to use the `!` syntax for breaking changes.
1516

@@ -40,10 +41,19 @@ jobs:
4041
- uses: amannn/[email protected]
4142
env:
4243
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43-
# Optionally you can configure which types are allowed.
44-
# Default: https://github.com/commitizen/conventional-commit-types
44+
# Optionally, you can provide options for further constraints.
4545
with:
46-
types: fix, feat
46+
# Configure which types are allowed.
47+
# Default: https://github.com/commitizen/conventional-commit-types
48+
types: |
49+
fix
50+
feat
51+
# Configure which scopes are allowed.
52+
scopes: |
53+
core
54+
ui
55+
# Configure that a scope must always be provided.
56+
requireScope: true
4757
```
4858
4959
## Event triggers

action.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,10 @@ branding:
1010
inputs:
1111
types:
1212
description: "Provide custom types if you don't want the default ones from https://www.conventionalcommits.org"
13+
required: false
14+
scopes:
15+
description: "Configure which scopes are allowed."
16+
required: false
17+
requireScope:
18+
description: "Configure that a scope must always be provided."
19+
required: false

src/ConfigParser.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const ENUM_SPLIT_REGEX = /[,\s]\s*/;
2+
3+
module.exports = {
4+
parseEnum(input) {
5+
return input
6+
.split(ENUM_SPLIT_REGEX)
7+
.map((part) => part.trim())
8+
.filter((part) => part.length > 0);
9+
},
10+
11+
parseBoolean(input) {
12+
return JSON.parse(input.trim());
13+
}
14+
};

src/ConfigParser.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const ConfigParser = require('./ConfigParser');
2+
3+
describe('parseEnum', () => {
4+
it('parses commas', () => {
5+
expect(ConfigParser.parseEnum('one, two,three, \nfour ')).toEqual([
6+
'one',
7+
'two',
8+
'three',
9+
'four'
10+
]);
11+
});
12+
13+
it('parses white space', () => {
14+
expect(ConfigParser.parseEnum('one two\nthree \n\rfour')).toEqual([
15+
'one',
16+
'two',
17+
'three',
18+
'four'
19+
]);
20+
});
21+
});

src/index.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
const core = require('@actions/core');
22
const github = require('@actions/github');
3+
const parseConfig = require('./parseConfig');
34
const validatePrTitle = require('./validatePrTitle');
45

56
module.exports = async function run() {
67
try {
78
const client = github.getOctokit(process.env.GITHUB_TOKEN);
8-
9-
let types;
10-
if (process.env.INPUT_TYPES) {
11-
types = process.env.INPUT_TYPES.split(',').map((type) => type.trim());
12-
}
9+
const {types, scopes, requireScope} = parseConfig();
1310

1411
const contextPullRequest = github.context.payload.pull_request;
1512
if (!contextPullRequest) {
@@ -37,7 +34,7 @@ module.exports = async function run() {
3734
let validationError;
3835
if (!isWip) {
3936
try {
40-
await validatePrTitle(pullRequest.title, types);
37+
await validatePrTitle(pullRequest.title, {types, scopes, requireScope});
4138
} catch (error) {
4239
validationError = error;
4340
}

src/parseConfig.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const ConfigParser = require('./ConfigParser');
2+
3+
module.exports = function parseConfig() {
4+
let types;
5+
if (process.env.INPUT_TYPES) {
6+
types = ConfigParser.parseEnum(process.env.INPUT_TYPES);
7+
}
8+
9+
let scopes;
10+
if (process.env.INPUT_SCOPES) {
11+
scopes = ConfigParser.parseEnum(process.env.INPUT_SCOPES);
12+
}
13+
14+
let requireScope;
15+
if (process.env.INPUT_REQUIRESCOPE) {
16+
requireScope = ConfigParser.parseBoolean(process.env.INPUT_REQUIRESCOPE);
17+
}
18+
19+
return {types, scopes, requireScope};
20+
};

src/validatePrTitle.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ const parser = require('conventional-commits-parser').sync;
44

55
const defaultTypes = Object.keys(conventionalCommitTypes.types);
66

7-
module.exports = async function validatePrTitle(prTitle, types = defaultTypes) {
7+
module.exports = async function validatePrTitle(
8+
prTitle,
9+
{types, scopes, requireScope} = {}
10+
) {
11+
if (!types) types = defaultTypes;
12+
813
const {parserOpts} = await conventionalCommitsConfig();
914
const result = parser(prTitle, parserOpts);
1015

@@ -35,4 +40,22 @@ module.exports = async function validatePrTitle(prTitle, types = defaultTypes) {
3540
}" found in pull request title "${prTitle}". \n\n${printAvailableTypes()}`
3641
);
3742
}
43+
44+
if (requireScope && !result.scope) {
45+
throw new Error(
46+
`No scope found in pull request title "${prTitle}". Use one of the available scopes: ${scopes.join(
47+
', '
48+
)}.`
49+
);
50+
}
51+
52+
if (scopes && result.scope && !scopes.includes(result.scope)) {
53+
throw new Error(
54+
`Unknown scope "${
55+
result.scope
56+
}" found in pull request title "${prTitle}". Use one of the available scopes: ${scopes.join(
57+
', '
58+
)}.`
59+
);
60+
}
3861
};

src/validatePrTitle.test.js

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,77 @@ it('allows valid PR titles that use the default types', async () => {
1616

1717
it('throws for PR titles without a type', async () => {
1818
await expect(validatePrTitle('Fix bug')).rejects.toThrow(
19-
/No release type found in pull request title "Fix bug"./
19+
'No release type found in pull request title "Fix bug".'
2020
);
2121
});
2222

2323
it('throws for PR titles with an unknown type', async () => {
2424
await expect(validatePrTitle('foo: Bar')).rejects.toThrow(
25-
/Unknown release type "foo" found in pull request title "foo: Bar"./
25+
'Unknown release type "foo" found in pull request title "foo: Bar".'
2626
);
2727
});
2828

29+
describe('defined scopes', () => {
30+
it('allows a missing scope by default', async () => {
31+
await validatePrTitle('fix: Bar');
32+
});
33+
34+
it('allows all scopes by default', async () => {
35+
await validatePrTitle('fix(core): Bar');
36+
});
37+
38+
it('allows a missing scope when custom scopes are defined', async () => {
39+
await validatePrTitle('fix: Bar', {scopes: ['foo']});
40+
});
41+
42+
it('allows a matching scope', async () => {
43+
await validatePrTitle('fix(core): Bar', {scopes: ['core']});
44+
});
45+
46+
it('throws when an unknown scope is detected', async () => {
47+
await expect(
48+
validatePrTitle('fix(core): Bar', {scopes: ['foo']})
49+
).rejects.toThrow(
50+
'Unknown scope "core" found in pull request title "fix(core): Bar". Use one of the available scopes: foo.'
51+
);
52+
});
53+
54+
describe('require scope', () => {
55+
it('passes when a scope is provided', async () => {
56+
await validatePrTitle('fix(core): Bar', {
57+
scopes: ['core'],
58+
requireScope: true
59+
});
60+
});
61+
62+
it('throws when a scope is missing', async () => {
63+
await expect(
64+
validatePrTitle('fix: Bar', {
65+
scopes: ['foo', 'bar'],
66+
requireScope: true
67+
})
68+
).rejects.toThrow(
69+
'No scope found in pull request title "fix: Bar". Use one of the available scopes: foo, bar.'
70+
);
71+
});
72+
});
73+
});
74+
2975
describe('custom types', () => {
3076
it('allows PR titles with a supported type', async () => {
3177
const inputs = ['foo: Foobar', 'bar: Foobar', 'baz: Foobar'];
3278
const types = ['foo', 'bar', 'baz'];
3379

3480
for (let index = 0; index < inputs.length; index++) {
35-
await validatePrTitle(inputs[index], types);
81+
await validatePrTitle(inputs[index], {types});
3682
}
3783
});
3884

3985
it('throws for PR titles with an unknown type', async () => {
4086
await expect(
41-
validatePrTitle('fix: Foobar', ['foo', 'bar'])
87+
validatePrTitle('fix: Foobar', {types: ['foo', 'bar']})
4288
).rejects.toThrow(
43-
/Unknown release type "fix" found in pull request title "fix: Foobar"./
89+
'Unknown release type "fix" found in pull request title "fix: Foobar".'
4490
);
4591
});
4692
});

0 commit comments

Comments
 (0)