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
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
## Dependency and Install Security
|
||||
|
||||
- Treat npm dep and lockfile changes as reviewed code. Direct external deps stay pinned to exact versions.
|
||||
- Treat npm dep and lockfile changes as reviewed code. Direct registry deps, including internal workspace deps, stay pinned to exact versions.
|
||||
- When updating `undici`, you MUST read its changelog/release notes for the target version and evaluate whether any changes may affect functionality before applying the update.
|
||||
- Hydrate/update locally with `npm install --ignore-scripts`; clean/CI-style with `npm ci --ignore-scripts`. Don't run lifecycle scripts unless the user asks.
|
||||
- If dep metadata changes, refresh `package-lock.json` with `npm install --package-lock-only --ignore-scripts`.
|
||||
|
|
|
|||
28
package-lock.json
generated
28
package-lock.json
generated
|
|
@ -5033,7 +5033,7 @@
|
|||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^1.2.0",
|
||||
"@earendil-works/pi-ai": "1.2.0",
|
||||
"typebox": "1.3.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -5058,7 +5058,7 @@
|
|||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.129.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1127.0",
|
||||
"@earendil-works/pi-telemetry": "^1.2.0",
|
||||
"@earendil-works/pi-telemetry": "1.2.0",
|
||||
"@google/genai": "2.21.0",
|
||||
"@smithy/node-http-handler": "4.12.1",
|
||||
"http-proxy-agent": "9.1.0",
|
||||
|
|
@ -5176,11 +5176,11 @@
|
|||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-agent-core": "^1.2.0",
|
||||
"@earendil-works/pi-ai": "^1.2.0",
|
||||
"@earendil-works/pi-codemode": "^1.2.0",
|
||||
"@earendil-works/pi-mcp": "^1.2.0",
|
||||
"@earendil-works/pi-tui": "^1.2.0",
|
||||
"@earendil-works/pi-agent-core": "1.2.0",
|
||||
"@earendil-works/pi-ai": "1.2.0",
|
||||
"@earendil-works/pi-codemode": "1.2.0",
|
||||
"@earendil-works/pi-mcp": "1.2.0",
|
||||
"@earendil-works/pi-tui": "1.2.0",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"brace-expansion": "5.0.12",
|
||||
"chalk": "6.0.0",
|
||||
|
|
@ -5300,9 +5300,9 @@
|
|||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/chord": "^1.2.0",
|
||||
"@earendil-works/pi-ai": "^1.2.0",
|
||||
"@earendil-works/pi-codemode": "^1.2.0",
|
||||
"@earendil-works/chord": "1.2.0",
|
||||
"@earendil-works/pi-ai": "1.2.0",
|
||||
"@earendil-works/pi-codemode": "1.2.0",
|
||||
"@silvia-odwyer/photon": "0.3.3",
|
||||
"diff": "8.0.4",
|
||||
"typebox": "1.3.27"
|
||||
|
|
@ -5320,8 +5320,8 @@
|
|||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/chord": "^1.2.0",
|
||||
"@earendil-works/pi-durable": "^1.2.0"
|
||||
"@earendil-works/chord": "1.2.0",
|
||||
"@earendil-works/pi-durable": "1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shx": "0.3.4",
|
||||
|
|
@ -5335,8 +5335,8 @@
|
|||
"name": "@earendil-works/pi-evals",
|
||||
"version": "1.2.0",
|
||||
"devDependencies": {
|
||||
"@earendil-works/pi-ai": "^1.2.0",
|
||||
"@earendil-works/pi-coding-agent": "^1.2.0",
|
||||
"@earendil-works/pi-ai": "1.2.0",
|
||||
"@earendil-works/pi-coding-agent": "1.2.0",
|
||||
"@types/node": "22.19.19",
|
||||
"@vitest-evals/core": "0.15.0",
|
||||
"autoevals": "0.3.0",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^1.2.0",
|
||||
"@earendil-works/pi-ai": "1.2.0",
|
||||
"typebox": "1.3.27"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.129.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1127.0",
|
||||
"@earendil-works/pi-telemetry": "^1.2.0",
|
||||
"@earendil-works/pi-telemetry": "1.2.0",
|
||||
"@google/genai": "2.21.0",
|
||||
"@smithy/node-http-handler": "4.12.1",
|
||||
"http-proxy-agent": "9.1.0",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Pinned internal Pi dependencies to exact release versions to prevent fresh installs from mixing incompatible packages ([#10865](https://github.com/earendil-works/pi/issues/10865))
|
||||
|
||||
## [1.2.0] - 2026-10-12
|
||||
|
||||
### New Features
|
||||
|
|
|
|||
14
packages/coding-agent/install-lock/package-lock.json
generated
14
packages/coding-agent/install-lock/package-lock.json
generated
|
|
@ -390,7 +390,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-1.2.0.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^1.2.0",
|
||||
"@earendil-works/pi-ai": "1.2.0",
|
||||
"typebox": "1.3.27"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -404,7 +404,7 @@
|
|||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.129.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1127.0",
|
||||
"@earendil-works/pi-telemetry": "^1.2.0",
|
||||
"@earendil-works/pi-telemetry": "1.2.0",
|
||||
"@google/genai": "2.21.0",
|
||||
"@smithy/node-http-handler": "4.12.1",
|
||||
"http-proxy-agent": "9.1.0",
|
||||
|
|
@ -496,11 +496,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-1.2.0.tgz",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-agent-core": "^1.2.0",
|
||||
"@earendil-works/pi-ai": "^1.2.0",
|
||||
"@earendil-works/pi-codemode": "^1.2.0",
|
||||
"@earendil-works/pi-mcp": "^1.2.0",
|
||||
"@earendil-works/pi-tui": "^1.2.0",
|
||||
"@earendil-works/pi-agent-core": "1.2.0",
|
||||
"@earendil-works/pi-ai": "1.2.0",
|
||||
"@earendil-works/pi-codemode": "1.2.0",
|
||||
"@earendil-works/pi-mcp": "1.2.0",
|
||||
"@earendil-works/pi-tui": "1.2.0",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"brace-expansion": "5.0.12",
|
||||
"chalk": "6.0.0",
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@
|
|||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-agent-core": "^1.2.0",
|
||||
"@earendil-works/pi-ai": "^1.2.0",
|
||||
"@earendil-works/pi-codemode": "^1.2.0",
|
||||
"@earendil-works/pi-mcp": "^1.2.0",
|
||||
"@earendil-works/pi-tui": "^1.2.0",
|
||||
"@earendil-works/pi-agent-core": "1.2.0",
|
||||
"@earendil-works/pi-ai": "1.2.0",
|
||||
"@earendil-works/pi-codemode": "1.2.0",
|
||||
"@earendil-works/pi-mcp": "1.2.0",
|
||||
"@earendil-works/pi-tui": "1.2.0",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"brace-expansion": "5.0.12",
|
||||
"chalk": "6.0.0",
|
||||
|
|
|
|||
|
|
@ -143,9 +143,9 @@
|
|||
"node": ">=22.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/chord": "^1.2.0",
|
||||
"@earendil-works/pi-ai": "^1.2.0",
|
||||
"@earendil-works/pi-codemode": "^1.2.0",
|
||||
"@earendil-works/chord": "1.2.0",
|
||||
"@earendil-works/pi-ai": "1.2.0",
|
||||
"@earendil-works/pi-codemode": "1.2.0",
|
||||
"@silvia-odwyer/photon": "0.3.3",
|
||||
"diff": "8.0.4",
|
||||
"typebox": "1.3.27"
|
||||
|
|
|
|||
4
packages/env/package.json
vendored
4
packages/env/package.json
vendored
|
|
@ -45,8 +45,8 @@
|
|||
"node": ">=22.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/chord": "^1.2.0",
|
||||
"@earendil-works/pi-durable": "^1.2.0"
|
||||
"@earendil-works/chord": "1.2.0",
|
||||
"@earendil-works/pi-durable": "1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shx": "0.3.4",
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
"test": "vitest run --config vitest.test.config.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@earendil-works/pi-ai": "^1.2.0",
|
||||
"@earendil-works/pi-coding-agent": "^1.2.0",
|
||||
"@earendil-works/pi-ai": "1.2.0",
|
||||
"@earendil-works/pi-coding-agent": "1.2.0",
|
||||
"@types/node": "22.19.19",
|
||||
"@vitest-evals/core": "0.15.0",
|
||||
"autoevals": "0.3.0",
|
||||
|
|
|
|||
|
|
@ -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