Skip to content

Commit

Permalink
feat: add support for label colors
Browse files Browse the repository at this point in the history
  • Loading branch information
overbit committed Sep 22, 2023
1 parent 2b2e7d0 commit 10b72b3
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 22 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion __mocks__/@actions/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
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:
pattern:
- any: ['*.pdf']
color: '#FF0011'
31 changes: 30 additions & 1 deletion __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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 = (
Expand Down Expand Up @@ -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')]
Expand Down
43 changes: 36 additions & 7 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ function run() {
core.warning(`Pull request #${prNumber} has no changed files, skipping`);
continue;
}
const labelGlobs = yield getLabelGlobs(client, configPath);
const labelsConfig = yield getLabelGlobs(client, configPath);
const preexistingLabels = pullRequest.labels.map(l => l.name);
const allLabels = 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);
Expand All @@ -102,7 +102,7 @@ function run() {
try {
let newLabels = [];
if (!isListEqual(labelsToAdd, preexistingLabels)) {
yield setLabels(client, prNumber, labelsToAdd);
yield setLabels(client, prNumber, labelsToAdd, getLabelsColor(labelsConfig));
newLabels = labelsToAdd.filter(l => !preexistingLabels.includes(l));
}
core.setOutput('new-labels', newLabels.join(','));
Expand Down Expand Up @@ -196,6 +196,15 @@ function getLabelGlobs(client, configurationPath) {
return getLabelGlobMapFromObject(configObject);
});
}
function getLabelsColor(labelsConfig) {
const labelsColor = new Map();
for (const [label, { color }] of labelsConfig.entries()) {
if (color) {
labelsColor.set(label, color);
}
}
return labelsColor;
}
function fetchContent(client, repoPath) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield client.rest.repos.getContent({
Expand All @@ -208,13 +217,21 @@ function fetchContent(client, repoPath) {
});
}
function getLabelGlobMapFromObject(configObject) {
var _a;
const labelGlobs = 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' &&
((_a = configObject[label]) === null || _a === void 0 ? void 0 : _a.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)`);
Expand Down Expand Up @@ -298,14 +315,26 @@ function checkMatch(changedFiles, matchConfig, dot) {
function isListEqual(listA, listB) {
return listA.length === listB.length && listA.every(el => listB.includes(el));
}
function setLabels(client, prNumber, labels) {
function setLabels(client, prNumber, labels, labelsColour) {
return __awaiter(this, void 0, void 0, function* () {
// remove previous labels
yield 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) {
yield client.rest.issues.updateLabel({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
name: label,
color: color !== null && color !== void 0 ? color : '#EDEDED'
});
}
}
});
}

Expand Down
70 changes: 57 additions & 13 deletions src/labeler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ interface MatchConfig {
}

type StringOrMatchConfig = string | MatchConfig;
type LabelsConfig = Map<
string,
{stringOrMatch: StringOrMatchConfig[]; color?: string}
>;
type ClientType = ReturnType<typeof github.getOctokit>;

// GitHub Issues cannot have more than 100 labels
Expand Down Expand Up @@ -55,13 +59,15 @@ export async function run() {
continue;
}

const labelGlobs: Map<string, StringOrMatchConfig[]> =
await getLabelGlobs(client, configPath);
const labelsConfig: LabelsConfig = await getLabelGlobs(
client,
configPath
);

const preexistingLabels = pullRequest.labels.map(l => l.name);
const allLabels: Set<string> = new Set<string>(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);
Expand All @@ -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));
}

Expand Down Expand Up @@ -164,7 +175,7 @@ async function getChangedFiles(
async function getLabelGlobs(
client: ClientType,
configurationPath: string
): Promise<Map<string, StringOrMatchConfig[]>> {
): Promise<LabelsConfig> {
let configurationContent: string;
try {
if (!fs.existsSync(configurationPath)) {
Expand Down Expand Up @@ -196,6 +207,16 @@ async function getLabelGlobs(
return getLabelGlobMapFromObject(configObject);
}

function getLabelsColor(labelsConfig: LabelsConfig): Map<string, string> {
const labelsColor: Map<string, string> = new Map();
for (const [label, {color}] of labelsConfig.entries()) {
if (color) {
labelsColor.set(label, color);
}
}
return labelsColor;
}

async function fetchContent(
client: ClientType,
repoPath: string
Expand All @@ -210,15 +231,24 @@ async function fetchContent(
return Buffer.from(response.data.content, response.data.encoding).toString();
}

function getLabelGlobMapFromObject(
configObject: any
): Map<string, StringOrMatchConfig[]> {
const labelGlobs: Map<string, StringOrMatchConfig[]> = 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)`
Expand Down Expand Up @@ -337,12 +367,26 @@ function isListEqual(listA: string[], listB: string[]): boolean {
async function setLabels(
client: ClientType,
prNumber: number,
labels: string[]
labels: string[],
labelsColour: Map<string, string>
) {
// 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'
});
}
}
}

0 comments on commit 10b72b3

Please sign in to comment.