mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
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:
parent
a3a2ee8ec2
commit
f13c00fb3a
14 changed files with 129 additions and 50 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
63
scripts/check-pinned-deps.test.mjs
Normal file
63
scripts/check-pinned-deps.test.mjs
Normal 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/);
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue