refactor(dev-infra): move common versioning tooling to shared location (#38656)
We initially added logic for determining active release trains into the merge script. Given we now build more tools that rely on this information, we move the logic into a more general "versioning" folder that can contain common logic following the versioning document for the Angular organization. PR Close #38656
This commit is contained in:
parent
e1c11a36c7
commit
9dccaa9570
|
@ -1,6 +1,7 @@
|
||||||
import {DevInfraMergeConfig} from '../dev-infra/pr/merge/config';
|
import {DevInfraMergeConfig} from '../dev-infra/pr/merge/config';
|
||||||
import {getDefaultTargetLabelConfiguration} from '../dev-infra/pr/merge/defaults';
|
import {getDefaultTargetLabelConfiguration} from '../dev-infra/pr/merge/defaults';
|
||||||
import {github} from './github';
|
import {github} from './github';
|
||||||
|
import {release} from './release';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for the merge tool in `ng-dev`. This sets up the labels which
|
* Configuration for the merge tool in `ng-dev`. This sets up the labels which
|
||||||
|
@ -13,7 +14,9 @@ export const merge: DevInfraMergeConfig['merge'] = async api => {
|
||||||
mergeReadyLabel: /^action: merge(-assistance)?/,
|
mergeReadyLabel: /^action: merge(-assistance)?/,
|
||||||
caretakerNoteLabel: 'action: merge-assistance',
|
caretakerNoteLabel: 'action: merge-assistance',
|
||||||
commitMessageFixupLabel: 'commit message fixup',
|
commitMessageFixupLabel: 'commit message fixup',
|
||||||
labels: await getDefaultTargetLabelConfiguration(api, github, '@angular/core'),
|
// We can pick any of the NPM packages as we are in a monorepo where all packages are
|
||||||
|
// published together with the same version and branching.
|
||||||
|
labels: await getDefaultTargetLabelConfiguration(api, github, release),
|
||||||
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.
|
||||||
|
|
|
@ -11,6 +11,8 @@ ts_library(
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
"//dev-infra/commit-message",
|
"//dev-infra/commit-message",
|
||||||
|
"//dev-infra/release/config",
|
||||||
|
"//dev-infra/release/versioning",
|
||||||
"//dev-infra/utils",
|
"//dev-infra/utils",
|
||||||
"@npm//@octokit/rest",
|
"@npm//@octokit/rest",
|
||||||
"@npm//@types/inquirer",
|
"@npm//@types/inquirer",
|
||||||
|
@ -28,6 +30,8 @@ ts_library(
|
||||||
srcs = glob(["**/*.spec.ts"]),
|
srcs = glob(["**/*.spec.ts"]),
|
||||||
deps = [
|
deps = [
|
||||||
":merge",
|
":merge",
|
||||||
|
"//dev-infra/release/config",
|
||||||
|
"//dev-infra/release/versioning",
|
||||||
"//dev-infra/utils",
|
"//dev-infra/utils",
|
||||||
"@npm//@types/jasmine",
|
"@npm//@types/jasmine",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
|
|
|
@ -7,5 +7,4 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './labels';
|
export * from './labels';
|
||||||
export * from './branches';
|
|
||||||
export * from './lts-branch';
|
export * from './lts-branch';
|
||||||
|
|
|
@ -7,8 +7,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as nock from 'nock';
|
import * as nock from 'nock';
|
||||||
import * as nodeFetch from 'node-fetch';
|
|
||||||
|
|
||||||
|
import {ReleaseConfig} from '../../../release/config/index';
|
||||||
|
import {_npmPackageInfoCache, NpmPackageInfo} from '../../../release/versioning/npm-registry';
|
||||||
import {GithubConfig} from '../../../utils/config';
|
import {GithubConfig} from '../../../utils/config';
|
||||||
import * as console from '../../../utils/console';
|
import * as console from '../../../utils/console';
|
||||||
import {GithubClient} from '../../../utils/git/github';
|
import {GithubClient} from '../../../utils/git/github';
|
||||||
|
@ -21,13 +22,17 @@ const API_ENDPOINT = `https://api.github.com`;
|
||||||
|
|
||||||
describe('default target labels', () => {
|
describe('default target labels', () => {
|
||||||
let api: GithubClient;
|
let api: GithubClient;
|
||||||
let config: GithubConfig;
|
let githubConfig: GithubConfig;
|
||||||
let npmPackageName: string;
|
let releaseConfig: ReleaseConfig;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
api = new GithubClient();
|
api = new GithubClient();
|
||||||
config = {owner: 'angular', name: 'dev-infra-test'};
|
githubConfig = {owner: 'angular', name: 'dev-infra-test'};
|
||||||
npmPackageName = '@angular/dev-infra-test-pkg';
|
releaseConfig = {
|
||||||
|
npmPackages: ['@angular/dev-infra-test-pkg'],
|
||||||
|
buildPackages: async () => [],
|
||||||
|
generateReleaseNotesForHead: async () => {},
|
||||||
|
};
|
||||||
|
|
||||||
// The label determination will print warn messages. These should not be
|
// The label determination will print warn messages. These should not be
|
||||||
// printed to the console, so we turn `console.warn` into a spy.
|
// printed to the console, so we turn `console.warn` into a spy.
|
||||||
|
@ -37,11 +42,11 @@ describe('default target labels', () => {
|
||||||
afterEach(() => nock.cleanAll());
|
afterEach(() => nock.cleanAll());
|
||||||
|
|
||||||
async function computeTargetLabels(): Promise<TargetLabel[]> {
|
async function computeTargetLabels(): Promise<TargetLabel[]> {
|
||||||
return getDefaultTargetLabelConfiguration(api, config, npmPackageName);
|
return getDefaultTargetLabelConfiguration(api, githubConfig, releaseConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRepoApiRequestUrl(): string {
|
function getRepoApiRequestUrl(): string {
|
||||||
return `${API_ENDPOINT}/repos/${config.owner}/${config.name}`;
|
return `${API_ENDPOINT}/repos/${githubConfig.owner}/${githubConfig.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,10 +66,9 @@ describe('default target labels', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fakes a NPM package query API request. */
|
/** Fakes a NPM package query API request. */
|
||||||
function fakeNpmPackageQueryRequest(data: unknown) {
|
function fakeNpmPackageQueryRequest(data: Partial<NpmPackageInfo>) {
|
||||||
// Note: We only need to mock the `json` function for a `Response`. Types
|
_npmPackageInfoCache[releaseConfig.npmPackages[0]] =
|
||||||
// would expect us to mock more functions, so we need to cast to `any`.
|
Promise.resolve({'dist-tags': {}, versions: {}, time: {}, ...data});
|
||||||
spyOn(nodeFetch, 'default').and.resolveTo({json: async () => data} as any);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -167,7 +171,7 @@ describe('default target labels', () => {
|
||||||
'time': {
|
'time': {
|
||||||
// v10 has been released at the given specified date. We pick a date that
|
// v10 has been released at the given specified date. We pick a date that
|
||||||
// guarantees that the version is no longer considered as active LTS version.
|
// guarantees that the version is no longer considered as active LTS version.
|
||||||
'10.0.0': new Date(1912, 5, 23),
|
'10.0.0': new Date(1912, 5, 23).toISOString(),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -234,7 +238,7 @@ describe('default target labels', () => {
|
||||||
'time': {
|
'time': {
|
||||||
// v10 has been released at the given specified date. We pick a date that
|
// v10 has been released at the given specified date. We pick a date that
|
||||||
// guarantees that the version is no longer considered as active LTS version.
|
// guarantees that the version is no longer considered as active LTS version.
|
||||||
'10.0.0': new Date(1912, 5, 23),
|
'10.0.0': new Date(1912, 5, 23).toISOString(),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -247,16 +251,14 @@ describe('default target labels', () => {
|
||||||
interceptBranchVersionRequest('10.5.x', '10.5.1');
|
interceptBranchVersionRequest('10.5.x', '10.5.1');
|
||||||
interceptBranchesListRequest(['10.5.x', '11.0.x']);
|
interceptBranchesListRequest(['10.5.x', '11.0.x']);
|
||||||
|
|
||||||
spyOn(require('node-fetch'), 'default').and.callFake(() => ({
|
fakeNpmPackageQueryRequest({
|
||||||
json: () => ({
|
'dist-tags': {
|
||||||
'dist-tags': {
|
'v10-lts': '10.5.1',
|
||||||
'v10-lts': '10.5.1',
|
},
|
||||||
},
|
'time': {
|
||||||
'time': {
|
'10.0.0': new Date().toISOString(),
|
||||||
'10.0.0': new Date().toISOString(),
|
}
|
||||||
}
|
});
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(await getBranchesForLabel('target: lts', '10.5.x')).toEqual(['10.5.x']);
|
expect(await getBranchesForLabel('target: lts', '10.5.x')).toEqual(['10.5.x']);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,12 +6,13 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {ReleaseConfig} from '../../../release/config/index';
|
||||||
|
import {fetchActiveReleaseTrains, isVersionBranch, nextBranchName} from '../../../release/versioning';
|
||||||
import {GithubConfig} from '../../../utils/config';
|
import {GithubConfig} from '../../../utils/config';
|
||||||
import {GithubClient} from '../../../utils/git/github';
|
import {GithubClient} from '../../../utils/git/github';
|
||||||
import {TargetLabel} from '../config';
|
import {TargetLabel} from '../config';
|
||||||
import {InvalidTargetBranchError, InvalidTargetLabelError} from '../target-label';
|
import {InvalidTargetBranchError, InvalidTargetLabelError} from '../target-label';
|
||||||
|
|
||||||
import {fetchActiveReleaseTrainBranches, getVersionOfBranch, GithubRepoWithApi, isReleaseTrainBranch, nextBranchName} from './branches';
|
|
||||||
import {assertActiveLtsBranch} from './lts-branch';
|
import {assertActiveLtsBranch} from './lts-branch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,13 +20,18 @@ import {assertActiveLtsBranch} from './lts-branch';
|
||||||
* organization-wide labeling and branching semantics as outlined in the specification.
|
* organization-wide labeling and branching semantics as outlined in the specification.
|
||||||
*
|
*
|
||||||
* https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU
|
* https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU
|
||||||
|
*
|
||||||
|
* @param api Instance of an authenticated Github client.
|
||||||
|
* @param githubConfig Configuration for the Github remote. Used as Git remote
|
||||||
|
* for the release train branches.
|
||||||
|
* @param releaseConfig Configuration for the release packages. Used to fetch
|
||||||
|
* NPM version data when LTS version branches are validated.
|
||||||
*/
|
*/
|
||||||
export async function getDefaultTargetLabelConfiguration(
|
export async function getDefaultTargetLabelConfiguration(
|
||||||
api: GithubClient, github: GithubConfig, npmPackageName: string): Promise<TargetLabel[]> {
|
api: GithubClient, githubConfig: GithubConfig,
|
||||||
const repo: GithubRepoWithApi = {owner: github.owner, name: github.name, api};
|
releaseConfig: ReleaseConfig): Promise<TargetLabel[]> {
|
||||||
const nextVersion = await getVersionOfBranch(repo, nextBranchName);
|
const repo = {owner: githubConfig.owner, name: githubConfig.name, api};
|
||||||
const hasNextMajorTrain = nextVersion.minor === 0;
|
const {latest, releaseCandidate, next} = await fetchActiveReleaseTrains(repo);
|
||||||
const {latest, releaseCandidate} = await fetchActiveReleaseTrainBranches(repo, nextVersion);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -33,7 +39,7 @@ export async function getDefaultTargetLabelConfiguration(
|
||||||
branches: () => {
|
branches: () => {
|
||||||
// If `next` is currently not designated to be a major version, we do not
|
// If `next` is currently not designated to be a major version, we do not
|
||||||
// allow merging of PRs with `target: major`.
|
// allow merging of PRs with `target: major`.
|
||||||
if (!hasNextMajorTrain) {
|
if (!next.isMajor) {
|
||||||
throw new InvalidTargetLabelError(
|
throw new InvalidTargetLabelError(
|
||||||
`Unable to merge pull request. The "${nextBranchName}" branch will be ` +
|
`Unable to merge pull request. The "${nextBranchName}" branch will be ` +
|
||||||
`released as a minor version.`);
|
`released as a minor version.`);
|
||||||
|
@ -99,7 +105,7 @@ export async function getDefaultTargetLabelConfiguration(
|
||||||
// commonly diverge quickly. This makes cherry-picking not an option for LTS changes.
|
// commonly diverge quickly. This makes cherry-picking not an option for LTS changes.
|
||||||
pattern: 'target: lts',
|
pattern: 'target: lts',
|
||||||
branches: async githubTargetBranch => {
|
branches: async githubTargetBranch => {
|
||||||
if (!isReleaseTrainBranch(githubTargetBranch)) {
|
if (!isVersionBranch(githubTargetBranch)) {
|
||||||
throw new InvalidTargetBranchError(
|
throw new InvalidTargetBranchError(
|
||||||
`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}"`);
|
||||||
|
@ -115,7 +121,7 @@ export async function getDefaultTargetLabelConfiguration(
|
||||||
`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, npmPackageName, githubTargetBranch);
|
await assertActiveLtsBranch(repo, releaseConfig, githubTargetBranch);
|
||||||
return [githubTargetBranch];
|
return [githubTargetBranch];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,46 +6,25 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
|
|
||||||
|
import {ReleaseConfig} from '../../../release/config/index';
|
||||||
|
import {computeLtsEndDateOfMajor, fetchProjectNpmPackageInfo, getLtsNpmDistTagOfMajor, getVersionOfBranch, GithubRepoWithApi} from '../../../release/versioning';
|
||||||
import {promptConfirm, red, warn, yellow} from '../../../utils/console';
|
import {promptConfirm, red, warn, yellow} from '../../../utils/console';
|
||||||
import {InvalidTargetBranchError} from '../target-label';
|
import {InvalidTargetBranchError} from '../target-label';
|
||||||
|
|
||||||
import {getVersionOfBranch, GithubRepoWithApi} from './branches';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of months a major version in Angular is actively supported. See:
|
|
||||||
* https://angular.io/guide/releases#support-policy-and-schedule.
|
|
||||||
*/
|
|
||||||
export const majorActiveSupportDuration = 6;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of months a major version has active long-term support. See:
|
|
||||||
* https://angular.io/guide/releases#support-policy-and-schedule.
|
|
||||||
*/
|
|
||||||
export const majorActiveTermSupportDuration = 12;
|
|
||||||
|
|
||||||
/** Regular expression that matches LTS NPM dist tags. */
|
|
||||||
export const ltsNpmDistTagRegex = /^v(\d+)-lts$/;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* backport 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.
|
||||||
*
|
*
|
||||||
* @param repo Github repository for which the given branch exists.
|
* @param repo Repository containing the given branch. Used for Github API queries.
|
||||||
* @param representativeNpmPackage NPM package representing the given repository. Angular
|
* @param releaseConfig Configuration for releases. Used to query NPM about past publishes.
|
||||||
* 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.
|
* @param branchName Branch that is checked to be an active LTS version-branch.
|
||||||
* */
|
* */
|
||||||
export async function assertActiveLtsBranch(
|
export async function assertActiveLtsBranch(
|
||||||
repo: GithubRepoWithApi, representativeNpmPackage: string, branchName: string) {
|
repo: GithubRepoWithApi, releaseConfig: ReleaseConfig, 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 fetchProjectNpmPackageInfo(releaseConfig);
|
||||||
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 ltsNpmTag = getLtsNpmDistTagOfMajor(version.major);
|
const ltsNpmTag = getLtsNpmDistTagOfMajor(version.major);
|
||||||
|
@ -87,21 +66,3 @@ export async function assertActiveLtsBranch(
|
||||||
`Pull request cannot be merged into the ${branchName} branch.`);
|
`Pull request cannot be merged into the ${branchName} branch.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the date when long-term support ends for a major released at the
|
|
||||||
* specified date.
|
|
||||||
*/
|
|
||||||
export function computeLtsEndDateOfMajor(majorReleaseDate: Date): Date {
|
|
||||||
return new Date(
|
|
||||||
majorReleaseDate.getFullYear(),
|
|
||||||
majorReleaseDate.getMonth() + majorActiveSupportDuration + majorActiveTermSupportDuration,
|
|
||||||
majorReleaseDate.getDate(), majorReleaseDate.getHours(), majorReleaseDate.getMinutes(),
|
|
||||||
majorReleaseDate.getSeconds(), majorReleaseDate.getMilliseconds());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets the long-term support NPM dist tag for a given major version. */
|
|
||||||
export function getLtsNpmDistTagOfMajor(major: number): string {
|
|
||||||
// LTS versions should be tagged in NPM in the following format: `v{major}-lts`.
|
|
||||||
return `v${major}-lts`;
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "versioning",
|
||||||
|
srcs = glob([
|
||||||
|
"**/*.ts",
|
||||||
|
]),
|
||||||
|
module_name = "@angular/dev-infra-private/release/versioning",
|
||||||
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
|
deps = [
|
||||||
|
"//dev-infra/release/config",
|
||||||
|
"//dev-infra/utils",
|
||||||
|
"@npm//@types/node-fetch",
|
||||||
|
"@npm//@types/semver",
|
||||||
|
"@npm//node-fetch",
|
||||||
|
"@npm//semver",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Versioning for the Angular organization
|
||||||
|
|
||||||
|
The folder contains common tooling needed for implementing the versioning as proposed
|
||||||
|
by [this design document](https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU/edit#heading=h.s3qlps8f4zq7).
|
||||||
|
Primary tooling is the determination of _active_ release trains.
|
|
@ -7,52 +7,28 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import {GithubClient, GithubRepo} from '../../../utils/git/github';
|
|
||||||
|
|
||||||
/** Type describing a Github repository with corresponding API client. */
|
import {ReleaseTrain} from './release-trains';
|
||||||
export interface GithubRepoWithApi extends GithubRepo {
|
import {getBranchesForMajorVersions, getVersionOfBranch, GithubRepoWithApi, VersionBranch} from './version-branches';
|
||||||
/** API client that can access the repository. */
|
|
||||||
api: GithubClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Type describing a version-branch. */
|
/** Interface describing determined active release trains for a project. */
|
||||||
export interface VersionBranch {
|
export interface ActiveReleaseTrains {
|
||||||
/** Name of the branch in Git. e.g. `10.0.x`. */
|
/** Release-train currently in the "release-candidate" or "feature-freeze" phase. */
|
||||||
name: string;
|
releaseCandidate: ReleaseTrain|null;
|
||||||
/**
|
/** Release-train currently in the "latest" phase. */
|
||||||
* Parsed SemVer version for the version-branch. Version branches technically do
|
latest: ReleaseTrain;
|
||||||
* not follow the SemVer format, but we can have representative SemVer versions
|
/** Release-train in the `next` phase */
|
||||||
* that can be used for comparisons, sorting and other checks.
|
next: ReleaseTrain;
|
||||||
*/
|
|
||||||
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';
|
||||||
|
|
||||||
/** Regular expression that matches version-branches for a release-train. */
|
/** Fetches the active release trains for the configured project. */
|
||||||
const releaseTrainBranchNameRegex = /(\d+)\.(\d+)\.x/;
|
export async function fetchActiveReleaseTrains(repo: GithubRepoWithApi):
|
||||||
|
Promise<ActiveReleaseTrains> {
|
||||||
/**
|
const nextVersion = await getVersionOfBranch(repo, nextBranchName);
|
||||||
* Fetches the active release train and its branches for the specified major version. i.e.
|
const next = new ReleaseTrain(nextBranchName, nextVersion);
|
||||||
* the latest active release-train branch name is resolved and an optional version-branch for
|
|
||||||
* a currently active feature-freeze/release-candidate release-train.
|
|
||||||
*/
|
|
||||||
export async function fetchActiveReleaseTrainBranches(
|
|
||||||
repo: GithubRepoWithApi, nextVersion: semver.SemVer): Promise<{
|
|
||||||
/** 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[] = [];
|
const majorVersionsToConsider: number[] = [];
|
||||||
let expectedReleaseCandidateMajor: number;
|
let expectedReleaseCandidateMajor: number;
|
||||||
|
|
||||||
|
@ -93,65 +69,7 @@ export async function fetchActiveReleaseTrainBranches(
|
||||||
`have been considered: [${branches.map(b => b.name).join(', ')}]`);
|
`have been considered: [${branches.map(b => b.name).join(', ')}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {releaseCandidate, latest};
|
return {releaseCandidate, latest, next};
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets the version of a given branch by reading the `package.json` upstream. */
|
|
||||||
export async function getVersionOfBranch(
|
|
||||||
repo: GithubRepoWithApi, branchName: string): Promise<semver.SemVer> {
|
|
||||||
const {data} = await repo.api.repos.getContents(
|
|
||||||
{owner: repo.owner, repo: repo.name, path: '/package.json', ref: branchName});
|
|
||||||
const {version} = JSON.parse(Buffer.from(data.content, 'base64').toString());
|
|
||||||
const parsedVersion = semver.parse(version);
|
|
||||||
if (parsedVersion === null) {
|
|
||||||
throw Error(`Invalid version detected in following branch: ${branchName}.`);
|
|
||||||
}
|
|
||||||
return parsedVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Whether the given branch corresponds to a release-train branch. */
|
|
||||||
export function isReleaseTrainBranch(branchName: string): boolean {
|
|
||||||
return releaseTrainBranchNameRegex.test(branchName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a given version-branch into a SemVer version that can be used with SemVer
|
|
||||||
* utilities. e.g. to determine semantic order, extract major digit, compare.
|
|
||||||
*
|
|
||||||
* For example `10.0.x` will become `10.0.0` in SemVer. The patch digit is not
|
|
||||||
* relevant but needed for parsing. SemVer does not allow `x` as patch digit.
|
|
||||||
*/
|
|
||||||
export function getVersionForReleaseTrainBranch(branchName: string): semver.SemVer|null {
|
|
||||||
// Convert a given version-branch into a SemVer version that can be used
|
|
||||||
// with the SemVer utilities. i.e. to determine semantic order.
|
|
||||||
return semver.parse(branchName.replace(releaseTrainBranchNameRegex, '$1.$2.0'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the version branches for the specified major versions in descending
|
|
||||||
* order. i.e. latest version branches first.
|
|
||||||
*/
|
|
||||||
export async function getBranchesForMajorVersions(
|
|
||||||
repo: GithubRepoWithApi, majorVersions: number[]): Promise<VersionBranch[]> {
|
|
||||||
const {data: branchData} =
|
|
||||||
await repo.api.repos.listBranches({owner: repo.owner, repo: repo.name, protected: true});
|
|
||||||
const branches: VersionBranch[] = [];
|
|
||||||
|
|
||||||
for (const {name} of branchData) {
|
|
||||||
if (!isReleaseTrainBranch(name)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Convert the version-branch into a SemVer version that can be used with the
|
|
||||||
// SemVer utilities. e.g. to determine semantic order, compare versions.
|
|
||||||
const parsed = getVersionForReleaseTrainBranch(name);
|
|
||||||
// Collect all version-branches that match the specified major versions.
|
|
||||||
if (parsed !== null && majorVersions.includes(parsed.major)) {
|
|
||||||
branches.push({name, parsed});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort captured version-branches in descending order.
|
|
||||||
return branches.sort((a, b) => semver.rcompare(a.parsed, b.parsed));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Finds the currently active release trains from the specified version branches. */
|
/** Finds the currently active release trains from the specified version branches. */
|
||||||
|
@ -196,7 +114,7 @@ export async function findActiveReleaseTrainsFromVersionBranches(
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = await getVersionOfBranch(repo, name);
|
const version = await getVersionOfBranch(repo, name);
|
||||||
const releaseTrain: ReleaseTrain = {branchName: name, version};
|
const releaseTrain = new ReleaseTrain(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) {
|
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './active-release-trains';
|
||||||
|
export * from './release-trains';
|
||||||
|
export * from './long-term-support';
|
||||||
|
export * from './version-branches';
|
||||||
|
export * from './npm-registry';
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of months a major version in Angular is actively supported. See:
|
||||||
|
* https://angular.io/guide/releases#support-policy-and-schedule.
|
||||||
|
*/
|
||||||
|
export const majorActiveSupportDuration = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of months a major version has active long-term support. See:
|
||||||
|
* https://angular.io/guide/releases#support-policy-and-schedule.
|
||||||
|
*/
|
||||||
|
export const majorActiveTermSupportDuration = 12;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the date when long-term support ends for a major released at the
|
||||||
|
* specified date.
|
||||||
|
*/
|
||||||
|
export function computeLtsEndDateOfMajor(majorReleaseDate: Date): Date {
|
||||||
|
return new Date(
|
||||||
|
majorReleaseDate.getFullYear(),
|
||||||
|
majorReleaseDate.getMonth() + majorActiveSupportDuration + majorActiveTermSupportDuration,
|
||||||
|
majorReleaseDate.getDate(), majorReleaseDate.getHours(), majorReleaseDate.getMinutes(),
|
||||||
|
majorReleaseDate.getSeconds(), majorReleaseDate.getMilliseconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the long-term support NPM dist tag for a given major version. */
|
||||||
|
export function getLtsNpmDistTagOfMajor(major: number): string {
|
||||||
|
// LTS versions should be tagged in NPM in the following format: `v{major}-lts`.
|
||||||
|
return `v${major}-lts`;
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* @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 fetch from 'node-fetch';
|
||||||
|
import * as semver from 'semver';
|
||||||
|
|
||||||
|
import {ReleaseConfig} from '../config/index';
|
||||||
|
|
||||||
|
/** Type describing an NPM package fetched from the registry. */
|
||||||
|
export interface NpmPackageInfo {
|
||||||
|
/** Maps of versions and their package JSON objects. */
|
||||||
|
'versions': {[name: string]: undefined|object};
|
||||||
|
/** Map of NPM dist-tags and their chosen version. */
|
||||||
|
'dist-tags': {[tagName: string]: string|undefined};
|
||||||
|
/** Map of versions and their ISO release time. */
|
||||||
|
'time': {[name: string]: string};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache for requested NPM package information. A cache is desirable as the NPM
|
||||||
|
* registry requests are usually very large and slow.
|
||||||
|
*/
|
||||||
|
export const _npmPackageInfoCache: {[pkgName: string]: Promise<NpmPackageInfo>} = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the NPM package representing the project. Angular repositories usually contain
|
||||||
|
* multiple packages in a monorepo scheme, but packages dealt with as part of the release
|
||||||
|
* tooling are released together with the same versioning and branching. This means that
|
||||||
|
* a single package can be used as source of truth for NPM package queries.
|
||||||
|
*/
|
||||||
|
export async function fetchProjectNpmPackageInfo(config: ReleaseConfig): Promise<NpmPackageInfo> {
|
||||||
|
const pkgName = getRepresentativeNpmPackage(config);
|
||||||
|
return await fetchPackageInfoFromNpmRegistry(pkgName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets whether the given version is published to NPM or not */
|
||||||
|
export async function isVersionPublishedToNpm(
|
||||||
|
version: semver.SemVer, config: ReleaseConfig): Promise<boolean> {
|
||||||
|
const {versions} = await fetchProjectNpmPackageInfo(config);
|
||||||
|
return versions[version.format()] !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the representative NPM package for the specified release configuration. Angular
|
||||||
|
* repositories usually contain multiple packages in a monorepo scheme, but packages dealt with
|
||||||
|
* as part of the release tooling are released together with the same versioning and branching.
|
||||||
|
* This means that a single package can be used as source of truth for NPM package queries.
|
||||||
|
*/
|
||||||
|
function getRepresentativeNpmPackage(config: ReleaseConfig) {
|
||||||
|
return config.npmPackages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetches the specified NPM package from the NPM registry. */
|
||||||
|
async function fetchPackageInfoFromNpmRegistry(pkgName: string): Promise<NpmPackageInfo> {
|
||||||
|
if (_npmPackageInfoCache[pkgName] !== undefined) {
|
||||||
|
return await _npmPackageInfoCache[pkgName];
|
||||||
|
}
|
||||||
|
const result = _npmPackageInfoCache[pkgName] =
|
||||||
|
fetch(`https://registry.npmjs.org/${pkgName}`).then(r => r.json());
|
||||||
|
return await result;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* @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';
|
||||||
|
|
||||||
|
/** Class describing a release-train. */
|
||||||
|
export class ReleaseTrain {
|
||||||
|
/** Whether the release train is currently targeting a major. */
|
||||||
|
isMajor = this.version.minor === 0 && this.version.patch === 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
/** Name of the branch for this release-train. */
|
||||||
|
public branchName: string,
|
||||||
|
/** Most recent version for this release train. */
|
||||||
|
public version: semver.SemVer) {}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* @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 {GithubClient, GithubRepo} from '../../utils/git/github';
|
||||||
|
|
||||||
|
/** Type describing a Github repository with corresponding API client. */
|
||||||
|
export interface GithubRepoWithApi extends GithubRepo {
|
||||||
|
/** API client that can access the repository. */
|
||||||
|
api: GithubClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type describing a version-branch. */
|
||||||
|
export interface VersionBranch {
|
||||||
|
/** Name of the branch in Git. e.g. `10.0.x`. */
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Parsed SemVer version for the version-branch. Version branches technically do
|
||||||
|
* not follow the SemVer format, but we can have representative SemVer versions
|
||||||
|
* that can be used for comparisons, sorting and other checks.
|
||||||
|
*/
|
||||||
|
parsed: semver.SemVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Regular expression that matches version-branches. */
|
||||||
|
const versionBranchNameRegex = /(\d+)\.(\d+)\.x/;
|
||||||
|
|
||||||
|
/** Gets the version of a given branch by reading the `package.json` upstream. */
|
||||||
|
export async function getVersionOfBranch(
|
||||||
|
repo: GithubRepoWithApi, branchName: string): Promise<semver.SemVer> {
|
||||||
|
const {data} = await repo.api.repos.getContents(
|
||||||
|
{owner: repo.owner, repo: repo.name, path: '/package.json', ref: branchName});
|
||||||
|
const {version} = JSON.parse(Buffer.from(data.content, 'base64').toString());
|
||||||
|
const parsedVersion = semver.parse(version);
|
||||||
|
if (parsedVersion === null) {
|
||||||
|
throw Error(`Invalid version detected in following branch: ${branchName}.`);
|
||||||
|
}
|
||||||
|
return parsedVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the given branch corresponds to a version branch. */
|
||||||
|
export function isVersionBranch(branchName: string): boolean {
|
||||||
|
return versionBranchNameRegex.test(branchName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a given version-branch into a SemVer version that can be used with SemVer
|
||||||
|
* utilities. e.g. to determine semantic order, extract major digit, compare.
|
||||||
|
*
|
||||||
|
* For example `10.0.x` will become `10.0.0` in SemVer. The patch digit is not
|
||||||
|
* relevant but needed for parsing. SemVer does not allow `x` as patch digit.
|
||||||
|
*/
|
||||||
|
export function getVersionForVersionBranch(branchName: string): semver.SemVer|null {
|
||||||
|
// Convert a given version-branch into a SemVer version that can be used
|
||||||
|
// with the SemVer utilities. i.e. to determine semantic order.
|
||||||
|
return semver.parse(branchName.replace(versionBranchNameRegex, '$1.$2.0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the version branches for the specified major versions in descending
|
||||||
|
* order. i.e. latest version branches first.
|
||||||
|
*/
|
||||||
|
export async function getBranchesForMajorVersions(
|
||||||
|
repo: GithubRepoWithApi, majorVersions: number[]): Promise<VersionBranch[]> {
|
||||||
|
const {data: branchData} =
|
||||||
|
await repo.api.repos.listBranches({owner: repo.owner, repo: repo.name, protected: true});
|
||||||
|
const branches: VersionBranch[] = [];
|
||||||
|
|
||||||
|
for (const {name} of branchData) {
|
||||||
|
if (!isVersionBranch(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Convert the version-branch into a SemVer version that can be used with the
|
||||||
|
// SemVer utilities. e.g. to determine semantic order, compare versions.
|
||||||
|
const parsed = getVersionForVersionBranch(name);
|
||||||
|
// Collect all version-branches that match the specified major versions.
|
||||||
|
if (parsed !== null && majorVersions.includes(parsed.major)) {
|
||||||
|
branches.push({name, parsed});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort captured version-branches in descending order.
|
||||||
|
return branches.sort((a, b) => semver.rcompare(a.parsed, b.parsed));
|
||||||
|
}
|
Loading…
Reference in New Issue