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
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
@@ -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'
```

3 changes: 2 additions & 1 deletion __mocks__/@actions/github.ts
Original file line number Diff line number Diff line change
@@ -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({
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
@@ -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(<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')]
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,
24 changes: 20 additions & 4 deletions src/api/get-label-configs.ts
Original file line number Diff line number Diff line change
@@ -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<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')
@@ -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.
17 changes: 15 additions & 2 deletions src/api/set-labels.ts
Original file line number Diff line number Diff line change
@@ -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
@@ -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;
@@ -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);
}
@@ -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'}
);
}