From f6a41b0c7834a86fcf24809e268d84c1e39c68be Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sun, 30 Aug 2026 05:40:23 +0530 Subject: [PATCH 1/7] fix: preserve deployment failure diagnostics --- .github/workflows/publish.yml | 34 +++++++++++++++++++++++++ src/tasks/deploy-with-composer.ts | 27 +++++++++++++++++--- src/telemetry/create.ts | 31 ++++++++++++++++++++++ tests/deploy-with-composer.test.ts | 41 ++++++++++++++++++++++++++++++ tests/telemetry.test.ts | 35 +++++++++++++++++++++++++ 5 files changed, 164 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3962500..0f66282 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,8 +21,42 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + windows-smoke: + name: Windows creation smoke + if: >- + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + (github.event.action != 'labeled' || github.event.label.name == 'release:next') + runs-on: windows-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22.18.0" + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Scaffold and build a Prisma app + run: >- + bun test --timeout 300000 + --test-name-pattern "builds a Next.js app with a TypeScript-authored contract" + ./tests/e2e/create-prisma.e2e.test.ts + preview: name: Publish PR preview + needs: windows-smoke if: >- github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && diff --git a/src/tasks/deploy-with-composer.ts b/src/tasks/deploy-with-composer.ts index c08a438..8e8bcb3 100644 --- a/src/tasks/deploy-with-composer.ts +++ b/src/tasks/deploy-with-composer.ts @@ -22,10 +22,24 @@ import { runSetupCommand } from "../utils/run-command"; type PrismaCliEnvelope = { ok: boolean; + command?: string; + commandId?: string; result?: Result; - error?: { summary?: string; message?: string; why?: string }; + error?: { code?: string; summary?: string; message?: string; why?: string }; }; +export class PrismaCliCommandError extends Error { + readonly prismaCliCommand?: string; + readonly prismaCliErrorCode?: string; + + constructor(options: { message: string; command?: string; code?: string }) { + super(options.message); + this.name = "PrismaCliCommandError"; + this.prismaCliCommand = options.command; + this.prismaCliErrorCode = options.code; + } +} + type PrismaWorkspace = { id: string; name: string | null; @@ -175,11 +189,16 @@ async function runPrismaJsonCommand(options: { if (result.exitCode !== 0 || !envelope.ok || envelope.result === undefined) { const summary = envelope.error?.summary ?? envelope.error?.message; - throw new Error( - [summary, envelope.error?.why].filter(Boolean).join(": ") || + throw new PrismaCliCommandError({ + message: + [summary, envelope.error?.why].filter(Boolean).join(": ") || result.stderr.trim() || "Prisma CLI command failed.", - ); + ...(envelope.commandId || envelope.command + ? { command: envelope.commandId ?? envelope.command } + : {}), + ...(envelope.error?.code ? { code: envelope.error.code } : {}), + }); } return envelope.result; } diff --git a/src/telemetry/create.ts b/src/telemetry/create.ts index f8d8c9d..e0d0633 100644 --- a/src/telemetry/create.ts +++ b/src/telemetry/create.ts @@ -14,6 +14,22 @@ export const CREATE_PRISMA_NEXT_CANCELLED_EVENT = "cli:create_prisma_next_comman export type CreateTelemetryFailureStage = CreateFailureStage; +const expectedRejectionReasons = new Set([ + "invalid_input", + "unsupported_node_version", + "invalid_project_name", + "target_path_not_directory", + "target_directory_not_empty", + "unsupported_configuration", + "not_authenticated", + "workspace_mismatch", + "project_name_collision", +]); + +function getFailureClass(reason: CreateFailureReason): "expected_rejection" | "technical_failure" { + return expectedRejectionReasons.has(reason) ? "expected_rejection" : "technical_failure"; +} + function getTargetDirectoryState(context: CreatePromptContext): string { if (!context.targetPathState.exists) { return "new"; @@ -68,6 +84,18 @@ function getErrorCode(error: unknown): number | string | null { return typeof code === "number" || typeof code === "string" ? code : null; } +function getPrismaCliFailureProperty( + error: unknown, + property: "prismaCliCommand" | "prismaCliErrorCode", +): string | null { + if (typeof error !== "object" || error === null) { + return null; + } + + const value = Reflect.get(error, property); + return typeof value === "string" && value.length > 0 ? value : null; +} + export async function trackCreateCompleted(params: { input: CreateCommandInput; context: CreatePromptContext; @@ -90,10 +118,13 @@ export async function trackCreateFailed(params: { await trackCliTelemetry(CREATE_PRISMA_NEXT_FAILED_EVENT, { ...getBaseCreateProperties(params.input, params.context), "duration-ms": params.durationMs, + "failure-class": getFailureClass(params.reason), "failure-stage": params.stage, "failure-reason": params.reason, "error-name": getErrorName(params.error), "error-code": getErrorCode(params.error), + "prisma-cli-command": getPrismaCliFailureProperty(params.error, "prismaCliCommand"), + "prisma-cli-error-code": getPrismaCliFailureProperty(params.error, "prismaCliErrorCode"), }); } diff --git a/tests/deploy-with-composer.test.ts b/tests/deploy-with-composer.test.ts index 66f5130..37f5fb4 100644 --- a/tests/deploy-with-composer.test.ts +++ b/tests/deploy-with-composer.test.ts @@ -7,6 +7,7 @@ import { getConsoleProjectUrl, parseComposerDeployResult, parsePrismaCliEnvelope, + PrismaCliCommandError, } from "../src/tasks/deploy-with-composer"; import { getErrorMessage, redactSecrets } from "../src/utils/errors"; @@ -91,6 +92,46 @@ describe("parsePrismaCliEnvelope", () => { ), ).toEqual({ ok: true, result: { summary: null } }); }); + + test("preserves stable command and error codes from a failure envelope", () => { + const envelope = parsePrismaCliEnvelope( + JSON.stringify({ + kind: "result", + envelope: { + ok: false, + commandId: "app.deploy", + error: { + code: "APP.DEPLOY_FAILED", + summary: "Deployment failed", + why: "The compute service was not created", + }, + }, + }), + ); + + expect(envelope).toMatchObject({ + ok: false, + commandId: "app.deploy", + error: { code: "APP.DEPLOY_FAILED" }, + }); + }); +}); + +describe("PrismaCliCommandError", () => { + test("exposes only stable structured fields for telemetry", () => { + const error = new PrismaCliCommandError({ + message: "Deployment failed", + command: "app.deploy", + code: "APP.DEPLOY_FAILED", + }); + + expect(error).toMatchObject({ + name: "PrismaCliCommandError", + message: "Deployment failed", + prismaCliCommand: "app.deploy", + prismaCliErrorCode: "APP.DEPLOY_FAILED", + }); + }); }); describe("parseComposerDeployResult", () => { diff --git a/tests/telemetry.test.ts b/tests/telemetry.test.ts index c745986..711d3f7 100644 --- a/tests/telemetry.test.ts +++ b/tests/telemetry.test.ts @@ -69,6 +69,7 @@ describe("create telemetry", () => { expect(properties).toEqual( expect.objectContaining({ "duration-ms": 456, + "failure-class": "technical_failure", "error-code": "ERR_TEST", "failure-stage": "plan_migration", "failure-reason": "migration_plan_failed", @@ -78,6 +79,40 @@ describe("create telemetry", () => { expect(JSON.stringify(properties)).not.toContain("secret"); }); + test("separates expected guard rejections from technical failures", async () => { + await trackCreateFailed({ + input: createInput, + context: createContext, + durationMs: 10, + stage: "collect_context", + reason: "target_directory_not_empty", + }); + const [, properties] = trackCliTelemetry.mock.calls[0] as [string, Record]; + expect(properties["failure-class"]).toBe("expected_rejection"); + }); + + test("tracks stable Prisma CLI failure fields without raw output", async () => { + await trackCreateFailed({ + input: createInput, + context: createContext, + durationMs: 456, + error: Object.assign(new Error("token=secret"), { + prismaCliCommand: "app.deploy", + prismaCliErrorCode: "APP.DEPLOY_FAILED", + }), + stage: "composer_deploy", + reason: "composer_deploy_failed", + }); + const [, properties] = trackCliTelemetry.mock.calls[0] as [string, Record]; + expect(properties).toEqual( + expect.objectContaining({ + "prisma-cli-command": "app.deploy", + "prisma-cli-error-code": "APP.DEPLOY_FAILED", + }), + ); + expect(JSON.stringify(properties)).not.toContain("secret"); + }); + test("tracks prompt cancellation as a separate outcome", async () => { await trackCreateCancelled({ input: createInput, From 88d524ad15e0891f6dc5c912b61cb66e76192752 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sun, 30 Aug 2026 05:46:56 +0530 Subject: [PATCH 2/7] chore: harden Windows smoke workflow --- .github/workflows/publish.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0f66282..5c69330 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -29,19 +29,22 @@ jobs: (github.event.action != 'labeled' || github.event.label.name == 'release:next') runs-on: windows-latest timeout-minutes: 15 + permissions: + contents: read steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "22.18.0" - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: latest From 489b1f3157dd0b4bd8bbffb2c905c3c08c520b3a Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sun, 30 Aug 2026 05:48:22 +0530 Subject: [PATCH 3/7] fix: classify unresolved workspaces as expected --- src/telemetry/create.ts | 1 + tests/telemetry.test.ts | 25 +++++++++++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/telemetry/create.ts b/src/telemetry/create.ts index e0d0633..53f2900 100644 --- a/src/telemetry/create.ts +++ b/src/telemetry/create.ts @@ -22,6 +22,7 @@ const expectedRejectionReasons = new Set([ "target_directory_not_empty", "unsupported_configuration", "not_authenticated", + "workspace_missing", "workspace_mismatch", "project_name_collision", ]); diff --git a/tests/telemetry.test.ts b/tests/telemetry.test.ts index 711d3f7..46ba3a9 100644 --- a/tests/telemetry.test.ts +++ b/tests/telemetry.test.ts @@ -79,16 +79,21 @@ describe("create telemetry", () => { expect(JSON.stringify(properties)).not.toContain("secret"); }); - test("separates expected guard rejections from technical failures", async () => { - await trackCreateFailed({ - input: createInput, - context: createContext, - durationMs: 10, - stage: "collect_context", - reason: "target_directory_not_empty", - }); - const [, properties] = trackCliTelemetry.mock.calls[0] as [string, Record]; - expect(properties["failure-class"]).toBe("expected_rejection"); + test("separates expected input and environment rejections from technical failures", async () => { + for (const reason of ["target_directory_not_empty", "workspace_missing"] as const) { + await trackCreateFailed({ + input: createInput, + context: createContext, + durationMs: 10, + stage: reason === "workspace_missing" ? "select_workspace" : "collect_context", + reason, + }); + } + for (const [, properties] of trackCliTelemetry.mock.calls as Array< + [string, Record] + >) { + expect(properties["failure-class"]).toBe("expected_rejection"); + } }); test("tracks stable Prisma CLI failure fields without raw output", async () => { From fea77a3e62273e36087db62f7a907075b66bbdaa Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 31 Aug 2026 14:21:25 +0530 Subject: [PATCH 4/7] test: assert expected rejection reasons --- tests/telemetry.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/telemetry.test.ts b/tests/telemetry.test.ts index 46ba3a9..c840d6f 100644 --- a/tests/telemetry.test.ts +++ b/tests/telemetry.test.ts @@ -89,9 +89,13 @@ describe("create telemetry", () => { reason, }); } - for (const [, properties] of trackCliTelemetry.mock.calls as Array< - [string, Record] - >) { + const calls = trackCliTelemetry.mock.calls as Array<[string, Record]>; + expect(calls).toHaveLength(2); + expect(calls.map(([, properties]) => properties["failure-reason"])).toEqual([ + "target_directory_not_empty", + "workspace_missing", + ]); + for (const [, properties] of calls) { expect(properties["failure-class"]).toBe("expected_rejection"); } }); From 067b0382dea0fcd687b5ab982e2a30b5698a677d Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 31 Aug 2026 14:28:50 +0530 Subject: [PATCH 5/7] ci: allow Windows smoke test to finish --- .github/workflows/publish.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5c69330..c2e6756 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,7 +28,7 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository && (github.event.action != 'labeled' || github.event.label.name == 'release:next') runs-on: windows-latest - timeout-minutes: 15 + timeout-minutes: 20 permissions: contents: read steps: @@ -52,8 +52,10 @@ jobs: run: bun install --frozen-lockfile - name: Scaffold and build a Prisma app + env: + CREATE_PRISMA_E2E_TIMEOUT_MS: "600000" run: >- - bun test --timeout 300000 + bun test --timeout 600000 --test-name-pattern "builds a Next.js app with a TypeScript-authored contract" ./tests/e2e/create-prisma.e2e.test.ts From 8be2f822b9bde73dad754d9b42fe5f63531afce5 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 31 Aug 2026 14:38:10 +0530 Subject: [PATCH 6/7] test: assert complete Windows scaffold --- tests/e2e/create-prisma.e2e.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/e2e/create-prisma.e2e.test.ts b/tests/e2e/create-prisma.e2e.test.ts index 5e56291..a48aec2 100644 --- a/tests/e2e/create-prisma.e2e.test.ts +++ b/tests/e2e/create-prisma.e2e.test.ts @@ -420,6 +420,14 @@ describe("create-prisma e2e", () => { expect(await pathExists(path.join(projectDir, "src/prisma/generated/contract.d.ts"))).toBe( true, ); + expect(await pathExists(path.join(projectDir, "prisma.config.ts"))).toBe(true); + expect(await pathExists(path.join(projectDir, "migrations/app"))).toBe(true); + expect( + await pathExists(path.join(projectDir, ".agents/skills/prisma-composer/SKILL.md")), + ).toBe(true); + expect( + await pathExists(path.join(projectDir, ".claude/skills/prisma-composer/SKILL.md")), + ).toBe(true); await runCommand(projectDir, ["bun", "run", "build"]); await runCommand(projectDir, ["bunx", "tsc", "--noEmit"]); From d22e796bed79e3ce227e0857bf8da152f5865614 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 31 Aug 2026 15:17:43 +0530 Subject: [PATCH 7/7] fix: use prisma latest for scaffolding --- README.md | 2 +- src/constants/dependencies.ts | 18 ++++++------------ src/utils/node-version.ts | 2 +- tests/dependencies.test.ts | 2 +- tests/e2e/create-prisma.e2e.test.ts | 2 +- 5 files changed, 10 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e82633a..1abe565 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ yarn dlx create-prisma@latest my-app bunx create-prisma@latest my-app ``` -The CLI initializes Prisma 8 with `prisma@next`, installs dependencies, emits the contract, and generates a deployable Composer app. PostgreSQL projects use Composer's native Prisma Postgres provider, including migrations and a typed runtime client. +The CLI initializes Prisma 8 with `prisma@latest`, installs dependencies, emits the contract, and generates a deployable Composer app. PostgreSQL projects use Composer's native Prisma Postgres provider, including migrations and a typed runtime client. The deployment prompt is: diff --git a/src/constants/dependencies.ts b/src/constants/dependencies.ts index df0d6fd..1550b20 100644 --- a/src/constants/dependencies.ts +++ b/src/constants/dependencies.ts @@ -18,7 +18,7 @@ export const dependencyVersionMap = { mongodb: "^7.1.0", "mongodb-memory-server": "^11.1.0", nitro: "^3.0.260610-beta", - prisma: "8.0.0-rc.12", + prisma: "latest", // The ORM runtime's timestamp columns need a global Temporal, which no // stable Node or Bun ships yet. "temporal-polyfill": "^1.0.4", @@ -26,17 +26,11 @@ export const dependencyVersionMap = { typescript: "^5.9.3", } as const; -// Pinned, not `prisma@next`: the scaffold's own invocations must not float -// with the dist-tag — the rc line ships breaking changes between releases -// (rc.10 broke every create). The pin must move in lockstep with the pins -// above: the CLI bundles its own copies of @prisma/composer-cli and -// @prisma/orm-toolchain, and those must match the @prisma/composer* and -// @prisma/orm-* versions this map installs into the project. -export const PRISMA_PLATFORM_CLI_PACKAGE = "prisma@8.0.0-rc.12"; -// Deno runs the same pinned consolidated CLI. The former `prisma-next` -// fallback is dead: under Deno the bare `npm:prisma-next` specifier resolves -// to the highest non-prerelease version (0.12.0, frozen), which cannot emit -// against the ORM releases pinned above. +// `prisma@next` is a compatibility tag that may intentionally lag behind the +// current release. New scaffolds and every delegated command use `latest`. +export const PRISMA_PLATFORM_CLI_PACKAGE = "prisma@latest"; +// Deno runs the same consolidated CLI. The former `prisma-next` fallback is +// frozen and cannot emit against the current ORM releases. export const PRISMA_DENO_CLI_PACKAGE = PRISMA_PLATFORM_CLI_PACKAGE; export type AvailableDependency = keyof typeof dependencyVersionMap; diff --git a/src/utils/node-version.ts b/src/utils/node-version.ts index c8ffb55..9515664 100644 --- a/src/utils/node-version.ts +++ b/src/utils/node-version.ts @@ -16,7 +16,7 @@ export function supportsPrisma(nodeVersion = process.versions.node): boolean { export function getUnsupportedNodeMessage(nodeVersion = process.versions.node): string { return [ - `Node.js ${nodeVersion} is unsupported by create-prisma@next.`, + `Node.js ${nodeVersion} is unsupported by create-prisma@latest.`, "Required: Node.js 22.18 or newer.", "Update Node.js and run the command again.", ].join("\n"); diff --git a/tests/dependencies.test.ts b/tests/dependencies.test.ts index f6f021a..f647472 100644 --- a/tests/dependencies.test.ts +++ b/tests/dependencies.test.ts @@ -8,7 +8,7 @@ describe("Prisma 8 dependency versions", () => { expect(getDependencyVersion("@prisma/orm-mongo")).toBe("8.0.0-rc.8"); expect(getDependencyVersion("@prisma/composer")).toBe("0.16.0"); expect(getDependencyVersion("@prisma/composer-prisma-cloud")).toBe("0.16.0"); - expect(getDependencyVersion("prisma")).toBe("8.0.0-rc.12"); + expect(getDependencyVersion("prisma")).toBe("latest"); expect(getDependencyVersion("alchemy")).toBe("2.0.0-beta.74"); expect(getDependencyVersion("effect")).toBe("4.0.0-rc.112"); }); diff --git a/tests/e2e/create-prisma.e2e.test.ts b/tests/e2e/create-prisma.e2e.test.ts index a48aec2..973d8cc 100644 --- a/tests/e2e/create-prisma.e2e.test.ts +++ b/tests/e2e/create-prisma.e2e.test.ts @@ -368,7 +368,7 @@ describe("create-prisma e2e", () => { expect( await pathExists(path.join(projectDir, ".claude/skills/prisma-composer/SKILL.md")), ).toBe(true); - expect(packageJson.devDependencies.prisma).toBe("8.0.0-rc.12"); + expect(packageJson.devDependencies.prisma).toBe("latest"); expect(packageJson.scripts.postinstall).toBe("prisma skills sync || exit 0"); expect(packageJson.scripts.deploy).toContain("bun run composer:deploy"); expect(packageJson.overrides.effect).toBe("4.0.0-rc.112");