refactor(dev-infra): expose version for determined release trains (#38656)

Previously, the logic for determing the active release trains did not
return the resolved version of a release train. With the publish script
being created, we need this information and can just pass it through,
so that we do not need to fetch and parse the package.json of given
branches multiple times.

PR Close #38656
This commit is contained in:
Paul Gschwendtner 2020-09-01 10:46:41 +02:00 committed by Alex Rickabaugh
parent 6af638d58e
commit b041c118e3
3 changed files with 57 additions and 48 deletions

View File

@ -17,13 +17,6 @@ export interface GithubRepo {
owner: string;
/** Name of the repository. */
repo: string;
/**
* NPM package representing this repository. Angular repositories usually contain
* multiple packages in a monorepo scheme, but packages commonly are released with
* the same versions. This means that a single package can be used for querying
* NPM about previously published versions (e.g. to determine active LTS versions).
* */
npmPackageName: string;
}
/** Type describing a version-branch. */
@ -38,6 +31,14 @@ export interface VersionBranch {
parsed: semver.SemVer;
}
/** Type describing a release-train. */
export interface ReleaseTrain {
/** Name of the branch for this release-train. */
branchName: string;
/** Current latest version for this release train. */
version: semver.SemVer;
}
/** Branch name for the `next` branch. */
export const nextBranchName = 'master';
@ -51,13 +52,10 @@ const releaseTrainBranchNameRegex = /(\d+)\.(\d+)\.x/;
*/
export async function fetchActiveReleaseTrainBranches(
repo: GithubRepo, nextVersion: semver.SemVer): Promise<{
/**
* Name of the currently active release-candidate branch. Null if no
* feature-freeze/release-candidate is currently active.
*/
releaseCandidateBranch: string | null,
/** Name of the latest non-prerelease version branch (i.e. the patch branch). */
latestVersionBranch: string
/** Release-train currently in active release-candidate/feature-freeze phase. */
releaseCandidate: ReleaseTrain | null,
/** Latest non-prerelease release train (i.e. for the patch branch). */
latest: ReleaseTrain
}> {
const majorVersionsToConsider: number[] = [];
let expectedReleaseCandidateMajor: number;
@ -90,16 +88,16 @@ export async function fetchActiveReleaseTrainBranches(
// Collect all version-branches that should be considered for the latest version-branch,
// or the feature-freeze/release-candidate.
const branches = (await getBranchesForMajorVersions(repo, majorVersionsToConsider));
const {latestVersionBranch, releaseCandidateBranch} =
await findActiveVersionBranches(repo, nextVersion, branches, expectedReleaseCandidateMajor);
const {latest, releaseCandidate} = await findActiveReleaseTrainsFromVersionBranches(
repo, nextVersion, branches, expectedReleaseCandidateMajor);
if (latestVersionBranch === null) {
if (latest === null) {
throw Error(
`Unable to determine the latest release-train. The following branches ` +
`have been considered: [${branches.join(', ')}]`);
`have been considered: [${branches.map(b => b.name).join(', ')}]`);
}
return {releaseCandidateBranch, latestVersionBranch};
return {releaseCandidate, latest};
}
/** Gets the version of a given branch by reading the `package.json` upstream. */
@ -159,19 +157,20 @@ export async function getBranchesForMajorVersions(
return branches.sort((a, b) => semver.rcompare(a.parsed, b.parsed));
}
export async function findActiveVersionBranches(
/** Finds the currently active release trains from the specified version branches. */
export async function findActiveReleaseTrainsFromVersionBranches(
repo: GithubRepo, nextVersion: semver.SemVer, branches: VersionBranch[],
expectedReleaseCandidateMajor: number): Promise<{
latestVersionBranch: string | null,
releaseCandidateBranch: string | null,
latest: ReleaseTrain | null,
releaseCandidate: ReleaseTrain | null,
}> {
// Version representing the release-train currently in the next phase. Note that we ignore
// patch and pre-release segments in order to be able to compare the next release train to
// other release trains from version branches (which follow the `N.N.x` pattern).
const nextReleaseTrainVersion = semver.parse(`${nextVersion.major}.${nextVersion.minor}.0`)!;
let latestVersionBranch: string|null = null;
let releaseCandidateBranch: string|null = null;
let latest: ReleaseTrain|null = null;
let releaseCandidate: ReleaseTrain|null = null;
// Iterate through the captured branches and find the latest non-prerelease branch and a
// potential release candidate branch. From the collected branches we iterate descending
@ -200,24 +199,26 @@ export async function findActiveVersionBranches(
}
const version = await getVersionOfBranch(repo, name);
const releaseTrain: ReleaseTrain = {branchName: name, version};
const isPrerelease = version.prerelease[0] === 'rc' || version.prerelease[0] === 'next';
if (isPrerelease) {
if (releaseCandidateBranch !== null) {
if (releaseCandidate !== null) {
throw Error(
`Unable to determine latest release-train. Found two consecutive ` +
`branches in feature-freeze/release-candidate phase. Did not expect both "${name}" ` +
`and "${releaseCandidateBranch}" to be in feature-freeze/release-candidate mode.`);
`and "${releaseCandidate.branchName}" to be in feature-freeze/release-candidate mode.`);
} else if (version.major !== expectedReleaseCandidateMajor) {
throw Error(
`Discovered unexpected old feature-freeze/release-candidate branch. Expected no ` +
`version-branch in feature-freeze/release-candidate mode for v${version.major}.`);
}
releaseCandidateBranch = name;
releaseCandidate = releaseTrain;
} else {
latestVersionBranch = name;
latest = releaseTrain;
break;
}
}
return {releaseCandidateBranch, latestVersionBranch};
return {releaseCandidate, latest};
}

View File

@ -22,11 +22,10 @@ import {assertActiveLtsBranch} from './lts-branch';
*/
export async function getDefaultTargetLabelConfiguration(
api: GithubClient, github: GithubConfig, npmPackageName: string): Promise<TargetLabel[]> {
const repo: GithubRepo = {owner: github.owner, repo: github.name, api, npmPackageName};
const repo: GithubRepo = {owner: github.owner, repo: github.name, api};
const nextVersion = await getVersionOfBranch(repo, nextBranchName);
const hasNextMajorTrain = nextVersion.minor === 0;
const {latestVersionBranch, releaseCandidateBranch} =
await fetchActiveReleaseTrainBranches(repo, nextVersion);
const {latest, releaseCandidate} = await fetchActiveReleaseTrainBranches(repo, nextVersion);
return [
{
@ -59,15 +58,15 @@ export async function getDefaultTargetLabelConfiguration(
// and is also labeled with `target: patch`, then we merge it directly into the
// branch without doing any cherry-picking. This is useful if a PR could not be
// applied cleanly, and a separate PR for the patch branch has been created.
if (githubTargetBranch === latestVersionBranch) {
return [latestVersionBranch];
if (githubTargetBranch === latest.branchName) {
return [latest.branchName];
}
// Otherwise, patch changes are always merged into the next and patch branch.
const branches = [nextBranchName, latestVersionBranch];
const branches = [nextBranchName, latest.branchName];
// Additionally, if there is a release-candidate/feature-freeze release-train
// currently active, also merge the PR into that version-branch.
if (releaseCandidateBranch !== null) {
branches.push(releaseCandidateBranch);
if (releaseCandidate !== null) {
branches.push(releaseCandidate.branchName);
}
return branches;
}
@ -77,7 +76,7 @@ export async function getDefaultTargetLabelConfiguration(
branches: githubTargetBranch => {
// The `target: rc` label cannot be applied if there is no active feature-freeze
// or release-candidate release train.
if (releaseCandidateBranch === null) {
if (releaseCandidate === null) {
throw new InvalidTargetLabelError(
`No active feature-freeze/release-candidate branch. ` +
`Unable to merge pull request using "target: rc" label.`);
@ -86,11 +85,11 @@ export async function getDefaultTargetLabelConfiguration(
// directly through the Github UI and has the `target: rc` label applied, merge it
// only into the release candidate branch. This is useful if a PR did not apply cleanly
// into the release-candidate/feature-freeze branch, and a separate PR has been created.
if (githubTargetBranch === releaseCandidateBranch) {
return [releaseCandidateBranch];
if (githubTargetBranch === releaseCandidate.branchName) {
return [releaseCandidate.branchName];
}
// Otherwise, merge into the next and active release-candidate/feature-freeze branch.
return [nextBranchName, releaseCandidateBranch];
return [nextBranchName, releaseCandidate.branchName];
},
},
{
@ -105,18 +104,18 @@ export async function getDefaultTargetLabelConfiguration(
`PR cannot be merged as it does not target a long-term support ` +
`branch: "${githubTargetBranch}"`);
}
if (githubTargetBranch === latestVersionBranch) {
if (githubTargetBranch === latest.branchName) {
throw new InvalidTargetBranchError(
`PR cannot be merged with "target: lts" into patch branch. ` +
`Consider changing the label to "target: patch" if this is intentional.`);
}
if (githubTargetBranch === releaseCandidateBranch && releaseCandidateBranch !== null) {
if (releaseCandidate !== null && githubTargetBranch === releaseCandidate.branchName) {
throw new InvalidTargetBranchError(
`PR cannot be merged with "target: lts" into feature-freeze/release-candidate ` +
`branch. Consider changing the label to "target: rc" if this is intentional.`);
}
// Assert that the selected branch is an active LTS branch.
await assertActiveLtsBranch(repo, githubTargetBranch);
await assertActiveLtsBranch(repo, npmPackageName, githubTargetBranch);
return [githubTargetBranch];
},
},

View File

@ -28,12 +28,21 @@ const majorActiveTermSupportDuration = 12;
/**
* Asserts that the given branch corresponds to an active LTS version-branch that can receive
* backported fixes. Throws an error if LTS expired or an invalid branch is selected.
*/
export async function assertActiveLtsBranch(repo: GithubRepo, branchName: string) {
* backport fixes. Throws an error if LTS expired or an invalid branch is selected.
*
* @param repo Github repository for which the given branch exists.
* @param representativeNpmPackage NPM package representing the given repository. Angular
* repositories usually contain multiple packages in a monorepo scheme, but packages commonly
* are released with the same versions. This means that a single package can be used for querying
* NPM about previously published versions (e.g. to determine active LTS versions). The package
* name is used to check if the given branch is containing an active LTS version.
* @param branchName Branch that is checked to be an active LTS version-branch.
* */
export async function assertActiveLtsBranch(
repo: GithubRepo, representativeNpmPackage: string, branchName: string) {
const version = await getVersionOfBranch(repo, branchName);
const {'dist-tags': distTags, time} =
await (await fetch(`https://registry.npmjs.org/${repo.npmPackageName}`)).json();
await (await fetch(`https://registry.npmjs.org/${representativeNpmPackage}`)).json();
// LTS versions should be tagged in NPM in the following format: `v{major}-lts`.
const ltsVersion = semver.parse(distTags[`v${version.major}-lts`]);