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; owner: string;
/** Name of the repository. */ /** Name of the repository. */
repo: string; 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. */ /** Type describing a version-branch. */
@ -38,6 +31,14 @@ export interface VersionBranch {
parsed: semver.SemVer; 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. */ /** Branch name for the `next` branch. */
export const nextBranchName = 'master'; export const nextBranchName = 'master';
@ -51,13 +52,10 @@ const releaseTrainBranchNameRegex = /(\d+)\.(\d+)\.x/;
*/ */
export async function fetchActiveReleaseTrainBranches( export async function fetchActiveReleaseTrainBranches(
repo: GithubRepo, nextVersion: semver.SemVer): Promise<{ repo: GithubRepo, nextVersion: semver.SemVer): Promise<{
/** /** Release-train currently in active release-candidate/feature-freeze phase. */
* Name of the currently active release-candidate branch. Null if no releaseCandidate: ReleaseTrain | null,
* feature-freeze/release-candidate is currently active. /** Latest non-prerelease release train (i.e. for the patch branch). */
*/ latest: ReleaseTrain
releaseCandidateBranch: string | null,
/** Name of the latest non-prerelease version branch (i.e. the patch branch). */
latestVersionBranch: string
}> { }> {
const majorVersionsToConsider: number[] = []; const majorVersionsToConsider: number[] = [];
let expectedReleaseCandidateMajor: 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, // Collect all version-branches that should be considered for the latest version-branch,
// or the feature-freeze/release-candidate. // or the feature-freeze/release-candidate.
const branches = (await getBranchesForMajorVersions(repo, majorVersionsToConsider)); const branches = (await getBranchesForMajorVersions(repo, majorVersionsToConsider));
const {latestVersionBranch, releaseCandidateBranch} = const {latest, releaseCandidate} = await findActiveReleaseTrainsFromVersionBranches(
await findActiveVersionBranches(repo, nextVersion, branches, expectedReleaseCandidateMajor); repo, nextVersion, branches, expectedReleaseCandidateMajor);
if (latestVersionBranch === null) { if (latest === null) {
throw Error( throw Error(
`Unable to determine the latest release-train. The following branches ` + `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. */ /** 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)); 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[], repo: GithubRepo, nextVersion: semver.SemVer, branches: VersionBranch[],
expectedReleaseCandidateMajor: number): Promise<{ expectedReleaseCandidateMajor: number): Promise<{
latestVersionBranch: string | null, latest: ReleaseTrain | null,
releaseCandidateBranch: string | null, releaseCandidate: ReleaseTrain | null,
}> { }> {
// Version representing the release-train currently in the next phase. Note that we ignore // 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 // 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). // other release trains from version branches (which follow the `N.N.x` pattern).
const nextReleaseTrainVersion = semver.parse(`${nextVersion.major}.${nextVersion.minor}.0`)!; const nextReleaseTrainVersion = semver.parse(`${nextVersion.major}.${nextVersion.minor}.0`)!;
let latestVersionBranch: string|null = null; let latest: ReleaseTrain|null = null;
let releaseCandidateBranch: string|null = null; let releaseCandidate: ReleaseTrain|null = null;
// Iterate through the captured branches and find the latest non-prerelease branch and a // 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 // 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 version = await getVersionOfBranch(repo, name);
const releaseTrain: ReleaseTrain = {branchName: name, version};
const isPrerelease = version.prerelease[0] === 'rc' || version.prerelease[0] === 'next'; const isPrerelease = version.prerelease[0] === 'rc' || version.prerelease[0] === 'next';
if (isPrerelease) { if (isPrerelease) {
if (releaseCandidateBranch !== null) { if (releaseCandidate !== null) {
throw Error( throw Error(
`Unable to determine latest release-train. Found two consecutive ` + `Unable to determine latest release-train. Found two consecutive ` +
`branches in feature-freeze/release-candidate phase. Did not expect both "${name}" ` + `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) { } else if (version.major !== expectedReleaseCandidateMajor) {
throw Error( throw Error(
`Discovered unexpected old feature-freeze/release-candidate branch. Expected no ` + `Discovered unexpected old feature-freeze/release-candidate branch. Expected no ` +
`version-branch in feature-freeze/release-candidate mode for v${version.major}.`); `version-branch in feature-freeze/release-candidate mode for v${version.major}.`);
} }
releaseCandidateBranch = name; releaseCandidate = releaseTrain;
} else { } else {
latestVersionBranch = name; latest = releaseTrain;
break; break;
} }
} }
return {releaseCandidateBranch, latestVersionBranch}; return {releaseCandidate, latest};
} }

View File

@ -22,11 +22,10 @@ import {assertActiveLtsBranch} from './lts-branch';
*/ */
export async function getDefaultTargetLabelConfiguration( export async function getDefaultTargetLabelConfiguration(
api: GithubClient, github: GithubConfig, npmPackageName: string): Promise<TargetLabel[]> { 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 nextVersion = await getVersionOfBranch(repo, nextBranchName);
const hasNextMajorTrain = nextVersion.minor === 0; const hasNextMajorTrain = nextVersion.minor === 0;
const {latestVersionBranch, releaseCandidateBranch} = const {latest, releaseCandidate} = await fetchActiveReleaseTrainBranches(repo, nextVersion);
await fetchActiveReleaseTrainBranches(repo, nextVersion);
return [ return [
{ {
@ -59,15 +58,15 @@ export async function getDefaultTargetLabelConfiguration(
// and is also labeled with `target: patch`, then we merge it directly into the // 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 // 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. // applied cleanly, and a separate PR for the patch branch has been created.
if (githubTargetBranch === latestVersionBranch) { if (githubTargetBranch === latest.branchName) {
return [latestVersionBranch]; return [latest.branchName];
} }
// Otherwise, patch changes are always merged into the next and patch branch. // 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 // Additionally, if there is a release-candidate/feature-freeze release-train
// currently active, also merge the PR into that version-branch. // currently active, also merge the PR into that version-branch.
if (releaseCandidateBranch !== null) { if (releaseCandidate !== null) {
branches.push(releaseCandidateBranch); branches.push(releaseCandidate.branchName);
} }
return branches; return branches;
} }
@ -77,7 +76,7 @@ export async function getDefaultTargetLabelConfiguration(
branches: githubTargetBranch => { branches: githubTargetBranch => {
// The `target: rc` label cannot be applied if there is no active feature-freeze // The `target: rc` label cannot be applied if there is no active feature-freeze
// or release-candidate release train. // or release-candidate release train.
if (releaseCandidateBranch === null) { if (releaseCandidate === null) {
throw new InvalidTargetLabelError( throw new InvalidTargetLabelError(
`No active feature-freeze/release-candidate branch. ` + `No active feature-freeze/release-candidate branch. ` +
`Unable to merge pull request using "target: rc" label.`); `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 // 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 // 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. // into the release-candidate/feature-freeze branch, and a separate PR has been created.
if (githubTargetBranch === releaseCandidateBranch) { if (githubTargetBranch === releaseCandidate.branchName) {
return [releaseCandidateBranch]; return [releaseCandidate.branchName];
} }
// Otherwise, merge into the next and active release-candidate/feature-freeze branch. // 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 ` + `PR cannot be merged as it does not target a long-term support ` +
`branch: "${githubTargetBranch}"`); `branch: "${githubTargetBranch}"`);
} }
if (githubTargetBranch === latestVersionBranch) { if (githubTargetBranch === latest.branchName) {
throw new InvalidTargetBranchError( throw new InvalidTargetBranchError(
`PR cannot be merged with "target: lts" into patch branch. ` + `PR cannot be merged with "target: lts" into patch branch. ` +
`Consider changing the label to "target: patch" if this is intentional.`); `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( throw new InvalidTargetBranchError(
`PR cannot be merged with "target: lts" into feature-freeze/release-candidate ` + `PR cannot be merged with "target: lts" into feature-freeze/release-candidate ` +
`branch. Consider changing the label to "target: rc" if this is intentional.`); `branch. Consider changing the label to "target: rc" if this is intentional.`);
} }
// Assert that the selected branch is an active LTS branch. // Assert that the selected branch is an active LTS branch.
await assertActiveLtsBranch(repo, githubTargetBranch); await assertActiveLtsBranch(repo, npmPackageName, githubTargetBranch);
return [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 * 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. * backport fixes. Throws an error if LTS expired or an invalid branch is selected.
*/ *
export async function assertActiveLtsBranch(repo: GithubRepo, branchName: string) { * @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 version = await getVersionOfBranch(repo, branchName);
const {'dist-tags': distTags, time} = 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`. // LTS versions should be tagged in NPM in the following format: `v{major}-lts`.
const ltsVersion = semver.parse(distTags[`v${version.major}-lts`]); const ltsVersion = semver.parse(distTags[`v${version.major}-lts`]);