From b68d29791f37b2fbda38d293fd3c7aab5baae842 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Mon, 11 Mar 2019 16:00:49 +0000 Subject: [PATCH] ci: rebase PRs on target branch (#29215) PR Close #29215 --- .circleci/config.yml | 21 +++----- tools/rebase-pr.js | 113 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 14 deletions(-) create mode 100644 tools/rebase-pr.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 0329484cdf..8c4f2b2996 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -63,26 +63,19 @@ var_6: &job_defaults docker: - image: *default_docker_image -# After checkout, rebase on top of master. -# Similar to travis behavior, but not quite the same. -# See https://discuss.circleci.com/t/1662 +# After checkout, rebase on top of target branch. var_7: &post_checkout run: - name: Post checkout step + name: Rebase PR on target branch command: > if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then - # Fetch the head and merge commits for this PR. - git fetch origin +refs/pull/$CIRCLE_PR_NUMBER/head:pr/$CIRCLE_PR_NUMBER/head - git fetch origin +refs/pull/$CIRCLE_PR_NUMBER/merge:pr/$CIRCLE_PR_NUMBER/merge || (echo "Could not fetch merge result with master for this PR. Please check and fix any merge conflicts." ; exit 1) - # Checkout the merged PR for testing as CircleCI will just use the PR head otherwise. - git checkout -qf pr/$CIRCLE_PR_NUMBER/merge - # Reset the merge commit into its PR head. - git reset pr/$CIRCLE_PR_NUMBER/head - # Commit the merge changes into the head of the PR. - # This way we keep the last commit message. + # User is required for rebase. git config user.name "angular-ci" git config user.email "angular-ci" - git commit . --amend --no-edit + # Rebase PR on top of target branch. + node tools/rebase-pr.js angular/angular ${CIRCLE_PR_NUMBER} + else + echo "This build is not over a PR, nothing to do." fi var_8: &yarn_install diff --git a/tools/rebase-pr.js b/tools/rebase-pr.js new file mode 100644 index 0000000000..eee42e4611 --- /dev/null +++ b/tools/rebase-pr.js @@ -0,0 +1,113 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * **Usage:** + * ``` + * node rebase-pr + * ``` + * **Example:** + * ``` + * node rebase-pr angular/angular 123 + * ``` + * + * Rebases the current branch on top of the GitHub PR target branch. + * + * **Context:** + * Since a GitHub PR is not necessarily up to date with its target branch, it is useful to rebase + * prior to testing it on CI to ensure more up to date test results. + * + * **Implementation details:** + * This script obtains the base for a GitHub PR via the + * [GitHub PR API](https://developer.github.com/v3/pulls/#get-a-single-pull-request), then + * fetches that branch, and rebases the current branch on top of it. + * + * **NOTE:** + * This script cannot use external dependencies or be compiled because it needs to run before the + * environment is setup. + * Use only features supported by the NodeJS versions used in the environment. + */ + +// This script uses `console` to print messages to the user. +// tslint:disable:no-console + +// Imports +const util = require('util'); +const https = require('https'); +const child_process = require('child_process'); +const exec = util.promisify(child_process.exec); + +// CLI validation +if (process.argv.length != 4) { + console.error(`This script requires the GitHub repository and PR number as arguments.`); + console.error(`Example: node tools/rebase-pr.js angular/angular 123`); + process.exitCode = 1; + return; +} + +// Run +_main(...process.argv.slice(2)).catch(err => { + console.log('Failed to rebase on top of target branch.\n'); + console.error(err); + process.exitCode = 1; +}); + +// Helpers +async function _main(repository, prNumber) { + console.log(`Determining target branch for PR ${prNumber} on ${repository}.`); + const targetBranch = await determineTargetBranch(repository, prNumber); + console.log(`Target branch is ${targetBranch}.`); + await exec(`git fetch origin ${targetBranch}`); + console.log(`Rebasing current branch on ${targetBranch}.`); + await exec(`git rebase origin/${targetBranch}`); + console.log('Rebase successful.'); +} + +function determineTargetBranch(repository, prNumber) { + const pullsUrl = `https://api.github.com/repos/${repository}/pulls/${prNumber}`; + // GitHub requires a user agent: https://developer.github.com/v3/#user-agent-required + const options = {headers: {'User-Agent': repository}}; + + return new Promise((resolve, reject) => { + https + .get( + pullsUrl, options, + (res) => { + const {statusCode} = res; + const contentType = res.headers['content-type']; + let rawData = ''; + + res.on('data', (chunk) => { rawData += chunk; }); + res.on('end', () => { + + let error; + if (statusCode !== 200) { + error = new Error( + `Request Failed.\nStatus Code: ${statusCode}.\nResponse: ${rawData}`); + } else if (!/^application\/json/.test(contentType)) { + error = new Error( + 'Invalid content-type.\n' + + `Expected application/json but received ${contentType}`); + } + + if (error) { + reject(error); + return; + } + + try { + const parsedData = JSON.parse(rawData); + resolve(parsedData['base']['ref']); + } catch (e) { + reject(e); + } + }); + }) + .on('error', (e) => { reject(e); }); + }); +}