Skip to content

Commit 37aef39

Browse files
Bump to version 8.0.0-rc.9: fix the prisma package's product pins (#221)
prisma@8.0.0-rc.8 crashes on every invocation: the wrapper package's own dependency list still pinned @prisma/orm-toolchain@8.0.0-rc.4 and @prisma/composer-cli@0.11.0, so the published bin resolved the old ORM family keys and the mount table's lookups came back undefined ("Cannot read properties of undefined (reading 'needs')"). Only packages/cli's pins were bumped in #218; the conformance sandbox masked the divergence by hoisting the good version from @prisma/cli's manifest, which a real `npm install prisma` does not do. This aligns packages/prisma's pins with packages/cli (composer-cli 0.12.0, orm-toolchain 8.0.0-rc.5) and bumps to 8.0.0-rc.9. The check gaps (no pin-equality check across the two manifests; the tarball bin-start not exercising a prisma-only install) are recorded in the deferred ledger. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
1 parent 14c0ec8 commit 37aef39

16 files changed

Lines changed: 286 additions & 54 deletions

File tree

.drive/projects/prisma-cli-v8/deferred.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,3 +471,8 @@ The agent-skills project (skills sync/list, `prisma init`, the staleness notice;
471471
- **Windows CI: `skills-sync.test.ts` timed out once at the 5s default** (run 32474645762) with a teardown ENOTEMPTY from cleanup racing the timed-out test. If it recurs, raise the suite's per-test timeout on Windows rather than chasing the race.
472472
- **`isLikelyGlobalNpmEntrypoint` (update-check.ts) matches only `prisma-cli` install paths**, so a globally-installed `prisma` gets the docs-link fallback instead of a concrete update command; `selectUpdateInstruction` still names `@prisma/cli`. Newly conspicuous after the CLI_NAME → prisma rename.
473473
- **The feedback client's user-agent changed from `prisma-cli/<version>` to `prisma/<version>`** — wire-visible; whoever reads that dashboard should know.
474+
## Left open by the rc.8 broken release (2026-08-24)
475+
476+
- **`prisma@8.0.0-rc.8` on npm is broken and immutable.** The `prisma` wrapper package carries its own copies of the product pins, and the grammar-cleanup branch bumped only `packages/cli/package.json` — so the published `prisma` bin resolved `@prisma/orm-toolchain@8.0.0-rc.4`, whose old family keys make the mount table's lookups undefined and every invocation crash ("Cannot read properties of undefined (reading 'needs')"). rc.9 fixes it. Consider `npm deprecate prisma@8.0.0-rc.8` (needs a maintainer's npm auth; CI publishes via OIDC and has no deprecate step).
477+
- ~~**The release checks did not catch a `prisma` bin that crashes on install.**~~ Closed (2026-08-24, on the rc.9 PR): worse than hoisting — check 3b never installed or started the wrapper's bin at all, only the shell's. Three guards now exist: `packages/cli/tests/manifest-pins.test.ts` (every PR: the wrapper's dependencies must deep-equal the shell's), the tarball check's new `sibling-pin-mismatch` finding (pack time: shared dependency names across packed manifests must carry identical specifiers), and per-package sandboxes in check 3b (every bin-bearing package installs and starts from its own tree). Each guard was proven against the planted rc.8 defect.
478+
- **Two manifests hand-carry the same pins.** `update-product-versions.mjs` rewrites both, and three checks now fail on divergence (see the closed entry above), so the class cannot ship again. Deriving one manifest from the other at pack time would remove the duplication itself — still a design call, no longer urgent.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "prisma-cli",
3-
"version": "8.0.0-rc.8",
3+
"version": "8.0.0-rc.9",
44
"private": true,
55
"engines": {
66
"node": ">=24"

packages/cli-conformance/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@repo/cli-conformance",
33
"private": true,
4-
"version": "8.0.0-rc.8",
4+
"version": "8.0.0-rc.9",
55
"description": "Reusable conformance checks for the engine's consumers: import purity over built output, config-section validators that never throw, and verification of the tarballs a registry would receive. Depends on no package it checks.",
66
"type": "module",
77
"exports": {
@@ -24,7 +24,7 @@
2424
"test": "pnpm run typecheck && vitest run"
2525
},
2626
"devDependencies": {
27-
"@repo/tsconfig": "workspace:8.0.0-rc.8",
27+
"@repo/tsconfig": "workspace:8.0.0-rc.9",
2828
"@types/node": "^22.19.19",
2929
"es-module-lexer": "^2.1.0",
3030
"tsx": "^4.22.4",

packages/cli-conformance/src/checks/tarball.ts

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { join } from "node:path";
12
import type { Finding, Suppression } from "../findings";
23
import { bareImportRoots } from "../module-graph";
34
import { checkImportPurity, type PackageManifest } from "./import-purity";
@@ -105,6 +106,10 @@ export interface TarballInput {
105106

106107
const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
107108

109+
/** Package-name characters that cannot appear in a directory name. */
110+
const SANDBOX_NAME_UNSAFE = /[@/]/g;
111+
const LEADING_DASH = /^-/;
112+
108113
/**
109114
* Check 3: the tarballs a registry would receive. 3a — packed output
110115
* imports only what the packed manifest declares. 3b — the root tarball
@@ -161,11 +166,57 @@ export async function checkTarball(
161166
const shell = packed.get(input.shellPackage);
162167
if (shell === undefined) return findings;
163168

169+
findings.push(...siblingPinAgreementFindings(input, shell.manifest, packed));
164170
findings.push(...manifestPinFindings(input, shell.manifest));
165-
findings.push(...(await sandboxFindings(input, shell, packed, io)));
171+
// Every packed package that declares a bin installs into its OWN
172+
// sandbox and starts there, so resolution happens the way that
173+
// package's real install resolves it. prisma@8.0.0-rc.8 shipped a
174+
// wrapper bin that crashed on every invocation while a shell-only
175+
// sandbox stayed green: the wrapper's stale product pin was hoisted
176+
// away by the shell's correct one.
177+
for (const [name, entry] of packed) {
178+
if (declaredBins(entry.manifest).length === 0) continue;
179+
// biome-ignore lint/performance/noAwaitInLoops: sandboxes install one at a time so a failure names its package and concurrent npm installs cannot confound each other
180+
findings.push(...(await sandboxFindings(input, name, entry, packed, io)));
181+
}
166182
return applyExceptions(findings, input.exceptions);
167183
}
168184

185+
/**
186+
* The rc.8 guard: sibling packages that ship the same bundled source —
187+
* the shell and the `prisma` wrapper — hand-carry their dependency
188+
* lists in separate manifests, and which copy of a dependency a user's
189+
* install resolves depends on hoisting. Any dependency name two packed
190+
* manifests share must therefore carry the identical specifier.
191+
*/
192+
function siblingPinAgreementFindings(
193+
input: TarballInput,
194+
shellManifest: PackedManifest,
195+
packed: ReadonlyMap<string, { tarball: string; manifest: PackedManifest }>,
196+
): readonly Finding[] {
197+
const findings: Finding[] = [];
198+
const shellDeps = shellManifest.dependencies ?? {};
199+
for (const [name, entry] of packed) {
200+
if (name === input.shellPackage) continue;
201+
for (const [dep, specifier] of Object.entries(
202+
entry.manifest.dependencies ?? {},
203+
)) {
204+
const shellSpecifier = shellDeps[dep];
205+
if (shellSpecifier === undefined || shellSpecifier === specifier) {
206+
continue;
207+
}
208+
findings.push(
209+
finding(
210+
"sibling-pin-mismatch",
211+
name,
212+
`${name} pins ${dep}@${specifier} while ${input.shellPackage} pins ${shellSpecifier} — which one an install resolves depends on hoisting`,
213+
),
214+
);
215+
}
216+
}
217+
return findings;
218+
}
219+
169220
/**
170221
* 3c, sibling leg: every packed manifest that depends on the engine
171222
* must pin exactly the engine version packed beside it. This is how
@@ -249,13 +300,18 @@ function manifestPinFindings(
249300
return findings;
250301
}
251302

252-
/** 3b + 3c's installed legs, all downstream of one sandbox install. */
303+
/** 3b + 3c's installed legs, one sandbox per bin-bearing package. */
253304
async function sandboxFindings(
254305
input: TarballInput,
255-
shell: { tarball: string; manifest: PackedManifest },
306+
packageName: string,
307+
root: { tarball: string; manifest: PackedManifest },
256308
packed: ReadonlyMap<string, { tarball: string; manifest: PackedManifest }>,
257309
io: TarballIo,
258310
): Promise<readonly Finding[]> {
311+
const sandboxDir = join(
312+
input.sandboxDir,
313+
packageName.replace(SANDBOX_NAME_UNSAFE, "-").replace(LEADING_DASH, ""),
314+
);
259315
// Transitive: a sibling reached only through another sibling still
260316
// needs its override, or the install falls back to the registry.
261317
const overrides: Record<string, string> = {};
@@ -269,38 +325,40 @@ async function sandboxFindings(
269325
visit(entry.manifest);
270326
}
271327
};
272-
visit(shell.manifest);
328+
visit(root.manifest);
273329
const install = await io.installSandbox({
274-
sandboxDir: input.sandboxDir,
275-
rootTarball: shell.tarball,
330+
sandboxDir,
331+
rootTarball: root.tarball,
276332
overrides,
277333
});
278334
if (!install.ok) {
279335
return [
280336
finding(
281337
"install-failed",
282-
input.shellPackage,
338+
packageName,
283339
"the packed tarball did not install into a clean tree",
284340
install.output,
285341
),
286342
];
287343
}
288344
return [
289-
...(await binFindings(input, shell.manifest, io)),
290-
...(await installedPinFindings(input, shell.manifest, io)),
345+
...(await binFindings(input, packageName, sandboxDir, root.manifest, io)),
346+
...(await installedPinFindings(input, sandboxDir, root.manifest, io)),
291347
];
292348
}
293349

294350
async function binFindings(
295351
input: TarballInput,
296-
shellManifest: PackedManifest,
352+
packageName: string,
353+
sandboxDir: string,
354+
manifest: PackedManifest,
297355
io: TarballIo,
298356
): Promise<readonly Finding[]> {
299357
const findings: Finding[] = [];
300-
for (const [binName, relPath] of declaredBins(shellManifest)) {
358+
for (const [binName, relPath] of declaredBins(manifest)) {
301359
// biome-ignore lint/performance/noAwaitInLoops: bins start one at a time so a failure names its bin and concurrent processes cannot confound each other's exit
302360
const run = await io.startBin({
303-
sandboxDir: input.sandboxDir,
361+
sandboxDir,
304362
binName,
305363
relPath,
306364
argv: ["--version"],
@@ -310,7 +368,7 @@ async function binFindings(
310368
findings.push(
311369
finding(
312370
"bin-failed",
313-
input.shellPackage,
371+
packageName,
314372
`bin ${binName} timed out instead of exiting`,
315373
run.stderr,
316374
),
@@ -319,7 +377,7 @@ async function binFindings(
319377
findings.push(
320378
finding(
321379
"bin-failed",
322-
input.shellPackage,
380+
packageName,
323381
`bin ${binName} exited ${run.exitCode} on plain node`,
324382
`stdout:\n${run.stdout}\nstderr:\n${run.stderr}`,
325383
),
@@ -376,31 +434,29 @@ function familyPinFindings(
376434

377435
async function installedPinFindings(
378436
input: TarballInput,
379-
shellManifest: PackedManifest,
437+
sandboxDir: string,
438+
manifest: PackedManifest,
380439
io: TarballIo,
381440
): Promise<readonly Finding[]> {
382441
const findings: Finding[] = [];
383-
const shellPin = shellManifest.dependencies?.[input.enginePackage];
442+
const shellPin = manifest.dependencies?.[input.enginePackage];
384443

385444
for (const family of input.familyPackages) {
386445
// biome-ignore lint/performance/noAwaitInLoops: one manifest read per mounted family — two today — keeps findings ordered with the family list
387-
const installed = await io.readInstalledManifest(input.sandboxDir, family);
446+
const installed = await io.readInstalledManifest(sandboxDir, family);
388447
if (installed === undefined) continue;
389448
findings.push(
390449
...familyPinFindings(
391450
input,
392451
family,
393-
shellManifest.dependencies?.[family],
452+
manifest.dependencies?.[family],
394453
installed,
395454
shellPin,
396455
),
397456
);
398457
}
399458

400-
const copies = await io.listInstalledCopies(
401-
input.sandboxDir,
402-
input.enginePackage,
403-
);
459+
const copies = await io.listInstalledCopies(sandboxDir, input.enginePackage);
404460
if (copies.length > 1) {
405461
findings.push(
406462
finding(

packages/cli-conformance/src/findings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export type FindingKind =
3030
| "bin-failed"
3131
/** The shell and a family it mounts disagree about the engine version. */
3232
| "engine-pin-mismatch"
33+
/** Two packed sibling manifests declare the same dependency at
34+
* different versions, so which one an install resolves depends on
35+
* hoisting. */
36+
| "sibling-pin-mismatch"
3337
/** A release depends on a dev build. */
3438
| "dev-build-in-release";
3539

packages/cli-conformance/src/tarball-io.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export function realTarballIo(
3838
// exact files CI uploads and attaches to the GitHub Release.
3939
const tarballDir = resolve(options.tarballDir ?? join(absWork, "tarballs"));
4040
rmSync(tarballDir, { recursive: true, force: true });
41-
const sandbox = () => join(absWork, "sandbox");
4241

4342
return {
4443
async pack(pkgDir) {
@@ -88,8 +87,9 @@ export function realTarballIo(
8887
return files;
8988
},
9089

91-
async installSandbox({ rootTarball, overrides }) {
92-
const dir = sandbox();
90+
async installSandbox({ sandboxDir, rootTarball, overrides }) {
91+
const dir = resolve(sandboxDir);
92+
rmSync(dir, { recursive: true, force: true });
9393
mkdirSync(dir, { recursive: true });
9494
const rootManifest = await this.readPackedManifest(rootTarball);
9595
const name = manifestName(rootManifest);
@@ -176,7 +176,7 @@ export function realTarballIo(
176176
argv,
177177
timeoutMs,
178178
}) {
179-
const rootManifestPath = join(sandbox(), "package.json");
179+
const rootManifestPath = join(sandboxDir, "package.json");
180180
const rootManifest = JSON.parse(
181181
readFileSync(rootManifestPath, "utf8"),
182182
) as {

0 commit comments

Comments
 (0)