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..860e40b65 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/src/labeler.ts b/src/labeler.ts index 272e3af2e..9ddb7c76c 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 ?? '#EDEDED' + }); + } + } }