fix: pin internal dependencies to exact versions

Update release synchronization and dependency checks to enforce exact versions. Refresh both lockfiles and add regression coverage.

closes #10865
This commit is contained in:
Armin Ronacher 2026-10-13 14:27:38 +02:00
parent a3a2ee8ec2
commit f13c00fb3a
14 changed files with 129 additions and 50 deletions

View file

@ -4,7 +4,6 @@ import { join } from "node:path";
const dependencySections = ["dependencies", "devDependencies", "optionalDependencies"];
const exactVersionPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
const ignoredDirectories = new Set([".git", "dist", "node_modules"]);
const internalPackageNames = new Set(["@earendil-works/chord"]);
const packageJsonFiles = [];
function collectPackageJsonFiles(directory) {
@ -22,10 +21,6 @@ function collectPackageJsonFiles(directory) {
}
}
function isInternalWorkspaceDependency(name) {
return name.startsWith("@earendil-works/pi-") || internalPackageNames.has(name);
}
function isNonRegistrySpecifier(specifier) {
return /^(?:workspace:|file:|link:|portal:|git\+|github:|git:|https?:|ssh:|git:\/\/)/.test(specifier);
}
@ -50,7 +45,7 @@ for (const file of packageJsonFiles.sort()) {
if (!dependencies) continue;
for (const [name, specifier] of Object.entries(dependencies)) {
if (isInternalWorkspaceDependency(name) || isNonRegistrySpecifier(specifier)) continue;
if (isNonRegistrySpecifier(specifier)) continue;
if (exactVersionPattern.test(getVersionSpecifier(specifier))) continue;
failures.push(`${file}: ${section}.${name} must be pinned, found ${specifier}`);
}
@ -58,7 +53,7 @@ for (const file of packageJsonFiles.sort()) {
}
if (failures.length > 0) {
console.error("Direct external dependencies must use exact versions:");
console.error("Direct registry dependencies must use exact versions:");
for (const failure of failures) console.error(` ${failure}`);
process.exit(1);
}

View file

@ -0,0 +1,63 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
const script = fileURLToPath(new URL("./check-pinned-deps.mjs", import.meta.url));
async function check(t, manifest) {
const root = await mkdtemp(join(tmpdir(), "pi-pinned-deps-"));
t.after(() => rm(root, { recursive: true, force: true }));
await writeFile(join(root, "package.json"), JSON.stringify(manifest));
return spawnSync(process.execPath, [script], { cwd: root, encoding: "utf8" });
}
// #10865: internal workspace dependencies must not allow incompatible newer releases.
test("rejects internal and external registry dependency ranges in every direct dependency section", async (t) => {
const result = await check(t, {
dependencies: {
"@earendil-works/pi-codemode": "^1.2.0",
"@earendil-works/chord": "~1.2.0",
external: "^1.0.0",
},
devDependencies: { "@earendil-works/pi-ai": "1.x" },
optionalDependencies: { "@earendil-works/pi-tui": "*" },
});
assert.equal(result.status, 1);
for (const dependency of [
"dependencies.@earendil-works/pi-codemode",
"dependencies.@earendil-works/chord",
"dependencies.external",
"devDependencies.@earendil-works/pi-ai",
"optionalDependencies.@earendil-works/pi-tui",
]) {
assert.ok(result.stderr.includes(`${dependency} must be pinned`), result.stderr);
}
});
test("accepts exact internal and external versions, exact registry aliases, and non-registry specifiers", async (t) => {
const result = await check(t, {
dependencies: {
"@earendil-works/pi-codemode": "1.2.0",
"@earendil-works/chord": "1.2.0",
external: "1.0.0",
alias: "npm:@earendil-works/pi-ai@1.1.0",
local: "file:../local",
workspace: "workspace:*",
},
devDependencies: { "@earendil-works/pi-ai": "1.2.0" },
optionalDependencies: { "@earendil-works/pi-tui": "1.2.0-rc.1" },
});
assert.equal(result.status, 0, result.stderr);
});
test("rejects registry alias ranges", async (t) => {
const result = await check(t, {
dependencies: { alias: "npm:@earendil-works/pi-ai@^1.2.0" },
});
assert.equal(result.status, 1);
assert.match(result.stderr, /dependencies\.alias must be pinned/);
});

View file

@ -2,7 +2,7 @@
/**
* Validates lockstep versions for published packages, then synchronizes
* internal dependency versions in all workspace packages, including private ones.
* exact internal dependency versions in all workspace packages, including private ones.
*/
import { readFileSync, writeFileSync } from "node:fs";
@ -41,7 +41,7 @@ console.log("\nAll non-private packages are at the same version (lockstep).");
let totalUpdates = 0;
const updatedPackages = new Set();
for (const pkg of workspacePackages) {
for (const dependencyType of ["dependencies", "devDependencies"]) {
for (const dependencyType of ["dependencies", "devDependencies", "optionalDependencies"]) {
const dependencies = pkg.data[dependencyType];
if (!dependencies) {
continue;
@ -50,15 +50,14 @@ for (const pkg of workspacePackages) {
for (const [dependencyName, currentSpecifier] of Object.entries(dependencies)) {
// Registry aliases such as `npm:@earendil-works/pi-ai@0.1.2` are never workspace-linked,
// so lockstep bumping them would point at a version that is not published yet.
const version = versionMap.get(dependencyName);
const newSpecifier = version ? `^${version}` : null;
const newSpecifier = versionMap.get(dependencyName);
if (!newSpecifier || currentSpecifier === newSpecifier) {
continue;
}
console.log(`\n${pkg.data.name}:`);
console.log(
` ${dependencyName}: ${currentSpecifier} → ${newSpecifier}${dependencyType === "devDependencies" ? " (devDependencies)" : ""}`,
` ${dependencyName}: ${currentSpecifier} → ${newSpecifier}${dependencyType === "dependencies" ? "" : ` (${dependencyType})`}`,
);
dependencies[dependencyName] = newSpecifier;
updatedPackages.add(pkg);

View file

@ -25,7 +25,8 @@ function runSyncVersions(root) {
});
}
test("synchronizes private dependencies without touching registry aliases, generated manifests, or published lockstep", async () => {
// #10865: caret ranges allowed older agents to install incompatible newer workspace dependencies.
test("pins published and private dependencies without touching registry aliases, generated manifests, or published lockstep", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-sync-versions-"));
try {
await writeManifest(root, "packages/ai", {
@ -35,6 +36,15 @@ test("synchronizes private dependencies without touching registry aliases, gener
await writeManifest(root, "packages/coding-agent", {
name: "@earendil-works/pi-coding-agent",
version: "2.0.0",
dependencies: {
"@earendil-works/pi-ai": "^2.0.0",
},
devDependencies: {
"@earendil-works/pi-ai": "~1.0.0",
},
optionalDependencies: {
"@earendil-works/pi-ai": "1.0.0",
},
});
await writeManifest(root, "packages/evals", {
name: "@earendil-works/pi-evals",
@ -57,12 +67,20 @@ test("synchronizes private dependencies without touching registry aliases, gener
const result = runSyncVersions(root);
assert.equal(result.status, 0, result.stderr);
const agentManifest = await readManifest(root, "packages/coding-agent");
for (const section of ["dependencies", "devDependencies", "optionalDependencies"]) {
assert.equal(agentManifest[section]["@earendil-works/pi-ai"], "2.0.0");
}
const evalsManifest = await readManifest(root, "packages/evals");
assert.equal(evalsManifest.dependencies["@earendil-works/pi-coding-agent"], "^2.0.0");
assert.equal(evalsManifest.dependencies["@earendil-works/pi-coding-agent"], "2.0.0");
assert.equal(evalsManifest.dependencies["@mariozechner/pi-ai"], "npm:@earendil-works/pi-ai@1.0.0");
const generatedManifest = await readManifest(root, "packages/coding-agent/install-lock");
assert.equal(generatedManifest.dependencies["@earendil-works/pi-coding-agent"], "^1.0.0");
const alreadySynced = runSyncVersions(root);
assert.equal(alreadySynced.status, 0, alreadySynced.stderr);
assert.match(alreadySynced.stdout, /All inter-package dependencies are already in sync/);
await writeManifest(root, "packages/ai", {
name: "@earendil-works/pi-ai",
version: "3.0.0",