Skip to content

Commit 16f0989

Browse files
committed
add pr title matching
1 parent 6463cdb commit 16f0989

File tree

7 files changed

+400
-8
lines changed

7 files changed

+400
-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('toBranchMatchConfig', () => {
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: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ 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 = ['changed-files', 'head-branch', 'base-branch', 'title'];
281282
const getLabelConfigs = (client, configurationPath) => Promise.resolve()
282283
.then(() => {
283284
if (!fs_1.default.existsSync(configurationPath)) {
@@ -352,7 +353,8 @@ function getLabelConfigMapFromObject(configObject) {
352353
function toMatchConfig(config) {
353354
const changedFilesConfig = (0, changedFiles_1.toChangedFilesMatchConfig)(config);
354355
const branchConfig = (0, branch_1.toBranchMatchConfig)(config);
355-
return Object.assign(Object.assign({}, changedFilesConfig), branchConfig);
356+
const titleConfig = (0, title_1.toTitleMatchConfig)(config);
357+
return Object.assign(Object.assign(Object.assign({}, changedFilesConfig), branchConfig), titleConfig);
356358
}
357359

358360

@@ -1039,6 +1041,7 @@ const lodash_isequal_1 = __importDefault(__nccwpck_require__(9471));
10391041
const get_inputs_1 = __nccwpck_require__(1219);
10401042
const changedFiles_1 = __nccwpck_require__(5145);
10411043
const branch_1 = __nccwpck_require__(2234);
1044+
const title_1 = __nccwpck_require__(9798);
10421045
// GitHub Issues cannot have more than 100 labels
10431046
const GITHUB_MAX_LABELS = 100;
10441047
const run = () => labeler().catch(error => {
@@ -1162,6 +1165,12 @@ function checkAny(matchConfigs, changedFiles, dot) {
11621165
return true;
11631166
}
11641167
}
1168+
if (matchConfig.title) {
1169+
if ((0, title_1.checkAnyTitle)(matchConfig.title)) {
1170+
core.debug(` "any" patterns matched`);
1171+
return true;
1172+
}
1173+
}
11651174
}
11661175
core.debug(` "any" patterns did not match any configs`);
11671176
return false;
@@ -1197,12 +1206,129 @@ function checkAll(matchConfigs, changedFiles, dot) {
11971206
return false;
11981207
}
11991208
}
1209+
if (matchConfig.title) {
1210+
if (!(0, title_1.checkAllTitle)(matchConfig.title)) {
1211+
core.debug(` "all" patterns did not match`);
1212+
return false;
1213+
}
1214+
}
12001215
}
12011216
core.debug(` "all" patterns matched all configs`);
12021217
return true;
12031218
}
12041219

12051220

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

12081334
/***/ 9277:

0 commit comments

Comments
 (0)