Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dev-infra): expose script for determining merge branches #37217

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 5 additions & 29 deletions .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 = {
Expand Down Expand Up @@ -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',
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions dev-infra/pr/merge/BUILD.bazel
Expand Up @@ -11,6 +11,7 @@ ts_library(
"@npm//@octokit/rest",
"@npm//@types/inquirer",
"@npm//@types/node",
"@npm//@types/semver",
"@npm//@types/yargs",
"@npm//chalk",
],
Expand Down
68 changes: 68 additions & 0 deletions 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`};
}
1 change: 1 addition & 0 deletions dev-infra/tmpl-package.json
Expand Up @@ -16,6 +16,7 @@
"inquirer": "<from-root>",
"minimatch": "<from-root>",
"multimatch": "<from-root>",
"semver": "<from-root>",
"shelljs": "<from-root>",
"typed-graphqlify": "<from-root>",
"yaml": "<from-root>",
Expand Down
10 changes: 8 additions & 2 deletions dev-infra/utils/config.ts
Expand Up @@ -7,8 +7,9 @@
*/

import {existsSync} from 'fs';
import {join} from 'path';
import {dirname, join} from 'path';
import {exec} from 'shelljs';

import {isTsNodeAvailable} from './ts-node';

/**
Expand Down Expand Up @@ -83,7 +84,12 @@ function readConfigFile(configPath: string): object {
// version of the given configuration seems to exist, set up `ts-node` if available.
if (require.extensions['.ts'] === undefined && existsSync(`${configPath}.ts`) &&
isTsNodeAvailable()) {
require('ts-node').register({skipProject: true, transpileOnly: true});
// Ensure the module target is set to `commonjs`. This is necessary because the
// dev-infra tool runs in NodeJS which does not support ES modules by default.
// Additionally, set the `dir` option to the directory that contains the configuration
// file. This allows for custom compiler options (such as `--strict`).
require('ts-node').register(
{dir: dirname(configPath), transpileOnly: true, compilerOptions: {module: 'commonjs'}});
}

try {
Expand Down