diff --git a/.github/github-bot.yml b/.github/github-bot.yml index 956722a..1109abd 100644 --- a/.github/github-bot.yml +++ b/.github/github-bot.yml @@ -2,3 +2,8 @@ _extends: probot-settings github-team: slug: 'go' + +prchecklist: + title: 'PR Guildelines checklist' + checklist: + - "All contribution guidelines were followed" diff --git a/.gitignore b/.gitignore index d570088..02746ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ - +.env +*.pem \ No newline at end of file diff --git a/bot_scripts/manage-pr-checklist.js b/bot_scripts/manage-pr-checklist.js new file mode 100644 index 0000000..e6e88c3 --- /dev/null +++ b/bot_scripts/manage-pr-checklist.js @@ -0,0 +1,154 @@ +// Description: +// Script that listens to GitHub pull requests events +// and manages a checklist based on configuration in github-bot.yml +// It sets the mergeable status based checklist state +// +// Dependencies: +// github: "^13.1.0" +// probot-config: "^0.1.0" +// +// Author: +// virneo + +const fetch = require('node-fetch') +const getConfig = require('probot-config') +const defaultConfig = require('../lib/config') +const botName = 'manage-pr-checklist' + +module.exports = (robot) => { + robot.on( + [ + 'pull_request.opened', + 'pull_request.edited' + ], + context => { + // Make sure we don't listen to our own messages + if (context.isBot) { return } + handlePullRequest(context, robot) + }) + robot.on('issue_comment', context => { + // Make sure we don't listen to our own messages + if (context.isBot) { return } + handleIssue(context, robot) + }) +} + +async function handleIssue (context, robot) { + if (context.payload.issue.pull_request) { + const res = await fetch(context.payload.issue.pull_request.url) + const pr = await res.json() + context.payload.pull_request = pr + return handlePullRequest(context, robot) + } +} + +async function handlePullRequest (context, robot) { + const config = await getConfig(context, 'github-bot.yml', defaultConfig(robot, '.github/github-bot.yml')) + const settings = config ? config['prchecklist'] : null + if (!settings) { + return + } + if (settings.title == null) settings.title = '' + if (settings.checklist == null) settings.checklist = {} + const currentStatus = await getCurrentStatus(context) + const {isChecklistComplete, firstCheck} = await verifyChecklist(context, settings) + const newStatus = isChecklistComplete ? 'success' : 'pending' + const hasChange = firstCheck || currentStatus !== newStatus + const logStatus = isChecklistComplete ? '✅' : '⏳' + const shortUrl = context.payload.pull_request.url + + if (!hasChange) { + robot.log.info(`${botName} - 😐${logStatus} ${shortUrl}`) + return + } + + try { + await context.github.repos.createStatus(context.repo({ + sha: context.payload.pull_request.head.sha, + state: newStatus, + target_url: 'https://github.com/status-im/status-github-bot.git', + description: isChecklistComplete ? 'ready for merge' : 'PR Checklist is incomplete', + context: 'PRChecklist' + })) + + robot.log.info(`${botName} - 💾${logStatus} ${shortUrl}`) + } catch (err) { + robot.log.error(`${botName} - Couldn't create status for commits in the PR: ${err}`, context.payload.pull_request.id) + } +} + +async function getCurrentStatus (context) { + const {data: {statuses}} = await context.github.repos.getCombinedStatusForRef(context.repo({ + ref: context.payload.pull_request.head.sha + })) + + return (statuses.find(status => status.context === 'PRChecklist') || {}).state +} + +async function createOrEditChecklist (context, checkList, header) { + const owner = context.payload.repository.owner.login + const repo = context.payload.repository.name + const number = context.payload.pull_request.number + if (checkList && checkList.length > 0) { + let body = ' \n' + header + '\n' + for (const key of checkList) { + body += '- [ ] ' + key + '\n' + } + await context.github.issues.createComment({ owner, repo, number, body }) + } +} + +async function verifyChecklist (context, settings) { + let isChecklistComplete = true + let firstCheck = false + const {found, body} = await getCheckList(context) + if (found) { + if (body) { + for (const str of body) { + const res = str.match(/(-\s\[(\s)])(.*)/gm) + if (res != null) { + isChecklistComplete = false + break + } + } + } else { + isChecklistComplete = false + } + } else { + await createOrEditChecklist(context, settings.checklist, settings.title) + isChecklistComplete = false + firstCheck = true + } + return {isChecklistComplete, firstCheck} +} + +async function getCheckList (context) { + try { + const owner = context.payload.repository.owner.login + const repo = context.payload.repository.name + const number = context.payload.pull_request.number + const comments = await context.github.paginate(context.github.issues.getComments({ owner, repo, number }), res => res.data) + for (const comment of comments) { + const {found, body} = checkPRChecklist(comment.body) + if (found) { + return {found, body} + } + } + return false + } catch (e) { + return true + } +} + +function checkPRChecklist (str) { + let found = false + let body = null + const isBotComment = str.match(/()/g) + if (isBotComment == null) return {found, body} + let res = str.match(/(-\s\[(\s|x)])(.*)/gm) + if (res && res.length > 0) { + found = true + body = res + } + return {found, body} +} diff --git a/index.js b/index.js index 438e61f..25c43f1 100644 --- a/index.js +++ b/index.js @@ -38,7 +38,8 @@ module.exports = async (robot) => { require('./bot_scripts/notify-reviewers-via-slack')(robot) require('./bot_scripts/tip-kudos-recipients')(robot) require('./bot_scripts/check-bot-balance')(robot) - + require('./bot_scripts/manage-pr-checklist')(robot) + // For more information on building apps: // https://probot.github.io/docs/