Skip to content
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

feat: add support for label colours #685

Open
wants to merge 2 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
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
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
@@ -0,0 +1,4 @@
touched-a-pdf-file:
pattern:
- any: ['*.pdf']
color: '#FF0011'
31 changes: 30 additions & 1 deletion __tests__/main.test.ts
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
44 changes: 37 additions & 7 deletions dist/index.js
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,27 @@ 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) {
var _a;
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: (_a = color.replace('#', '')) !== null && _a !== void 0 ? _a : 'EDEDED'
});
}
}
});
}

Expand Down
70 changes: 57 additions & 13 deletions src/labeler.ts
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.replace('#', '') ?? 'EDEDED'
});
}
}
}