UNPKG

snowball-slackbot

Version:

Send reminder messages to Slack users about GitHub PRs that require review, changes, or are approved.

357 lines (314 loc) 12.7 kB
const github = require('octonode'); const token = process.env.GITHUB_TOKEN; const utils = require('./utils'); const _ = require('lodash'); let prVitalInfo = []; const client = github.client(token); const self = module.exports = { // Iteratively retrieve all repos associated with all orgs listed allOrgsRepos: (orgs) => { const promiseArr = []; const orgList = orgs.split(','); // Iterate through each org, retrieve the repo owner name and repo name itself orgList.forEach((orgName) => { const repos = []; promiseArr.push( new Promise((resolve, reject) => { // Make an API call with each repo name in the orgList using octonode's .repos method client.org(orgName).repos({}, (err, data, headers) => { if (err) { reject(err); } else { for (let i in data) { // Push these values to a repos array that will be used in subsequent API calls repos.push({[data[i]['owner']['login']]: data[i]['name']}); } } // Resolves to [{'org1': 'repo'}, {'org2': 'repo2'}, ...] resolve(repos); }); }) ) }); return Promise.all(promiseArr); }, // Composes pull request slugs for subsequent API calls composePullRequestSlugs: (repos) => { const promiseArr = []; // Takes in the array from the previous function, // retrieves the values for each owner and repo repos.forEach((repo) => { const owner = Object.keys(repo).toString(); const pulls = []; const repoName = repo[owner]; // For each repo, compose a partial URL containing org, repo and PR info promiseArr.push( new Promise((resolve, reject) => { // Make an API call to get PRs for each repo and owner with a partial URL made of // owner/repo_name using octonode's .repo and .prs methods client.repo(owner + '/' + repoName).prs({}, (err, data, headers) => { if (err) { reject(err); } else { for (let i in data) { // Push values of org, repo, and PR #s to an array for use in subsequent API calls (composeRequest) pulls.push(data[i]['url'].replace('https://api.github.com/repos/','').replace('/pulls/', ':')); } } // Resolves to ['org/repo:PR#', ...] resolve(pulls); }); }) ) }); return Promise.all(promiseArr); }, // Grabs all open pull requests for all repos in all orgs getOpenPullRequestInfo: (repos) => { const promiseArr = []; repos.forEach((repo) => { const owner = Object.keys(repo).toString(); const prInfo = []; const repoName = repo[owner]; // For each repo, retrieve info on all open pull requests promiseArr.push( new Promise((resolve, reject) => { // Make an API call to get PRs for each repo and owner with a partial URL made of // owner/repo_name using octonode's .repo and .prs methods client.repo(owner + '/' + repoName).prs({}, (err, data, headers) => { if (err) { reject(err); } else { for (let i in data) { prInfo.push({ 'author': data[i]['user']['login'], 'title': data[i]['title'], 'pr_url': data[i]['html_url'], 'api_url': data[i]['url'], 'pr_id': data[i]['id'], 'created_at': data[i]['created_at'] }); } } // Resolves to [{author: 'PR author', title: 'PR title', pr_url: 'PR URL', pr_id: 'ID', created_at: 'date created'}, ...] resolve(prInfo); }); }) ) }); return Promise.all(promiseArr); }, // Assign globally accessible object to contain info on PRs (author, url, title, timestamp) referenceOpenPulls: (pullRequestInfo) => { prVitalInfo = pullRequestInfo; return prVitalInfo; }, // Prepare a partial URL for use in retrieving info on individual PRs composeRequest: (pullRequestSlug) => { promiseArr = []; // For each partial URL with repo and PR # info, reformat the info into usable API params // for use with subsequent octonode calls. pullRequestSlug.forEach((slug) => { const params = {}; const repo = slug.split(':')[0]; const pullNumber = slug.split(':')[1]; params[repo] = pullNumber; requestArgs = []; promiseArr.push( new Promise((resolve, reject) => { requestArgs.push(params); // Resolves to [{'org/repo': 'PR #'}, ...] resolve(requestArgs); }) ) }); return Promise.all(promiseArr); }, // Get all reviews associated with a given PR with certain review states allReviews: (requests) => { promiseArr = []; // For all open PRs, look for those reviews that have either a state of 'APPROVED' or 'CHANGES REQUESTED' requests.forEach((request) => { const repoStr = Object.keys(request).toString(); const pullNum = Number(Object.values(request)); const reviews = []; promiseArr.push( new Promise((resolve, reject) => { // Make an API call to get reviews for each open PR // using octonode's .pr and .reviews methods with the params from composeRequest client.pr(repoStr, pullNum).reviews((err, data, headers) => { if (err) { reject(err); } else { for (let i in data) { if (data[i]['state'] == ['APPROVED'] || data[i]['state'] == ['CHANGES_REQUESTED']) { reviews.push(data[i]); } } } // Resolves to what one would get from https://developer.github.com/v3/pulls/reviews/ resolve(reviews); }); }) ) }); return Promise.all(promiseArr); }, // Gets most recent reviews on each open PR for each unique reviewer getMostRecentReviews: (data) => { promiseArr = []; // Reverse the reviews data response, this orders the reviews reverse chronologically // placing the most recent reviews at the front of the array const reversed = data.reverse(); // Get all reviwer IDs associated with reviewers on a PR and then filter out the duplicate values const reviewerIds = reversed.map(item => item.user.id); const uniqueReviewers = _.uniq(reviewerIds); // For each unique reviewer, get their most recent review on the PR for (let i in uniqueReviewers) { const reviews = []; // Get the indices of the unique reviwers and the retrieve the corresponding review const uniqueIndices = reviewerIds.indexOf(uniqueReviewers[i]); reviews.push(reversed[uniqueIndices]); // For each of these most recent reviews, push some relevant infomration about them to a new obj for (let i in reviews) { const lastReviews = []; if (reviews.length) { promiseArr.push( new Promise((resolve, reject) => { lastReviews.push({ reviewer_name: reviews[i]['user']['login'], reviewer_id: reviews[i]['user']['id'], review_id: reviews[i]['id'], review_state: reviews[i]['state'], review_pr: reviews[i]['pull_request_url'] }); // Resolves to [ { reviewer_name: string, reviewer_id: integer, review_id: integer, review_state: string, review_pr: string }, ... ] resolve(lastReviews); }) ) } } } return Promise.all(promiseArr); }, // Match up the review states for each open review with its parent PR matchPulls: (recentReviews) => { const matched = []; const pullInfo = [].concat(prVitalInfo); // For each recent review, find the corresponding parent PR for (let i in recentReviews) { // Filter array of all open PRs by matching the on the API URLs for each PR and its reviews const matchedPull = pullInfo.filter(obj => obj.api_url.match(recentReviews[i]['review_pr'])); const reviewer = recentReviews[i]['reviewer_name']; const reviewState = recentReviews[i]['review_state']; for (let i in matchedPull) { // Insert reviewer ID and state into the rest of the info on that PR matchedPull[i].reviewer = reviewer; matchedPull[i].review_state = reviewState; } matched.push(matchedPull); } // Returns { author: string, title: string, pr_url: string, api_url: string, created_at: datetime, reviewer: string, review_state: string }, ...] return [].concat(...matched); }, // Group open PRs by whether they were approved or not approvedOrNot: (matchedPulls) => { // Get all unique PR ids const uniqPulls = _.uniq((matchedPulls.map(pull => pull.pr_id))); // An accumulator to store the grouped PRs by whether they're approved or not const sortedPulls = { approved: [], notApproved: [], }; // Reduce input array to group pulls by review state const reviewReducer = (accumulator, prId) => { const approvedPulls = matchedPulls.filter((pull) => pull.pr_id === prId && pull.review_state === 'APPROVED'); const unApprovedPulls = matchedPulls.filter((pull) => pull.pr_id === prId && pull.review_state !== 'APPROVED'); // Checks if there's more than one review for a PR and only pushes // to approved if all reviews are in the `APPROVED` state. if (!unApprovedPulls.length) { // We don't care who the reviewer is, just need know that the PR and its state, // otherwise there'll be multiple messages sent, so just push the first element. accumulator.approved.push(approvedPulls[0]); } else { accumulator.notApproved.push(unApprovedPulls[0]); } return accumulator; }; // Reduce PRs grouped by review states into accumulator const pullRequestsToMsg = uniqPulls.reduce(reviewReducer, sortedPulls); // Returns // { // approved: [ // { // author: string, // title: string, // pr_url: string, // api_url: string, // created_at: datetime, // reviewer: string, // review_state: string // }, // ... // ], // notApproved: [(objs w same keys as above)}, ...] // }; return pullRequestsToMsg; }, // Retrieve all reviewers requested on all open PRs // Resolves to an array of hashes containing info on requested reviewers for pull requests // If no reviewer was requested on a PR this returns an empty array for that pull request allReviewsRequested: (requests) => { promiseArr = []; // Iterates over the hash above to retrieve reviews for each PR requests.forEach((request) => { const repoStr = Object.keys(request).toString(); const pullNum = Number(Object.values(request)); const reviewers = []; promiseArr.push( new Promise((resolve, reject) => { // Make an API call to get reviews requests for each open PR // using octonode's .pr and .reviews methods with the params from composeRequest client.pr(repoStr, pullNum).reviewRequests((err, data, headers) => { if (err) { reject(err); } else { reviewers.push(data.users); } resolve(reviewers); }); }) ) }); return Promise.all(promiseArr); }, // Maps reviewers requested to the PRs that they were requested to review mapReviewsRequestedtoPRs: (reviewers) => { const requestedReviewers = prVitalInfo.map((item, i) => { const formatted = {}; formatted['author'] = item.author, formatted['created'] = item.created_at, formatted['pr_url'] = item.pr_url, formatted['pr_title'] = item.title, formatted['reviewers_requested'] = reviewers[i]; return formatted; }); // Returns an array of hashes containing info on PRs and reviewers requested return requestedReviewers; }, // Generic function to call the GitHub API callApi: (url) => { const headers = { 'Authorization': 'token ' + token, 'User-Agent': 'Request-Promise' }; return client.get(url, {}, (err, status, data, headers) => { if (err) { console.log('There was an error: ', err); console.log('Status code: ', status); } else { return(data); } }); } }