diff --git a/.circleci/config.yml b/.circleci/config.yml index 8cf5a5c39a2b..6eafe6d70f02 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,6 +149,7 @@ orbs: # to make sure that snapshot builds are not deployed out of order, resulting in Git # push conflicts. queue: eddiewebb/queue@1.5.0 + devinfra: angular/dev-infra@1.0.2 commands: checkout_and_rebase: @@ -156,18 +157,9 @@ commands: steps: - checkout # After checkout, rebase on top of target branch. - - run: - name: Rebase PR on target branch - environment: - CIRCLE_GIT_BASE_REVISION: << pipeline.git.base_revision >> - CIRCLE_GIT_REVISION: << pipeline.git.revision >> - command: | - if [ -n "$CIRCLE_PR_NUMBER" ]; then - # User is required for rebase. - git config user.name "angular-ci" - git config user.email "angular-ci" - node .circleci/rebase-pr.js - fi + - devinfra/rebase-pr-on-target-branch: + base_revision: << pipeline.git.base_revision >> + head_revision: << pipeline.git.revision >> # ----------------------------------------------------------------------------------------- # Job definitions. Jobs which are defined just here, will not run automatically. Each job diff --git a/.circleci/rebase-pr.js b/.circleci/rebase-pr.js deleted file mode 100644 index aae99d00e9ec..000000000000 --- a/.circleci/rebase-pr.js +++ /dev/null @@ -1,215 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 - */ - -/** - * 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. - * - * **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. - */ -// tslint:disable:no-console -const {execSync: execSync_} = require('child_process'); - -/** A regex to select a ref that matches our semver refs. */ -const semverRegex = /^(\d+)\.(\d+)\.x$/; - -/** - * Synchronously executes the command. - * - * Return the trimmed stdout as a string, with an added attribute of the exit code. - */ -function exec(command, allowStderr = true) { - let output = new String(); - output.code = 0; - try { - output += execSync_(command, {stdio: ['pipe', 'pipe', 'pipe']}) - .toString() - .trim(); - } catch (err) { - allowStderr && console.error(err.stderr.toString()); - output.code = err.status; - } - return output; -} - -// Run - -// Helpers -async function _main() { - const refs = await getRefsAndShasForChange(); - - // Log known refs and shas - console.log(`--------------------------------`); - console.log(` Target Branch: ${refs.base.ref}`); - console.log(` Latest Commit for Target Branch: ${refs.target.latestSha}`); - console.log(` Latest Commit for PR: ${refs.base.latestSha}`); - console.log(` First Common Ancestor SHA: ${refs.commonAncestorSha}`); - console.log(`--------------------------------`); - console.log(); - - // Get the count of commits between the latest commit from origin and the common ancestor SHA. - const commitCount = exec( - `git rev-list --count origin/${refs.base.ref}...${refs.commonAncestorSha}`, - ); - console.log(`Checking ${commitCount} commits for changes in the CircleCI config file.`); - - // Check if the files changed between the latest commit from origin and the common ancestor SHA - // includes the CircleCI config. - const circleCIConfigChanged = exec( - `git diff --name-only origin/${refs.base.ref} ${refs.commonAncestorSha} -- .circleci/config.yml`, - ); - - if (!!circleCIConfigChanged) { - throw Error(` - CircleCI config on ${refs.base.ref} has been modified since commit - ${refs.commonAncestorSha.slice(0, 7)}, which this PR is based on. - - Please rebase the PR on ${refs.base.ref} after fetching from upstream. - - Rebase instructions for PR Author, please run the following commands: - - git fetch upstream ${refs.base.ref}; - git checkout ${refs.target.ref}; - git rebase upstream/${refs.base.ref}; - git push --force-with-lease; - `); - } else { - console.log('No change found in the CircleCI config file, continuing.'); - } - console.log(); - - // Rebase the PR. - exec(`git rebase origin/${refs.base.ref}`); - console.log(`Rebased current branch onto ${refs.base.ref}.`); -} - -/** - * Sort a list of fullpath refs into a list and then provide the first entry. - * - * The sort order will first find main branch, and then any semver ref, followed - * by the rest of the refs in the order provided. - * - * Branches are sorted in this order as work is primarily done on main branches, - * and otherwise on a semver branch. If neither of those were to match, the most - * likely correct branch will be the first one encountered in the list. - */ -function getRefFromBranchList(gitOutput) { - const branches = gitOutput.split('\n').map(b => b.split('/').slice(1).join('').trim()); - return branches.sort((a, b) => { - if (a === 'main') { - return -1; - } - if (b === 'main') { - return 1; - } - - const aIsSemver = semverRegex.test(a); - const bIsSemver = semverRegex.test(b); - if (aIsSemver && bIsSemver) { - const [, aMajor, aMinor] = a.match(semverRegex); - const [, bMajor, bMinor] = b.match(semverRegex); - return ( - parseInt(bMajor, 10) - parseInt(aMajor, 10) || - parseInt(aMinor, 10) - parseInt(bMinor, 10) || - 0 - ); - } - if (aIsSemver) { - return -1; - } - if (bIsSemver) { - return 1; - } - return 0; - })[0]; -} - -/** - * Get the full sha of the ref provided. - * - * example: 1bc0c1a6c01ede7168f22fa9b3508ba51f1f464e - */ -function getShaFromRef(ref) { - return exec(`git rev-parse ${ref}`); -} - -/** - * Get the list of branches which contain the provided sha, sorted in descending order - * by committerdate. - * - * example: - * upstream/main - * upstream/9.0.x - * upstream/test - * upstream/1.1.x - */ -function getBranchListForSha(sha, remote) { - return exec(`git branch -r '${remote}/*' --sort=-committerdate --contains ${sha}`); -} - -/** Get the common ancestor sha of the two provided shas. */ -function getCommonAncestorSha(sha1, sha2) { - return exec(`git merge-base ${sha1} ${sha2}`); -} - -/** - * Adds the remote to git, if it doesn't already exist. Returns a boolean indicating - * whether the remote was added by the command. - */ -function addAndFetchRemote(owner, name) { - const remoteName = `${owner}_${name}`; - exec(`git remote add ${remoteName} https://github.com/${owner}/${name}.git`, false); - exec(`git fetch ${remoteName}`); - return remoteName; -} - -/** Get the ref and latest shas for the provided sha on a specific remote. */ -function getRefAndShas(sha, owner, name) { - const remoteName = addAndFetchRemote(owner, name); - // Get the ref on the remote for the sha provided. - const branches = getBranchListForSha(sha, remoteName); - const ref = getRefFromBranchList(branches); - // Get the latest sha on the discovered remote ref. - const latestSha = getShaFromRef(`${remoteName}/${ref}`); - - return {remote: remoteName, ref, latestSha, sha}; -} - -/** Gets the refs and shas for the base and target of the current environment. */ -function getRefsAndShasForChange() { - const base = getRefAndShas( - process.env['CIRCLE_GIT_BASE_REVISION'], - process.env['CIRCLE_PROJECT_USERNAME'], - process.env['CIRCLE_PROJECT_REPONAME'], - ); - - const target = getRefAndShas( - process.env['CIRCLE_GIT_REVISION'], - process.env['CIRCLE_PR_USERNAME'], - process.env['CIRCLE_PR_REPONAME'], - ); - - const commonAncestorSha = getCommonAncestorSha(base.sha, target.sha); - return { - base, - target, - commonAncestorSha, - }; -} - -_main().catch(err => { - console.log('Failed to rebase on top of target branch.\n'); - console.error(err); - process.exitCode = 1; -});