From 8ae7fd55fe07f7468d0875e26a9ae649263ca9ab Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 18 Aug 2026 10:02:30 +0200 Subject: [PATCH 1/4] Engine 0.2.0: the changed engine must ship, and CI now forces the bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prisma@8.0.0-rc.4 crashes on import (`npx prisma@next --version`): the CLI was built against engine exports that #183 added and #184 extended, but neither PR bumped the engine version, so the registry's @prisma/cli-engine@0.1.1 — published ten minutes before #183 merged — does not contain them. The registry is immutable, so the changed engine ships as 0.2.0; the next publish run picks it up. test.yml gains an engine-version job: a pull request that changes packages/cli-engine while leaving its version at one the registry already has now fails, with the bump command in the error. That is the check whose absence let rc.4 ship broken. The CLI itself becomes installable again only after both products re-declare their exact engine peer at 0.2.0 and release, and 8.0.0-rc.5 ships depending on those releases. Signed-off-by: willbot Signed-off-by: Will Madden --- .github/workflows/test.yml | 23 ++++++ packages/cli-engine/package.json | 2 +- packages/cli/package.json | 2 +- pnpm-lock.yaml | 2 +- scripts/check-engine-version.mjs | 106 ++++++++++++++++++++++++++ scripts/check-engine-version.test.mjs | 48 ++++++++++++ 6 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 scripts/check-engine-version.mjs create mode 100644 scripts/check-engine-version.test.mjs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4314fa0e..7fadabc9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,3 +58,26 @@ jobs: - name: Run CLI tests run: pnpm --filter @prisma/cli test + + # A changed engine must claim a version the registry does not have + # yet, or the release ships a CLI built against exports the published + # engine lacks (how prisma@8.0.0-rc.4 broke `npx prisma@next`). + engine-version: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + fetch-depth: 0 + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 + + - name: The engine version must be new if the engine changed + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: node scripts/check-engine-version.mjs "$BASE_SHA" diff --git a/packages/cli-engine/package.json b/packages/cli-engine/package.json index 8dcb2cf5..acaca244 100644 --- a/packages/cli-engine/package.json +++ b/packages/cli-engine/package.json @@ -1,6 +1,6 @@ { "name": "@prisma/cli-engine", - "version": "0.1.1", + "version": "0.2.0", "description": "The execution engine of the unified Prisma CLI.", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index f61a4976..2a57d5d5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -48,7 +48,7 @@ "conformance": "tsx scripts/conformance.ts" }, "dependencies": { - "@prisma/cli-engine": "workspace:0.1.1", + "@prisma/cli-engine": "workspace:0.2.0", "@prisma/composer-cli": "0.7.0", "@prisma/compute-sdk": "0.39.0", "@prisma/credentials-store": "^7.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec2504e2..94ce25ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: packages/cli: dependencies: '@prisma/cli-engine': - specifier: workspace:0.1.1 + specifier: workspace:0.2.0 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.7.0 diff --git a/scripts/check-engine-version.mjs b/scripts/check-engine-version.mjs new file mode 100644 index 00000000..441c6f9c --- /dev/null +++ b/scripts/check-engine-version.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node + +// Fails a pull request that changes `packages/cli-engine` while leaving +// its version at one the registry already has. That is how +// `prisma@8.0.0-rc.4` shipped: the CLI was built against engine exports +// that the published `@prisma/cli-engine@0.1.1` does not contain, and +// `npx prisma@next` crashed on import. The registry is immutable, so a +// changed engine must claim a new version (`pnpm bump-cli-engine-version`). +// +// Usage: node scripts/check-engine-version.mjs + +import { execFile } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +const NPM_NOT_FOUND_PATTERN = /\bE404\b/; + +/** Whether a failed `npm view` means the version is absent, as opposed + * to npm itself failing — no binary, no network, no auth. */ +function isNotFoundError(error) { + if (typeof error !== "object" || error === null) return false; + const { stderr, stdout } = error; + return NPM_NOT_FOUND_PATTERN.test(`${stderr ?? ""}\n${stdout ?? ""}`); +} + +const rootDir = dirname(dirname(fileURLToPath(import.meta.url))); + +/** + * @param {{ changedFiles: readonly string[], engineVersion: string, versionOnRegistry: boolean }} input + * @returns {string | null} the failure message, or null when the change is fine + */ +export function engineBumpVerdict({ + changedFiles, + engineVersion, + versionOnRegistry, +}) { + const engineChanged = changedFiles.some((file) => + file.startsWith("packages/cli-engine/"), + ); + if (!engineChanged || !versionOnRegistry) return null; + return ( + `packages/cli-engine changed, but its version (${engineVersion}) is already on the registry, ` + + "and published versions are immutable. Run `pnpm bump-cli-engine-version ` " + + "so the changed engine ships under a new version." + ); +} + +async function main() { + const baseSha = process.argv[2]; + if (!baseSha) { + console.error("Usage: node scripts/check-engine-version.mjs "); + process.exit(1); + } + + const { stdout: mergeBase } = await execFileAsync( + "git", + ["merge-base", baseSha, "HEAD"], + { cwd: rootDir }, + ); + const { stdout: diff } = await execFileAsync( + "git", + ["diff", "--name-only", mergeBase.trim(), "HEAD"], + { cwd: rootDir }, + ); + const changedFiles = diff.split("\n").filter(Boolean); + + const manifest = JSON.parse( + readFileSync(join(rootDir, "packages/cli-engine/package.json"), "utf-8"), + ); + const engineVersion = manifest.version; + + let versionOnRegistry = true; + try { + await execFileAsync("npm", [ + "view", + `@prisma/cli-engine@${engineVersion}`, + "version", + "--prefer-online", + ]); + } catch (error) { + if (!isNotFoundError(error)) throw error; + versionOnRegistry = false; + } + + const verdict = engineBumpVerdict({ + changedFiles, + engineVersion, + versionOnRegistry, + }); + if (verdict !== null) { + console.error(`::error::${verdict}`); + process.exit(1); + } + console.log( + `Engine version ${engineVersion} is consistent with this change set.`, + ); +} + +const isDirectRun = + process.argv[1] !== undefined && + import.meta.url === pathToFileURL(process.argv[1]).href; +if (isDirectRun) await main(); diff --git a/scripts/check-engine-version.test.mjs b/scripts/check-engine-version.test.mjs new file mode 100644 index 00000000..25b869fb --- /dev/null +++ b/scripts/check-engine-version.test.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { engineBumpVerdict } from "./check-engine-version.mjs"; + +const NAMES_THE_STALE_VERSION = /0\.1\.1/; +const NAMES_THE_BUMP_COMMAND = /bump-cli-engine-version/; + +describe("engineBumpVerdict", () => { + it("passes when the change does not touch the engine", () => { + const verdict = engineBumpVerdict({ + changedFiles: ["packages/cli/src/runtime.ts", "scripts/set-version.ts"], + engineVersion: "0.1.1", + versionOnRegistry: true, + }); + assert.equal(verdict, null); + }); + + it("passes when the engine changed and its version is new to the registry", () => { + const verdict = engineBumpVerdict({ + changedFiles: ["packages/cli-engine/src/commands.ts"], + engineVersion: "0.2.0", + versionOnRegistry: false, + }); + assert.equal(verdict, null); + }); + + it("fails when the engine changed but its version already shipped", () => { + const verdict = engineBumpVerdict({ + changedFiles: [ + "packages/cli-engine/src/exports/index.ts", + "packages/cli/src/auth/refresh.ts", + ], + engineVersion: "0.1.1", + versionOnRegistry: true, + }); + assert.match(verdict, NAMES_THE_STALE_VERSION); + assert.match(verdict, NAMES_THE_BUMP_COMMAND); + }); + + it("does not mistake other packages' paths for the engine", () => { + const verdict = engineBumpVerdict({ + changedFiles: ["packages/cli-engine-docs/readme.md"], + engineVersion: "0.1.1", + versionOnRegistry: true, + }); + assert.equal(verdict, null); + }); +}); From edcb8807ae835378e486f7345460573739d622ab Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 18 Aug 2026 10:05:31 +0200 Subject: [PATCH 2/4] Record the rc.4 repair chain in deferred.md Signed-off-by: willbot Signed-off-by: Will Madden --- .drive/projects/prisma-cli-v8/deferred.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.drive/projects/prisma-cli-v8/deferred.md b/.drive/projects/prisma-cli-v8/deferred.md index bbc23c33..a8032062 100644 --- a/.drive/projects/prisma-cli-v8/deferred.md +++ b/.drive/projects/prisma-cli-v8/deferred.md @@ -429,6 +429,16 @@ CLI does not do, and each restarts as engine work if wanted: for exactly this kind of consumer, or cut the cloud extension's use of it. Spans both product repos; Will decides which. +## The rc.4 engine mismatch (2026-08-18) — the repair chain + +`prisma@8.0.0-rc.4` on `next` crashes on import: #183 and #184 changed the engine without bumping its version, so the registry's `@prisma/cli-engine@0.1.1` (published ten minutes before #183 merged) lacks exports the CLI imports. The registry is immutable and both products peer the engine exactly, so npm fails with ERESOLVE on any mismatch; the repair is a chain, in order: + +1. **prisma-cli #200**: engine → 0.2.0, plus the CI job that fails any PR changing `packages/cli-engine/` while its version is already on the registry (the check whose absence let rc.4 ship). Merging it publishes engine 0.2.0. +2. **Both product repos**: re-declare the exact engine peer at 0.2.0 and release (`composer-cli` 0.7.1, `orm-toolchain` 8.0.0-rc.3). Their lockfiles cannot resolve 0.2.0 before step 1 publishes. +3. **prisma-cli**: pick up those product versions and cut `8.0.0-rc.5` — the first version on `next` that works again. rc.4 itself cannot be repaired. + +The cost the incident exposes: every engine API change forces this three-repo, three-release sequence, because the exact peer is what guarantees one engine per install (ADR 0004). Making the chain cheaper — automation that opens the product peer-bump PRs when a new engine publishes, riding the same `DEPLOY_GITHUB_TOKEN` as the notification step below — is a design question for Will. + ## Left open by the dev-build fix (2026-08-17) The release channel is green as of 2026-08-17: `@prisma/composer-cli@0.7.0` and `@prisma/orm-toolchain@8.0.0-rc.2` are both released and both peer `@prisma/cli-engine@0.1.1`, so the conformance run reports nothing and `packages/cli/scripts/conformance.ts` carries no exceptions. What closed, for the record: composer's `0.6.0` was uninstallable (published out-of-band with `npm publish`, leaving `workspace:0.6.0` in its manifest) and the ORM had no released version carrying the command family. From 5ea5d9c27b6227a685c61a2e38b7d0343e11906f Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 18 Aug 2026 10:13:38 +0200 Subject: [PATCH 3/4] Two conformance exceptions carry the engine transition A family cannot peer an engine version that is not on the registry, so the engine publishes first and the pin mismatch is real until both families release against 0.2.0. The exceptions are keyed on the exact version pair they describe, stay visible in every run, and the 8.0.0-rc.5 bump PR removes them. Signed-off-by: willbot Signed-off-by: Will Madden --- packages/cli/scripts/conformance.ts | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/cli/scripts/conformance.ts b/packages/cli/scripts/conformance.ts index 46880bc0..362669f8 100644 --- a/packages/cli/scripts/conformance.ts +++ b/packages/cli/scripts/conformance.ts @@ -100,15 +100,36 @@ async function tarball(): Promise { shellPackage: "@prisma/cli", enginePackage: "@prisma/cli-engine", familyPackages: ["@prisma/composer-cli", "@prisma/orm-toolchain"], - // No exceptions. Both families declare @prisma/cli-engine as an - // exact peer at the version this repo ships, so one engine - // resolves in an install — what ADR 0004 asks for, reached - // 2026-08-17 by @prisma/composer-cli@0.7.0 and - // @prisma/orm-toolchain@8.0.0-rc.2. Anything that reopens the - // two-engine defect now fails the publish instead of being - // excused, and adding an entry here is a decision to be argued - // for, not a way to get green. - exceptions: [], + // The empty list is the goal state: both families peering the + // exact engine version this repo ships, one engine per install + // (ADR 0004). The two entries below are an engine version + // transition in flight — a family cannot peer an engine version + // that is not on the registry, so the engine publishes first and + // the mismatch is real until both families release against it. + // The entries expire with the versions they name, and the rc.5 + // bump PR removes them; while they stand, a release could ship + // the two-engine install they describe, which is why they must + // not outlive the transition. + exceptions: [ + { + familyPackage: "@prisma/composer-cli", + familyPin: "0.1.1", + shellPin: "0.2.0", + reason: + "engine 0.2.0 must publish before composer-cli can peer it", + removeWhen: + "composer-cli releases peering 0.2.0 and the 8.0.0-rc.5 bump PR pins that release", + }, + { + familyPackage: "@prisma/orm-toolchain", + familyPin: "0.1.1", + shellPin: "0.2.0", + reason: + "engine 0.2.0 must publish before orm-toolchain can peer it", + removeWhen: + "orm-toolchain releases peering 0.2.0 and the 8.0.0-rc.5 bump PR pins that release", + }, + ], channel: CHANNEL, sandboxDir: join(WORK_DIR, "sandbox"), }, From 4d3885a4a509dadafa8bb3f6215386c9c4eb7fce Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 18 Aug 2026 10:15:43 +0200 Subject: [PATCH 4/4] Format the exception entries Signed-off-by: willbot Signed-off-by: Will Madden --- packages/cli/scripts/conformance.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/cli/scripts/conformance.ts b/packages/cli/scripts/conformance.ts index 362669f8..75e3f899 100644 --- a/packages/cli/scripts/conformance.ts +++ b/packages/cli/scripts/conformance.ts @@ -115,8 +115,7 @@ async function tarball(): Promise { familyPackage: "@prisma/composer-cli", familyPin: "0.1.1", shellPin: "0.2.0", - reason: - "engine 0.2.0 must publish before composer-cli can peer it", + reason: "engine 0.2.0 must publish before composer-cli can peer it", removeWhen: "composer-cli releases peering 0.2.0 and the 8.0.0-rc.5 bump PR pins that release", }, @@ -124,8 +123,7 @@ async function tarball(): Promise { familyPackage: "@prisma/orm-toolchain", familyPin: "0.1.1", shellPin: "0.2.0", - reason: - "engine 0.2.0 must publish before orm-toolchain can peer it", + reason: "engine 0.2.0 must publish before orm-toolchain can peer it", removeWhen: "orm-toolchain releases peering 0.2.0 and the 8.0.0-rc.5 bump PR pins that release", },