From cdb5d076062dd3b4f938b54158dcfd68de71a800 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Wed, 20 May 2020 12:50:18 +0200 Subject: [PATCH] feat(dev-infra): expose script for determining merge branches (#37217) The components repo and framework repository follow the same patch branch concept. We should be able to share a script for determining these merge branches. Additonally the logic has been improved compared to the old merge script because we no longer consult `git ls-remote` unless really needed. Currently, `git ls-remote` is always consulted, even though not necessarily needed. This can slow down the merge script and the caretaker process when a couple of PRs are merged (personally saw around ~4 seconds per merge). Additionally, the new logic is more strict and will ensure (in most cases) that no wrong patch/minor branch is determined. Previously, the script just used the lexicographically greatest patch branch. This _could_ be wrong when a new patch branch has been created too early, or by accident. PR Close #37217 --- .ng-dev-config.ts | 34 ++-------- dev-infra/pr/merge/BUILD.bazel | 1 + .../pr/merge/determine-merge-branches.ts | 68 +++++++++++++++++++ dev-infra/tmpl-package.json | 1 + 4 files changed, 75 insertions(+), 29 deletions(-) create mode 100644 dev-infra/pr/merge/determine-merge-branches.ts diff --git a/.ng-dev-config.ts b/.ng-dev-config.ts index d85be46331..89d572b77f 100644 --- a/.ng-dev-config.ts +++ b/.ng-dev-config.ts @@ -1,6 +1,5 @@ -import {exec} from 'shelljs'; - import {MergeConfig} from './dev-infra/pr/merge/config'; +import {determineMergeBranches} from './dev-infra/pr/merge/determine-merge-branches'; // The configuration for `ng-dev commit-message` commands. const commitMessage = { @@ -82,33 +81,10 @@ const github = { name: 'angular', }; -/** - * Gets the name of the current patch branch. The patch branch is determined by - * looking for upstream branches that follow the format of `{major}.{minor}.x`. - */ -const getPatchBranchName = (): string => { - const branches = - exec( - `git ls-remote --heads https://github.com/${github.owner}/${github.name}.git`, - {silent: true}) - .trim() - .split('\n'); - - for (let i = branches.length - 1; i >= 0; i--) { - const branchName = branches[i]; - const matches = branchName.match(/refs\/heads\/([0-9]+\.[0-9]+\.x)/); - if (matches !== null) { - return matches[1]; - } - } - - throw Error('Could not determine patch branch name.'); -}; - // Configuration for the `ng-dev pr merge` command. The command can be used // for merging upstream pull requests into branches based on a PR target label. const merge = () => { - const patchBranch = getPatchBranchName(); + const {patch} = determineMergeBranches(require('./package.json').version, '@angular/core'); const config: MergeConfig = { githubApiMerge: false, claSignedLabel: 'cla: yes', @@ -121,18 +97,18 @@ const merge = () => { }, { pattern: 'PR target: patch-only', - branches: [patchBranch], + branches: [patch], }, { pattern: 'PR target: master & patch', - branches: ['master', patchBranch], + branches: ['master', patch], }, ], requiredBaseCommits: { // PRs that target either `master` or the patch branch, need to be rebased // on top of the latest commit message validation fix. 'master': '4341743b4a6d7e23c6f944aa9e34166b701369a1', - [patchBranch]: '2a53f471592f424538802907aca1f60f1177a86d' + [patch]: '2a53f471592f424538802907aca1f60f1177a86d' }, }; return config; diff --git a/dev-infra/pr/merge/BUILD.bazel b/dev-infra/pr/merge/BUILD.bazel index 68609c668a..e17c742eef 100644 --- a/dev-infra/pr/merge/BUILD.bazel +++ b/dev-infra/pr/merge/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( "@npm//@octokit/rest", "@npm//@types/inquirer", "@npm//@types/node", + "@npm//@types/semver", "@npm//@types/yargs", "@npm//chalk", ], diff --git a/dev-infra/pr/merge/determine-merge-branches.ts b/dev-infra/pr/merge/determine-merge-branches.ts new file mode 100644 index 0000000000..bf4a479caa --- /dev/null +++ b/dev-infra/pr/merge/determine-merge-branches.ts @@ -0,0 +1,68 @@ +/** + * @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 + */ + +import * as semver from 'semver'; +import {exec} from '../../utils/shelljs'; + +/** + * Helper function that can be used to determine merge branches based on a given + * project version. The function determines merge branches primarily through the + * specified version, but falls back to consulting the NPM registry when needed. + * + * Consulting the NPM registry for determining the patch branch may slow down merging, + * so whenever possible, the branches are determined statically based on the current + * version. In some cases, consulting the NPM registry is inevitable because for major + * pre-releases, we cannot determine the latest stable minor version from the current + * pre-release version. + */ +export function determineMergeBranches( + currentVersion: string, npmPackageName: string): {minor: string, patch: string} { + const projectVersion = semver.parse(currentVersion); + if (projectVersion === null) { + throw Error('Cannot parse version set in project "package.json" file.'); + } + const {major, minor, patch, prerelease} = projectVersion; + const isMajor = minor === 0 && patch === 0; + const isMinor = minor !== 0 && patch === 0; + + // If there is no prerelease, then we compute patch and minor branches based + // on the current version major and minor. + if (prerelease.length === 0) { + return {minor: `${major}.x`, patch: `${major}.${minor}.x`}; + } + + // If current version is set to a minor prerelease, we can compute the merge branches + // statically. e.g. if we are set to `9.3.0-next.0`, then our merge branches should + // be set to `9.x` and `9.2.x`. + if (isMinor) { + return {minor: `${major}.x`, patch: `${major}.${minor - 1}.x`}; + } else if (!isMajor) { + throw Error('Unexpected version. Cannot have prerelease for patch version.'); + } + + // If we are set to a major prerelease, we cannot statically determine the stable patch + // branch (as the latest minor segment is unknown). We determine it by looking in the NPM + // registry for the latest stable release that will tell us about the current minor segment. + // e.g. if the current major is `v10.0.0-next.0`, then we need to look for the latest release. + // Let's say this is `v9.2.6`. Our patch branch will then be called `9.2.x`. + const latestVersion = exec(`yarn -s info ${npmPackageName} dist-tags.latest`).trim(); + if (!latestVersion) { + throw Error('Could not determine version of latest release.'); + } + const expectedMajor = major - 1; + const parsedLatestVersion = semver.parse(latestVersion); + if (parsedLatestVersion === null) { + throw Error(`Could not parse latest version from NPM registry: ${latestVersion}`); + } else if (parsedLatestVersion.major !== expectedMajor) { + throw Error( + `Expected latest release to have major version: v${expectedMajor}, ` + + `but got: v${latestVersion}`); + } + + return {patch: `${expectedMajor}.${parsedLatestVersion.minor}.x`, minor: `${expectedMajor}.x`}; +} diff --git a/dev-infra/tmpl-package.json b/dev-infra/tmpl-package.json index 5903d5f223..250da626dd 100644 --- a/dev-infra/tmpl-package.json +++ b/dev-infra/tmpl-package.json @@ -16,6 +16,7 @@ "inquirer": "", "minimatch": "", "multimatch": "", + "semver": "", "shelljs": "", "typed-graphqlify": "", "yaml": "",