From 663b410b0b04477912d03ccdf02fbfec30e30b4d Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Wed, 2 Nov 2016 18:59:04 -0400 Subject: [PATCH] scripts: new attempt-backport script for PRs --- lib/node-repo.js | 1 + scripts/attempt-backport.js | 205 ++++++++++++++++++++++++++++++++++++ server.js | 15 +++ 3 files changed, 221 insertions(+) create mode 100644 scripts/attempt-backport.js diff --git a/lib/node-repo.js b/lib/node-repo.js index 762f0df7..97bb83c5 100644 --- a/lib/node-repo.js +++ b/lib/node-repo.js @@ -45,4 +45,5 @@ function updatePrWithLabels (options, labels) { }) } +exports.updatePrWithLabels = updatePrWithLabels exports.resolveLabelsThenUpdatePr = deferredResolveLabelsThenUpdatePr diff --git a/scripts/attempt-backport.js b/scripts/attempt-backport.js new file mode 100644 index 00000000..32fe2f8c --- /dev/null +++ b/scripts/attempt-backport.js @@ -0,0 +1,205 @@ +'use strict' + +const child_process = require('child_process') +const debug = require('debug')('attempt-backport') +const request = require('request') +const updatePrWithLabels = require('../lib/node-repo').updatePrWithLabels + +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 + + 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('/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() { + 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(() => { + 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 + 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() { + exited = true + if (!cb) { + debug(`error before exit, code: ${code}, on '${argsString}'`) + return + } + gitRemoteUpdate() + }) + } + + function gitRemoteUpdate() { + debug(`updating git remotes...`) + wrapCP('git', ['remote', 'update', '-p'], gitCheckout) + } + + function gitCheckout() { + debug(`checking out upstream/v${version}.x-staging...`) + wrapCP('git', ['checkout', `upstream/v${version}.x-staging`], gitReset) + } + + function gitReset() { + debug(`resetting upstream/v${version}.x-staging...`) + wrapCP('git', ['reset', `upstream/v${version}.x-staging`, '--hard'], fetchDiff) + } + + function fetchDiff() { + 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) { + 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`]) + + setImmediate(() => { + debug(`backport to v${version} successful`) + inProgress = false + cb() + }) + }) + + req.pipe(cp.stdin) + } +} diff --git a/server.js b/server.js index f7676129..1790f35d 100644 --- a/server.js +++ b/server.js @@ -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')