Skip to content

Commit 8930881

Browse files
authored
Merge pull request #54 from prisma/feat/j1-prod-deploy-gate-prework
feat: require --prod for production deploys
2 parents b9a0aab + 39f1034 commit 8930881

11 files changed

Lines changed: 733 additions & 29 deletions

File tree

docs/product/command-spec.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ prisma-cli app run --build-type nextjs
610610
prisma-cli app run --build-type bun --entry server.ts --port 3000
611611
```
612612

613-
## `prisma-cli app deploy --project <id-or-name> --create-project <name> --app <name> --branch <name> --framework <nextjs|hono|tanstack-start|bun> --entry <path> --http-port <port> --env <name=value>`
613+
## `prisma-cli app deploy --project <id-or-name> --create-project <name> --app <name> --branch <name> --framework <nextjs|hono|tanstack-start|bun> --entry <path> --http-port <port> --env <name=value> --prod`
614614

615615
Purpose:
616616

@@ -637,7 +637,10 @@ Behavior:
637637
- `--yes` alone does not choose Project scope; use `--project` or `--create-project`
638638
- `--project` and `--create-project` are mutually exclusive with each other and with `PRISMA_PROJECT_ID`
639639
- resolves or creates branch context from `--branch`, local Git branch, or `main`
640+
- treats only the resolved Branch `role` as production authority; branch name, `main`, `production`, and `isDefault` are not production authority
640641
- resolves or creates app context inside the resolved branch from `--app`, `PRISMA_APP_ID`, `package.json#name`, or current directory name
642+
- auto-promotes the first production deploy for an App without `--prod`
643+
- requires `--prod` for subsequent deploys to a production Branch; `--yes` only skips the confirmation prompt when `--prod` is also present
641644
- does not prompt when there is no real choice; zero matching apps creates the inferred app
642645
- writes `.prisma/local.json` after Project binding succeeds and before build/deploy starts, so retries after a failed deploy do not repeat setup
643646
- before asking `Customize build settings? (y/N)`, previews the detected framework and runtime so the user can see the defaults they are accepting or changing
@@ -662,6 +665,7 @@ prisma-cli app deploy --create-project my-app --yes
662665
prisma-cli app deploy --app my-app --env DATABASE_URL=postgresql://example
663666
prisma-cli app deploy --framework nextjs --http-port 3000
664667
prisma-cli app deploy --branch feat-login --framework hono --http-port 3000
668+
prisma-cli app deploy --prod --yes
665669
prisma-cli app deploy --framework bun --entry src/server.ts --http-port 3000
666670
prisma-cli app deploy --entry src/server.ts --http-port 3000
667671
```

docs/product/error-conventions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ These codes are the minimum stable set for the MVP:
172172
- `DEPLOYMENT_NOT_FOUND`
173173
- `NO_DEPLOYMENTS`
174174
- `NO_PREVIOUS_DEPLOYMENT`
175+
- `PROD_DEPLOY_REQUIRES_FLAG`
175176
- `PROMOTE_SOURCE_INVALID`
176177
- `ROLLBACK_UNAVAILABLE`
177178
- `CONFIRMATION_REQUIRED`
@@ -213,6 +214,7 @@ Recommended meanings:
213214
- `DEPLOYMENT_NOT_FOUND`: requested deployment id does not exist
214215
- `NO_DEPLOYMENTS`: command resolved a branch or app but found no deployments
215216
- `NO_PREVIOUS_DEPLOYMENT`: rollback could not find an earlier deployment for the selected app
217+
- `PROD_DEPLOY_REQUIRES_FLAG`: app deploy resolved a production Branch with a prior production deployment, but `--prod` was not passed
216218
- `PROMOTE_SOURCE_INVALID`: source for promote is missing, invalid, or not promotable
217219
- `ROLLBACK_UNAVAILABLE`: no previous healthy production deployment exists
218220
- `CONFIRMATION_REQUIRED`: command cannot continue without confirmation in the current mode

packages/cli/src/commands/app/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ function createDeployCommand(runtime: CliRuntime): Command {
181181
.addOption(
182182
new Option("--env <name=value>", "Environment variable")
183183
.argParser(collectRepeatableValues),
184-
);
184+
)
185+
.addOption(new Option("--prod", "Confirm intent to deploy to production"));
185186
addGlobalFlags(command);
186187

187188
command.action(async (options) => {
@@ -193,6 +194,7 @@ function createDeployCommand(runtime: CliRuntime): Command {
193194
const envAssignments = (options as { env?: string[] }).env;
194195
const projectRef = (options as { project?: string }).project;
195196
const createProjectName = (options as { createProject?: string }).createProject;
197+
const prod = (options as { prod?: boolean }).prod;
196198

197199
await runCommand<AppDeployResult>(
198200
runtime,
@@ -206,6 +208,7 @@ function createDeployCommand(runtime: CliRuntime): Command {
206208
framework,
207209
httpPort,
208210
envAssignments,
211+
prod: prod === true,
209212
}),
210213
{
211214
renderHuman: (context, descriptor, result) => renderAppDeploy(context, descriptor, result),

packages/cli/src/controllers/app.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import {
9090
type PreviewAppRecord,
9191
type PreviewDomainRecord,
9292
} from "../lib/app/preview-provider";
93+
import { enforceProductionDeployGate } from "../lib/app/production-deploy-gate";
9394
import { formatDomainFailureFix } from "../lib/app/domain-guidance";
9495
import { requireAuthenticatedAuthState } from "./auth";
9596
import { listRealWorkspaceProjects } from "./project";
@@ -221,6 +222,7 @@ export async function runAppDeploy(
221222
framework?: string;
222223
httpPort?: string;
223224
envAssignments?: string[];
225+
prod?: boolean;
224226
},
225227
): Promise<CommandSuccess<AppDeployResult>> {
226228
ensurePreviewAppMode(context);
@@ -304,6 +306,13 @@ export async function runAppDeploy(
304306
framework = customized.framework;
305307
runtime = customized.runtime;
306308

309+
await enforceProductionDeployGate(context, provider, {
310+
appId: selectedApp.appId,
311+
appName: selectedApp.displayName,
312+
branchKind: target.branch.kind,
313+
prod: options?.prod === true,
314+
});
315+
307316
// Customization can switch from a Bun-compatible framework to one that
308317
// derives its entrypoint from build output, so validate --entry again after it.
309318
const buildType = framework.buildType;
@@ -2301,7 +2310,7 @@ async function resolveDeployProjectContext(
23012310

23022311
if (explicitProject) {
23032312
const project = resolveProjectForSetup(explicitProject, projects, workspace);
2304-
return withDeployBranch({
2313+
return withRemoteDeployBranch(provider, {
23052314
workspace,
23062315
project: toProjectSummary(project),
23072316
resolution: {
@@ -2310,7 +2319,7 @@ async function resolveDeployProjectContext(
23102319
targetNameSource: "explicit",
23112320
},
23122321
localPinAction: "linked",
2313-
}, branch);
2322+
}, branch, context.runtime.signal);
23142323
}
23152324

23162325
if (options.createProjectName) {
@@ -2320,7 +2329,7 @@ async function resolveDeployProjectContext(
23202329
}
23212330

23222331
const created = await createProjectForDeploySetup(provider, projectName, workspace, context.runtime.signal);
2323-
return withDeployBranch({
2332+
return withRemoteDeployBranch(provider, {
23242333
workspace,
23252334
project: toProjectSummary(created),
23262335
resolution: {
@@ -2329,23 +2338,23 @@ async function resolveDeployProjectContext(
23292338
targetNameSource: "explicit",
23302339
},
23312340
localPinAction: "created",
2332-
}, branch);
2341+
}, branch, context.runtime.signal);
23332342
}
23342343

23352344
if (options.envProjectId) {
23362345
const project = projects.find((candidate) => candidate.id === options.envProjectId);
23372346
if (!project) {
23382347
throw projectNotFoundError(options.envProjectId, workspace);
23392348
}
2340-
return withDeployBranch({
2349+
return withRemoteDeployBranch(provider, {
23412350
workspace,
23422351
project: toProjectSummary(project),
23432352
resolution: {
23442353
projectSource: "env",
23452354
targetName: options.envProjectId,
23462355
targetNameSource: "env",
23472356
},
2348-
}, branch);
2357+
}, branch, context.runtime.signal);
23492358
}
23502359

23512360
const localPin = options.localPin;
@@ -2359,33 +2368,33 @@ async function resolveDeployProjectContext(
23592368
throw localResolutionPinStaleError();
23602369
}
23612370

2362-
return withDeployBranch({
2371+
return withRemoteDeployBranch(provider, {
23632372
workspace,
23642373
project: toProjectSummary(project),
23652374
resolution: {
23662375
projectSource: "local-pin",
23672376
targetName: project.name,
23682377
targetNameSource: "local-pin",
23692378
},
2370-
}, branch);
2379+
}, branch, context.runtime.signal);
23712380
}
23722381

23732382
const platformMapping = await resolveDurablePlatformMapping();
23742383
if (platformMapping && platformMapping.workspace.id === workspace.id) {
2375-
return withDeployBranch({
2384+
return withRemoteDeployBranch(provider, {
23762385
workspace,
23772386
project: toProjectSummary(platformMapping),
23782387
resolution: {
23792388
projectSource: "platform-mapping",
23802389
targetName: platformMapping.name,
23812390
targetNameSource: "platform-mapping",
23822391
},
2383-
}, branch);
2392+
}, branch, context.runtime.signal);
23842393
}
23852394

23862395
if (canPrompt(context) && !context.flags.yes) {
23872396
const resolved = await resolveInteractiveDeployProjectSetup(context, provider, workspace, projects);
2388-
return withDeployBranch(resolved, branch);
2397+
return withRemoteDeployBranch(provider, resolved, branch, context.runtime.signal);
23892398
}
23902399

23912400
const suggestedName = await inferTargetName(context.runtime.cwd, context.runtime.signal);
@@ -2446,15 +2455,22 @@ async function createProjectForDeploySetup(
24462455
};
24472456
}
24482457

2449-
function withDeployBranch(
2458+
async function withRemoteDeployBranch(
2459+
provider: ReturnType<typeof createPreviewAppProvider>,
24502460
target: Omit<ResolvedAppProjectContext, "branch">,
24512461
branch: ResolvedDeployBranch,
2452-
): ResolvedAppProjectContext {
2462+
signal: AbortSignal,
2463+
): Promise<ResolvedAppProjectContext> {
2464+
const remoteBranch = await provider.resolveBranch(target.project.id, {
2465+
branchName: branch.name,
2466+
signal,
2467+
});
2468+
24532469
return {
24542470
...target,
24552471
branch: {
2456-
name: branch.name,
2457-
kind: toBranchKind(branch.name),
2472+
name: remoteBranch.name,
2473+
kind: remoteBranch.role,
24582474
},
24592475
};
24602476
}

packages/cli/src/lib/app/preview-provider.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ManagementApiClient } from "@prisma/management-api-sdk";
77
import { envVarNames } from "./env-vars";
88
import { PreviewBuildStrategy } from "./preview-build";
99
import type { PreviewBuildType } from "./preview-build";
10+
import type { BranchKind } from "../../types/branch";
1011

1112
export interface PreviewAppRecord {
1213
id: string;
@@ -22,6 +23,12 @@ export interface PreviewProjectRecord {
2223
name: string;
2324
}
2425

26+
export interface PreviewBranchRecord {
27+
id: string;
28+
name: string;
29+
role: BranchKind;
30+
}
31+
2532
export interface PreviewDeploymentRecord {
2633
id: string;
2734
status: string;
@@ -105,6 +112,7 @@ export class PreviewDomainApiError extends Error {
105112

106113
export interface PreviewAppProvider {
107114
createProject(options: { name: string; signal?: AbortSignal }): Promise<PreviewProjectRecord>;
115+
resolveBranch(projectId: string, options: { branchName: string; signal?: AbortSignal }): Promise<PreviewBranchRecord>;
108116
listApps(projectId: string, options?: { branchName?: string; signal?: AbortSignal }): Promise<PreviewAppRecord[]>;
109117
removeApp(appId: string, options?: { signal?: AbortSignal }): Promise<PreviewRemovedAppRecord>;
110118
listDomains(appId: string, options?: { signal?: AbortSignal }): Promise<PreviewDomainRecord[]>;
@@ -181,12 +189,26 @@ export function createPreviewAppProvider(
181189

182190
async listApps(projectId, options) {
183191
return listComputeServices(client, {
184-
projectId,
185-
branchGitName: options?.branchName,
186-
signal: options?.signal,
192+
projectId,
193+
branchGitName: options?.branchName,
194+
signal: options?.signal,
187195
});
188196
},
189197

198+
async resolveBranch(projectId, options) {
199+
const branch = await resolveOrCreateBranch(client, {
200+
projectId,
201+
gitName: options.branchName,
202+
signal: options.signal,
203+
});
204+
205+
return {
206+
id: branch.id,
207+
name: branch.gitName,
208+
role: branch.role,
209+
};
210+
},
211+
190212
async removeApp(appId, options) {
191213
const appResult = await sdk.showService({ serviceId: appId, signal: options?.signal });
192214
if (appResult.isErr()) {
@@ -556,6 +578,7 @@ interface RawBranchRecord {
556578
id: string;
557579
gitName: string;
558580
isDefault: boolean;
581+
role: BranchKind;
559582
}
560583

561584
interface RawComputeServiceRecord {
@@ -621,7 +644,12 @@ async function listBranches(
621644
throw apiCallError("Failed to list branches", result.response, result.error);
622645
}
623646

624-
return result.data.data as RawBranchRecord[];
647+
return result.data.data.map((branch) => ({
648+
id: branch.id,
649+
gitName: branch.gitName,
650+
isDefault: branch.isDefault,
651+
role: branch.role,
652+
}));
625653
}
626654

627655
async function resolveOrCreateBranch(
@@ -643,7 +671,6 @@ async function resolveOrCreateBranch(
643671
},
644672
body: {
645673
gitName: options.gitName,
646-
isDefault: options.gitName === "main",
647674
},
648675
signal: options.signal,
649676
});
@@ -658,7 +685,13 @@ async function resolveOrCreateBranch(
658685
throw apiCallError(`Failed to create branch "${options.gitName}"`, result.response, result.error);
659686
}
660687

661-
return result.data.data as RawBranchRecord;
688+
const branch = result.data.data;
689+
return {
690+
id: branch.id,
691+
gitName: branch.gitName,
692+
isDefault: branch.isDefault,
693+
role: branch.role,
694+
};
662695
}
663696

664697
async function listComputeServices(

0 commit comments

Comments
 (0)