diff --git a/README.md b/README.md index 5ba6f3e0a..ad54a2679 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,31 @@ From a boolean logic perspective, top-level match objects are `OR`-ed together a > You need to set `dot: true` to change this behavior. > See [Inputs](#inputs) table below for details. +#### Advanced configuration + +In order to define label colors, the `.github/labeler.yml` can be extended as follow: +```yml +# Add 'label1' to any changes within 'example' folder or any subfolders +label1: + pattern: + - example/** + color: + '#FFFF00' + + +# Add 'label2' to any file changes within 'example2' folder +label2: example2/* + +# Add label3 to any change to .txt files within the entire repository. Quotation marks are required for the leading asterisk +label3: + pattern: + - '**/*.txt' + color: + '#ECECEC' + +``` + + #### Basic Examples ```yml diff --git a/__mocks__/@actions/github.ts b/__mocks__/@actions/github.ts index 9e857c537..4e213c163 100644 --- a/__mocks__/@actions/github.ts +++ b/__mocks__/@actions/github.ts @@ -13,7 +13,8 @@ export const context = { const mockApi = { rest: { issues: { - setLabels: jest.fn() + setLabels: jest.fn(), + updateLabel: jest.fn() }, pulls: { get: jest.fn().mockResolvedValue({ diff --git a/__tests__/fixtures/only_pdfs_with_color.yml b/__tests__/fixtures/only_pdfs_with_color.yml new file mode 100644 index 000000000..ed5090a29 --- /dev/null +++ b/__tests__/fixtures/only_pdfs_with_color.yml @@ -0,0 +1,4 @@ +touched-a-pdf-file: + pattern: + - any: ['*.pdf'] + color: '#FF0011' diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 425861566..860a05157 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -9,6 +9,7 @@ jest.mock('@actions/github'); const gh = github.getOctokit('_'); const setLabelsMock = jest.spyOn(gh.rest.issues, 'setLabels'); +const updateLabelMock = jest.spyOn(gh.rest.issues, 'updateLabel'); const reposMock = jest.spyOn(gh.rest.repos, 'getContent'); const paginateMock = jest.spyOn(gh, 'paginate'); const getPullMock = jest.spyOn(gh.rest.pulls, 'get'); @@ -34,7 +35,10 @@ class NotFound extends Error { } const yamlFixtures = { - 'only_pdfs.yml': fs.readFileSync('__tests__/fixtures/only_pdfs.yml') + 'only_pdfs.yml': fs.readFileSync('__tests__/fixtures/only_pdfs.yml'), + 'only_pdfs_with_color.yml': fs.readFileSync( + '__tests__/fixtures/only_pdfs_with_color.yml' + ) }; const configureInput = ( @@ -352,6 +356,31 @@ describe('run', () => { expect(reposMock).toHaveBeenCalled(); }); + it('does update label color when defined in the configuration', async () => { + setLabelsMock.mockClear(); + + usingLabelerConfigYaml('only_pdfs_with_color.yml'); + mockGitHubResponseChangedFiles('foo.pdf'); + + await run(); + + console.log(setLabelsMock.mock.calls); + expect(setLabelsMock).toHaveBeenCalledTimes(1); + expect(setLabelsMock).toHaveBeenCalledWith({ + owner: 'monalisa', + repo: 'helloworld', + issue_number: 123, + labels: ['manually-added', 'touched-a-pdf-file'] + }); + expect(updateLabelMock).toHaveBeenCalledTimes(1); + expect(updateLabelMock).toHaveBeenCalledWith({ + owner: 'monalisa', + repo: 'helloworld', + name: 'touched-a-pdf-file', + color: 'FF0011' + }); + }); + test.each([ [new HttpError('Error message')], [new NotFound('Error message')] diff --git a/dist/index.js b/dist/index.js index c96d37a93..0bc0f4534 100644 --- a/dist/index.js +++ b/dist/index.js @@ -85,10 +85,10 @@ function run() { core.warning(`Pull request #${prNumber} has no changed files, skipping`); continue; } - const labelGlobs = yield getLabelGlobs(client, configPath); + const labelsConfig = yield getLabelGlobs(client, configPath); const preexistingLabels = pullRequest.labels.map(l => l.name); const allLabels = new Set(preexistingLabels); - for (const [label, globs] of labelGlobs.entries()) { + for (const [label, { stringOrMatch: globs }] of labelsConfig.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs, dot)) { allLabels.add(label); @@ -102,7 +102,7 @@ function run() { try { let newLabels = []; if (!isListEqual(labelsToAdd, preexistingLabels)) { - yield setLabels(client, prNumber, labelsToAdd); + yield setLabels(client, prNumber, labelsToAdd, getLabelsColor(labelsConfig)); newLabels = labelsToAdd.filter(l => !preexistingLabels.includes(l)); } core.setOutput('new-labels', newLabels.join(',')); @@ -196,6 +196,15 @@ function getLabelGlobs(client, configurationPath) { return getLabelGlobMapFromObject(configObject); }); } +function getLabelsColor(labelsConfig) { + const labelsColor = new Map(); + for (const [label, { color }] of labelsConfig.entries()) { + if (color) { + labelsColor.set(label, color); + } + } + return labelsColor; +} function fetchContent(client, repoPath) { return __awaiter(this, void 0, void 0, function* () { const response = yield client.rest.repos.getContent({ @@ -208,13 +217,21 @@ function fetchContent(client, repoPath) { }); } function getLabelGlobMapFromObject(configObject) { + var _a; const labelGlobs = new Map(); for (const label in configObject) { if (typeof configObject[label] === 'string') { - labelGlobs.set(label, [configObject[label]]); + labelGlobs.set(label, { stringOrMatch: [configObject[label]] }); } else if (configObject[label] instanceof Array) { - labelGlobs.set(label, configObject[label]); + labelGlobs.set(label, { stringOrMatch: configObject[label] }); + } + else if (typeof configObject[label] === 'object' && + ((_a = configObject[label]) === null || _a === void 0 ? void 0 : _a.pattern)) { + labelGlobs.set(label, { + stringOrMatch: configObject[label].pattern, + color: configObject[label].color + }); } else { throw Error(`found unexpected type for label ${label} (should be string or array of globs)`); @@ -298,14 +315,27 @@ function checkMatch(changedFiles, matchConfig, dot) { function isListEqual(listA, listB) { return listA.length === listB.length && listA.every(el => listB.includes(el)); } -function setLabels(client, prNumber, labels) { +function setLabels(client, prNumber, labels, labelsColour) { + var _a; return __awaiter(this, void 0, void 0, function* () { + // remove previous labels yield client.rest.issues.setLabels({ owner: github.context.repo.owner, repo: github.context.repo.repo, issue_number: prNumber, - labels: labels + labels }); + for (const label of labels) { + const color = labelsColour.get(label); + if (color) { + yield client.rest.issues.updateLabel({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + name: label, + color: (_a = color.replace('#', '')) !== null && _a !== void 0 ? _a : 'EDEDED' + }); + } + } }); } diff --git a/src/labeler.ts b/src/labeler.ts index 272e3af2e..71805f82c 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -11,6 +11,10 @@ interface MatchConfig { } type StringOrMatchConfig = string | MatchConfig; +type LabelsConfig = Map< + string, + {stringOrMatch: StringOrMatchConfig[]; color?: string} +>; type ClientType = ReturnType; // GitHub Issues cannot have more than 100 labels @@ -55,13 +59,15 @@ export async function run() { continue; } - const labelGlobs: Map = - await getLabelGlobs(client, configPath); + const labelsConfig: LabelsConfig = await getLabelGlobs( + client, + configPath + ); const preexistingLabels = pullRequest.labels.map(l => l.name); const allLabels: Set = new Set(preexistingLabels); - for (const [label, globs] of labelGlobs.entries()) { + for (const [label, {stringOrMatch: globs}] of labelsConfig.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs, dot)) { allLabels.add(label); @@ -77,7 +83,12 @@ export async function run() { let newLabels: string[] = []; if (!isListEqual(labelsToAdd, preexistingLabels)) { - await setLabels(client, prNumber, labelsToAdd); + await setLabels( + client, + prNumber, + labelsToAdd, + getLabelsColor(labelsConfig) + ); newLabels = labelsToAdd.filter(l => !preexistingLabels.includes(l)); } @@ -164,7 +175,7 @@ async function getChangedFiles( async function getLabelGlobs( client: ClientType, configurationPath: string -): Promise> { +): Promise { let configurationContent: string; try { if (!fs.existsSync(configurationPath)) { @@ -196,6 +207,16 @@ async function getLabelGlobs( return getLabelGlobMapFromObject(configObject); } +function getLabelsColor(labelsConfig: LabelsConfig): Map { + const labelsColor: Map = new Map(); + for (const [label, {color}] of labelsConfig.entries()) { + if (color) { + labelsColor.set(label, color); + } + } + return labelsColor; +} + async function fetchContent( client: ClientType, repoPath: string @@ -210,15 +231,24 @@ async function fetchContent( return Buffer.from(response.data.content, response.data.encoding).toString(); } -function getLabelGlobMapFromObject( - configObject: any -): Map { - const labelGlobs: Map = new Map(); +function getLabelGlobMapFromObject(configObject: any): LabelsConfig { + const labelGlobs: Map< + string, + {stringOrMatch: StringOrMatchConfig[]; color?: string} + > = new Map(); for (const label in configObject) { if (typeof configObject[label] === 'string') { - labelGlobs.set(label, [configObject[label]]); + labelGlobs.set(label, {stringOrMatch: [configObject[label]]}); } else if (configObject[label] instanceof Array) { - labelGlobs.set(label, configObject[label]); + labelGlobs.set(label, {stringOrMatch: configObject[label]}); + } else if ( + typeof configObject[label] === 'object' && + configObject[label]?.pattern + ) { + labelGlobs.set(label, { + stringOrMatch: configObject[label].pattern, + color: configObject[label].color + }); } else { throw Error( `found unexpected type for label ${label} (should be string or array of globs)` @@ -337,12 +367,26 @@ function isListEqual(listA: string[], listB: string[]): boolean { async function setLabels( client: ClientType, prNumber: number, - labels: string[] + labels: string[], + labelsColour: Map ) { + // remove previous labels await client.rest.issues.setLabels({ owner: github.context.repo.owner, repo: github.context.repo.repo, issue_number: prNumber, - labels: labels + labels }); + + for (const label of labels) { + const color = labelsColour.get(label); + if (color) { + await client.rest.issues.updateLabel({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + name: label, + color: color.replace('#', '') ?? 'EDEDED' + }); + } + } }