Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .drive/projects/prisma-cli-v8/deferred.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion packages/cli-engine/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
37 changes: 28 additions & 9 deletions packages/cli/scripts/conformance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,34 @@ async function tarball(): Promise<readonly Finding[]> {
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"),
},
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

106 changes: 106 additions & 0 deletions scripts/check-engine-version.mjs
Original file line number Diff line number Diff line change
@@ -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 <base-sha>

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 <patch|minor>` " +
"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 <base-sha>");
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();
48 changes: 48 additions & 0 deletions scripts/check-engine-version.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading