Skip to content

Commit

Permalink
build: add pre-release checks for dependency on framework
Browse files Browse the repository at this point in the history
Adds an automatic check that runs before cutting releases. This
will help avoiding issues where we forget to update peer dependencies
or the `latest-versions.ts` file.

(cherry picked from commit 5b4188f)
  • Loading branch information
devversion authored and alan-agius4 committed Feb 24, 2023
1 parent adb92ac commit 69390ae
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .ng-dev/release.mts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import semver from 'semver';
import { ReleaseConfig } from '@angular/ng-dev';
import packages from '../lib/packages.js';

Expand All @@ -16,6 +17,14 @@ export const release: ReleaseConfig = {
const { performNpmReleaseBuild } = await import('../scripts/build-packages-dist.mjs');
return performNpmReleaseBuild();
},
prereleaseCheck: async (newVersionStr: string) => {
const newVersion = new semver.SemVer(newVersionStr);
const { assertValidDependencyRanges } = await import(
'../scripts/release-checks/dependency-ranges/index.mjs'
);

await assertValidDependencyRanges(newVersion, packages.releasePackages);
},
releaseNotes: {
groupOrder: [
'@angular/cli',
Expand Down
54 changes: 54 additions & 0 deletions scripts/release-checks/dependency-ranges/index.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @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 semver from 'semver';
import { Log, bold, ReleasePrecheckError } from '@angular/ng-dev';
import { checkPeerDependencies } from './peer-deps-check.mjs';
import { checkSchematicsAngularLatestVersion } from './latest-versions-check.mjs';
import { PackageMap } from '../../../lib/packages.js';

/** Environment variable that can be used to skip this pre-check. */
const skipEnvVar = 'SKIP_DEPENDENCY_RANGE_PRECHECK';

/**
* Ensures that dependency ranges are properly updated before publishing
* a new version. This check includes:
*
* - checking of `latest-versions.ts` of `@schematics/angular`.
* - checking of peer dependencies in `@angular-devkit/build-angular`.
*
* @throws {ReleasePrecheckError} If validation fails.
*/
export async function assertValidDependencyRanges(
newVersion: semver.SemVer,
allPackages: PackageMap,
) {
if (process.env[skipEnvVar] === '1') {
return;
}

const failures: string[] = [
...(await checkPeerDependencies(newVersion, allPackages)),
...(await checkSchematicsAngularLatestVersion(newVersion)),
];

if (failures.length !== 0) {
Log.error('Discovered errors when validating dependency ranges.');

for (const f of failures) {
Log.error(` - ${bold(f)}`);
}

Log.warn();
Log.warn('Please fix these failures before publishing a new release.');
Log.warn(`These checks can be forcibly ignored by setting: ${skipEnvVar}=1`);
Log.warn();

throw new ReleasePrecheckError();
}
}
37 changes: 37 additions & 0 deletions scripts/release-checks/dependency-ranges/latest-versions-check.mts
Original file line number Diff line number Diff line change
@@ -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
*/

import semver from 'semver';

export async function checkSchematicsAngularLatestVersion(
newVersion: semver.SemVer,
): Promise<string[]> {
const {
default: { latestVersions },
} = await import('../../../packages/schematics/angular/utility/latest-versions.js');

const keysToCheck = ['ng-packagr', 'Angular'];
const { major, minor } = newVersion;
const isPrerelease = !!newVersion.prerelease[0];
const failures: string[] = [];

let expectedFwDep = `^${major}.${minor}.0`;
if (isPrerelease) {
expectedFwDep = `^${major}.${minor}.0-next.0`;
}

for (const key of keysToCheck) {
if (latestVersions[key] !== expectedFwDep) {
failures.push(
`latest-versions: Invalid dependency range for "${key}". Expected: ${expectedFwDep}`,
);
}
}

return failures;
}
74 changes: 74 additions & 0 deletions scripts/release-checks/dependency-ranges/peer-deps-check.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @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 path from 'path';
import url from 'url';
import semver from 'semver';
import { PackageMap } from '../../../lib/packages.js';

/** Path to the current directory. */
const currentDir = path.dirname(url.fileURLToPath(import.meta.url));

/** Path to the project directory. */
const projectDir = path.join(currentDir, '../../../');

/** Describes a parsed `package.json` file. */
interface PackageJson {
name?: string;
peerDependencies?: Record<string, string>;
}

export async function checkPeerDependencies(
newVersion: semver.SemVer,
allPackages: PackageMap,
): Promise<string[]> {
const { major, minor } = newVersion;
const isPrerelease = !!newVersion.prerelease[0];
const isMajor = minor === 0;

let expectedFwPeerDep = `^${major}.0.0`;
if (isMajor && isPrerelease) {
expectedFwPeerDep = `^${major}.0.0-next.0`;
} else if (isPrerelease) {
expectedFwPeerDep = `^${major}.0.0 || ^${major}.${minor}.0-next.0`;
}

const failures: string[] = [];
for (const pkgInfo of Object.values(allPackages)) {
failures.push(...checkPackage(pkgInfo.packageJson, expectedFwPeerDep));
}

return failures;
}

/** Checks the given package and collects errors for the peer dependency ranges. */
function checkPackage(pkgJson: PackageJson, expectedFwPeerDep: string): string[] {
if (pkgJson.peerDependencies === undefined) {
return [];
}

const failures: string[] = [];

for (const [depName, range] of Object.entries(pkgJson.peerDependencies)) {
// Even though `ng-packagr` might not strictly follow the same release schedules
// like official Angular packages, we generally expect it to match. It's better
// flagging it than silently passing pre-checks. The caretaker can always forcibly
// ignore this check.
if (!depName.startsWith('@angular/') && depName !== 'ng-packagr') {
continue;
}

if (range !== expectedFwPeerDep) {
failures.push(
`${pkgJson.name}: Unexpected peer dependency range for "${depName}". Expected: ${expectedFwPeerDep}`,
);
}
}

return failures;
}
11 changes: 11 additions & 0 deletions scripts/release-checks/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16",
"noEmit": true,
"types": []
},
"include": ["**/*.mts"],
"exclude": []
}

0 comments on commit 69390ae

Please sign in to comment.