1 | const {isNil, uniqBy, template, flatten} = require('lodash');
|
2 | const parseGithubUrl = require('parse-github-url');
|
3 | const pFilter = require('p-filter');
|
4 | const AggregateError = require('aggregate-error');
|
5 | const issueParser = require('issue-parser');
|
6 | const debug = require('debug')('semantic-release:github');
|
7 | const resolveConfig = require('./resolve-config');
|
8 | const getClient = require('./get-client');
|
9 | const getSearchQueries = require('./get-search-queries');
|
10 | const getSuccessComment = require('./get-success-comment');
|
11 | const findSRIssues = require('./find-sr-issues');
|
12 |
|
13 | module.exports = async (pluginConfig, context) => {
|
14 | const {
|
15 | options: {branch, repositoryUrl},
|
16 | lastRelease,
|
17 | commits,
|
18 | nextRelease,
|
19 | releases,
|
20 | logger,
|
21 | } = context;
|
22 | const {
|
23 | githubToken,
|
24 | githubUrl,
|
25 | githubApiPathPrefix,
|
26 | proxy,
|
27 | successComment,
|
28 | failComment,
|
29 | failTitle,
|
30 | releasedLabels,
|
31 | } = resolveConfig(pluginConfig, context);
|
32 |
|
33 | const github = getClient({githubToken, githubUrl, githubApiPathPrefix, proxy});
|
34 | let {name: repo, owner} = parseGithubUrl(repositoryUrl);
|
35 |
|
36 | [owner, repo] = (await github.repos.get({repo, owner})).data.full_name.split('/');
|
37 |
|
38 | const errors = [];
|
39 |
|
40 | if (successComment === false) {
|
41 | logger.log('Skip commenting on issues and pull requests.');
|
42 | } else {
|
43 | const parser = issueParser('github', githubUrl ? {hosts: [githubUrl]} : {});
|
44 | const releaseInfos = releases.filter(release => Boolean(release.name));
|
45 | const shas = commits.map(({hash}) => hash);
|
46 |
|
47 | const searchQueries = getSearchQueries(`repo:${owner}/${repo}+type:pr+is:merged`, shas).map(
|
48 | async q => (await github.search.issuesAndPullRequests({q})).data.items
|
49 | );
|
50 |
|
51 | const prs = await pFilter(
|
52 | uniqBy(flatten(await Promise.all(searchQueries)), 'number'),
|
53 | async ({number}) =>
|
54 |
|
55 | (await github.pullRequests.listCommits({owner, repo, pull_number: number})).data.find(({sha}) =>
|
56 | shas.includes(sha)
|
57 | ) || shas.includes((await github.pullRequests.get({owner, repo, pull_number: number})).data.merge_commit_sha)
|
58 | );
|
59 |
|
60 | debug('found pull requests: %O', prs.map(pr => pr.number));
|
61 |
|
62 |
|
63 | const issues = [...prs.map(pr => pr.body), ...commits.map(commit => commit.message)].reduce((issues, message) => {
|
64 | return message
|
65 | ? issues.concat(
|
66 | parser(message)
|
67 | .actions.close.filter(action => isNil(action.slug) || action.slug === `${owner}/${repo}`)
|
68 | .map(action => ({number: parseInt(action.issue, 10)}))
|
69 | )
|
70 | : issues;
|
71 | }, []);
|
72 |
|
73 | debug('found issues via comments: %O', issues);
|
74 |
|
75 | await Promise.all(
|
76 | uniqBy([...prs, ...issues], 'number').map(async issue => {
|
77 | const body = successComment
|
78 | ? template(successComment)({branch, lastRelease, commits, nextRelease, releases, issue})
|
79 | : getSuccessComment(issue, releaseInfos, nextRelease);
|
80 | try {
|
81 |
|
82 | const state = issue.state || (await github.issues.get({owner, repo, issue_number: issue.number})).data.state;
|
83 |
|
84 | if (state === 'closed') {
|
85 |
|
86 | const comment = {owner, repo, issue_number: issue.number, body};
|
87 | debug('create comment: %O', comment);
|
88 | const {
|
89 | data: {html_url: url},
|
90 | } = await github.issues.createComment(comment);
|
91 | logger.log('Added comment to issue #%d: %s', issue.number, url);
|
92 |
|
93 | if (releasedLabels) {
|
94 |
|
95 |
|
96 | await github.request('POST /repos/:owner/:repo/issues/:number/labels', {
|
97 | owner,
|
98 | repo,
|
99 | number: issue.number,
|
100 | data: releasedLabels,
|
101 | });
|
102 | logger.log('Added labels %O to issue #%d', releasedLabels, issue.number);
|
103 | }
|
104 | } else {
|
105 | logger.log("Skip comment and labels on issue #%d as it's open: %s", issue.number);
|
106 | }
|
107 | } catch (error) {
|
108 | if (error.status === 404) {
|
109 | logger.error("Failed to add a comment to the issue #%d as it doesn't exists.", issue.number);
|
110 | } else {
|
111 | errors.push(error);
|
112 | logger.error('Failed to add a comment to the issue #%d.', issue.number);
|
113 |
|
114 | }
|
115 | }
|
116 | })
|
117 | );
|
118 | }
|
119 |
|
120 | if (failComment === false || failTitle === false) {
|
121 | logger.log('Skip closing issue.');
|
122 | } else {
|
123 | const srIssues = await findSRIssues(github, failTitle, owner, repo);
|
124 |
|
125 | debug('found semantic-release issues: %O', srIssues);
|
126 |
|
127 | await Promise.all(
|
128 | srIssues.map(async issue => {
|
129 | debug('close issue: %O', issue);
|
130 | try {
|
131 |
|
132 | const updateIssue = {owner, repo, issue_number: issue.number, state: 'closed'};
|
133 | debug('closing issue: %O', updateIssue);
|
134 | const {
|
135 | data: {html_url: url},
|
136 | } = await github.issues.update(updateIssue);
|
137 | logger.log('Closed issue #%d: %s.', issue.number, url);
|
138 | } catch (error) {
|
139 | errors.push(error);
|
140 | logger.error('Failed to close the issue #%d.', issue.number);
|
141 |
|
142 | }
|
143 | })
|
144 | );
|
145 | }
|
146 |
|
147 | if (errors.length > 0) {
|
148 | throw new AggregateError(errors);
|
149 | }
|
150 | };
|