Skip to content

Commit 1884eaf

Browse files
author
Karel Alvarez
committed
feat(match): matches by PR title and body
documentation
1 parent 2b2e7d0 commit 1884eaf

File tree

4 files changed

+353
-33
lines changed

4 files changed

+353
-33
lines changed

README.md

+38-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ label1:
4040

4141
From a boolean logic perspective, top-level match objects are `OR`-ed together and individual match rules within an object are `AND`-ed. Combined with `!` negation, you can write complex matching rules.
4242

43-
> ⚠️ This action uses [minimatch](https://www.npmjs.com/package/minimatch) to apply glob patterns.
43+
> ⚠️ This action uses [minimatch](https://www.npmjs.com/package/minimatch) to apply glob patterns to the names of files changed.
4444
> For historical reasons, paths starting with dot (e.g. `.github`) are not matched by default.
4545
> You need to set `dot: true` to change this behavior.
4646
> See [Inputs](#inputs) table below for details.
@@ -156,6 +156,43 @@ label1:
156156
- path/to/folder/**
157157
```
158158

159+
160+
##### Matching based on body or title
161+
The match expression can also have the prefixes 'body:' or 'title:'. This are matched against the PR title and description. Can be combined like any other file name match expression.
162+
163+
164+
Examples 1:
165+
166+
```yml
167+
slackNotify:
168+
- "body:flagProduction"
169+
```
170+
171+
Would add the label "slackNotify" if the PR has the text "flagProduction" somewhere in the description
172+
173+
Examples 2:
174+
175+
```yml
176+
impactsRealease:
177+
- all:
178+
- "body:flagProduction"
179+
- *.properties
180+
```
181+
182+
Would add the label "impactsRelease" if the PR has the text "flagProduction" somewhere in the description, and affects any file with the extension "properties"
183+
184+
Example 3:
185+
186+
```yml
187+
customer:
188+
- all:
189+
- "body:customer"
190+
- "title:customer"
191+
```
192+
193+
Would add the label customer if both the body and the title contain "customer"
194+
195+
159196
##### Example workflow specifying Pull request numbers
160197

161198
```yml

__tests__/labeler.test.ts

+152-4
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,177 @@ const matchConfig = [{any: ['*.txt']}];
1515
describe('checkGlobs', () => {
1616
it('returns true when our pattern does match changed files', () => {
1717
const changedFiles = ['foo.txt', 'bar.txt'];
18-
const result = checkGlobs(changedFiles, matchConfig, false);
18+
const result = checkGlobs('', '', changedFiles, matchConfig, false);
1919

2020
expect(result).toBeTruthy();
2121
});
2222

2323
it('returns false when our pattern does not match changed files', () => {
2424
const changedFiles = ['foo.docx'];
25-
const result = checkGlobs(changedFiles, matchConfig, false);
25+
const result = checkGlobs('', '', changedFiles, matchConfig, false);
2626

2727
expect(result).toBeFalsy();
2828
});
2929

3030
it('returns false for a file starting with dot if `dot` option is false', () => {
3131
const changedFiles = ['.foo.txt'];
32-
const result = checkGlobs(changedFiles, matchConfig, false);
32+
const result = checkGlobs('', '', changedFiles, matchConfig, false);
3333

3434
expect(result).toBeFalsy();
3535
});
3636

3737
it('returns true for a file starting with dot if `dot` option is true', () => {
3838
const changedFiles = ['.foo.txt'];
39-
const result = checkGlobs(changedFiles, matchConfig, true);
39+
const result = checkGlobs('', '', changedFiles, matchConfig, true);
4040

4141
expect(result).toBeTruthy();
4242
});
43+
44+
describe('by body', () => {
45+
it('returns true when our pattern does match PR body', () => {
46+
const anyBodyWithFooConfig = [{any: ['body:baz']}];
47+
const changedFiles = ['foo.txt', 'bar.txt'];
48+
const result = checkGlobs(
49+
'',
50+
'blah baz potato',
51+
changedFiles,
52+
anyBodyWithFooConfig,
53+
false
54+
);
55+
56+
expect(result).toBeTruthy();
57+
});
58+
59+
it('returns false when our pattern does not match PR body', () => {
60+
const anyBodyWithBazConfig = [{any: ['body:bar']}];
61+
const changedFiles = ['foo.txt', 'bar.txt'];
62+
const result = checkGlobs(
63+
'',
64+
'blah bass potato',
65+
changedFiles,
66+
anyBodyWithBazConfig,
67+
false
68+
);
69+
70+
expect(result).toBeFalsy();
71+
});
72+
});
73+
describe('by title', () => {
74+
it('returns true when our pattern does match PR title', () => {
75+
const anyBodyWithFooConfig = [{any: ['title:baz']}];
76+
const changedFiles = ['foo.txt', 'bar.txt'];
77+
const result = checkGlobs(
78+
'blah baz potato',
79+
'',
80+
changedFiles,
81+
anyBodyWithFooConfig,
82+
false
83+
);
84+
85+
expect(result).toBeTruthy();
86+
});
87+
88+
it('returns false when our pattern does not match PR title', () => {
89+
const anyBodyWithBazConfig = [{any: ['title:bar']}];
90+
const changedFiles = ['foo.txt', 'bar.txt'];
91+
const result = checkGlobs(
92+
'blah bass potato',
93+
'',
94+
changedFiles,
95+
anyBodyWithBazConfig,
96+
false
97+
);
98+
99+
expect(result).toBeFalsy();
100+
});
101+
});
102+
103+
describe('by body or title', () => {
104+
it('returns true when our pattern does not match PR body, but matches a file', () => {
105+
const anyBodyWithBazConfig = [{any: ['body:bar', 'bar.*']}];
106+
const changedFiles = ['foo.txt', 'bar.txt'];
107+
const result = checkGlobs(
108+
'',
109+
'blah bass potato',
110+
changedFiles,
111+
anyBodyWithBazConfig,
112+
false
113+
);
114+
115+
expect(result).toBeTruthy();
116+
});
117+
118+
it('returns true when our pattern does not match PR body but matches a title', () => {
119+
const anyBodyWithBazConfig = [{any: ['body:bar', 'title:zoo']}];
120+
const changedFiles = ['foo.txt', 'bar.txt'];
121+
const result = checkGlobs(
122+
'welcome to the zoo',
123+
'blah bass potato',
124+
changedFiles,
125+
anyBodyWithBazConfig,
126+
false
127+
);
128+
129+
expect(result).toBeTruthy();
130+
});
131+
132+
it('returns true when our pattern does not match PR body or title but matches a file', () => {
133+
const anyBodyWithBazConfig = [
134+
{any: ['body:bar', 'title:potato', 'bar.*']}
135+
];
136+
const changedFiles = ['foo.txt', 'bar.txt'];
137+
const result = checkGlobs(
138+
'welcome to the zoo',
139+
'blah bass potato',
140+
changedFiles,
141+
anyBodyWithBazConfig,
142+
false
143+
);
144+
145+
expect(result).toBeTruthy();
146+
});
147+
});
148+
149+
describe('by body and title', () => {
150+
it('returns true when our pattern matches PR body and title', () => {
151+
const anyBodyWithBazConfig = [{all: ['body:bass', 'title:bar']}];
152+
const result = checkGlobs(
153+
'some bar here',
154+
'blah bass potato',
155+
[],
156+
anyBodyWithBazConfig,
157+
false
158+
);
159+
160+
expect(result).toBeTruthy();
161+
});
162+
163+
it('returns true when our pattern matches PR body, title and files', () => {
164+
const anyBodyWithBazConfig = [{all: ['body:bass', 'title:zoo', '*.txt']}];
165+
const changedFiles = ['foo.txt', 'bar.txt'];
166+
const result = checkGlobs(
167+
'welcome to the zoo.',
168+
'blah bass potato',
169+
changedFiles,
170+
anyBodyWithBazConfig,
171+
false
172+
);
173+
174+
expect(result).toBeTruthy();
175+
});
176+
177+
it('returns false when our pattern does not match PR body, even if it matches files', () => {
178+
const anyBodyWithBazConfig = [{all: ['body:not_here', '*.txt']}];
179+
const changedFiles = ['foo.txt', 'bar.txt'];
180+
const result = checkGlobs(
181+
'welcome to the zoo',
182+
'blah bass potato',
183+
changedFiles,
184+
anyBodyWithBazConfig,
185+
false
186+
);
187+
188+
expect(result).toBeFalsy();
189+
});
190+
});
43191
});

dist/index.js

+73-16
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ function run() {
9090
const allLabels = new Set(preexistingLabels);
9191
for (const [label, globs] of labelGlobs.entries()) {
9292
core.debug(`processing ${label}`);
93-
if (checkGlobs(changedFiles, globs, dot)) {
93+
if (checkGlobs(pullRequest.title, pullRequest.body, changedFiles, globs, dot)) {
9494
allLabels.add(label);
9595
}
9696
else if (syncLabels) {
@@ -233,17 +233,41 @@ function toMatchConfig(config) {
233233
function printPattern(matcher) {
234234
return (matcher.negate ? '!' : '') + matcher.pattern;
235235
}
236-
function checkGlobs(changedFiles, globs, dot) {
236+
function checkGlobs(prTitle, prBody, changedFiles, globs, dot) {
237237
for (const glob of globs) {
238238
core.debug(` checking pattern ${JSON.stringify(glob)}`);
239239
const matchConfig = toMatchConfig(glob);
240-
if (checkMatch(changedFiles, matchConfig, dot)) {
240+
if (checkMatch(prTitle, prBody, changedFiles, matchConfig, dot)) {
241241
return true;
242242
}
243243
}
244244
return false;
245245
}
246246
exports.checkGlobs = checkGlobs;
247+
function isMatchTitle(prTitle, titleMatchers) {
248+
core.debug(` matching patterns against title ${prTitle}`);
249+
for (const titleMatcher of titleMatchers) {
250+
core.debug(` - pattern ${titleMatcher}`);
251+
if (!prTitle.includes(titleMatcher)) {
252+
core.debug(` pattern ${titleMatcher} did not match`);
253+
return false;
254+
}
255+
}
256+
core.debug(` all patterns matched title`);
257+
return true;
258+
}
259+
function isMatchBody(prBody, bodyMatchers) {
260+
core.debug(` matching patterns against body ${prBody}`);
261+
for (const bodyMatcher of bodyMatchers) {
262+
core.debug(` - pattern ${bodyMatcher}`);
263+
if (!prBody.includes(bodyMatcher)) {
264+
core.debug(` pattern ${bodyMatcher} did not match`);
265+
return false;
266+
}
267+
}
268+
core.debug(` all patterns matched body`);
269+
return true;
270+
}
247271
function isMatch(changedFile, matchers) {
248272
core.debug(` matching patterns against file ${changedFile}`);
249273
for (const matcher of matchers) {
@@ -257,39 +281,72 @@ function isMatch(changedFile, matchers) {
257281
return true;
258282
}
259283
// equivalent to "Array.some()" but expanded for debugging and clarity
260-
function checkAny(changedFiles, globs, dot) {
261-
const matchers = globs.map(g => new minimatch_1.Minimatch(g, { dot }));
262-
core.debug(` checking "any" patterns`);
263-
for (const changedFile of changedFiles) {
264-
if (isMatch(changedFile, matchers)) {
265-
core.debug(` "any" patterns matched against ${changedFile}`);
266-
return true;
284+
function checkAny(prTitle, prBody, changedFiles, globs, dot) {
285+
const matchers = groupMatchers(globs, dot);
286+
core.debug(` checking "any" patterns`);
287+
if (matchers.byTitle.length > 0 && isMatchTitle(prTitle, matchers.byTitle)) {
288+
core.debug(` "any" patterns matched against pr title ${prTitle}`);
289+
return true;
290+
}
291+
if (matchers.byBody.length > 0 && isMatchBody(prBody, matchers.byBody)) {
292+
core.debug(` "any" patterns matched against pr body ${prBody}`);
293+
return true;
294+
}
295+
if (matchers.byFile.length > 0) {
296+
for (const changedFile of changedFiles) {
297+
if (isMatch(changedFile, matchers.byFile)) {
298+
core.debug(` "any" patterns matched against ${changedFile}`);
299+
return true;
300+
}
267301
}
268302
}
269303
core.debug(` "any" patterns did not match any files`);
270304
return false;
271305
}
306+
function groupMatchers(globs, dot) {
307+
const grouped = { byBody: [], byTitle: [], byFile: [] };
308+
return globs.reduce((g, glob) => {
309+
if (glob.startsWith('title:')) {
310+
g.byTitle.push(glob.substring(6));
311+
}
312+
else if (glob.startsWith('body:')) {
313+
g.byBody.push(glob.substring(5));
314+
}
315+
else {
316+
g.byFile.push(new minimatch_1.Minimatch(glob, { dot }));
317+
}
318+
return g;
319+
}, grouped);
320+
}
272321
// equivalent to "Array.every()" but expanded for debugging and clarity
273-
function checkAll(changedFiles, globs, dot) {
274-
const matchers = globs.map(g => new minimatch_1.Minimatch(g, { dot }));
322+
function checkAll(prTitle, prBody, changedFiles, globs, dot) {
323+
const matchers = groupMatchers(globs, dot);
275324
core.debug(` checking "all" patterns`);
325+
if (!isMatchTitle(prTitle, matchers.byTitle)) {
326+
core.debug(` "all" patterns dit not match against pr title ${prTitle}`);
327+
return false;
328+
}
329+
if (!isMatchBody(prBody, matchers.byBody)) {
330+
core.debug(` "all" patterns dit not match against pr body ${prBody}`);
331+
return false;
332+
}
276333
for (const changedFile of changedFiles) {
277-
if (!isMatch(changedFile, matchers)) {
334+
if (!isMatch(changedFile, matchers.byFile)) {
278335
core.debug(` "all" patterns did not match against ${changedFile}`);
279336
return false;
280337
}
281338
}
282339
core.debug(` "all" patterns matched all files`);
283340
return true;
284341
}
285-
function checkMatch(changedFiles, matchConfig, dot) {
342+
function checkMatch(prTitle, prBody, changedFiles, matchConfig, dot) {
286343
if (matchConfig.all !== undefined) {
287-
if (!checkAll(changedFiles, matchConfig.all, dot)) {
344+
if (!checkAll(prTitle, prBody, changedFiles, matchConfig.all, dot)) {
288345
return false;
289346
}
290347
}
291348
if (matchConfig.any !== undefined) {
292-
if (!checkAny(changedFiles, matchConfig.any, dot)) {
349+
if (!checkAny(prTitle, prBody, changedFiles, matchConfig.any, dot)) {
293350
return false;
294351
}
295352
}

0 commit comments

Comments
 (0)