diff --git a/dev-infra/pr/merge/defaults/branches.ts b/dev-infra/pr/merge/defaults/branches.ts index 293df1f0e0..8cab89bc8f 100644 --- a/dev-infra/pr/merge/defaults/branches.ts +++ b/dev-infra/pr/merge/defaults/branches.ts @@ -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}; } diff --git a/dev-infra/pr/merge/defaults/labels.ts b/dev-infra/pr/merge/defaults/labels.ts index 1ef039bdb9..2ee896336f 100644 --- a/dev-infra/pr/merge/defaults/labels.ts +++ b/dev-infra/pr/merge/defaults/labels.ts @@ -22,11 +22,10 @@ import {assertActiveLtsBranch} from './lts-branch'; */ export async function getDefaultTargetLabelConfiguration( api: GithubClient, github: GithubConfig, npmPackageName: string): Promise { - 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]; }, }, diff --git a/dev-infra/pr/merge/defaults/lts-branch.ts b/dev-infra/pr/merge/defaults/lts-branch.ts index 5db07f8ad4..a259b28b8a 100644 --- a/dev-infra/pr/merge/defaults/lts-branch.ts +++ b/dev-infra/pr/merge/defaults/lts-branch.ts @@ -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`]);