@prisma/cli 3.0.0-dev.54.1 → 3.0.0-dev.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -64,7 +64,7 @@ function createDeployCommand(runtime) {
|
|
|
64
64
|
"hono",
|
|
65
65
|
"tanstack-start",
|
|
66
66
|
"bun"
|
|
67
|
-
])).addOption(new Option("--entry <path>", "Entrypoint path for Bun deploys")).addOption(new Option("--http-port <port>", "HTTP port override for the deployed app")).addOption(new Option("--env <name=value>", "Environment variable").argParser(collectRepeatableValues));
|
|
67
|
+
])).addOption(new Option("--entry <path>", "Entrypoint path for Bun deploys")).addOption(new Option("--http-port <port>", "HTTP port override for the deployed app")).addOption(new Option("--env <name=value>", "Environment variable").argParser(collectRepeatableValues)).addOption(new Option("--prod", "Confirm intent to deploy to production"));
|
|
68
68
|
addGlobalFlags(command);
|
|
69
69
|
command.action(async (options) => {
|
|
70
70
|
const appName = options.app;
|
|
@@ -75,6 +75,7 @@ function createDeployCommand(runtime) {
|
|
|
75
75
|
const envAssignments = options.env;
|
|
76
76
|
const projectRef = options.project;
|
|
77
77
|
const createProjectName = options.createProject;
|
|
78
|
+
const prod = options.prod;
|
|
78
79
|
await runCommand(runtime, "app.deploy", options, (context) => runAppDeploy(context, appName, {
|
|
79
80
|
projectRef,
|
|
80
81
|
createProjectName,
|
|
@@ -82,7 +83,8 @@ function createDeployCommand(runtime) {
|
|
|
82
83
|
entrypoint: entry,
|
|
83
84
|
framework,
|
|
84
85
|
httpPort,
|
|
85
|
-
envAssignments
|
|
86
|
+
envAssignments,
|
|
87
|
+
prod: prod === true
|
|
86
88
|
}), {
|
|
87
89
|
renderHuman: (context, descriptor, result) => renderAppDeploy(context, descriptor, result),
|
|
88
90
|
renderJson: (result) => serializeAppDeploy(result)
|
package/dist/controllers/app.js
CHANGED
|
@@ -20,6 +20,7 @@ import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild
|
|
|
20
20
|
import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
|
|
21
21
|
import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress } from "../lib/app/preview-progress.js";
|
|
22
22
|
import { PreviewDomainApiError, createPreviewAppProvider } from "../lib/app/preview-provider.js";
|
|
23
|
+
import { enforceProductionDeployGate } from "../lib/app/production-deploy-gate.js";
|
|
23
24
|
import { formatDomainFailureFix } from "../lib/app/domain-guidance.js";
|
|
24
25
|
import { createSelectPromptPort } from "./select-prompt-port.js";
|
|
25
26
|
import { requireAuthenticatedAuthState } from "./auth.js";
|
|
@@ -158,6 +159,12 @@ async function runAppDeploy(context, appName, options) {
|
|
|
158
159
|
});
|
|
159
160
|
framework = customized.framework;
|
|
160
161
|
runtime = customized.runtime;
|
|
162
|
+
await enforceProductionDeployGate(context, provider, {
|
|
163
|
+
appId: selectedApp.appId,
|
|
164
|
+
appName: selectedApp.displayName,
|
|
165
|
+
branchKind: target.branch.kind,
|
|
166
|
+
prod: options?.prod === true
|
|
167
|
+
});
|
|
161
168
|
const buildType = framework.buildType;
|
|
162
169
|
assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
|
|
163
170
|
const entrypoint = await resolveDeployEntrypoint(context.runtime.cwd, framework, options?.entrypoint, context.runtime.signal);
|
|
@@ -1322,7 +1329,7 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1322
1329
|
if (!workspace) throw workspaceRequiredError();
|
|
1323
1330
|
const branch = options.branch ?? await resolveDeployBranch(context, void 0);
|
|
1324
1331
|
const projects = await listRealWorkspaceProjects(client, workspace, context.runtime.signal);
|
|
1325
|
-
if (explicitProject) return
|
|
1332
|
+
if (explicitProject) return withRemoteDeployBranch(provider, {
|
|
1326
1333
|
workspace,
|
|
1327
1334
|
project: toProjectSummary(resolveProjectForSetup(explicitProject, projects, workspace)),
|
|
1328
1335
|
resolution: {
|
|
@@ -1331,11 +1338,11 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1331
1338
|
targetNameSource: "explicit"
|
|
1332
1339
|
},
|
|
1333
1340
|
localPinAction: "linked"
|
|
1334
|
-
}, branch);
|
|
1341
|
+
}, branch, context.runtime.signal);
|
|
1335
1342
|
if (options.createProjectName) {
|
|
1336
1343
|
const projectName = options.createProjectName.trim();
|
|
1337
1344
|
if (!projectName) throw projectSetupNameRequiredError("app deploy --create-project");
|
|
1338
|
-
return
|
|
1345
|
+
return withRemoteDeployBranch(provider, {
|
|
1339
1346
|
workspace,
|
|
1340
1347
|
project: toProjectSummary(await createProjectForDeploySetup(provider, projectName, workspace, context.runtime.signal)),
|
|
1341
1348
|
resolution: {
|
|
@@ -1344,12 +1351,12 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1344
1351
|
targetNameSource: "explicit"
|
|
1345
1352
|
},
|
|
1346
1353
|
localPinAction: "created"
|
|
1347
|
-
}, branch);
|
|
1354
|
+
}, branch, context.runtime.signal);
|
|
1348
1355
|
}
|
|
1349
1356
|
if (options.envProjectId) {
|
|
1350
1357
|
const project = projects.find((candidate) => candidate.id === options.envProjectId);
|
|
1351
1358
|
if (!project) throw projectNotFoundError(options.envProjectId, workspace);
|
|
1352
|
-
return
|
|
1359
|
+
return withRemoteDeployBranch(provider, {
|
|
1353
1360
|
workspace,
|
|
1354
1361
|
project: toProjectSummary(project),
|
|
1355
1362
|
resolution: {
|
|
@@ -1357,14 +1364,14 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1357
1364
|
targetName: options.envProjectId,
|
|
1358
1365
|
targetNameSource: "env"
|
|
1359
1366
|
}
|
|
1360
|
-
}, branch);
|
|
1367
|
+
}, branch, context.runtime.signal);
|
|
1361
1368
|
}
|
|
1362
1369
|
const localPin = options.localPin;
|
|
1363
1370
|
if (localPin.kind === "present") {
|
|
1364
1371
|
if (localPin.pin.workspaceId !== workspace.id) throw localResolutionPinStaleError();
|
|
1365
1372
|
const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
|
|
1366
1373
|
if (!project) throw localResolutionPinStaleError();
|
|
1367
|
-
return
|
|
1374
|
+
return withRemoteDeployBranch(provider, {
|
|
1368
1375
|
workspace,
|
|
1369
1376
|
project: toProjectSummary(project),
|
|
1370
1377
|
resolution: {
|
|
@@ -1372,10 +1379,10 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1372
1379
|
targetName: project.name,
|
|
1373
1380
|
targetNameSource: "local-pin"
|
|
1374
1381
|
}
|
|
1375
|
-
}, branch);
|
|
1382
|
+
}, branch, context.runtime.signal);
|
|
1376
1383
|
}
|
|
1377
1384
|
const platformMapping = await resolveDurablePlatformMapping();
|
|
1378
|
-
if (platformMapping && platformMapping.workspace.id === workspace.id) return
|
|
1385
|
+
if (platformMapping && platformMapping.workspace.id === workspace.id) return withRemoteDeployBranch(provider, {
|
|
1379
1386
|
workspace,
|
|
1380
1387
|
project: toProjectSummary(platformMapping),
|
|
1381
1388
|
resolution: {
|
|
@@ -1383,8 +1390,8 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1383
1390
|
targetName: platformMapping.name,
|
|
1384
1391
|
targetNameSource: "platform-mapping"
|
|
1385
1392
|
}
|
|
1386
|
-
}, branch);
|
|
1387
|
-
if (canPrompt(context) && !context.flags.yes) return
|
|
1393
|
+
}, branch, context.runtime.signal);
|
|
1394
|
+
if (canPrompt(context) && !context.flags.yes) return withRemoteDeployBranch(provider, await resolveInteractiveDeployProjectSetup(context, provider, workspace, projects), branch, context.runtime.signal);
|
|
1388
1395
|
throw projectSetupRequiredError(projects, await inferTargetName(context.runtime.cwd, context.runtime.signal));
|
|
1389
1396
|
}
|
|
1390
1397
|
async function resolveInteractiveDeployProjectSetup(context, provider, workspace, projects) {
|
|
@@ -1430,12 +1437,16 @@ async function createProjectForDeploySetup(provider, projectName, workspace, sig
|
|
|
1430
1437
|
workspace
|
|
1431
1438
|
};
|
|
1432
1439
|
}
|
|
1433
|
-
function
|
|
1440
|
+
async function withRemoteDeployBranch(provider, target, branch, signal) {
|
|
1441
|
+
const remoteBranch = await provider.resolveBranch(target.project.id, {
|
|
1442
|
+
branchName: branch.name,
|
|
1443
|
+
signal
|
|
1444
|
+
});
|
|
1434
1445
|
return {
|
|
1435
1446
|
...target,
|
|
1436
1447
|
branch: {
|
|
1437
|
-
name:
|
|
1438
|
-
kind:
|
|
1448
|
+
name: remoteBranch.name,
|
|
1449
|
+
kind: remoteBranch.role
|
|
1439
1450
|
}
|
|
1440
1451
|
};
|
|
1441
1452
|
}
|
|
@@ -36,6 +36,18 @@ function createPreviewAppProvider(client, options) {
|
|
|
36
36
|
signal: options?.signal
|
|
37
37
|
});
|
|
38
38
|
},
|
|
39
|
+
async resolveBranch(projectId, options) {
|
|
40
|
+
const branch = await resolveOrCreateBranch(client, {
|
|
41
|
+
projectId,
|
|
42
|
+
gitName: options.branchName,
|
|
43
|
+
signal: options.signal
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
id: branch.id,
|
|
47
|
+
name: branch.gitName,
|
|
48
|
+
role: branch.role
|
|
49
|
+
};
|
|
50
|
+
},
|
|
39
51
|
async removeApp(appId, options) {
|
|
40
52
|
const appResult = await sdk.showService({
|
|
41
53
|
serviceId: appId,
|
|
@@ -316,17 +328,19 @@ async function listBranches(client, options) {
|
|
|
316
328
|
signal: options.signal
|
|
317
329
|
});
|
|
318
330
|
if (result.error || !result.data) throw apiCallError("Failed to list branches", result.response, result.error);
|
|
319
|
-
return result.data.data
|
|
331
|
+
return result.data.data.map((branch) => ({
|
|
332
|
+
id: branch.id,
|
|
333
|
+
gitName: branch.gitName,
|
|
334
|
+
isDefault: branch.isDefault,
|
|
335
|
+
role: branch.role
|
|
336
|
+
}));
|
|
320
337
|
}
|
|
321
338
|
async function resolveOrCreateBranch(client, options) {
|
|
322
339
|
const existing = (await listBranches(client, options))[0];
|
|
323
340
|
if (existing) return existing;
|
|
324
341
|
const result = await client.POST("/v1/projects/{projectId}/branches", {
|
|
325
342
|
params: { path: { projectId: options.projectId } },
|
|
326
|
-
body: {
|
|
327
|
-
gitName: options.gitName,
|
|
328
|
-
isDefault: options.gitName === "main"
|
|
329
|
-
},
|
|
343
|
+
body: { gitName: options.gitName },
|
|
330
344
|
signal: options.signal
|
|
331
345
|
});
|
|
332
346
|
if (result.error || !result.data) {
|
|
@@ -336,7 +350,13 @@ async function resolveOrCreateBranch(client, options) {
|
|
|
336
350
|
}
|
|
337
351
|
throw apiCallError(`Failed to create branch "${options.gitName}"`, result.response, result.error);
|
|
338
352
|
}
|
|
339
|
-
|
|
353
|
+
const branch = result.data.data;
|
|
354
|
+
return {
|
|
355
|
+
id: branch.id,
|
|
356
|
+
gitName: branch.gitName,
|
|
357
|
+
isDefault: branch.isDefault,
|
|
358
|
+
role: branch.role
|
|
359
|
+
};
|
|
340
360
|
}
|
|
341
361
|
async function listComputeServices(client, options) {
|
|
342
362
|
const services = [];
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { CliError } from "../../shell/errors.js";
|
|
2
|
+
import { canPrompt } from "../../shell/runtime.js";
|
|
3
|
+
import { confirmPrompt } from "../../shell/prompt.js";
|
|
4
|
+
//#region src/lib/app/production-deploy-gate.ts
|
|
5
|
+
async function enforceProductionDeployGate(context, provider, options) {
|
|
6
|
+
if (options.branchKind !== "production") return;
|
|
7
|
+
if (!options.appId) {
|
|
8
|
+
renderFirstProductionDeployLine(context, options.appName);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const currentLiveDeployment = resolveCurrentProductionDeployment(await provider.listDeployments(options.appId).catch((error) => {
|
|
12
|
+
throw productionDeployInspectionFailedError(error);
|
|
13
|
+
}));
|
|
14
|
+
if (!currentLiveDeployment) {
|
|
15
|
+
renderFirstProductionDeployLine(context, options.appName);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!options.prod) throw productionDeployRequiresFlagError();
|
|
19
|
+
if (context.flags.yes) {
|
|
20
|
+
renderProductionDeployYesLine(context);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!canPrompt(context)) throw productionDeployConfirmationRequiredError(options.appName);
|
|
24
|
+
renderProductionDeployConfirmation(context, currentLiveDeployment);
|
|
25
|
+
if (!await confirmPrompt({
|
|
26
|
+
input: context.runtime.stdin,
|
|
27
|
+
output: context.output.stderr,
|
|
28
|
+
message: "Deploy to production?",
|
|
29
|
+
initialValue: false
|
|
30
|
+
})) throw productionDeployCancelledError();
|
|
31
|
+
}
|
|
32
|
+
function resolveCurrentProductionDeployment(result) {
|
|
33
|
+
if (result.deployments.length === 0) return null;
|
|
34
|
+
if (result.app.liveDeploymentId) {
|
|
35
|
+
const live = result.deployments.find((deployment) => deployment.id === result.app.liveDeploymentId);
|
|
36
|
+
if (live) return live;
|
|
37
|
+
}
|
|
38
|
+
return result.deployments.find((deployment) => deployment.live === true) ?? result.deployments[0] ?? null;
|
|
39
|
+
}
|
|
40
|
+
function renderFirstProductionDeployLine(context, appName) {
|
|
41
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
42
|
+
context.output.stderr.write(`First deploy of "${appName}" -- promoting to production.\n\n`);
|
|
43
|
+
}
|
|
44
|
+
function renderProductionDeployYesLine(context) {
|
|
45
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
46
|
+
context.output.stderr.write("Deploying to production (--prod --yes).\n\n");
|
|
47
|
+
}
|
|
48
|
+
function renderProductionDeployConfirmation(context, currentLiveDeployment) {
|
|
49
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
50
|
+
const lines = [
|
|
51
|
+
"This will deploy to production and replace the live deployment.",
|
|
52
|
+
"",
|
|
53
|
+
` Current live: ${currentLiveDeployment.id} deployed ${formatDeploymentAge(currentLiveDeployment.createdAt)}`,
|
|
54
|
+
" New deploy: will be built from your local code",
|
|
55
|
+
""
|
|
56
|
+
];
|
|
57
|
+
context.output.stderr.write(`${lines.join("\n")}\n`);
|
|
58
|
+
}
|
|
59
|
+
function formatDeploymentAge(createdAt) {
|
|
60
|
+
const createdAtMs = Date.parse(createdAt);
|
|
61
|
+
if (!Number.isFinite(createdAtMs)) return createdAt;
|
|
62
|
+
const elapsedMs = Math.max(0, Date.now() - createdAtMs);
|
|
63
|
+
for (const unit of [
|
|
64
|
+
{
|
|
65
|
+
label: "day",
|
|
66
|
+
ms: 1440 * 60 * 1e3
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: "hour",
|
|
70
|
+
ms: 3600 * 1e3
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
label: "minute",
|
|
74
|
+
ms: 60 * 1e3
|
|
75
|
+
}
|
|
76
|
+
]) if (elapsedMs >= unit.ms) {
|
|
77
|
+
const value = Math.floor(elapsedMs / unit.ms);
|
|
78
|
+
return `${value} ${unit.label}${value === 1 ? "" : "s"} ago`;
|
|
79
|
+
}
|
|
80
|
+
return "less than a minute ago";
|
|
81
|
+
}
|
|
82
|
+
function productionDeployRequiresFlagError() {
|
|
83
|
+
return new CliError({
|
|
84
|
+
code: "PROD_DEPLOY_REQUIRES_FLAG",
|
|
85
|
+
domain: "app",
|
|
86
|
+
summary: "Production deploy requires --prod",
|
|
87
|
+
why: "The resolved Branch is production and this App already has a production deployment.",
|
|
88
|
+
fix: "Re-run with --prod, or deploy from a preview Branch.",
|
|
89
|
+
exitCode: 2,
|
|
90
|
+
nextActions: [{
|
|
91
|
+
kind: "run-command",
|
|
92
|
+
journey: "deploy-app",
|
|
93
|
+
label: "Deploy to production",
|
|
94
|
+
command: "prisma-cli app deploy --prod"
|
|
95
|
+
}, {
|
|
96
|
+
kind: "run-command",
|
|
97
|
+
journey: "deploy-app",
|
|
98
|
+
label: "Create a preview branch",
|
|
99
|
+
commands: ["git checkout -b <branch-name>", "prisma-cli app deploy"]
|
|
100
|
+
}],
|
|
101
|
+
humanLines: [
|
|
102
|
+
"This would deploy to production.",
|
|
103
|
+
"",
|
|
104
|
+
"Production deploys require explicit intent. Re-run with:",
|
|
105
|
+
"",
|
|
106
|
+
" prisma app deploy --prod",
|
|
107
|
+
"",
|
|
108
|
+
"Or deploy a preview from a feature branch:",
|
|
109
|
+
"",
|
|
110
|
+
" git checkout -b <branch-name>",
|
|
111
|
+
" prisma app deploy"
|
|
112
|
+
]
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function productionDeployConfirmationRequiredError(appName) {
|
|
116
|
+
return new CliError({
|
|
117
|
+
code: "CONFIRMATION_REQUIRED",
|
|
118
|
+
domain: "app",
|
|
119
|
+
summary: "Production deploy requires confirmation in the current mode",
|
|
120
|
+
why: "This command cannot prompt for production deploy confirmation in the current mode.",
|
|
121
|
+
fix: `Pass --prod --yes to confirm deployment of "${appName}" to production.`,
|
|
122
|
+
exitCode: 1,
|
|
123
|
+
nextSteps: ["prisma-cli app deploy --prod --yes"],
|
|
124
|
+
nextActions: [{
|
|
125
|
+
kind: "run-command",
|
|
126
|
+
journey: "deploy-app",
|
|
127
|
+
label: "Deploy to production non-interactively",
|
|
128
|
+
command: "prisma-cli app deploy --prod --yes"
|
|
129
|
+
}]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function productionDeployCancelledError() {
|
|
133
|
+
return new CliError({
|
|
134
|
+
code: "CONFIRMATION_REQUIRED",
|
|
135
|
+
domain: "app",
|
|
136
|
+
summary: "Production deploy cancelled",
|
|
137
|
+
why: null,
|
|
138
|
+
fix: null,
|
|
139
|
+
exitCode: 0,
|
|
140
|
+
humanLines: ["Cancelled."]
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function productionDeployInspectionFailedError(error) {
|
|
144
|
+
return new CliError({
|
|
145
|
+
code: "DEPLOY_FAILED",
|
|
146
|
+
domain: "app",
|
|
147
|
+
summary: "Failed to inspect production deployments",
|
|
148
|
+
why: error instanceof Error ? error.message : String(error),
|
|
149
|
+
fix: "Retry the command, or rerun with --trace for more detailed diagnostics.",
|
|
150
|
+
debug: formatDebugDetails(error),
|
|
151
|
+
exitCode: 1,
|
|
152
|
+
nextSteps: ["prisma-cli app list-deploys"]
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function formatDebugDetails(error) {
|
|
156
|
+
if (error instanceof Error) return error.stack ?? error.message;
|
|
157
|
+
return typeof error === "string" ? error : null;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
export { enforceProductionDeployGate };
|
|
@@ -216,6 +216,7 @@ const DESCRIPTORS = [
|
|
|
216
216
|
"prisma-cli app deploy --app my-app --env DATABASE_URL=postgresql://example",
|
|
217
217
|
"prisma-cli app deploy --app my-app --framework nextjs --http-port 3000",
|
|
218
218
|
"prisma-cli app deploy --branch feat-login --framework hono",
|
|
219
|
+
"prisma-cli app deploy --prod --yes",
|
|
219
220
|
"pnpm dlx skills@latest add prisma/prisma-cli/skills#cli-v<cli-version> --all",
|
|
220
221
|
"prisma-cli app deploy --framework bun --entry src/server.ts"
|
|
221
222
|
]
|