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
19 changes: 18 additions & 1 deletion .drive/projects/prisma-cli-v8/deferred.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ Nothing here is tracked outside this file.

## After the latest cutover (2026-08-25, PR #230)

- **compute-sdk 0.42.0 asks for a `@prisma/management-api-sdk` this repo
does not install.** Its peer range is `^1.69.0`; all three manifests
here pin `1.55.0`, so `pnpm peers check` reports one unmet peer. Left
that way deliberately: the SDK version is a shared type surface, so
moving it in `packages/cli` and `packages/prisma` alone makes the
engine's `ManagementApiClient` a second, incompatible copy and every
command that hands the engine's client to a CLI function stops
typechecking. Moving all three means `packages/cli-engine` changed,
which needs a new engine version, which both families peer-pin
exactly — the three-repo release sequence. Nothing is broken in the
meantime: the field is declared by compute-sdk's own types and the
server sends it whatever the client was generated from. Two ways out,
operator's call: move the SDK with the next engine version train, or
relax compute-sdk's floor back to `^1.44.0`, which is arguably where
it belongs — the floor protects a build-time concern of compute-sdk's
own that no consumer shares.

- **A stale product `dev` dist-tag can block a release publish.** The
publish run checks the dev channel before the release leg, and the
dev channel resolves each product's `dev` tag with no fallback — so
Expand Down Expand Up @@ -481,7 +498,7 @@ The cleanup PR removed the compute config and `init`, made service commands para
- ~~**`PRISMA_PROJECT_ID` is honoured only by the domain commands**~~ Closed on the PR branch (2026-08-21, operator ruling): the env var served the deleted `app deploy` headless flow and survived only in the domain commands by accident; it is removed entirely. Project targeting is `--project` and the link file.
- ~~**orm-toolchain's shipped help examples name retired spellings.**~~ Closed (2026-08-22) by prisma#30102: the family keys are the mount paths and the examples follow; `tests/orm-mount.test.ts` now asserts upstream stays clean.
- ~~**The deployment-id targeting asymmetry is undocumented.**~~ Closed on the PR branch (2026-08-21): every deployment-id command (`promote|start|stop|delete|show`, `logs --deployment`) now resolves the id globally with no service parameter, per the "Subjects are positional" ruling.
- **`GET /v1/deployments/{id}` omits the parent `appId`.** Verified against `@prisma/management-api-sdk@1.55.0`: the response carries id/status/url/previewDomain/envVars/createdAt and no owning-app pointer, so `showDeployment` finds the owner via `findAppForDeployment` — a scan of every project's service list and each service's deployments — and every id-targeted command pays it per run. The fix is in pdp-control-plane: include `appId` in the deployment representation; the CLI then swaps the scan for one `GET /v1/apps/{appId}`.
- ~~**`GET /v1/deployments/{id}` omits the parent `appId`.**~~ Closed on this branch (2026-08-25): pdp-control-plane#4983 added the owner to the deployment representation as `serviceId` (ADR-012 vocabulary, not `appId`), compute-sdk 0.42.0 exposes it as `DeploymentDetail.serviceId`, and `showDeployment` now resolves the owner with one `GET /v1/apps/{id}` — `findAppForDeployment` and its per-service scan are deleted.

## From the agent-skills delivery (project closed 2026-08-22)

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@manypkg/tools": "^2.1.2",
"@prisma/cli-engine": "workspace:0.2.3",
"@prisma/composer-cli": "0.14.0",
"@prisma/compute-sdk": "0.39.0",
"@prisma/compute-sdk": "0.42.0",
"@prisma/management-api-sdk": "1.55.0",
"@prisma/orm-toolchain": "8.0.0-rc.7",
"@vercel/detect-agent": "^1.2.3",
Expand Down
103 changes: 26 additions & 77 deletions packages/cli/src/lib/app/app-provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// biome-ignore-all lint/performance/noAwaitInLoops: API pagination and deployment lookup scans are intentionally sequential.
// biome-ignore-all lint/performance/noAwaitInLoops: API pagination walks pages in order.
// biome-ignore-all lint/performance/useTopLevelRegex: Existing hostname normalization regexes are kept inline for readability.
// biome-ignore-all lint/style/noNestedTernary: Existing app resolution expression is intentionally compact.
import type { StreamRecord } from "@prisma/compute-sdk";
Expand Down Expand Up @@ -697,9 +697,9 @@ export function createAppProvider(
throw new Error(deploymentResult.error.message);
}

const app = await findAppForDeployment(
const app = await owningService(
sdk,
deploymentId,
deploymentResult.value.serviceId,
options?.signal,
);

Expand Down Expand Up @@ -920,6 +920,29 @@ async function listComputeServices(
return services.map(toAppRecord);
}

async function owningService(
sdk: ComputeClient,
serviceId: string,
signal?: AbortSignal,
): Promise<AppRecord | null> {
const result = await sdk.showApp({ appId: serviceId, signal });
if (result.isErr()) {
// Only a deleted service reads as ownerless; any other failure is real
// and must not be reported as a version with no service.
if (ApiError.is(result.error) && result.error.statusCode === 404) {
return null;
}
throw new Error(result.error.message);
}
return {
id: result.value.id,
name: result.value.name,
region: result.value.region ?? null,
liveDeploymentId: result.value.latestDeploymentId ?? null,
liveUrl: toAbsoluteUrl(result.value.appEndpointDomain ?? null),
};
}

function toAppRecord(service: RawAppRecord): AppRecord {
return {
id: service.id,
Expand Down Expand Up @@ -1100,80 +1123,6 @@ function domainApiCallError(
});
}

async function findAppForDeployment(
sdk: ComputeClient,
deploymentId: string,
signal?: AbortSignal,
): Promise<AppRecord | null> {
const projectsResult = await sdk.listProjects({ signal });
if (projectsResult.isErr()) {
throw new Error(projectsResult.error.message);
}

for (const project of projectsResult.value) {
const servicesResult = await sdk.listApps({
projectId: project.id,
signal,
});
if (servicesResult.isErr()) {
throw new Error(servicesResult.error.message);
}

for (const service of servicesResult.value) {
const app = await findServiceAppForDeployment(
sdk,
service.id,
deploymentId,
signal,
);
if (app) {
return app;
}
}
}

return null;
}

async function findServiceAppForDeployment(
sdk: ComputeClient,
serviceId: string,
deploymentId: string,
signal?: AbortSignal,
): Promise<AppRecord | null> {
const detailResult = await sdk.showApp({
appId: serviceId,
signal,
});
if (detailResult.isErr()) {
throw new Error(detailResult.error.message);
}

const app: AppRecord = {
id: detailResult.value.id,
name: detailResult.value.name,
region: detailResult.value.region ?? null,
liveDeploymentId: detailResult.value.latestDeploymentId ?? null,
liveUrl: toAbsoluteUrl(detailResult.value.appEndpointDomain ?? null),
};

if (app.liveDeploymentId === deploymentId) {
return app;
}

const versionsResult = await sdk.listDeployments({
appId: serviceId,
signal,
});
if (versionsResult.isErr()) {
throw new Error(versionsResult.error.message);
}

return versionsResult.value.some((version) => version.id === deploymentId)
? app
: null;
}

function toAbsoluteUrl(url: string | null): string | null {
if (!url) {
return null;
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/tests/service-testkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface RawServiceDetail {

export interface RawDeployment {
id: string;
serviceId: string;
status: string;
createdAt: string;
previewDomain: string | null;
Expand Down Expand Up @@ -178,12 +179,14 @@ export const SERVICE_DETAIL: RawServiceDetail = {
export const DEPLOYMENTS: RawDeployment[] = [
{
id: "dep_1",
serviceId: "svc_1",
status: "stopped",
createdAt: "2026-08-01T00:00:00.000Z",
previewDomain: "dep1.prisma.app",
},
{
id: "dep_2",
serviceId: "svc_1",
status: "running",
createdAt: "2026-08-02T00:00:00.000Z",
previewDomain: "dep2.prisma.app",
Expand Down Expand Up @@ -261,6 +264,7 @@ export function releaseRoutes(overrides: Routes = {}): Routes {
data: {
data: {
id,
serviceId: "svc_1",
status,
createdAt: "2026-08-01T00:00:00.000Z",
previewDomain: `${id}.prisma.app`,
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/tests/service-version-promote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { describe, expect, it } from "vitest";

import {
makeServiceCli,
page,
presentedSummary,
releaseRoutes,
} from "./service-testkit";
Expand Down Expand Up @@ -188,7 +187,12 @@ describe("prisma service version promote", () => {

it("settles a deployment with no owning service as SERVICE.VERSION_DETACHED", async () => {
const harness = await makeServiceCli({
routes: releaseRoutes({ "GET /v1/apps": () => ({ data: page([]) }) }),
routes: releaseRoutes({
"GET /v1/apps/{appId}": () => ({
error: { error: { message: "not found" } },
status: 404,
}),
}),
});

const result = await harness.cli.run(
Expand Down
2 changes: 1 addition & 1 deletion packages/prisma/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@manypkg/tools": "^2.1.2",
"@prisma/cli-engine": "workspace:0.2.3",
"@prisma/composer-cli": "0.14.0",
"@prisma/compute-sdk": "0.39.0",
"@prisma/compute-sdk": "0.42.0",
"@prisma/management-api-sdk": "1.55.0",
"@prisma/orm-toolchain": "8.0.0-rc.7",
"@vercel/detect-agent": "^1.2.3",
Expand Down
26 changes: 13 additions & 13 deletions pnpm-lock.yaml

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

Loading