Skip to content

Commit 4c6fd80

Browse files
committed
add pr title matching
1 parent 6463cdb commit 4c6fd80

File tree

7 files changed

+410
-8
lines changed

7 files changed

+410
-8
lines changed

README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ The base match object is defined as:
3737
- all-globs-to-all-files: ['list', 'of', 'globs']
3838
- base-branch: ['list', 'of', 'regexps']
3939
- head-branch: ['list', 'of', 'regexps']
40+
- title: ['list', 'of', 'regexps']
4041
```
4142
4243
There are two top-level keys, `any` and `all`, which both accept the same configuration options:
@@ -49,6 +50,7 @@ There are two top-level keys, `any` and `all`, which both accept the same config
4950
- all-globs-to-all-files: ['list', 'of', 'globs']
5051
- base-branch: ['list', 'of', 'regexps']
5152
- head-branch: ['list', 'of', 'regexps']
53+
- title: ['list', 'of', 'regexps']
5254
- all:
5355
- changed-files:
5456
- any-glob-to-any-file: ['list', 'of', 'globs']
@@ -57,6 +59,7 @@ There are two top-level keys, `any` and `all`, which both accept the same config
5759
- all-globs-to-all-files: ['list', 'of', 'globs']
5860
- base-branch: ['list', 'of', 'regexps']
5961
- head-branch: ['list', 'of', 'regexps']
62+
- title: ['list', 'of', 'regexps']
6063
```
6164

6265
From a boolean logic perspective, top-level match objects, and options within `all` are `AND`-ed together and individual match rules within the `any` object are `OR`-ed.
@@ -65,13 +68,14 @@ One or all fields can be provided for fine-grained matching.
6568
The fields are defined as follows:
6669
- `all`: ALL of the provided options must match for the label to be applied
6770
- `any`: if ANY of the provided options match then the label will be applied
68-
- `base-branch`: match regexps against the base branch name
69-
- `head-branch`: match regexps against the head branch name
7071
- `changed-files`: match glob patterns against the changed paths
7172
- `any-glob-to-any-file`: ANY glob must match against ANY changed file
7273
- `any-glob-to-all-files`: ANY glob must match against ALL changed files
7374
- `all-globs-to-any-file`: ALL globs must match against ANY changed file
7475
- `all-globs-to-all-files`: ALL globs must match against ALL changed files
76+
- `base-branch`: match regexps against the base branch name
77+
- `head-branch`: match regexps against the head branch name
78+
- `title`: match regexps against the pull request title
7579

7680
If a base option is provided without a top-level key, then it will default to `any`. More specifically, the following two configurations are equivalent:
7781
```yml
@@ -144,6 +148,19 @@ feature:
144148
# Add 'release' label to any PR that is opened against the `main` branch
145149
release:
146150
- base-branch: 'main'
151+
152+
# Add 'chore' label to any PR where the title starts with `chore`
153+
chore:
154+
- title: '^chore'
155+
156+
# Add 'ci' label to any PR where the title starts with `ci` or `build`:
157+
ci:
158+
- title: ['^ci', '^build']
159+
160+
# Add 'web' label to any PR where the title includes conventional commits optional scope
161+
web:
162+
- title: '^\w+\(web\):'
163+
147164
```
148165

149166
### Create Workflow

__mocks__/@actions/github.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export const context = {
77
},
88
base: {
99
ref: 'base-branch-name'
10-
}
10+
},
11+
title: 'pr-title'
1112
}
1213
},
1314
repo: {

__tests__/title.test.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import {
2+
getTitle,
3+
checkAnyTitle,
4+
checkAllTitle,
5+
toTitleMatchConfig,
6+
TitleMatchConfig
7+
} from '../src/title';
8+
import * as github from '@actions/github';
9+
10+
jest.mock('@actions/core');
11+
jest.mock('@actions/github');
12+
13+
describe('getTitle', () => {
14+
describe('when the pull requests title is requested', () => {
15+
it('returns the title', () => {
16+
const result = getTitle();
17+
expect(result).toEqual('pr-title');
18+
});
19+
});
20+
});
21+
22+
describe('checkAllTitle', () => {
23+
beforeEach(() => {
24+
github.context.payload.pull_request!.title = 'type(scope): description';
25+
});
26+
27+
describe('when a single pattern is provided', () => {
28+
describe('and the pattern matches the title', () => {
29+
it('returns true', () => {
30+
const result = checkAllTitle(['^type']);
31+
expect(result).toBe(true);
32+
});
33+
});
34+
35+
describe('and the pattern does not match the title', () => {
36+
it('returns false', () => {
37+
const result = checkAllTitle(['^feature/']);
38+
expect(result).toBe(false);
39+
});
40+
});
41+
});
42+
43+
describe('when multiple patterns are provided', () => {
44+
describe('and not all patterns matched', () => {
45+
it('returns false', () => {
46+
const result = checkAllTitle(['^type', '^test']);
47+
expect(result).toBe(false);
48+
});
49+
});
50+
51+
describe('and all patterns match', () => {
52+
it('returns true', () => {
53+
const result = checkAllTitle(['^type', '^\\w+\\(scope\\):']);
54+
expect(result).toBe(true);
55+
});
56+
});
57+
58+
describe('and no patterns match', () => {
59+
it('returns false', () => {
60+
const result = checkAllTitle(['^feature', 'test$']);
61+
expect(result).toBe(false);
62+
});
63+
});
64+
});
65+
});
66+
67+
describe('checkAnyTitle', () => {
68+
beforeEach(() => {
69+
github.context.payload.pull_request!.title = 'type(scope): description';
70+
});
71+
72+
describe('when a single pattern is provided', () => {
73+
describe('and the pattern matches the title', () => {
74+
it('returns true', () => {
75+
const result = checkAnyTitle(['^type']);
76+
expect(result).toBe(true);
77+
});
78+
});
79+
80+
describe('and the pattern does not match the title', () => {
81+
it('returns false', () => {
82+
const result = checkAnyTitle(['^test']);
83+
expect(result).toBe(false);
84+
});
85+
});
86+
});
87+
88+
describe('when multiple patterns are provided', () => {
89+
describe('and at least one pattern matches', () => {
90+
it('returns true', () => {
91+
const result = checkAnyTitle(['^type', '^test']);
92+
expect(result).toBe(true);
93+
});
94+
});
95+
96+
describe('and all patterns match', () => {
97+
it('returns true', () => {
98+
const result = checkAnyTitle(['^type', '^\\w+\\(scope\\):']);
99+
expect(result).toBe(true);
100+
});
101+
});
102+
103+
describe('and no patterns match', () => {
104+
it('returns false', () => {
105+
const result = checkAllTitle(['^feature', 'test$']);
106+
expect(result).toBe(false);
107+
});
108+
});
109+
});
110+
});
111+
112+
describe('toTitleMatchConfig', () => {
113+
describe('when there are no title keys in the config', () => {
114+
const config = {'changed-files': [{any: ['testing']}]};
115+
116+
it('returns an empty object', () => {
117+
const result = toTitleMatchConfig(config);
118+
expect(result).toEqual({});
119+
});
120+
});
121+
122+
describe('when the config contains a title option', () => {
123+
const config = {title: ['testing']};
124+
125+
it('sets title in the matchConfig', () => {
126+
const result = toTitleMatchConfig(config);
127+
expect(result).toEqual<TitleMatchConfig>({
128+
title: ['testing']
129+
});
130+
});
131+
132+
describe('and the matching option is a string', () => {
133+
const stringConfig = {title: 'testing'};
134+
135+
it('sets title in the matchConfig', () => {
136+
const result = toTitleMatchConfig(stringConfig);
137+
expect(result).toEqual<TitleMatchConfig>({
138+
title: ['testing']
139+
});
140+
});
141+
});
142+
});
143+
});

dist/index.js

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,13 @@ const fs_1 = __importDefault(__nccwpck_require__(9896));
277277
const get_content_1 = __nccwpck_require__(6519);
278278
const changedFiles_1 = __nccwpck_require__(5145);
279279
const branch_1 = __nccwpck_require__(2234);
280-
const ALLOWED_CONFIG_KEYS = ['changed-files', 'head-branch', 'base-branch'];
280+
const title_1 = __nccwpck_require__(9798);
281+
const ALLOWED_CONFIG_KEYS = [
282+
'changed-files',
283+
'head-branch',
284+
'base-branch',
285+
'title'
286+
];
281287
const getLabelConfigs = (client, configurationPath) => Promise.resolve()
282288
.then(() => {
283289
if (!fs_1.default.existsSync(configurationPath)) {
@@ -352,7 +358,8 @@ function getLabelConfigMapFromObject(configObject) {
352358
function toMatchConfig(config) {
353359
const changedFilesConfig = (0, changedFiles_1.toChangedFilesMatchConfig)(config);
354360
const branchConfig = (0, branch_1.toBranchMatchConfig)(config);
355-
return Object.assign(Object.assign({}, changedFilesConfig), branchConfig);
361+
const titleConfig = (0, title_1.toTitleMatchConfig)(config);
362+
return Object.assign(Object.assign(Object.assign({}, changedFilesConfig), branchConfig), titleConfig);
356363
}
357364

358365

@@ -1039,6 +1046,7 @@ const lodash_isequal_1 = __importDefault(__nccwpck_require__(9471));
10391046
const get_inputs_1 = __nccwpck_require__(1219);
10401047
const changedFiles_1 = __nccwpck_require__(5145);
10411048
const branch_1 = __nccwpck_require__(2234);
1049+
const title_1 = __nccwpck_require__(9798);
10421050
// GitHub Issues cannot have more than 100 labels
10431051
const GITHUB_MAX_LABELS = 100;
10441052
const run = () => labeler().catch(error => {
@@ -1162,6 +1170,12 @@ function checkAny(matchConfigs, changedFiles, dot) {
11621170
return true;
11631171
}
11641172
}
1173+
if (matchConfig.title) {
1174+
if ((0, title_1.checkAnyTitle)(matchConfig.title)) {
1175+
core.debug(` "any" patterns matched`);
1176+
return true;
1177+
}
1178+
}
11651179
}
11661180
core.debug(` "any" patterns did not match any configs`);
11671181
return false;
@@ -1197,12 +1211,129 @@ function checkAll(matchConfigs, changedFiles, dot) {
11971211
return false;
11981212
}
11991213
}
1214+
if (matchConfig.title) {
1215+
if (!(0, title_1.checkAllTitle)(matchConfig.title)) {
1216+
core.debug(` "all" patterns did not match`);
1217+
return false;
1218+
}
1219+
}
12001220
}
12011221
core.debug(` "all" patterns matched all configs`);
12021222
return true;
12031223
}
12041224

12051225

1226+
/***/ }),
1227+
1228+
/***/ 9798:
1229+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
1230+
1231+
"use strict";
1232+
1233+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
1234+
if (k2 === undefined) k2 = k;
1235+
var desc = Object.getOwnPropertyDescriptor(m, k);
1236+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1237+
desc = { enumerable: true, get: function() { return m[k]; } };
1238+
}
1239+
Object.defineProperty(o, k2, desc);
1240+
}) : (function(o, m, k, k2) {
1241+
if (k2 === undefined) k2 = k;
1242+
o[k2] = m[k];
1243+
}));
1244+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
1245+
Object.defineProperty(o, "default", { enumerable: true, value: v });
1246+
}) : function(o, v) {
1247+
o["default"] = v;
1248+
});
1249+
var __importStar = (this && this.__importStar) || (function () {
1250+
var ownKeys = function(o) {
1251+
ownKeys = Object.getOwnPropertyNames || function (o) {
1252+
var ar = [];
1253+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
1254+
return ar;
1255+
};
1256+
return ownKeys(o);
1257+
};
1258+
return function (mod) {
1259+
if (mod && mod.__esModule) return mod;
1260+
var result = {};
1261+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
1262+
__setModuleDefault(result, mod);
1263+
return result;
1264+
};
1265+
})();
1266+
Object.defineProperty(exports, "__esModule", ({ value: true }));
1267+
exports.toTitleMatchConfig = toTitleMatchConfig;
1268+
exports.getTitle = getTitle;
1269+
exports.checkAnyTitle = checkAnyTitle;
1270+
exports.checkAllTitle = checkAllTitle;
1271+
const core = __importStar(__nccwpck_require__(7484));
1272+
const github = __importStar(__nccwpck_require__(3228));
1273+
function toTitleMatchConfig(config) {
1274+
if (!config['title']) {
1275+
return {};
1276+
}
1277+
const titleConfig = {
1278+
title: config['title']
1279+
};
1280+
if (typeof titleConfig.title === 'string') {
1281+
titleConfig.title = [titleConfig.title];
1282+
}
1283+
return titleConfig;
1284+
}
1285+
function getTitle() {
1286+
const pullRequest = github.context.payload.pull_request;
1287+
if (!pullRequest) {
1288+
return undefined;
1289+
}
1290+
return pullRequest.title;
1291+
}
1292+
function checkAnyTitle(regexps) {
1293+
const title = getTitle();
1294+
if (!title) {
1295+
core.debug(` cannot fetch title from the pull request`);
1296+
return false;
1297+
}
1298+
core.debug(` checking "title" pattern against ${title}`);
1299+
const matchers = regexps.map(regexp => new RegExp(regexp));
1300+
for (const matcher of matchers) {
1301+
if (matchTitlePattern(matcher, title)) {
1302+
core.debug(` "title" patterns matched against ${title}`);
1303+
return true;
1304+
}
1305+
}
1306+
core.debug(` "title" patterns did not match against ${title}`);
1307+
return false;
1308+
}
1309+
function checkAllTitle(regexps) {
1310+
const title = getTitle();
1311+
if (!title) {
1312+
core.debug(` cannot fetch title from the pull request`);
1313+
return false;
1314+
}
1315+
core.debug(` checking "title" pattern against ${title}`);
1316+
const matchers = regexps.map(regexp => new RegExp(regexp));
1317+
for (const matcher of matchers) {
1318+
if (!matchTitlePattern(matcher, title)) {
1319+
core.debug(` "title" patterns did not match against ${title}`);
1320+
return false;
1321+
}
1322+
}
1323+
core.debug(` "title" patterns matched against ${title}`);
1324+
return true;
1325+
}
1326+
function matchTitlePattern(matcher, title) {
1327+
core.debug(` - ${matcher}`);
1328+
if (matcher.test(title)) {
1329+
core.debug(` "title" pattern matched`);
1330+
return true;
1331+
}
1332+
core.debug(` ${matcher} did not match`);
1333+
return false;
1334+
}
1335+
1336+
12061337
/***/ }),
12071338

12081339
/***/ 9277:

0 commit comments

Comments
 (0)