diff --git a/README.md b/README.md
index 0c0da9b..ccfcf46 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
# Github auto-labeler action
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDecathlon%2Fpull-request-labeler-action.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FDecathlon%2Fpull-request-labeler-action?ref=badge_shield)
-
GitHub actions to auto label a pull request based on committed files.
@@ -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.
@@ -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%2Bgithub.com%2FDecathlon%2Fpull-request-labeler-action.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDecathlon%2Fpull-request-labeler-action?ref=badge_large)
\ No newline at end of file
+[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDecathlon%2Fpull-request-labeler-action.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDecathlon%2Fpull-request-labeler-action?ref=badge_large)
diff --git a/src/entrypoint.ts b/src/entrypoint.ts
index 2677e66..a6db5ca 100644
--- a/src/entrypoint.ts
+++ b/src/entrypoint.ts
@@ -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')
@@ -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 => {
+ // Find issue labels that are configured in .github/label-pr.yml
+ return issues.listLabelsOnIssue(issuesListLabelsOnIssueParams)
+ .then(({ data: labels }: Response) => 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');
@@ -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) => 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) => response.data)
@@ -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) => 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
);
diff --git a/src/utils.ts b/src/utils.ts
index 4efc44e..1ba443a 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -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));
diff --git a/tests/utils.test.ts b/tests/utils.test.ts
index ac1500a..fcbdca5 100644
--- a/tests/utils.test.ts
+++ b/tests/utils.test.ts
@@ -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+$";
@@ -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', () => {
@@ -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"]));
});