Skip to content

Commit

Permalink
feat: The BOT
Browse files Browse the repository at this point in the history
  • Loading branch information
marcbachmann committed Dec 2, 2019
1 parent b01a9c8 commit 473c0bb
Show file tree
Hide file tree
Showing 10 changed files with 2,003 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.env
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# GitHub App ID
APP_ID=
WEBHOOK_SECRET=CAFFCF94-2B07-4EBC-BEC8-646CBE46FBA5
PRIVATE_KEY=
PORT=8080
LOG_LEVEL=debug
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.env
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM livingdocs/node:12.0

COPY package.json /app/
RUN npm install --production && npm cache clean -f
COPY . /app/
ENV PORT 8080
EXPOSE 8080
CMD ["node", "/app/index.js"]
8 changes: 8 additions & 0 deletions app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
default_events:
- issue_comment
- pull_request_review_comment

default_permissions:
contents: write
issues: write
pull_requests: write
60 changes: 60 additions & 0 deletions backport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const cherryPick = require('./cherry-pick')

module.exports = async function (context, targetBase) {
const pr = await getPullRequest(context)
const reviewers = await getReviewers(context)
reviewers.push(pr.user.login)

const token = await getToken(context.payload.installation.id)
const targetBranch = await cherryPick(context, pr, targetBase, token)
const backport = await createPullRequest(context, pr, targetBase, targetBranch)
await requestReviewers(context, backport.data.number, reviewers)

return backport
}

// Obtain token to push to this repo
let cached
const githubAuth = require('github-app')({
id: process.env.APP_ID,
cert: process.env.PRIVATE_KEY ?
process.env.PRIVATE_KEY :
(process.env.PRIVATE_KEY_FILE && require('fs').readFileSync(process.env.PRIVATE_KEY_FILE))
})

async function getToken (installationId) {
if (cached && Date.parse(cached.expires_at) > (Date.now() + 1000 * 60)) return cached.token
const token = await githubAuth.createToken(installationId)
cached = token.data
return cached.token
}

async function createPullRequest (context, origPR, targetBase, targetBranch) {
return context.github.pullRequests.create(context.repo({
title: `${origPR.title} [${targetBase}] `,
head: targetBranch,
base: targetBase,
body: `Backport of #${origPR.number}\n\n${origPR.body}`
}))
}

async function getPullRequest (context) {
if (context.payload.pull_request) return context.payload.pull_request
const pullRequest = await context.github.pullRequests.get(context.issue())
return pullRequest.data
}

async function getReviewers (context) {
const reviewers = await context.github.pullRequests.listReviews(context.issue())
const rewiewRequests = await context.github.pullRequests.listReviewRequests(context.issue())
const reviewerIds = reviewers.data.map(reviewer => reviewer.user.login)
const reviewerRequestedIds = rewiewRequests.data.users.map(reviewer => reviewer.login)
return [...reviewerIds, ...reviewerRequestedIds]
}

async function requestReviewers (context, prId, reviewers) {
return context.github.pullRequests.createReviewRequest(context.repo({
number: prId,
reviewers: reviewers
}))
}
63 changes: 63 additions & 0 deletions cherry-pick.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const path = require('path')
const tmp = require('os').tmpdir()
const fs = require('fs-extra')
const execFile = require('util').promisify(require('child_process').execFile)

function createGit (dir, context) {
function exec (file, args = [], options) {
context.log.debug(file, ...args)
return execFile(file, args, {cwd: dir, ...options})
}
function git (args, options) { return exec('/usr/bin/git', args, options) }
function child (subdir) { return createGit(path.join(dir, subdir), context) }
exec.git = git
exec.child = child
exec.exec = exec
exec.path = dir
return exec
}

async function getWorktree (slug, context, token) {
const dir = path.join(tmp, 'backport', slug)
const worktree = createGit(dir, context)
try {
await fs.stat(path.join(worktree.path, '.git'))
} catch (err) {
if (err.code !== 'ENOENT') throw err
await fs.mkdir(worktree.path, {recursive: true})
await worktree.git(['clone', '--bare', `https://x-access-token:${token}@github.com/${slug}.git`, '.git'])
await worktree.git(['update-ref', '--no-deref', 'HEAD', 'HEAD^{commit}'])
await fs.appendFile(path.join(worktree.path, '.git/config'), ' fetch = +refs/heads/*:refs/remotes/origin/*')

// Setup config
await worktree.git(['config', '--local', 'user.email', '[email protected]'])
await worktree.git(['config', '--local', 'user.name', 'Machine User'])
await worktree.git(['config', '--local', 'commit.gpgsign', 'false'])
}
return worktree
}

module.exports = async function (context, pr, targetBase, token) {
const slug = `${context.repo().owner}/${context.repo().repo}`
const worktree = await getWorktree(slug, context, token)
const targetBranch = 'backport/' + pr.number + '/' + targetBase
const targetDir = `${Date.now()}-${targetBranch}`
const branch = worktree.child(targetDir)

try {
// Fetch and create branch
await worktree.git(['fetch', 'origin', targetBase, pr.head.ref])
await worktree.git(['worktree', 'add', '-b', targetDir, targetDir, pr.head.ref])

// Rebase the branch onto the new base and push
await branch.git(['rebase', '--onto', targetBase, pr.base.sha])
await branch.git(['push', 'origin', `${targetDir}:${targetBranch}`])
return targetBranch
} catch (err) {
throw err
} finally {
try {
await worktree.git(['worktree', 'remove', '--force', targetDir])
} catch {}
}
}
46 changes: 46 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const {Probot} = require('probot')
const backport = require('./backport')

Probot.run(backportApp)

async function backportApp (app) {
async function handler (context) {
const issue = context.payload.issue || context.payload.pull_request
const comment = context.payload.comment
if (!issue.html_url.endsWith(`pull/${issue.number}`)) return

const targetBranch = matchComment(comment.body)
if (!targetBranch) return

try {
await updateComment(context, `🕑 ${comment.body}`)
await backport(context, targetBranch)
await updateComment(context, `🎉 ${comment.body}`)
} catch (err) {
context.log.warn(`Backport to ${targetBranch} failed`, err)
return updateComment(context, [
`💥 ${comment.body}`,
'',
`The backport to ${targetBranch} failed.`,
`Please do this backport manually.`,
'```js',
err.stack,
'```'
].join('\n'))
}
}

app.on('pull_request_review_comment.created', handler)
app.on('issue_comment.created', handler)
}

async function updateComment (context, body) {
const comment = context.payload.comment
const resource = comment.pull_request_review_id ? context.github.pulls : context.github.issues;
return resource.updateComment(context.issue({comment_id: comment.id, body}))
}

const commandRegexp = /^ *\/backport ([a-zA-Z0-9\/\-._]+) *([a-zA-Z0-9\/\-._]+)?$/im
function matchComment (comment) {
return commandRegexp.exec(comment.trim()) && RegExp.$1
}
Loading

0 comments on commit 473c0bb

Please sign in to comment.