Skip to content

feat: add support for label colours #685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,22 @@ 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:
- color: '#F3F3F3'
- all:
- 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'
```

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
17 changes: 15 additions & 2 deletions src/api/set-labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,25 @@ 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)
});

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('#', '')
});
}
})
);
};
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