Skip to content

Commit

Permalink
feat: add support for labels with color
Browse files Browse the repository at this point in the history
  • Loading branch information
overbit committed Feb 2, 2025
1 parent 3629d55 commit 5a04e9c
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 21 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
3 changes: 2 additions & 1 deletion __mocks__/@actions/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
4 changes: 4 additions & 0 deletions __tests__/fixtures/only_pdfs_with_color.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
touched-a-pdf-file:
- color: '#FF0011'
- changed-files:
- any-glob-to-any-file: ['*.pdf']
36 changes: 36 additions & 0 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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')
};
Expand Down Expand Up @@ -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(<any>{
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')]
Expand Down
4 changes: 2 additions & 2 deletions src/api/get-changed-pull-requests.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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,
prNumbers: number[]
) {
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,
Expand Down
24 changes: 20 additions & 4 deletions src/api/get-label-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import {toBranchMatchConfig, BranchMatchConfig} from '../branch';

export interface MatchConfig {
color?: string;
all?: BaseMatchConfig[];
any?: BaseMatchConfig[];
}
Expand Down Expand Up @@ -63,7 +64,13 @@ export function getLabelConfigMapFromObject(
): Map<string, MatchConfig[]> {
const labelMap: Map<string, MatchConfig[]> = 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')
Expand All @@ -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.
Expand Down
15 changes: 13 additions & 2 deletions src/api/set-labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('#', '')
});
}
}
};
5 changes: 5 additions & 0 deletions src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
import * as github from '@actions/github';
import {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods/dist-types';

export type ClientType = ReturnType<typeof github.getOctokit>;

export type PullRequest =
RestEndpointMethodTypes['pulls']['get']['response']['data'];
29 changes: 17 additions & 12 deletions src/labeler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof github.getOctokit>;
import {ClientType} from './api';

// GitHub Issues cannot have more than 100 labels
const GITHUB_MAX_LABELS = 100;
Expand Down Expand Up @@ -39,13 +38,16 @@ async function labeler() {
client,
configPath
);
const preexistingLabels = pullRequest.data.labels.map(l => l.name);
const allLabels: Set<string> = new Set<string>(preexistingLabels);
const preexistingLabels: [string, string][] = pullRequest.data.labels.map(
(l: {name: string; color: string}) => [l.name, l.color]
);
const allLabels = new Map<string, string>();
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);
}
Expand All @@ -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) {
Expand All @@ -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'}
);
}
Expand Down

0 comments on commit 5a04e9c

Please sign in to comment.