build: add script to diff packages and ensure no unexpected changes (#61275)

Adds a script to diff snapshot packages to allow for easy verification
that no unexpected changes were made.

PR Close #61275
This commit is contained in:
Paul Gschwendtner 2025-05-12 15:34:36 +00:00 committed by Jessica Janiuk
parent 925b923e85
commit 8e78b4e438
4 changed files with 152 additions and 11 deletions

View file

@ -2,10 +2,10 @@
# Input hashes for repository rule npm_translate_lock(name = "npm2", pnpm_lock = "@//:pnpm-lock.yaml").
# This file should be checked into version control along with the pnpm-lock.yaml file.
.npmrc=-1406867100
package.json=-686175807
package.json=1344581013
packages/compiler-cli/package.json=-1767555217
packages/compiler/package.json=-426903429
pnpm-lock.yaml=-445193031
pnpm-lock.yaml=953012603
pnpm-workspace.yaml=353334404
tools/bazel/rules_angular_store/package.json=-239561259
yarn.lock=1067917288

View file

@ -44,7 +44,8 @@
"devtools:test": "bazelisk test --//devtools/projects/shell-browser/src:flag_browser=chrome -- //devtools/...",
"docs": "[[ -n $CI ]] && echo 'Cannot run this yarn script on CI' && exit 1 || yarn ibazel run //adev:serve",
"docs:build": "[[ -n $CI ]] && echo 'Cannot run this yarn script on CI' && exit 1 || yarn bazel build //adev:build",
"benchmarks": "tsx --tsconfig=scripts/tsconfig.json scripts/benchmarks/index.mts"
"benchmarks": "tsx --tsconfig=scripts/tsconfig.json scripts/benchmarks/index.mts",
"diff-release-package": "tsx --tsconfig=scripts/tsconfig.json scripts/diff-release-package.mts"
},
"// 1": "dependencies are used locally and by bazel",
"dependencies": {

View file

@ -6515,13 +6515,8 @@ packages:
dependencies:
ajv: 8.13.0
/ajv-formats@2.1.1(ajv@8.17.1):
/ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
dependencies:
ajv: 8.17.1
@ -9816,7 +9811,7 @@ packages:
dependencies:
'@apidevtools/json-schema-ref-parser': 9.1.2
ajv: 8.17.1
ajv-formats: 2.1.1(ajv@8.17.1)
ajv-formats: 2.1.1
body-parser: 1.20.3
content-type: 1.0.5
deep-freeze: 0.0.1
@ -15947,7 +15942,7 @@ packages:
dependencies:
'@types/json-schema': 7.0.15
ajv: 8.17.1
ajv-formats: 2.1.1(ajv@8.17.1)
ajv-formats: 2.1.1
ajv-keywords: 5.1.0(ajv@8.17.1)
dev: false

View file

@ -0,0 +1,145 @@
/**
* @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.dev/license
*/
/**
* Script that can be used to compare the local `npm_package` snapshot artifact
* with the snapshot artifact from GitHub at upstream `HEAD`.
*
* This is useful during the `rules_js` migration to verify the npm artifact
* doesn't differ unexpectedly.
*
* Example command: pnpm diff-release-package @angular/cli
*/
import {GitClient} from '@angular/ng-dev';
import childProcess from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import sh from 'shelljs';
import {glob} from 'tinyglobby';
// Do not remove `.git` as we use Git for comparisons later.
// Also preserve `uniqueId` as it's irrelevant for the diff and not included via Bazel.
// The `README.md` is also put together outside of Bazel, so ignore it too.
const SKIP_FILES = [/^README\.md$/, /^uniqueId$/, /\.map$/];
const packageName = process.argv[2];
if (!packageName) {
console.error('Expected package name to be specified.');
process.exit(1);
}
try {
await main(packageName);
} catch (e) {
console.error(e);
process.exitCode = 1;
}
async function main(packageName: string) {
const bazel = process.env.BAZEL ?? 'bazel';
const git = await GitClient.get();
const targetDir = packageName.replace(/^@/g, '').replace(/-/g, '_');
const snapshotRepoName = `angular/${packageName}-builds`;
const tmpDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), `diff-release-package-${snapshotRepoName.replace(/\//g, '_')}`),
);
console.info(`Cloning snapshot repo (${snapshotRepoName}) into ${tmpDir}..`);
git.run(['clone', '--depth=1', `https://github.com/${snapshotRepoName}.git`, tmpDir]);
console.info(`--> Cloned snapshot repo.`);
const bazelBinDir = childProcess
.spawnSync(bazel, ['info', 'bazel-bin'], {
shell: true,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'inherit'],
})
.stdout.trim();
if (bazelBinDir === '') {
throw new Error('Could not determine bazel-bin directory.');
}
const outputPath = path.join(bazelBinDir, 'packages/', targetDir, 'npm_package');
// Delete old directory to avoid surprises, or stamping being outdated.
await deleteDir(outputPath);
childProcess.spawnSync(
bazel,
['build', `//packages/${targetDir}:npm_package`, '--config=snapshot-build'],
{
shell: true,
stdio: 'inherit',
encoding: 'utf8',
},
);
console.info('--> Built npm package with --config=snapshot-build');
console.error(`--> Output: ${outputPath}`);
const removeTasks: Promise<void>[] = [];
for (const subentry of await glob('**/*', {
dot: true,
cwd: tmpDir,
onlyFiles: true,
ignore: ['.git'],
})) {
if (!SKIP_FILES.some((s) => s.test(subentry))) {
continue;
}
removeTasks.push(fs.promises.rm(path.join(tmpDir, subentry), {maxRetries: 3}));
}
await Promise.all(removeTasks);
// Stage all removed files that were skipped; to exclude them from the diff.
git.run(['add', '-A'], {cwd: tmpDir});
git.run(['commit', '-m', 'Delete skipped files for diff'], {cwd: tmpDir});
const copyTasks: Promise<void>[] = [];
for (const subentry of await glob('**/*', {
dot: true,
cwd: outputPath,
onlyFiles: true,
ignore: ['.git'],
})) {
if (SKIP_FILES.some((s) => s.test(subentry))) {
continue;
}
copyTasks.push(
fs.promises.cp(path.join(outputPath, subentry), path.join(tmpDir, subentry), {
recursive: true,
}),
);
}
await Promise.all(copyTasks);
git.run(['config', 'core.filemode', 'false'], {cwd: tmpDir});
const diff = git.run(['diff', '--color'], {cwd: tmpDir}).stdout;
console.info('\n\n----- Diff ------');
console.info(diff);
await deleteDir(tmpDir);
}
async function deleteDir(dirPath: string) {
if (!fs.existsSync(dirPath)) {
return;
}
// Needed as Bazel artifacts are readonly and cannot be deleted otherwise.
sh.chmod('-R', 'u+w', dirPath);
await fs.promises.rm(dirPath, {recursive: true, force: true, maxRetries: 3});
}