UNPKG

@robotical/martyblocks

Version:
181 lines (163 loc) 6.8 kB
const {isNil, uniqBy, template, flatten, isEmpty} = require('lodash'); const pFilter = require('p-filter'); const AggregateError = require('aggregate-error'); const issueParser = require('issue-parser'); const debug = require('debug')('semantic-release:github'); const parseGithubUrl = require('./parse-github-url'); const resolveConfig = require('./resolve-config'); const getClient = require('./get-client'); const getSearchQueries = require('./get-search-queries'); const getSuccessComment = require('./get-success-comment'); const findSRIssues = require('./find-sr-issues'); const {RELEASE_NAME} = require('./definitions/constants'); const getReleaseLinks = require('./get-release-links'); module.exports = async (pluginConfig, context) => { const { options: {repositoryUrl}, commits, nextRelease, releases, logger, } = context; const { githubToken, githubUrl, githubApiPathPrefix, proxy, successComment, failComment, failTitle, releasedLabels, addReleases, } = resolveConfig(pluginConfig, context); const octokit = getClient({githubToken, githubUrl, githubApiPathPrefix, proxy}); // In case the repo changed name, get the new `repo`/`owner` as the search API will not follow redirects const {data: repoData} = await octokit.request('GET /repos/{owner}/{repo}', parseGithubUrl(repositoryUrl)); const [owner, repo] = repoData.full_name.split('/'); const errors = []; if (successComment === false) { logger.log('Skip commenting on issues and pull requests.'); } else { const parser = issueParser('github', githubUrl ? {hosts: [githubUrl]} : {}); const releaseInfos = releases.filter((release) => Boolean(release.name)); const shas = commits.map(({hash}) => hash); const searchQueries = getSearchQueries(`repo:${owner}/${repo}+type:pr+is:merged`, shas).map( async (q) => (await octokit.request('GET /search/issues', {q})).data.items ); const searchQueriesResults = await Promise.all(searchQueries); const uniqueSearchQueriesResults = uniqBy(flatten(searchQueriesResults), 'number'); const prs = await pFilter(uniqueSearchQueriesResults, async ({number}) => { const commits = await octokit.paginate('GET /repos/{owner}/{repo}/pulls/{pull_number}/commits', { owner, repo, pull_number: number, }); const matchingCommit = commits.find(({sha}) => shas.includes(sha)); if (matchingCommit) return matchingCommit; const {data: pullRequest} = await octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', { owner, repo, pull_number: number, }); return shas.includes(pullRequest.merge_commit_sha); }); debug( 'found pull requests: %O', prs.map((pr) => pr.number) ); // Parse the release commits message and PRs body to find resolved issues/PRs via comment keyworkds const issues = [...prs.map((pr) => pr.body), ...commits.map((commit) => commit.message)].reduce( (issues, message) => { return message ? issues.concat( parser(message) .actions.close.filter((action) => isNil(action.slug) || action.slug === `${owner}/${repo}`) .map((action) => ({number: Number.parseInt(action.issue, 10)})) ) : issues; }, [] ); debug('found issues via comments: %O', issues); await Promise.all( uniqBy([...prs, ...issues], 'number').map(async (issue) => { const body = successComment ? template(successComment)({...context, issue}) : getSuccessComment(issue, releaseInfos, nextRelease); try { const comment = {owner, repo, issue_number: issue.number, body}; debug('create comment: %O', comment); const { data: {html_url: url}, } = await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', comment); logger.log('Added comment to issue #%d: %s', issue.number, url); if (releasedLabels) { const labels = releasedLabels.map((label) => template(label)(context)); await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/labels', { owner, repo, issue_number: issue.number, data: labels, }); logger.log('Added labels %O to issue #%d', labels, issue.number); } } catch (error) { if (error.status === 403) { logger.error('Not allowed to add a comment to the issue #%d.', issue.number); } else if (error.status === 404) { logger.error("Failed to add a comment to the issue #%d as it doesn't exist.", issue.number); } else { errors.push(error); logger.error('Failed to add a comment to the issue #%d.', issue.number); // Don't throw right away and continue to update other issues } } }) ); } if (failComment === false || failTitle === false) { logger.log('Skip closing issue.'); } else { const srIssues = await findSRIssues(octokit, failTitle, owner, repo); debug('found semantic-release issues: %O', srIssues); await Promise.all( srIssues.map(async (issue) => { debug('close issue: %O', issue); try { const updateIssue = {owner, repo, issue_number: issue.number, state: 'closed'}; debug('closing issue: %O', updateIssue); const { data: {html_url: url}, } = await octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', updateIssue); logger.log('Closed issue #%d: %s.', issue.number, url); } catch (error) { errors.push(error); logger.error('Failed to close the issue #%d.', issue.number); // Don't throw right away and continue to close other issues } }) ); } if (addReleases !== false && errors.length === 0) { const ghRelease = releases.find((release) => release.name && release.name === RELEASE_NAME); if (!isNil(ghRelease)) { const ghRelaseId = ghRelease.id; const additionalReleases = getReleaseLinks(releases); if (!isEmpty(additionalReleases) && !isNil(ghRelaseId)) { const newBody = addReleases === 'top' ? additionalReleases.concat('\n---\n', nextRelease.notes) : nextRelease.notes.concat('\n---\n', additionalReleases); await octokit.request('PATCH /repos/{owner}/{repo}/releases/{release_id}', { owner, repo, release_id: ghRelaseId, body: newBody, }); } } } if (errors.length > 0) { throw new AggregateError(errors); } };