From 5a04e9c12e8d0a4c7d446623e4cd073d60d26257 Mon Sep 17 00:00:00 2001 From: Daniele Iasella <2861984+overbit@users.noreply.github.com> Date: Sun, 2 Feb 2025 17:19:25 +0000 Subject: [PATCH 1/3] feat: add support for labels with color --- README.md | 8 +++++ __mocks__/@actions/github.ts | 3 +- __tests__/fixtures/only_pdfs_with_color.yml | 4 +++ __tests__/main.test.ts | 36 +++++++++++++++++++++ src/api/get-changed-pull-requests.ts | 4 +-- src/api/get-label-configs.ts | 24 +++++++++++--- src/api/set-labels.ts | 15 +++++++-- src/api/types.ts | 5 +++ src/labeler.ts | 29 ++++++++++------- 9 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 __tests__/fixtures/only_pdfs_with_color.yml diff --git a/README.md b/README.md index 05bfaea6a..a8288b1df 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,14 @@ source: - any-glob-to-any-file: 'src/**/*' - all-globs-to-all-files: '!src/docs/*' +# Add 'source' label with color #F3F3F3 to any change to src files within the source dir EXCEPT for the docs sub-folder +source: +- all: + - color: '#F3F3F3' + - changed-files: + - any-glob-to-any-file: 'src/**/*' + - all-globs-to-all-files: '!src/docs/*' + # Add 'feature' label to any PR where the head branch name starts with `feature` or has a `feature` section in the name feature: - head-branch: ['^feature', 'feature'] diff --git a/__mocks__/@actions/github.ts b/__mocks__/@actions/github.ts index 5d6ecd56d..2f668f735 100644 --- a/__mocks__/@actions/github.ts +++ b/__mocks__/@actions/github.ts @@ -19,7 +19,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..0bc1d520d --- /dev/null +++ b/__tests__/fixtures/only_pdfs_with_color.yml @@ -0,0 +1,4 @@ +touched-a-pdf-file: + - color: '#FF0011' + - changed-files: + - any-glob-to-any-file: ['*.pdf'] diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 0490f7953..af9908489 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -3,12 +3,14 @@ import * as github from '@actions/github'; import * as core from '@actions/core'; import path from 'path'; import fs from 'fs'; +import {PullRequest} from '../src/api/types'; jest.mock('@actions/core'); 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'); @@ -36,6 +38,9 @@ class NotFound extends Error { const yamlFixtures = { 'branches.yml': fs.readFileSync('__tests__/fixtures/branches.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' + ), 'not_supported.yml': fs.readFileSync('__tests__/fixtures/not_supported.yml'), 'any_and_all.yml': fs.readFileSync('__tests__/fixtures/any_and_all.yml') }; @@ -471,6 +476,37 @@ 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'); + + getPullMock.mockResolvedValueOnce({ + data: { + labels: [{name: 'manually-added'}] + } + }); + + 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/api/get-changed-pull-requests.ts b/src/api/get-changed-pull-requests.ts index f83838917..a848fa5df 100644 --- a/src/api/get-changed-pull-requests.ts +++ b/src/api/get-changed-pull-requests.ts @@ -1,7 +1,7 @@ import * as core from '@actions/core'; import * as github from '@actions/github'; import {getChangedFiles} from './get-changed-files'; -import {ClientType} from './types'; +import {ClientType, PullRequest} from './types'; export async function* getPullRequests( client: ClientType, @@ -9,7 +9,7 @@ export async function* getPullRequests( ) { for (const prNumber of prNumbers) { core.debug(`looking for pr #${prNumber}`); - let prData: any; + let prData: PullRequest; try { const result = await client.rest.pulls.get({ owner: github.context.repo.owner, diff --git a/src/api/get-label-configs.ts b/src/api/get-label-configs.ts index 4db33f28e..730e2380d 100644 --- a/src/api/get-label-configs.ts +++ b/src/api/get-label-configs.ts @@ -12,6 +12,7 @@ import { import {toBranchMatchConfig, BranchMatchConfig} from '../branch'; export interface MatchConfig { + color?: string; all?: BaseMatchConfig[]; any?: BaseMatchConfig[]; } @@ -63,7 +64,13 @@ export function getLabelConfigMapFromObject( ): Map { const labelMap: Map = new Map(); for (const label in configObject) { - const configOptions = configObject[label]; + const configOptions: [] = configObject[label]; + + // Get the color from the label if it exists. + const color = configOptions.find(x => Object.keys(x).includes('color'))?.[ + 'color' + ]; + if ( !Array.isArray(configOptions) || !configOptions.every(opts => typeof opts === 'object') @@ -84,17 +91,26 @@ export function getLabelConfigMapFromObject( if (key === 'any' || key === 'all') { if (Array.isArray(value)) { const newConfigs = value.map(toMatchConfig); - updatedConfig.push({[key]: newConfigs}); + updatedConfig.push({ + color, + [key]: newConfigs + }); } } else if (ALLOWED_CONFIG_KEYS.includes(key)) { - const newMatchConfig = toMatchConfig({[key]: value}); + const newMatchConfig = toMatchConfig({ + color, + [key]: value + }); // Find or set the `any` key so that we can add these properties to that rule, // Or create a new `any` key and add that to our array of configs. const indexOfAny = updatedConfig.findIndex(mc => !!mc['any']); if (indexOfAny >= 0) { updatedConfig[indexOfAny].any?.push(newMatchConfig); } else { - updatedConfig.push({any: [newMatchConfig]}); + updatedConfig.push({ + color, + any: [newMatchConfig] + }); } } else { // Log the key that we don't know what to do with. diff --git a/src/api/set-labels.ts b/src/api/set-labels.ts index 6d598535d..24b2264b0 100644 --- a/src/api/set-labels.ts +++ b/src/api/set-labels.ts @@ -4,12 +4,23 @@ import {ClientType} from './types'; export const setLabels = async ( client: ClientType, prNumber: number, - labels: string[] + labels: [string, string][] ) => { await client.rest.issues.setLabels({ owner: github.context.repo.owner, repo: github.context.repo.repo, issue_number: prNumber, - labels: labels + labels: labels.map(([label]) => label) }); + + for (const [label, color] of labels) { + if (color) { + await client.rest.issues.updateLabel({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + name: label, + color: color?.replace('#', '') + }); + } + } }; diff --git a/src/api/types.ts b/src/api/types.ts index 03af2dfe9..0bffdc4eb 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,2 +1,7 @@ import * as github from '@actions/github'; +import {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods/dist-types'; + export type ClientType = ReturnType; + +export type PullRequest = + RestEndpointMethodTypes['pulls']['get']['response']['data']; diff --git a/src/labeler.ts b/src/labeler.ts index 816544390..9bd9bba6e 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -10,8 +10,7 @@ import {BaseMatchConfig, MatchConfig} from './api/get-label-configs'; import {checkAllChangedFiles, checkAnyChangedFiles} from './changedFiles'; import {checkAnyBranch, checkAllBranch} from './branch'; - -type ClientType = ReturnType; +import {ClientType} from './api'; // GitHub Issues cannot have more than 100 labels const GITHUB_MAX_LABELS = 100; @@ -39,13 +38,16 @@ async function labeler() { client, configPath ); - const preexistingLabels = pullRequest.data.labels.map(l => l.name); - const allLabels: Set = new Set(preexistingLabels); + const preexistingLabels: [string, string][] = pullRequest.data.labels.map( + (l: {name: string; color: string}) => [l.name, l.color] + ); + const allLabels = new Map(); + preexistingLabels.forEach(([label, color]) => allLabels.set(label, color)); for (const [label, configs] of labelConfigs.entries()) { core.debug(`processing ${label}`); if (checkMatchConfigs(pullRequest.changedFiles, configs, dot)) { - allLabels.add(label); + allLabels.set(label, configs[0]?.color || ''); } else if (syncLabels) { allLabels.delete(label); } @@ -54,13 +56,16 @@ async function labeler() { const labelsToAdd = [...allLabels].slice(0, GITHUB_MAX_LABELS); const excessLabels = [...allLabels].slice(GITHUB_MAX_LABELS); - let newLabels: string[] = []; + let newLabels: [string, string][] = []; try { if (!isEqual(labelsToAdd, preexistingLabels)) { await api.setLabels(client, pullRequest.number, labelsToAdd); newLabels = labelsToAdd.filter( - label => !preexistingLabels.includes(label) + ([label]) => + !preexistingLabels.some( + existingsLabel => existingsLabel[0] === label + ) ); } } catch (error: any) { @@ -83,14 +88,14 @@ async function labeler() { return; } - core.setOutput('new-labels', newLabels.join(',')); - core.setOutput('all-labels', labelsToAdd.join(',')); + core.setOutput('new-labels', newLabels.map(([label]) => label).join(',')); + core.setOutput('all-labels', labelsToAdd.map(([label]) => label).join(',')); if (excessLabels.length) { core.warning( - `Maximum of ${GITHUB_MAX_LABELS} labels allowed. Excess labels: ${excessLabels.join( - ', ' - )}`, + `Maximum of ${GITHUB_MAX_LABELS} labels allowed. Excess labels: ${excessLabels + .map(([label]) => [label]) + .join(', ')}`, {title: 'Label limit for a PR exceeded'} ); } From 4ca5b2f86019e01bd1d56d8ddb7d553e3161786e Mon Sep 17 00:00:00 2001 From: Daniele Iasella <2861984+overbit@users.noreply.github.com> Date: Sun, 2 Feb 2025 17:55:21 +0000 Subject: [PATCH 2/3] docs: update readme with label with colors --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a8288b1df..b48876930 100644 --- a/README.md +++ b/README.md @@ -139,18 +139,20 @@ source: # Add 'source' label with color #F3F3F3 to any change to src files within the source dir EXCEPT for the docs sub-folder source: +- color: '#F3F3F3' - all: - - color: '#F3F3F3' - changed-files: - any-glob-to-any-file: 'src/**/*' - all-globs-to-all-files: '!src/docs/*' # Add 'feature' label to any PR where the head branch name starts with `feature` or has a `feature` section in the name feature: + - color: '#F3F3F3' - head-branch: ['^feature', 'feature'] # Add 'release' label to any PR that is opened against the `main` branch release: + - color: '#F3F3F3' - base-branch: 'main' ``` From b28cc5c4de5618e6f962918a98adf6dbe78b4d85 Mon Sep 17 00:00:00 2001 From: Daniele Iasella <2861984+overbit@users.noreply.github.com> Date: Sun, 9 Feb 2025 09:39:23 +0000 Subject: [PATCH 3/3] refactor: improve label update logic --- src/api/set-labels.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/api/set-labels.ts b/src/api/set-labels.ts index 24b2264b0..6163a8848 100644 --- a/src/api/set-labels.ts +++ b/src/api/set-labels.ts @@ -13,14 +13,16 @@ export const setLabels = async ( labels: labels.map(([label]) => label) }); - for (const [label, color] of labels) { - if (color) { - await client.rest.issues.updateLabel({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - name: label, - color: color?.replace('#', '') - }); - } - } + await Promise.all( + labels.map(async ([label, color]) => { + if (color) { + client.rest.issues.updateLabel({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + name: label, + color: color.replace('#', '') + }); + } + }) + ); };