Skip to content
This repository has been archived by the owner on May 15, 2023. It is now read-only.

Commit

Permalink
feat(labels): avoid removing all labels (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
Rémi Delgatte authored and mmornati committed Jun 25, 2019
1 parent 4c3672e commit 25ff8ed
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 85 deletions.
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Github auto-labeler action
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgit.luolix.top%2FDecathlon%2Fpull-request-labeler-action.svg?type=shield)](https://app.fossa.io/projects/git%2Bgit.luolix.top%2FDecathlon%2Fpull-request-labeler-action?ref=badge_shield)


GitHub actions to auto label a pull request based on committed files.

<p align="center">
Expand Down Expand Up @@ -54,7 +53,6 @@ action "PR label by Files" {
uses = "decathlon/pull-request-labeler-action@v1.0.0"
secrets = ["GITHUB_TOKEN"]
}
```

When configuring the action, you need to provide the right link into `uses` field here.
Expand All @@ -72,6 +70,5 @@ You may need to set it up so it uses the node configuration (`package.json`) and
- To run unit tests: `npm run test:watch`
- To build: `npm run build:main`


## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgit.luolix.top%2FDecathlon%2Fpull-request-labeler-action.svg?type=large)](https://app.fossa.io/projects/git%2Bgit.luolix.top%2FDecathlon%2Fpull-request-labeler-action?ref=badge_large)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgit.luolix.top%2FDecathlon%2Fpull-request-labeler-action.svg?type=large)](https://app.fossa.io/projects/git%2Bgit.luolix.top%2FDecathlon%2Fpull-request-labeler-action?ref=badge_large)
72 changes: 46 additions & 26 deletions src/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,24 @@ import { Exit } from 'actions-toolkit/lib/exit';
import { GitHub } from 'actions-toolkit/lib/github';
import { LoggerFunc, Signale } from 'signale';
import { Filter, Repository } from './types';
import { buildIssueRemoveLabelParams, processListFilesResponses } from './utils';
import { buildIssueRemoveLabelParams, filterConfiguredIssueLabels, intersectLabels, processListFilesResponses } from './utils';

const LOGO: string = `
██████╗ ███████╗ ██████╗ █████╗ ████████╗██╗ ██╗██╗ ██████╗ ███╗ ██╗
██╔══██╗██╔════╝██╔════╝██╔══██╗╚══██╔══╝██║ ██║██║ ██╔═══██╗████╗ ██║
██║ ██║█████╗ ██║ ███████║ ██║ ███████║██║ ██║ ██║██╔██╗ ██║
██║ ██║██╔══╝ ██║ ██╔══██║ ██║ ██╔══██║██║ ██║ ██║██║╚██╗██║
██████╔╝███████╗╚██████╗██║ ██║ ██║ ██║ ██║███████╗╚██████╔╝██║ ╚████║
╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝
`;

const args: ToolkitOptions = {
event: ['pull_request.opened', 'pull_request.synchronize'],
secrets: ['GITHUB_TOKEN']
};

const findRepositoryInformation = (gitHubEventPath: string, log: LoggerFunc & Signale, exit: Exit): Repository => {
// Returns the repository information using provided gitHubEventPath
const findRepositoryInformation = (gitHubEventPath: string, log: LoggerFunc & Signale, exit: Exit): IssuesListLabelsOnIssueParams => {
const payload: WebhookPayloadWithRepository = require(gitHubEventPath);
if (payload.number === undefined) {
exit.neutral('Action not triggered by a PullRequest action. PR ID is missing')
Expand All @@ -28,17 +38,33 @@ const findRepositoryInformation = (gitHubEventPath: string, log: LoggerFunc & Si
};
};

const logo = `
██████╗ ███████╗ ██████╗ █████╗ ████████╗██╗ ██╗██╗ ██████╗ ███╗ ██╗
██╔══██╗██╔════╝██╔════╝██╔══██╗╚══██╔══╝██║ ██║██║ ██╔═══██╗████╗ ██║
██║ ██║█████╗ ██║ ███████║ ██║ ███████║██║ ██║ ██║██╔██╗ ██║
██║ ██║██╔══╝ ██║ ██╔══██║ ██║ ██╔══██║██║ ██║ ██║██║╚██╗██║
██████╔╝███████╗╚██████╗██║ ██║ ██║ ██║ ██║███████╗╚██████╔╝██║ ╚████║
╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝
`
// Find configured filters from the issue labels
const findIssueLabels = (issuesListLabelsOnIssueParams: IssuesListLabelsOnIssueParams, issues, filters: Filter[]): Promise<string[]> => {
// Find issue labels that are configured in .github/label-pr.yml
return issues.listLabelsOnIssue(issuesListLabelsOnIssueParams)
.then(({ data: labels }: Response<IssuesListLabelsOnIssueResponse>) => labels.reduce((acc, label) => acc.concat(label.name), []))
.then(issueLabels => filterConfiguredIssueLabels(issueLabels, filters));
};

// Remove provided labels
const removeIssueLabels = (labels: string[], { log, exit }: Toolkit, repository: Repository, issues): void => {
log.info('Labels to remove: ', labels);
buildIssueRemoveLabelParams(repository, labels)
.forEach(value => issues.removeLabel(value).catch(reason => exit.failure(reason)));
};

// Build labels to add
const getLabelsToAdd = (labels: string[], issueLabels: string[], { log, exit }: Toolkit): string[] => {
const labelsToAdd: string[] = intersectLabels(labels, issueLabels);
log.info('Labels to add: ', labelsToAdd);
if (labelsToAdd.length === 0) {
exit.neutral("No labels to add");
}
return labelsToAdd;
};

Toolkit.run(async (toolkit: Toolkit) => {
toolkit.log.info('Open sourced by\n'+logo);
toolkit.log.info('Open sourced by\n' + LOGO);

toolkit.log.info('Running Action');
const filters: Filter[] = toolkit.config('.github/label-pr.yml');
Expand All @@ -47,17 +73,13 @@ Toolkit.run(async (toolkit: Toolkit) => {
if (!process.env.GITHUB_EVENT_PATH) {
toolkit.exit.failure('Process env GITHUB_EVENT_PATH is undefined');
} else {
const { owner, issue_number, repo }: Repository = findRepositoryInformation(process.env.GITHUB_EVENT_PATH, toolkit.log, toolkit.exit);
const params: PullsListFilesParams = { owner, pull_number: issue_number, repo };
const { owner, issue_number, repo }: IssuesListLabelsOnIssueParams = findRepositoryInformation(process.env.GITHUB_EVENT_PATH, toolkit.log, toolkit.exit);
const { pulls: { listFiles }, issues }: GitHub = toolkit.github;

const issuesListLabelsOnIssueParams: IssuesListLabelsOnIssueParams = { issue_number, owner, repo };
// Remove issue labels that are configured in .github/label-pr.yml
await issues.listLabelsOnIssue(issuesListLabelsOnIssueParams)
.then(({ data }: Response<IssuesListLabelsOnIssueResponse>) => data.reduce((acc, item) => acc.concat(item.name), []))
.then(issueLabels => buildIssueRemoveLabelParams({ owner, issue_number, repo }, issueLabels, filters))
.then(issueRemoveLabelParams => issueRemoveLabelParams.forEach(value => issues.removeLabel(value)))
.catch(reason => toolkit.log.error(reason));
// First, we need to retrieve the existing issue labels and filter them over the configured one in config file
const issueLabels: string[] = await findIssueLabels({ issue_number, owner, repo }, issues, filters);

const params: PullsListFilesParams = { owner, pull_number: issue_number, repo };

await listFiles(params)
.then((response: Response<PullsListFilesResponse>) => response.data)
Expand All @@ -68,17 +90,15 @@ Toolkit.run(async (toolkit: Toolkit) => {
.then((files: PullsListFilesResponseItem[]) => processListFilesResponses(files, filters))
.then((eligibleFilters: Filter[]) => eligibleFilters.reduce((acc: string[], eligibleFilter: Filter) => acc.concat(eligibleFilter.labels), []))
.then((labels: string[]) => {
toolkit.log.info('Labels to add: ', labels);
if (labels.length === 0) {
toolkit.exit.neutral("No labels to add");
removeIssueLabels(intersectLabels(issueLabels, labels), toolkit, { owner, issue_number, repo }, issues);
return { issue_number, labels: getLabelsToAdd(labels, issueLabels, toolkit), owner, repo };
}
return { issue_number, labels, owner, repo };
})
)
.then((addLabelsParams: IssuesAddLabelsParams) => issues.addLabels(addLabelsParams))
.then((value: Response<IssuesAddLabelsResponseItem[]>) => toolkit.log.info(`Adding label status: ${value.status}`))
.catch(reason => toolkit.exit.failure(reason));
}
toolkit.exit.success('Labels were added to pull request')
toolkit.exit.success('Labels were update into pull request')
},
args
);
10 changes: 7 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@ export const processListFilesResponses = (files: PullsListFilesResponseItem[], f
filters.filter(filter => files.find(file => new RegExp(filter.regExp).test(file.filename)));

// Filter the list of provided labels to return those that are part of provided filters
export const getLabelsToRemove = (labels: string[], filters: Filter[]): string[] => {
export const filterConfiguredIssueLabels = (labels: string[], filters: Filter[]): string[] => {
const configuredLabels: string[] = filters.reduce((acc: string[], filter: Filter) => acc.concat(filter.labels), []);
// To filter and have a distinct list of labels to remove
return [...new Set(configuredLabels.filter(label => labels.includes(label)))];
};

// Build a list of IssueRemoveLabelParams from the list of provided labels
export const buildIssueRemoveLabelParams = ({ repo, issue_number, owner }: Repository, labels: string[], filters: Filter[]): IssuesRemoveLabelParams[] => {
return getLabelsToRemove(labels, filters).map(label => ({
export const buildIssueRemoveLabelParams = ({ repo, issue_number, owner }: Repository, labels: string[]): IssuesRemoveLabelParams[] => {
return labels.map(label => ({
issue_number,
name: label,
owner,
repo
}));
};

// Filter over the provided labels to return only those that do not appear in provided standard list
export const intersectLabels = (labels: string[], standard: string[]): string[] =>
labels.filter(label => !standard.includes(label));
96 changes: 44 additions & 52 deletions tests/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// tslint:disable-next-line:no-implicit-dependencies
import { IssuesRemoveLabelParams, PullsListFilesResponseItem } from '@octokit/rest';
import { Filter, Repository } from '../src/types';
import { buildIssueRemoveLabelParams, getLabelsToRemove, processListFilesResponses } from '../src/utils';
import { buildIssueRemoveLabelParams, filterConfiguredIssueLabels, intersectLabels, processListFilesResponses } from '../src/utils';

const IMAGE_REGEXP_AS_STRING: string = ".*\\.png+$";
const DOCUMENTATION_REGEXP_AS_STRING: string = ".*\\.md+$";
Expand Down Expand Up @@ -51,52 +51,37 @@ describe('processListFilesResponses', () => {
filename: "whatever.png"
};

it('should return an empty array if no filters are provided', () => {
const result: Filter[] = processListFilesResponses([ANY_RESPONSE_ITEM], []);
expect(result).toEqual([]);
});
it('should return an empty array if no filters are provided',
() => expect(processListFilesResponses([ANY_RESPONSE_ITEM], [])).toEqual([]));

it('should return an empty array if response has no data', () => {
const result: Filter[] = processListFilesResponses([], ANY_FILTERS);
expect(result).toEqual([]);
});
it('should return an empty array if response has no data',
() => expect(processListFilesResponses([], ANY_FILTERS)).toEqual([]));

it('should return an empty array if none filename are defined in filters', () => {
const result: Filter[] = processListFilesResponses([ANY_RESPONSE_ITEM], ANY_FILTERS);
expect(result).toEqual([]);
});
it('should return an empty array if none filename are defined in filters',
() => expect(processListFilesResponses([ANY_RESPONSE_ITEM], ANY_FILTERS)).toEqual([]));

it('should return an array with eligible filter if files are defined for provided filters', () => {
const result: Filter[] = processListFilesResponses([ANY_RESPONSE_ITEM, ANY_DOCUMENTATION_RESPONSE_ITEM], ANY_FILTERS);
expect(result).toEqual([DOCUMENTATION_FILTER]);
});
it('should return an array with eligible filter if files are defined for provided filters',
() => expect(processListFilesResponses([ANY_RESPONSE_ITEM, ANY_DOCUMENTATION_RESPONSE_ITEM], ANY_FILTERS)).toEqual([DOCUMENTATION_FILTER]));

it('should return an array with all filters if multiple files are defined for provided filters', () => {
const result: Filter[] = processListFilesResponses([ANY_RESPONSE_ITEM, ANY_DOCUMENTATION_RESPONSE_ITEM, ANY_OTHER_DOCUMENTATION_RESPONSE_ITEM, ANY_IMAGE_RESPONSE_ITEM], ANY_FILTERS);
expect(result).toEqual(ANY_FILTERS);
});
it('should return an array with all filters if multiple files are defined for provided filters',
() => expect(processListFilesResponses([ANY_RESPONSE_ITEM, ANY_DOCUMENTATION_RESPONSE_ITEM, ANY_OTHER_DOCUMENTATION_RESPONSE_ITEM, ANY_IMAGE_RESPONSE_ITEM], ANY_FILTERS)).toEqual(ANY_FILTERS));
});

describe('getLabelsToRemove', () => {
it('should return empty list when no filters', () => {
expect(getLabelsToRemove(ANY_LABELS, [])).toStrictEqual([]);
});
describe('filterConfiguredIssueLabels', () => {
it('should return empty list when no filters',
() => expect(filterConfiguredIssueLabels(ANY_LABELS, [])).toStrictEqual([]));

it('should return full list of filters if no labels', () => {
expect(getLabelsToRemove([], ANY_FILTERS)).toStrictEqual([]);
});
it('should return full list of filters if no labels',
() => expect(filterConfiguredIssueLabels([], ANY_FILTERS)).toStrictEqual([]));

it('should return empty list if none of the labels are in filters', () => {
expect(getLabelsToRemove(ANY_OTHER_LABELS, ANY_FILTERS)).toStrictEqual([]);
});
it('should return empty list if none of the labels are in filters',
() => expect(filterConfiguredIssueLabels(ANY_OTHER_LABELS, ANY_FILTERS)).toStrictEqual([]));

it('should return labels that are common with filters', () => {
expect(getLabelsToRemove(ANY_LABELS, ANY_FILTERS)).toStrictEqual(ANY_LABELS);
});
it('should return labels that are common with filters',
() => expect(filterConfiguredIssueLabels(ANY_LABELS, ANY_FILTERS)).toStrictEqual(ANY_LABELS));

it('should return labels that are common with filters but with distinct', () => {
expect(getLabelsToRemove(ANY_LABELS, ANY_FILTERS_WITH_DUPLICATES)).toStrictEqual(ANY_LABELS);
});
it('should return labels that are common with filters but with distinct',
() => expect(filterConfiguredIssueLabels(ANY_LABELS, ANY_FILTERS_WITH_DUPLICATES)).toStrictEqual(ANY_LABELS));
});

describe('buildIssueRemoveLabelParams', () => {
Expand All @@ -112,27 +97,34 @@ describe('buildIssueRemoveLabelParams', () => {
repo: ANY_REPOSITORY.repo
};

it('should return an empty list if no labels are provided', () => {
expect(buildIssueRemoveLabelParams(ANY_REPOSITORY, [], ANY_FILTERS)).toEqual([]);
});

it('should return an empty list if no labels match provided filters', () => {
expect(buildIssueRemoveLabelParams(ANY_REPOSITORY, ANY_OTHER_LABELS, ANY_FILTERS)).toEqual([]);
});
it('should return an empty list if no labels are provided',
() => expect(buildIssueRemoveLabelParams(ANY_REPOSITORY, [])).toEqual([]));

it('should return labelParams with provided repository and labels', () => {
const expected: IssuesRemoveLabelParams[] = [{
...DOCUMENTATION_REMOVE_LABEL_PARAM,
name: "images"
}, DOCUMENTATION_REMOVE_LABEL_PARAM];
expect(buildIssueRemoveLabelParams(ANY_REPOSITORY, ANY_LABELS, ANY_FILTERS)).toEqual(expected);
expect(buildIssueRemoveLabelParams(ANY_REPOSITORY, ANY_LABELS)).toEqual(expected);
});
});

it('should return only distinct labelParams to delete when providing repository and labels', () => {
const expected: IssuesRemoveLabelParams[] = [{
...DOCUMENTATION_REMOVE_LABEL_PARAM,
name: "images"
}, DOCUMENTATION_REMOVE_LABEL_PARAM];
expect(buildIssueRemoveLabelParams(ANY_REPOSITORY, ANY_LABELS, ANY_FILTERS_WITH_DUPLICATES)).toEqual(expected);
});
describe('intersectLabels', () => {
it('should return an empty list when providing list are both empty',
() => expect(intersectLabels([], [])).toEqual([]));

it('should return an empty list when providing list is empty and standard is not',
() => expect(intersectLabels([], ANY_LABELS)).toEqual([]));

it('should return provided list of labels when providing an empty standard list',
() => expect(intersectLabels(ANY_LABELS, [])).toEqual(ANY_LABELS));

it('should return an empty list when providing both identical list',
() => expect(intersectLabels(ANY_LABELS, ANY_LABELS)).toEqual([]));

it('should return provided list of labels when none of the standard list match',
() => expect(intersectLabels(ANY_LABELS, ANY_OTHER_LABELS)).toEqual(ANY_LABELS));

it('should return an intersection of both list of labels when some matches',
() => expect(intersectLabels(ANY_LABELS, ANY_OTHER_LABELS.concat("documentation"))).toEqual(["images"]));
});

0 comments on commit 25ff8ed

Please sign in to comment.