Skip to content

Commit 04b071e

Browse files
authored
feat: Add regex validation for subject (#71)
* feat: Add regex validation for subject * Update yml * Remove config * Reenable * Add logging * Fix assignment * Remove config
1 parent 21d965a commit 04b071e

File tree

7 files changed

+90
-5
lines changed

7 files changed

+90
-5
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
main:
3333
runs-on: ubuntu-latest
3434
steps:
35-
- uses: amannn/action-semantic-pull-request@v2.1.0
35+
- uses: amannn/action-semantic-pull-request@v3.1.0
3636
env:
3737
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3838
# Optionally, you can provide options for further constraints.
@@ -48,6 +48,9 @@ jobs:
4848
ui
4949
# Configure that a scope must always be provided.
5050
requireScope: true
51+
# Configure additional validation for the subject based on a regex.
52+
# This example ensures the subject doesn't start with an uppercase character.
53+
subjectPattern: ^(?![A-Z]).+$
5154
# For work-in-progress PRs you can typically use draft pull requests
5255
# from Github. However, private repositories on the free plan don't have
5356
# this option and therefore this action allows you to opt-in to using the

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ inputs:
1717
requireScope:
1818
description: "Configure that a scope must always be provided."
1919
required: false
20+
subjectPattern:
21+
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."
22+
required: false
2023
wip:
2124
description: "For work-in-progress PRs you can typically use draft pull requests from Github. However, private repositories on the free plan don't have this option and therefore this action allows you to opt-in to using the special '[WIP]' prefix to indicate this state. This will avoid the validation of the PR title and the pull request checks remain pending. Note that a second check will be reported if this is enabled."
2225
required: false

src/ConfigParser.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ module.exports = {
1010

1111
parseBoolean(input) {
1212
return JSON.parse(input.trim());
13+
},
14+
15+
parseString(input) {
16+
return input;
1317
}
1418
};

src/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const validatePrTitle = require('./validatePrTitle');
66
module.exports = async function run() {
77
try {
88
const client = github.getOctokit(process.env.GITHUB_TOKEN);
9-
const {types, scopes, requireScope, wip} = parseConfig();
9+
const {types, scopes, requireScope, wip, subjectPattern} = parseConfig();
1010

1111
const contextPullRequest = github.context.payload.pull_request;
1212
if (!contextPullRequest) {
@@ -34,7 +34,12 @@ module.exports = async function run() {
3434
let validationError;
3535
if (!isWip) {
3636
try {
37-
await validatePrTitle(pullRequest.title, {types, scopes, requireScope});
37+
await validatePrTitle(pullRequest.title, {
38+
types,
39+
scopes,
40+
requireScope,
41+
subjectPattern
42+
});
3843
} catch (error) {
3944
validationError = error;
4045
}

src/parseConfig.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@ module.exports = function parseConfig() {
1616
requireScope = ConfigParser.parseBoolean(process.env.INPUT_REQUIRESCOPE);
1717
}
1818

19+
let subjectPattern;
20+
if (process.env.INPUT_SUBJECTPATTERN) {
21+
subjectPattern = ConfigParser.parseString(process.env.INPUT_SUBJECTPATTERN);
22+
}
23+
1924
let wip;
2025
if (process.env.INPUT_WIP) {
2126
wip = ConfigParser.parseBoolean(process.env.INPUT_WIP);
2227
}
2328

24-
return {types, scopes, requireScope, wip};
29+
return {types, scopes, requireScope, wip, subjectPattern};
2530
};

src/validatePrTitle.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const defaultTypes = Object.keys(conventionalCommitTypes.types);
66

77
module.exports = async function validatePrTitle(
88
prTitle,
9-
{types, scopes, requireScope} = {}
9+
{types, scopes, requireScope, subjectPattern} = {}
1010
) {
1111
if (!types) types = defaultTypes;
1212

@@ -33,6 +33,10 @@ module.exports = async function validatePrTitle(
3333
);
3434
}
3535

36+
if (!result.subject) {
37+
throw new Error(`No subject found in pull request title "${prTitle}".`);
38+
}
39+
3640
if (!types.includes(result.type)) {
3741
throw new Error(
3842
`Unknown release type "${
@@ -58,4 +62,21 @@ module.exports = async function validatePrTitle(
5862
)}.`
5963
);
6064
}
65+
66+
if (subjectPattern) {
67+
const match = result.subject.match(new RegExp(subjectPattern));
68+
69+
if (!match) {
70+
throw new Error(
71+
`The subject "${result.subject}" found in pull request title "${prTitle}" doesn't match the configured pattern "${subjectPattern}".`
72+
);
73+
}
74+
75+
const matchedPart = match[0];
76+
if (matchedPart.length !== result.subject.length) {
77+
throw new Error(
78+
`The subject "${result.subject}" found in pull request title "${prTitle}" isn't an exact match for the configured pattern "${subjectPattern}". Please provide a subject that matches the whole pattern exactly.`
79+
);
80+
}
81+
}
6182
};

src/validatePrTitle.test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ it('throws for PR titles without a type', async () => {
2020
);
2121
});
2222

23+
it('throws for PR titles with only a type', async () => {
24+
await expect(validatePrTitle('fix:')).rejects.toThrow(
25+
'No release type found in pull request title "fix:".'
26+
);
27+
});
28+
29+
it('throws for PR titles without a subject', async () => {
30+
await expect(validatePrTitle('fix: ')).rejects.toThrow(
31+
'No subject found in pull request title "fix: ".'
32+
);
33+
});
34+
2335
it('throws for PR titles with an unknown type', async () => {
2436
await expect(validatePrTitle('foo: Bar')).rejects.toThrow(
2537
'Unknown release type "foo" found in pull request title "foo: Bar".'
@@ -90,3 +102,35 @@ describe('custom types', () => {
90102
);
91103
});
92104
});
105+
106+
describe('description validation', () => {
107+
it('does not validate the description by default', async () => {
108+
await validatePrTitle('fix: sK!"§4123');
109+
});
110+
111+
it('throws for invalid subjects', async () => {
112+
await expect(
113+
validatePrTitle('fix: Foobar', {
114+
subjectPattern: '^(?![A-Z]).+$'
115+
})
116+
).rejects.toThrow(
117+
'The subject "Foobar" found in pull request title "fix: Foobar" doesn\'t match the configured pattern "^(?![A-Z]).+$".'
118+
);
119+
});
120+
121+
it('throws for only partial matches', async () => {
122+
await expect(
123+
validatePrTitle('fix: Foobar', {
124+
subjectPattern: 'Foo'
125+
})
126+
).rejects.toThrow(
127+
'The subject "Foobar" found in pull request title "fix: Foobar" isn\'t an exact match for the configured pattern "Foo". Please provide a subject that matches the whole pattern exactly.'
128+
);
129+
});
130+
131+
it('accepts valid subjects', async () => {
132+
await validatePrTitle('fix: foobar', {
133+
subjectPattern: '^(?![A-Z]).+$'
134+
});
135+
});
136+
});

0 commit comments

Comments
 (0)