UNPKG

5.85 kBJavaScriptView Raw
1const {isNil, uniqBy, template, flatten} = require('lodash');
2const parseGithubUrl = require('parse-github-url');
3const pFilter = require('p-filter');
4const AggregateError = require('aggregate-error');
5const issueParser = require('issue-parser');
6const debug = require('debug')('semantic-release:github');
7const resolveConfig = require('./resolve-config');
8const getClient = require('./get-client');
9const getSearchQueries = require('./get-search-queries');
10const getSuccessComment = require('./get-success-comment');
11const findSRIssues = require('./find-sr-issues');
12
13module.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 // In case the repo changed name, get the new `repo`/`owner` as the search API will not follow redirects
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 // eslint-disable-next-line camelcase
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) // eslint-disable-line camelcase
58 );
59
60 debug('found pull requests: %O', prs.map(pr => pr.number));
61
62 // Parse the release commits message and PRs body to find resolved issues/PRs via comment keyworkds
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 // eslint-disable-next-line camelcase
82 const state = issue.state || (await github.issues.get({owner, repo, issue_number: issue.number})).data.state;
83
84 if (state === 'closed') {
85 // eslint-disable-next-line camelcase
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 // Don’t use .issues.addLabels for GHE < 2.16 support
95 // https://github.com/semantic-release/github/issues/138
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 // Don't throw right away and continue to update other issues
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 // eslint-disable-next-line camelcase
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 // Don't throw right away and continue to close other issues
142 }
143 })
144 );
145 }
146
147 if (errors.length > 0) {
148 throw new AggregateError(errors);
149 }
150};