Skip to content

Commit 5b4188f

Browse files
devversionangular-robot[bot]
authored andcommitted
build: add pre-release checks for dependency on framework
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.
1 parent 71731f2 commit 5b4188f

File tree

5 files changed

+185
-0
lines changed

5 files changed

+185
-0
lines changed

.ng-dev/release.mts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import semver from 'semver';
12
import { ReleaseConfig } from '@angular/ng-dev';
23
import packages from '../lib/packages.js';
34

@@ -16,6 +17,14 @@ export const release: ReleaseConfig = {
1617
const { performNpmReleaseBuild } = await import('../scripts/build-packages-dist.mjs');
1718
return performNpmReleaseBuild();
1819
},
20+
prereleaseCheck: async (newVersionStr: string) => {
21+
const newVersion = new semver.SemVer(newVersionStr);
22+
const { assertValidDependencyRanges } = await import(
23+
'../scripts/release-checks/dependency-ranges/index.mjs'
24+
);
25+
26+
await assertValidDependencyRanges(newVersion, packages.releasePackages);
27+
},
1928
releaseNotes: {
2029
groupOrder: [
2130
'@angular/cli',
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import semver from 'semver';
10+
import { Log, bold, ReleasePrecheckError } from '@angular/ng-dev';
11+
import { checkPeerDependencies } from './peer-deps-check.mjs';
12+
import { checkSchematicsAngularLatestVersion } from './latest-versions-check.mjs';
13+
import { PackageMap } from '../../../lib/packages.js';
14+
15+
/** Environment variable that can be used to skip this pre-check. */
16+
const skipEnvVar = 'SKIP_DEPENDENCY_RANGE_PRECHECK';
17+
18+
/**
19+
* Ensures that dependency ranges are properly updated before publishing
20+
* a new version. This check includes:
21+
*
22+
* - checking of `latest-versions.ts` of `@schematics/angular`.
23+
* - checking of peer dependencies in `@angular-devkit/build-angular`.
24+
*
25+
* @throws {ReleasePrecheckError} If validation fails.
26+
*/
27+
export async function assertValidDependencyRanges(
28+
newVersion: semver.SemVer,
29+
allPackages: PackageMap,
30+
) {
31+
if (process.env[skipEnvVar] === '1') {
32+
return;
33+
}
34+
35+
const failures: string[] = [
36+
...(await checkPeerDependencies(newVersion, allPackages)),
37+
...(await checkSchematicsAngularLatestVersion(newVersion)),
38+
];
39+
40+
if (failures.length !== 0) {
41+
Log.error('Discovered errors when validating dependency ranges.');
42+
43+
for (const f of failures) {
44+
Log.error(` - ${bold(f)}`);
45+
}
46+
47+
Log.warn();
48+
Log.warn('Please fix these failures before publishing a new release.');
49+
Log.warn(`These checks can be forcibly ignored by setting: ${skipEnvVar}=1`);
50+
Log.warn();
51+
52+
throw new ReleasePrecheckError();
53+
}
54+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import semver from 'semver';
10+
11+
export async function checkSchematicsAngularLatestVersion(
12+
newVersion: semver.SemVer,
13+
): Promise<string[]> {
14+
const {
15+
default: { latestVersions },
16+
} = await import('../../../packages/schematics/angular/utility/latest-versions.js');
17+
18+
const keysToCheck = ['ng-packagr', 'Angular'];
19+
const { major, minor } = newVersion;
20+
const isPrerelease = !!newVersion.prerelease[0];
21+
const failures: string[] = [];
22+
23+
let expectedFwDep = `^${major}.${minor}.0`;
24+
if (isPrerelease) {
25+
expectedFwDep = `^${major}.${minor}.0-next.0`;
26+
}
27+
28+
for (const key of keysToCheck) {
29+
if (latestVersions[key] !== expectedFwDep) {
30+
failures.push(
31+
`latest-versions: Invalid dependency range for "${key}". Expected: ${expectedFwDep}`,
32+
);
33+
}
34+
}
35+
36+
return failures;
37+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import path from 'path';
10+
import url from 'url';
11+
import semver from 'semver';
12+
import { PackageMap } from '../../../lib/packages.js';
13+
14+
/** Path to the current directory. */
15+
const currentDir = path.dirname(url.fileURLToPath(import.meta.url));
16+
17+
/** Path to the project directory. */
18+
const projectDir = path.join(currentDir, '../../../');
19+
20+
/** Describes a parsed `package.json` file. */
21+
interface PackageJson {
22+
name?: string;
23+
peerDependencies?: Record<string, string>;
24+
}
25+
26+
export async function checkPeerDependencies(
27+
newVersion: semver.SemVer,
28+
allPackages: PackageMap,
29+
): Promise<string[]> {
30+
const { major, minor } = newVersion;
31+
const isPrerelease = !!newVersion.prerelease[0];
32+
const isMajor = minor === 0;
33+
34+
let expectedFwPeerDep = `^${major}.0.0`;
35+
if (isMajor && isPrerelease) {
36+
expectedFwPeerDep = `^${major}.0.0-next.0`;
37+
} else if (isPrerelease) {
38+
expectedFwPeerDep = `^${major}.0.0 || ^${major}.${minor}.0-next.0`;
39+
}
40+
41+
const failures: string[] = [];
42+
for (const pkgInfo of Object.values(allPackages)) {
43+
failures.push(...checkPackage(pkgInfo.packageJson, expectedFwPeerDep));
44+
}
45+
46+
return failures;
47+
}
48+
49+
/** Checks the given package and collects errors for the peer dependency ranges. */
50+
function checkPackage(pkgJson: PackageJson, expectedFwPeerDep: string): string[] {
51+
if (pkgJson.peerDependencies === undefined) {
52+
return [];
53+
}
54+
55+
const failures: string[] = [];
56+
57+
for (const [depName, range] of Object.entries(pkgJson.peerDependencies)) {
58+
// Even though `ng-packagr` might not strictly follow the same release schedules
59+
// like official Angular packages, we generally expect it to match. It's better
60+
// flagging it than silently passing pre-checks. The caretaker can always forcibly
61+
// ignore this check.
62+
if (!depName.startsWith('@angular/') && depName !== 'ng-packagr') {
63+
continue;
64+
}
65+
66+
if (range !== expectedFwPeerDep) {
67+
failures.push(
68+
`${pkgJson.name}: Unexpected peer dependency range for "${depName}". Expected: ${expectedFwPeerDep}`,
69+
);
70+
}
71+
}
72+
73+
return failures;
74+
}

scripts/release-checks/tsconfig.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"module": "Node16",
5+
"moduleResolution": "Node16",
6+
"noEmit": true,
7+
"types": []
8+
},
9+
"include": ["**/*.mts"],
10+
"exclude": []
11+
}

0 commit comments

Comments
 (0)