Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scripts: new attempt-backport script for PRs #90

Merged
merged 6 commits into from
Nov 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions lib/node-repo.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,30 @@ function updatePrWithLabels (options, labels) {
})
}

function removeLabelFromPR (options, label) {
// no need to request github if we didn't resolve a label
if (!label) {
return
}

options.logger.debug('Trying to remove label: ' + label)

githubClient.issues.removeLabel({
user: options.owner,
repo: options.repo,
number: options.prId,
name: label
}, (err) => {
if (err) {
if (err.code === 404) return options.logger.info('Label to remove did not exist, bailing ' + label)

return options.logger.error(err, 'Error while removing a label')
}

options.logger.info('Removed a label ' + label)
})
}

function fetchExistingLabels (options, cb) {
const cacheKey = `${options.owner}:${options.repo}`

Expand Down Expand Up @@ -93,6 +117,8 @@ function itemsInCommon (arr1, arr2) {
return arr1.filter((item) => arr2.indexOf(item) !== -1)
}

exports.removeLabelFromPR = removeLabelFromPR
exports.updatePrWithLabels = updatePrWithLabels
exports.resolveLabelsThenUpdatePr = deferredResolveLabelsThenUpdatePr

// exposed for testability
Expand Down
215 changes: 215 additions & 0 deletions scripts/attempt-backport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
'use strict'

const child_process = require('child_process')
const debug = require('debug')('attempt-backport')
const request = require('request')
const node_repo = require('../lib/node-repo')
const updatePrWithLabels = node_repo.updatePrWithLabels
// const removeLabelFromPR = node_repo.removeLabelFromPR

const enabledRepos = ['node']
const queue = []
let inProgress = false

module.exports = function (app) {
if (!global._node_repo_dir) return

app.on('pull_request.opened', handlePrUpdate)
// Pull Request updates
app.on('pull_request.synchronize', handlePrUpdate)

function handlePrUpdate (event, owner, repo) {
if (!~enabledRepos.indexOf(repo)) return

if (event.pull_request.base.ref !== 'master') return

const prId = event.number
const options = { owner, repo, prId, logger: event.logger }

debug(`/${owner}/${repo}/pull/${prId} sync`)
queueAttemptBackport(options, 7, false)
queueAttemptBackport(options, 6, true)
queueAttemptBackport(options, 4, true)

if (!inProgress) processNextBackport()
}

// to trigger polling manually
app.get('/attempt-backport/pr/:owner/:repo/:id', (req, res) => {
const owner = req.params.owner
const repo = req.params.repo
const prId = parseInt(req.params.id, 10)
const options = { owner, repo, prId, logger: req.log }

if (~enabledRepos.indexOf(repo)) {
queueAttemptBackport(options, 7, false)
queueAttemptBackport(options, 6, true)
queueAttemptBackport(options, 4, true)
}

if (!inProgress) processNextBackport()

res.end()
})
}

function processNextBackport () {
const item = queue.shift()
if (!item) return

if (typeof item !== 'function') {
debug(`item was not a function! - queue size: ${queue.length}`)
return
} else if (inProgress) {
debug(`was still in progress! - queue size: ${queue.length}`)
return
}
item()
}

function queueAttemptBackport (options, version, isLTS) {
queue.push(function () {
options.logger.debug(`processing a new backport to v${version}`)
attemptBackport(options, version, isLTS, processNextBackport)
})
}

function attemptBackport (options, version, isLTS, cb) {
// Start
gitAmAbort()

function wrapCP (cmd, args, opts, callback) {
let exited = false

if (arguments.length === 3) {
callback = opts
opts = {}
}

opts.cwd = global._node_repo_dir

const cp = child_process.spawn(cmd, args, opts)
const argsString = [cmd, ...args].join(' ')

cp.on('error', function (err) {
debug(`child_process err: ${err}`)
if (!exited) onError()
})
cp.on('exit', function (code) {
exited = true
if (!cb) {
debug(`error before exit, code: ${code}, on '${argsString}'`)
return
} else if (code > 0) {
debug(`exit code > 0: ${code}, on '${argsString}'`)
onError()
return
}
callback()
})
// Useful when debugging.
//
// cp.stdout.on('data', (data) => {
// console.log(data.toString())
// })
// cp.stderr.on('data', (data) => {
// console.log(data.toString())
// })

return cp
}

function onError () {
if (!cb) return
const _cb = cb
setImmediate(() => {
options.logger.debug(`backport to ${version} failed`)
if (!isLTS) updatePrWithLabels(options, [`dont-land-on-v${version}.x`])
setImmediate(() => {
inProgress = false
_cb()
})
})
cb = null
}

function gitAmAbort () {
// TODO(Fishrock123): this should probably just merge into wrapCP
let exited = false
options.logger.debug('aborting any previous backport attempt...')

const cp = child_process.spawn('git', ['am', '--abort'], { cwd: global._node_repo_dir })
const argsString = 'git am --abort'

cp.on('error', function (err) {
debug(`child_process err: ${err}`)
if (!exited) onError()
})
cp.on('exit', function (code) {
exited = true
if (!cb) {
debug(`error before exit, code: ${code}, on '${argsString}'`)
return
}
gitRemoteUpdate()
})
}

function gitRemoteUpdate () {
options.logger.debug('updating git remotes...')
wrapCP('git', ['remote', 'update', '-p'], gitCheckout)
}

function gitCheckout () {
options.logger.debug(`checking out upstream/v${version}.x-staging...`)
wrapCP('git', ['checkout', `upstream/v${version}.x-staging`], gitReset)
}

function gitReset () {
options.logger.debug(`resetting upstream/v${version}.x-staging...`)
wrapCP('git', ['reset', `upstream/v${version}.x-staging`, '--hard'], fetchDiff)
}

function fetchDiff () {
options.logger.debug(`fetching diff from pr ${options.prId}...`)

const url = `https://patch-diff.githubusercontent.com/raw/${options.owner}/${options.repo}/pull/${options.prId}.patch`

const req = request(url)

req.on('error', function (err) {
debug(`request err: ${err}`)
return onError()
})
req.on('response', function (response) {
if (response.statusCode !== 200) {
debug(`request non-200 status: ${response.statusCode}`)
return onError()
}
})

gitAttemptBackport(req)
}

function gitAttemptBackport (req) {
options.logger.debug(`attempting a backport to v${version}...`)
const cp = wrapCP('git', ['am'], { stdio: 'pipe' }, function done () {
// Success!
if (isLTS) {
updatePrWithLabels(options, [`lts-watch-v${version}.x`])
}// else {
// TODO(Fishrock123): Re-enable this, but do a check first
// to make sure the label was set by the bot only.
// removeLabelFromPR(options, `dont-land-on-v${version}.x`)
// }

setImmediate(() => {
options.logger.debug(`backport to v${version} successful`)
inProgress = false
cb()
})
})

req.pipe(cp.stdin)
}
}
15 changes: 15 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
'use strict'

const child_process = require('child_process')

if (process.env.NODE_REPO_DIR) {
const fs = require('fs')
global._node_repo_dir = fs.realpathSync(process.env.NODE_REPO_DIR)
const out = child_process.spawnSync('git', ['status'], { cwd: global._node_repo_dir })

if (out.status !== 0) {
logger.info(out.stdout)
logger.error(out.stderr)
logger.error('Bad NODE_REPO_DIR. Backport patch testing disabled.')
global._node_repo_dir = false
}
}

const app = require('./app')
const logger = require('./lib/logger')

Expand Down