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
This commit is contained in:
		
							parent
							
								
									3e5fa56956
								
							
						
					
					
						commit
						cdb5d07606
					
				| @ -1,6 +1,5 @@ | |||||||
| import {exec} from 'shelljs'; |  | ||||||
| 
 |  | ||||||
| import {MergeConfig} from './dev-infra/pr/merge/config'; | 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.
 | // The configuration for `ng-dev commit-message` commands.
 | ||||||
| const commitMessage = { | const commitMessage = { | ||||||
| @ -82,33 +81,10 @@ const github = { | |||||||
|   name: 'angular', |   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
 | // 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.
 | // for merging upstream pull requests into branches based on a PR target label.
 | ||||||
| const merge = () => { | const merge = () => { | ||||||
|   const patchBranch = getPatchBranchName(); |   const {patch} = determineMergeBranches(require('./package.json').version, '@angular/core'); | ||||||
|   const config: MergeConfig = { |   const config: MergeConfig = { | ||||||
|     githubApiMerge: false, |     githubApiMerge: false, | ||||||
|     claSignedLabel: 'cla: yes', |     claSignedLabel: 'cla: yes', | ||||||
| @ -121,18 +97,18 @@ const merge = () => { | |||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         pattern: 'PR target: patch-only', |         pattern: 'PR target: patch-only', | ||||||
|         branches: [patchBranch], |         branches: [patch], | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         pattern: 'PR target: master & patch', |         pattern: 'PR target: master & patch', | ||||||
|         branches: ['master', patchBranch], |         branches: ['master', patch], | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|     requiredBaseCommits: { |     requiredBaseCommits: { | ||||||
|       // PRs that target either `master` or the patch branch, need to be rebased
 |       // PRs that target either `master` or the patch branch, need to be rebased
 | ||||||
|       // on top of the latest commit message validation fix.
 |       // on top of the latest commit message validation fix.
 | ||||||
|       'master': '4341743b4a6d7e23c6f944aa9e34166b701369a1', |       'master': '4341743b4a6d7e23c6f944aa9e34166b701369a1', | ||||||
|       [patchBranch]: '2a53f471592f424538802907aca1f60f1177a86d' |       [patch]: '2a53f471592f424538802907aca1f60f1177a86d' | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|   return config; |   return config; | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ ts_library( | |||||||
|         "@npm//@octokit/rest", |         "@npm//@octokit/rest", | ||||||
|         "@npm//@types/inquirer", |         "@npm//@types/inquirer", | ||||||
|         "@npm//@types/node", |         "@npm//@types/node", | ||||||
|  |         "@npm//@types/semver", | ||||||
|         "@npm//@types/yargs", |         "@npm//@types/yargs", | ||||||
|         "@npm//chalk", |         "@npm//chalk", | ||||||
|     ], |     ], | ||||||
|  | |||||||
							
								
								
									
										68
									
								
								dev-infra/pr/merge/determine-merge-branches.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								dev-infra/pr/merge/determine-merge-branches.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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`}; | ||||||
|  | } | ||||||
| @ -16,6 +16,7 @@ | |||||||
|     "inquirer": "<from-root>", |     "inquirer": "<from-root>", | ||||||
|     "minimatch": "<from-root>", |     "minimatch": "<from-root>", | ||||||
|     "multimatch": "<from-root>", |     "multimatch": "<from-root>", | ||||||
|  |     "semver": "<from-root>", | ||||||
|     "shelljs": "<from-root>", |     "shelljs": "<from-root>", | ||||||
|     "typed-graphqlify": "<from-root>", |     "typed-graphqlify": "<from-root>", | ||||||
|     "yaml": "<from-root>", |     "yaml": "<from-root>", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user