Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 10b72b3

Browse files
committedSep 22, 2023
feat: add support for label colors
1 parent 2b2e7d0 commit 10b72b3

File tree

6 files changed

+154
-22
lines changed

6 files changed

+154
-22
lines changed
 

‎README.md

+25
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,31 @@ From a boolean logic perspective, top-level match objects are `OR`-ed together a
4545
> You need to set `dot: true` to change this behavior.
4646
> See [Inputs](#inputs) table below for details.
4747

48+
#### Advanced configuration
49+
50+
In order to define label colors, the `.github/labeler.yml` can be extended as follow:
51+
```yml
52+
# Add 'label1' to any changes within 'example' folder or any subfolders
53+
label1:
54+
pattern:
55+
- example/**
56+
color:
57+
'#FFFF00'
58+
59+
60+
# Add 'label2' to any file changes within 'example2' folder
61+
label2: example2/*
62+
63+
# Add label3 to any change to .txt files within the entire repository. Quotation marks are required for the leading asterisk
64+
label3:
65+
pattern:
66+
- '**/*.txt'
67+
color:
68+
'#ECECEC'
69+
70+
```
71+
72+
4873
#### Basic Examples
4974

5075
```yml

‎__mocks__/@actions/github.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export const context = {
1313
const mockApi = {
1414
rest: {
1515
issues: {
16-
setLabels: jest.fn()
16+
setLabels: jest.fn(),
17+
updateLabel: jest.fn()
1718
},
1819
pulls: {
1920
get: jest.fn().mockResolvedValue({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
touched-a-pdf-file:
2+
pattern:
3+
- any: ['*.pdf']
4+
color: '#FF0011'

‎__tests__/main.test.ts

+30-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ jest.mock('@actions/github');
99

1010
const gh = github.getOctokit('_');
1111
const setLabelsMock = jest.spyOn(gh.rest.issues, 'setLabels');
12+
const updateLabelMock = jest.spyOn(gh.rest.issues, 'updateLabel');
1213
const reposMock = jest.spyOn(gh.rest.repos, 'getContent');
1314
const paginateMock = jest.spyOn(gh, 'paginate');
1415
const getPullMock = jest.spyOn(gh.rest.pulls, 'get');
@@ -34,7 +35,10 @@ class NotFound extends Error {
3435
}
3536

3637
const yamlFixtures = {
37-
'only_pdfs.yml': fs.readFileSync('__tests__/fixtures/only_pdfs.yml')
38+
'only_pdfs.yml': fs.readFileSync('__tests__/fixtures/only_pdfs.yml'),
39+
'only_pdfs_with_color.yml': fs.readFileSync(
40+
'__tests__/fixtures/only_pdfs_with_color.yml'
41+
)
3842
};
3943

4044
const configureInput = (
@@ -352,6 +356,31 @@ describe('run', () => {
352356
expect(reposMock).toHaveBeenCalled();
353357
});
354358

359+
it('does update label color when defined in the configuration', async () => {
360+
setLabelsMock.mockClear();
361+
362+
usingLabelerConfigYaml('only_pdfs_with_color.yml');
363+
mockGitHubResponseChangedFiles('foo.pdf');
364+
365+
await run();
366+
367+
console.log(setLabelsMock.mock.calls);
368+
expect(setLabelsMock).toHaveBeenCalledTimes(1);
369+
expect(setLabelsMock).toHaveBeenCalledWith({
370+
owner: 'monalisa',
371+
repo: 'helloworld',
372+
issue_number: 123,
373+
labels: ['manually-added', 'touched-a-pdf-file']
374+
});
375+
expect(updateLabelMock).toHaveBeenCalledTimes(1);
376+
expect(updateLabelMock).toHaveBeenCalledWith({
377+
owner: 'monalisa',
378+
repo: 'helloworld',
379+
name: 'touched-a-pdf-file',
380+
color: '#FF0011'
381+
});
382+
});
383+
355384
test.each([
356385
[new HttpError('Error message')],
357386
[new NotFound('Error message')]

‎dist/index.js

+36-7
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ function run() {
8585
core.warning(`Pull request #${prNumber} has no changed files, skipping`);
8686
continue;
8787
}
88-
const labelGlobs = yield getLabelGlobs(client, configPath);
88+
const labelsConfig = yield getLabelGlobs(client, configPath);
8989
const preexistingLabels = pullRequest.labels.map(l => l.name);
9090
const allLabels = new Set(preexistingLabels);
91-
for (const [label, globs] of labelGlobs.entries()) {
91+
for (const [label, { stringOrMatch: globs }] of labelsConfig.entries()) {
9292
core.debug(`processing ${label}`);
9393
if (checkGlobs(changedFiles, globs, dot)) {
9494
allLabels.add(label);
@@ -102,7 +102,7 @@ function run() {
102102
try {
103103
let newLabels = [];
104104
if (!isListEqual(labelsToAdd, preexistingLabels)) {
105-
yield setLabels(client, prNumber, labelsToAdd);
105+
yield setLabels(client, prNumber, labelsToAdd, getLabelsColor(labelsConfig));
106106
newLabels = labelsToAdd.filter(l => !preexistingLabels.includes(l));
107107
}
108108
core.setOutput('new-labels', newLabels.join(','));
@@ -196,6 +196,15 @@ function getLabelGlobs(client, configurationPath) {
196196
return getLabelGlobMapFromObject(configObject);
197197
});
198198
}
199+
function getLabelsColor(labelsConfig) {
200+
const labelsColor = new Map();
201+
for (const [label, { color }] of labelsConfig.entries()) {
202+
if (color) {
203+
labelsColor.set(label, color);
204+
}
205+
}
206+
return labelsColor;
207+
}
199208
function fetchContent(client, repoPath) {
200209
return __awaiter(this, void 0, void 0, function* () {
201210
const response = yield client.rest.repos.getContent({
@@ -208,13 +217,21 @@ function fetchContent(client, repoPath) {
208217
});
209218
}
210219
function getLabelGlobMapFromObject(configObject) {
220+
var _a;
211221
const labelGlobs = new Map();
212222
for (const label in configObject) {
213223
if (typeof configObject[label] === 'string') {
214-
labelGlobs.set(label, [configObject[label]]);
224+
labelGlobs.set(label, { stringOrMatch: [configObject[label]] });
215225
}
216226
else if (configObject[label] instanceof Array) {
217-
labelGlobs.set(label, configObject[label]);
227+
labelGlobs.set(label, { stringOrMatch: configObject[label] });
228+
}
229+
else if (typeof configObject[label] === 'object' &&
230+
((_a = configObject[label]) === null || _a === void 0 ? void 0 : _a.pattern)) {
231+
labelGlobs.set(label, {
232+
stringOrMatch: configObject[label].pattern,
233+
color: configObject[label].color
234+
});
218235
}
219236
else {
220237
throw Error(`found unexpected type for label ${label} (should be string or array of globs)`);
@@ -298,14 +315,26 @@ function checkMatch(changedFiles, matchConfig, dot) {
298315
function isListEqual(listA, listB) {
299316
return listA.length === listB.length && listA.every(el => listB.includes(el));
300317
}
301-
function setLabels(client, prNumber, labels) {
318+
function setLabels(client, prNumber, labels, labelsColour) {
302319
return __awaiter(this, void 0, void 0, function* () {
320+
// remove previous labels
303321
yield client.rest.issues.setLabels({
304322
owner: github.context.repo.owner,
305323
repo: github.context.repo.repo,
306324
issue_number: prNumber,
307-
labels: labels
325+
labels
308326
});
327+
for (const label of labels) {
328+
const color = labelsColour.get(label);
329+
if (color) {
330+
yield client.rest.issues.updateLabel({
331+
owner: github.context.repo.owner,
332+
repo: github.context.repo.repo,
333+
name: label,
334+
color: color !== null && color !== void 0 ? color : '#EDEDED'
335+
});
336+
}
337+
}
309338
});
310339
}
311340

‎src/labeler.ts

+57-13
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ interface MatchConfig {
1111
}
1212

1313
type StringOrMatchConfig = string | MatchConfig;
14+
type LabelsConfig = Map<
15+
string,
16+
{stringOrMatch: StringOrMatchConfig[]; color?: string}
17+
>;
1418
type ClientType = ReturnType<typeof github.getOctokit>;
1519

1620
// GitHub Issues cannot have more than 100 labels
@@ -55,13 +59,15 @@ export async function run() {
5559
continue;
5660
}
5761

58-
const labelGlobs: Map<string, StringOrMatchConfig[]> =
59-
await getLabelGlobs(client, configPath);
62+
const labelsConfig: LabelsConfig = await getLabelGlobs(
63+
client,
64+
configPath
65+
);
6066

6167
const preexistingLabels = pullRequest.labels.map(l => l.name);
6268
const allLabels: Set<string> = new Set<string>(preexistingLabels);
6369

64-
for (const [label, globs] of labelGlobs.entries()) {
70+
for (const [label, {stringOrMatch: globs}] of labelsConfig.entries()) {
6571
core.debug(`processing ${label}`);
6672
if (checkGlobs(changedFiles, globs, dot)) {
6773
allLabels.add(label);
@@ -77,7 +83,12 @@ export async function run() {
7783
let newLabels: string[] = [];
7884

7985
if (!isListEqual(labelsToAdd, preexistingLabels)) {
80-
await setLabels(client, prNumber, labelsToAdd);
86+
await setLabels(
87+
client,
88+
prNumber,
89+
labelsToAdd,
90+
getLabelsColor(labelsConfig)
91+
);
8192
newLabels = labelsToAdd.filter(l => !preexistingLabels.includes(l));
8293
}
8394

@@ -164,7 +175,7 @@ async function getChangedFiles(
164175
async function getLabelGlobs(
165176
client: ClientType,
166177
configurationPath: string
167-
): Promise<Map<string, StringOrMatchConfig[]>> {
178+
): Promise<LabelsConfig> {
168179
let configurationContent: string;
169180
try {
170181
if (!fs.existsSync(configurationPath)) {
@@ -196,6 +207,16 @@ async function getLabelGlobs(
196207
return getLabelGlobMapFromObject(configObject);
197208
}
198209

210+
function getLabelsColor(labelsConfig: LabelsConfig): Map<string, string> {
211+
const labelsColor: Map<string, string> = new Map();
212+
for (const [label, {color}] of labelsConfig.entries()) {
213+
if (color) {
214+
labelsColor.set(label, color);
215+
}
216+
}
217+
return labelsColor;
218+
}
219+
199220
async function fetchContent(
200221
client: ClientType,
201222
repoPath: string
@@ -210,15 +231,24 @@ async function fetchContent(
210231
return Buffer.from(response.data.content, response.data.encoding).toString();
211232
}
212233

213-
function getLabelGlobMapFromObject(
214-
configObject: any
215-
): Map<string, StringOrMatchConfig[]> {
216-
const labelGlobs: Map<string, StringOrMatchConfig[]> = new Map();
234+
function getLabelGlobMapFromObject(configObject: any): LabelsConfig {
235+
const labelGlobs: Map<
236+
string,
237+
{stringOrMatch: StringOrMatchConfig[]; color?: string}
238+
> = new Map();
217239
for (const label in configObject) {
218240
if (typeof configObject[label] === 'string') {
219-
labelGlobs.set(label, [configObject[label]]);
241+
labelGlobs.set(label, {stringOrMatch: [configObject[label]]});
220242
} else if (configObject[label] instanceof Array) {
221-
labelGlobs.set(label, configObject[label]);
243+
labelGlobs.set(label, {stringOrMatch: configObject[label]});
244+
} else if (
245+
typeof configObject[label] === 'object' &&
246+
configObject[label]?.pattern
247+
) {
248+
labelGlobs.set(label, {
249+
stringOrMatch: configObject[label].pattern,
250+
color: configObject[label].color
251+
});
222252
} else {
223253
throw Error(
224254
`found unexpected type for label ${label} (should be string or array of globs)`
@@ -337,12 +367,26 @@ function isListEqual(listA: string[], listB: string[]): boolean {
337367
async function setLabels(
338368
client: ClientType,
339369
prNumber: number,
340-
labels: string[]
370+
labels: string[],
371+
labelsColour: Map<string, string>
341372
) {
373+
// remove previous labels
342374
await client.rest.issues.setLabels({
343375
owner: github.context.repo.owner,
344376
repo: github.context.repo.repo,
345377
issue_number: prNumber,
346-
labels: labels
378+
labels
347379
});
380+
381+
for (const label of labels) {
382+
const color = labelsColour.get(label);
383+
if (color) {
384+
await client.rest.issues.updateLabel({
385+
owner: github.context.repo.owner,
386+
repo: github.context.repo.repo,
387+
name: label,
388+
color: color ?? '#EDEDED'
389+
});
390+
}
391+
}
348392
}

0 commit comments

Comments
 (0)
Please sign in to comment.