@prisma/cli 3.0.0-dev.54.1 → 3.0.0-dev.57.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.
- package/README.md +1 -0
- package/dist/commands/app/index.js +4 -2
- package/dist/controllers/app-env.js +152 -21
- package/dist/controllers/app.js +26 -50
- package/dist/lib/app/preview-provider.js +26 -6
- package/dist/lib/app/production-deploy-gate.js +160 -0
- package/dist/lib/git/local-branch.js +41 -0
- package/dist/presenters/app-env.js +14 -3
- package/dist/shell/command-meta.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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)
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
|
|
2
2
|
import { requireComputeAuth } from "../lib/auth/guard.js";
|
|
3
3
|
import { resolveProjectTarget } from "../lib/project/resolution.js";
|
|
4
|
+
import { readLocalGitBranch } from "../lib/git/local-branch.js";
|
|
4
5
|
import { requireAuthenticatedAuthState } from "./auth.js";
|
|
5
6
|
import { listRealWorkspaceProjects } from "./project.js";
|
|
6
7
|
import { formatScopeLabel, parseKeyValuePositional, resolveEnvScope } from "../lib/app/env-config.js";
|
|
7
8
|
//#region src/controllers/app-env.ts
|
|
8
|
-
function defaultRoleScope() {
|
|
9
|
-
return {
|
|
10
|
-
kind: "role",
|
|
11
|
-
role: "production"
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
9
|
async function runEnvAdd(context, rawAssignment, flags) {
|
|
15
10
|
const { key, value } = parseKeyValuePositional(rawAssignment, "add", context.runtime.env);
|
|
16
11
|
const scope = resolveEnvScope(flags, {
|
|
@@ -108,25 +103,30 @@ async function runEnvUpdate(context, rawAssignment, flags) {
|
|
|
108
103
|
};
|
|
109
104
|
}
|
|
110
105
|
async function runEnvList(context, flags) {
|
|
111
|
-
const
|
|
106
|
+
const explicit = resolveEnvScope(flags, {
|
|
112
107
|
requireExplicit: false,
|
|
113
108
|
command: "list"
|
|
114
|
-
})
|
|
109
|
+
});
|
|
115
110
|
const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env list");
|
|
116
|
-
const resolved = await
|
|
117
|
-
|
|
111
|
+
const resolved = await resolveListScopeToApi(client, projectId, explicit, {
|
|
112
|
+
cwd: context.runtime.cwd,
|
|
118
113
|
signal: context.runtime.signal
|
|
119
114
|
});
|
|
120
|
-
const variables = await listVariables(client, projectId,
|
|
115
|
+
const variables = resolved.kind === "scoped" ? await listVariables(client, projectId, {
|
|
116
|
+
scope: resolved.addScope,
|
|
117
|
+
descriptor: resolved.descriptor,
|
|
118
|
+
apiTarget: resolved.apiTarget
|
|
119
|
+
}, context.runtime.signal) : await listOverviewVariables(client, projectId, context.runtime.signal);
|
|
121
120
|
return {
|
|
122
121
|
command: "project.env.list",
|
|
123
122
|
result: {
|
|
124
123
|
projectId,
|
|
125
124
|
scope: resolved.descriptor,
|
|
125
|
+
target: resolved.target,
|
|
126
126
|
variables: variables.map((row) => toMetadata(row, resolved.descriptor))
|
|
127
127
|
},
|
|
128
128
|
warnings: [],
|
|
129
|
-
nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value ${formatScopeFlag(
|
|
129
|
+
nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value ${formatScopeFlag(resolved.addScope)}`] : []
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
async function runEnvRemove(context, key, flags) {
|
|
@@ -196,12 +196,12 @@ async function resolveScopeToApi(client, projectId, scope, options) {
|
|
|
196
196
|
}
|
|
197
197
|
};
|
|
198
198
|
const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName, options.signal) : await resolveExistingBranch(client, projectId, scope.branchName, options.signal);
|
|
199
|
-
if (branch.
|
|
199
|
+
if (branch.role === "production") throw new CliError({
|
|
200
200
|
code: "ENV_BRANCH_SCOPE_IS_PRODUCTION",
|
|
201
201
|
domain: "app",
|
|
202
|
-
summary: `Branch "${scope.branchName}" is the
|
|
202
|
+
summary: `Branch "${scope.branchName}" is the production branch`,
|
|
203
203
|
why: "Production variables are project-level only; branch overrides apply to preview branches.",
|
|
204
|
-
fix: "Use --role production for the
|
|
204
|
+
fix: "Use --role production for the production branch.",
|
|
205
205
|
exitCode: 1,
|
|
206
206
|
nextSteps: ["prisma-cli project env list --role production"]
|
|
207
207
|
});
|
|
@@ -218,6 +218,123 @@ async function resolveScopeToApi(client, projectId, scope, options) {
|
|
|
218
218
|
}
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
|
+
async function resolveListScopeToApi(client, projectId, explicit, options) {
|
|
222
|
+
if (explicit) {
|
|
223
|
+
const resolved = await resolveScopeToApi(client, projectId, explicit, {
|
|
224
|
+
createBranchIfMissing: false,
|
|
225
|
+
signal: options.signal
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
kind: "scoped",
|
|
229
|
+
descriptor: resolved.descriptor,
|
|
230
|
+
target: targetFromExplicitScope(resolved.descriptor),
|
|
231
|
+
apiTarget: resolved.apiTarget,
|
|
232
|
+
addScope: resolved.scope
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const gitBranch = await readLocalGitBranch(options.cwd, options.signal);
|
|
236
|
+
if (gitBranch) {
|
|
237
|
+
const branch = (await listBranchesByName(client, projectId, gitBranch, options.signal))[0];
|
|
238
|
+
if (!branch) return {
|
|
239
|
+
kind: "scoped",
|
|
240
|
+
descriptor: {
|
|
241
|
+
kind: "role",
|
|
242
|
+
role: "preview"
|
|
243
|
+
},
|
|
244
|
+
target: {
|
|
245
|
+
source: "local-git",
|
|
246
|
+
branchName: gitBranch,
|
|
247
|
+
branchExists: false,
|
|
248
|
+
envMap: "preview"
|
|
249
|
+
},
|
|
250
|
+
apiTarget: {
|
|
251
|
+
class: "preview",
|
|
252
|
+
branchId: null
|
|
253
|
+
},
|
|
254
|
+
addScope: {
|
|
255
|
+
kind: "branch",
|
|
256
|
+
branchName: gitBranch
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
if (branch.role === "production") return {
|
|
260
|
+
kind: "scoped",
|
|
261
|
+
descriptor: {
|
|
262
|
+
kind: "role",
|
|
263
|
+
role: "production"
|
|
264
|
+
},
|
|
265
|
+
target: {
|
|
266
|
+
source: "local-git",
|
|
267
|
+
branchName: branch.gitName,
|
|
268
|
+
branchId: branch.id,
|
|
269
|
+
branchRole: branch.role,
|
|
270
|
+
branchExists: true,
|
|
271
|
+
envMap: "production"
|
|
272
|
+
},
|
|
273
|
+
apiTarget: {
|
|
274
|
+
class: "production",
|
|
275
|
+
branchId: null
|
|
276
|
+
},
|
|
277
|
+
addScope: {
|
|
278
|
+
kind: "role",
|
|
279
|
+
role: "production"
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
return {
|
|
283
|
+
kind: "scoped",
|
|
284
|
+
descriptor: {
|
|
285
|
+
kind: "branch",
|
|
286
|
+
branchName: branch.gitName,
|
|
287
|
+
branchId: branch.id
|
|
288
|
+
},
|
|
289
|
+
target: {
|
|
290
|
+
source: "local-git",
|
|
291
|
+
branchName: branch.gitName,
|
|
292
|
+
branchId: branch.id,
|
|
293
|
+
branchRole: branch.role,
|
|
294
|
+
branchExists: true,
|
|
295
|
+
envMap: "preview"
|
|
296
|
+
},
|
|
297
|
+
apiTarget: {
|
|
298
|
+
class: "preview",
|
|
299
|
+
branchId: branch.id
|
|
300
|
+
},
|
|
301
|
+
addScope: {
|
|
302
|
+
kind: "branch",
|
|
303
|
+
branchName: branch.gitName
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
kind: "overview",
|
|
309
|
+
descriptor: { kind: "overview" },
|
|
310
|
+
target: {
|
|
311
|
+
source: "overview",
|
|
312
|
+
envMap: "overview"
|
|
313
|
+
},
|
|
314
|
+
addScope: {
|
|
315
|
+
kind: "role",
|
|
316
|
+
role: "preview"
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function targetFromExplicitScope(scope) {
|
|
321
|
+
if (scope.kind === "branch") return {
|
|
322
|
+
source: "explicit",
|
|
323
|
+
branchName: scope.branchName,
|
|
324
|
+
branchId: scope.branchId,
|
|
325
|
+
branchRole: "preview",
|
|
326
|
+
branchExists: true,
|
|
327
|
+
envMap: "preview"
|
|
328
|
+
};
|
|
329
|
+
if (scope.kind === "role") return {
|
|
330
|
+
source: "explicit",
|
|
331
|
+
envMap: scope.role
|
|
332
|
+
};
|
|
333
|
+
return {
|
|
334
|
+
source: "overview",
|
|
335
|
+
envMap: "overview"
|
|
336
|
+
};
|
|
337
|
+
}
|
|
221
338
|
function formatScopeFlag(scope) {
|
|
222
339
|
if (scope.kind === "role") return `--role ${scope.role}`;
|
|
223
340
|
return `--branch ${scope.branchName}`;
|
|
@@ -306,25 +423,38 @@ async function findVariableByNaturalKey(client, projectId, key, resolved, signal
|
|
|
306
423
|
return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
|
|
307
424
|
}
|
|
308
425
|
async function listVariables(client, projectId, resolved, signal) {
|
|
426
|
+
return materializeEffectiveRows(await collectEnvironmentVariables(client, projectId, signal, {
|
|
427
|
+
className: resolved.apiTarget.class,
|
|
428
|
+
filter: (row) => rowMatchesScope(row, resolved)
|
|
429
|
+
}), resolved);
|
|
430
|
+
}
|
|
431
|
+
async function listOverviewVariables(client, projectId, signal) {
|
|
432
|
+
return (await collectEnvironmentVariables(client, projectId, signal, { filter: (row) => row.branchId === null && (row.class === "production" || row.class === "preview") })).sort((left, right) => {
|
|
433
|
+
const roleOrder = roleSortOrder(left.class) - roleSortOrder(right.class);
|
|
434
|
+
return roleOrder !== 0 ? roleOrder : left.key.localeCompare(right.key);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
async function collectEnvironmentVariables(client, projectId, signal, options) {
|
|
309
438
|
const collected = [];
|
|
310
439
|
let cursor;
|
|
311
440
|
while (true) {
|
|
312
|
-
const query = {
|
|
313
|
-
|
|
314
|
-
class: resolved.apiTarget.class
|
|
315
|
-
};
|
|
441
|
+
const query = { projectId };
|
|
442
|
+
if (options.className !== void 0) query.class = options.className;
|
|
316
443
|
if (cursor !== void 0) query.cursor = cursor;
|
|
317
444
|
const result = await client.GET("/v1/environment-variables", {
|
|
318
445
|
params: { query },
|
|
319
446
|
signal
|
|
320
447
|
});
|
|
321
448
|
if (result.error || !result.data) throw apiCallError(`Failed to list environment variables`, result.response, result.error);
|
|
322
|
-
const page = result.data.data.filter(
|
|
449
|
+
const page = result.data.data.filter(options.filter);
|
|
323
450
|
collected.push(...page);
|
|
324
451
|
if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
|
|
325
452
|
cursor = result.data.pagination.nextCursor;
|
|
326
453
|
}
|
|
327
|
-
return
|
|
454
|
+
return collected;
|
|
455
|
+
}
|
|
456
|
+
function roleSortOrder(role) {
|
|
457
|
+
return role === "production" ? 0 : 1;
|
|
328
458
|
}
|
|
329
459
|
function rowMatchesScope(row, resolved) {
|
|
330
460
|
if (row.class !== resolved.apiTarget.class) return false;
|
|
@@ -357,6 +487,7 @@ function toMetadata(row, requestedScope) {
|
|
|
357
487
|
}
|
|
358
488
|
function formatDescriptorLabel(scope) {
|
|
359
489
|
if (scope.kind === "role") return scope.role ?? "unknown";
|
|
490
|
+
if (scope.kind === "overview") return "overview";
|
|
360
491
|
return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
|
|
361
492
|
}
|
|
362
493
|
function apiCallError(summary, response, error) {
|
package/dist/controllers/app.js
CHANGED
|
@@ -16,10 +16,12 @@ import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "../l
|
|
|
16
16
|
import { buildProjectSetupNextActions, inferTargetName, projectNotFoundError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
|
|
17
17
|
import { bindProjectToDirectory, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
|
|
18
18
|
import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
|
|
19
|
+
import { readLocalGitBranch } from "../lib/git/local-branch.js";
|
|
19
20
|
import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild } from "../lib/app/preview-build.js";
|
|
20
21
|
import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
|
|
21
22
|
import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress } from "../lib/app/preview-progress.js";
|
|
22
23
|
import { PreviewDomainApiError, createPreviewAppProvider } from "../lib/app/preview-provider.js";
|
|
24
|
+
import { enforceProductionDeployGate } from "../lib/app/production-deploy-gate.js";
|
|
23
25
|
import { formatDomainFailureFix } from "../lib/app/domain-guidance.js";
|
|
24
26
|
import { createSelectPromptPort } from "./select-prompt-port.js";
|
|
25
27
|
import { requireAuthenticatedAuthState } from "./auth.js";
|
|
@@ -158,6 +160,12 @@ async function runAppDeploy(context, appName, options) {
|
|
|
158
160
|
});
|
|
159
161
|
framework = customized.framework;
|
|
160
162
|
runtime = customized.runtime;
|
|
163
|
+
await enforceProductionDeployGate(context, provider, {
|
|
164
|
+
appId: selectedApp.appId,
|
|
165
|
+
appName: selectedApp.displayName,
|
|
166
|
+
branchKind: target.branch.kind,
|
|
167
|
+
prod: options?.prod === true
|
|
168
|
+
});
|
|
161
169
|
const buildType = framework.buildType;
|
|
162
170
|
assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
|
|
163
171
|
const entrypoint = await resolveDeployEntrypoint(context.runtime.cwd, framework, options?.entrypoint, context.runtime.signal);
|
|
@@ -1322,7 +1330,7 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1322
1330
|
if (!workspace) throw workspaceRequiredError();
|
|
1323
1331
|
const branch = options.branch ?? await resolveDeployBranch(context, void 0);
|
|
1324
1332
|
const projects = await listRealWorkspaceProjects(client, workspace, context.runtime.signal);
|
|
1325
|
-
if (explicitProject) return
|
|
1333
|
+
if (explicitProject) return withRemoteDeployBranch(provider, {
|
|
1326
1334
|
workspace,
|
|
1327
1335
|
project: toProjectSummary(resolveProjectForSetup(explicitProject, projects, workspace)),
|
|
1328
1336
|
resolution: {
|
|
@@ -1331,11 +1339,11 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1331
1339
|
targetNameSource: "explicit"
|
|
1332
1340
|
},
|
|
1333
1341
|
localPinAction: "linked"
|
|
1334
|
-
}, branch);
|
|
1342
|
+
}, branch, context.runtime.signal);
|
|
1335
1343
|
if (options.createProjectName) {
|
|
1336
1344
|
const projectName = options.createProjectName.trim();
|
|
1337
1345
|
if (!projectName) throw projectSetupNameRequiredError("app deploy --create-project");
|
|
1338
|
-
return
|
|
1346
|
+
return withRemoteDeployBranch(provider, {
|
|
1339
1347
|
workspace,
|
|
1340
1348
|
project: toProjectSummary(await createProjectForDeploySetup(provider, projectName, workspace, context.runtime.signal)),
|
|
1341
1349
|
resolution: {
|
|
@@ -1344,12 +1352,12 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1344
1352
|
targetNameSource: "explicit"
|
|
1345
1353
|
},
|
|
1346
1354
|
localPinAction: "created"
|
|
1347
|
-
}, branch);
|
|
1355
|
+
}, branch, context.runtime.signal);
|
|
1348
1356
|
}
|
|
1349
1357
|
if (options.envProjectId) {
|
|
1350
1358
|
const project = projects.find((candidate) => candidate.id === options.envProjectId);
|
|
1351
1359
|
if (!project) throw projectNotFoundError(options.envProjectId, workspace);
|
|
1352
|
-
return
|
|
1360
|
+
return withRemoteDeployBranch(provider, {
|
|
1353
1361
|
workspace,
|
|
1354
1362
|
project: toProjectSummary(project),
|
|
1355
1363
|
resolution: {
|
|
@@ -1357,14 +1365,14 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1357
1365
|
targetName: options.envProjectId,
|
|
1358
1366
|
targetNameSource: "env"
|
|
1359
1367
|
}
|
|
1360
|
-
}, branch);
|
|
1368
|
+
}, branch, context.runtime.signal);
|
|
1361
1369
|
}
|
|
1362
1370
|
const localPin = options.localPin;
|
|
1363
1371
|
if (localPin.kind === "present") {
|
|
1364
1372
|
if (localPin.pin.workspaceId !== workspace.id) throw localResolutionPinStaleError();
|
|
1365
1373
|
const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
|
|
1366
1374
|
if (!project) throw localResolutionPinStaleError();
|
|
1367
|
-
return
|
|
1375
|
+
return withRemoteDeployBranch(provider, {
|
|
1368
1376
|
workspace,
|
|
1369
1377
|
project: toProjectSummary(project),
|
|
1370
1378
|
resolution: {
|
|
@@ -1372,10 +1380,10 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1372
1380
|
targetName: project.name,
|
|
1373
1381
|
targetNameSource: "local-pin"
|
|
1374
1382
|
}
|
|
1375
|
-
}, branch);
|
|
1383
|
+
}, branch, context.runtime.signal);
|
|
1376
1384
|
}
|
|
1377
1385
|
const platformMapping = await resolveDurablePlatformMapping();
|
|
1378
|
-
if (platformMapping && platformMapping.workspace.id === workspace.id) return
|
|
1386
|
+
if (platformMapping && platformMapping.workspace.id === workspace.id) return withRemoteDeployBranch(provider, {
|
|
1379
1387
|
workspace,
|
|
1380
1388
|
project: toProjectSummary(platformMapping),
|
|
1381
1389
|
resolution: {
|
|
@@ -1383,8 +1391,8 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1383
1391
|
targetName: platformMapping.name,
|
|
1384
1392
|
targetNameSource: "platform-mapping"
|
|
1385
1393
|
}
|
|
1386
|
-
}, branch);
|
|
1387
|
-
if (canPrompt(context) && !context.flags.yes) return
|
|
1394
|
+
}, branch, context.runtime.signal);
|
|
1395
|
+
if (canPrompt(context) && !context.flags.yes) return withRemoteDeployBranch(provider, await resolveInteractiveDeployProjectSetup(context, provider, workspace, projects), branch, context.runtime.signal);
|
|
1388
1396
|
throw projectSetupRequiredError(projects, await inferTargetName(context.runtime.cwd, context.runtime.signal));
|
|
1389
1397
|
}
|
|
1390
1398
|
async function resolveInteractiveDeployProjectSetup(context, provider, workspace, projects) {
|
|
@@ -1430,12 +1438,16 @@ async function createProjectForDeploySetup(provider, projectName, workspace, sig
|
|
|
1430
1438
|
workspace
|
|
1431
1439
|
};
|
|
1432
1440
|
}
|
|
1433
|
-
function
|
|
1441
|
+
async function withRemoteDeployBranch(provider, target, branch, signal) {
|
|
1442
|
+
const remoteBranch = await provider.resolveBranch(target.project.id, {
|
|
1443
|
+
branchName: branch.name,
|
|
1444
|
+
signal
|
|
1445
|
+
});
|
|
1434
1446
|
return {
|
|
1435
1447
|
...target,
|
|
1436
1448
|
branch: {
|
|
1437
|
-
name:
|
|
1438
|
-
kind:
|
|
1449
|
+
name: remoteBranch.name,
|
|
1450
|
+
kind: remoteBranch.role
|
|
1439
1451
|
}
|
|
1440
1452
|
};
|
|
1441
1453
|
}
|
|
@@ -1470,42 +1482,6 @@ async function resolveDeployBranch(context, explicitBranchName) {
|
|
|
1470
1482
|
annotation: "default"
|
|
1471
1483
|
};
|
|
1472
1484
|
}
|
|
1473
|
-
async function readLocalGitBranch(cwd, signal) {
|
|
1474
|
-
const headPath = await resolveGitHeadPath(path.join(cwd, ".git"), signal);
|
|
1475
|
-
if (!headPath) return null;
|
|
1476
|
-
try {
|
|
1477
|
-
const head = (await readFile(headPath, {
|
|
1478
|
-
encoding: "utf8",
|
|
1479
|
-
signal
|
|
1480
|
-
})).trim();
|
|
1481
|
-
if (head.startsWith("ref: refs/heads/")) return head.slice(16);
|
|
1482
|
-
} catch (error) {
|
|
1483
|
-
if (signal.aborted) throw error;
|
|
1484
|
-
return null;
|
|
1485
|
-
}
|
|
1486
|
-
return null;
|
|
1487
|
-
}
|
|
1488
|
-
async function resolveGitHeadPath(gitPath, signal) {
|
|
1489
|
-
signal.throwIfAborted();
|
|
1490
|
-
try {
|
|
1491
|
-
const raw = await readFile(gitPath, {
|
|
1492
|
-
encoding: "utf8",
|
|
1493
|
-
signal
|
|
1494
|
-
});
|
|
1495
|
-
if (raw.startsWith("gitdir:")) return path.join(path.resolve(path.dirname(gitPath), raw.slice(7).trim()), "HEAD");
|
|
1496
|
-
} catch (error) {
|
|
1497
|
-
if (signal.aborted) throw error;
|
|
1498
|
-
}
|
|
1499
|
-
signal.throwIfAborted();
|
|
1500
|
-
try {
|
|
1501
|
-
await access(path.join(gitPath, "HEAD"));
|
|
1502
|
-
signal.throwIfAborted();
|
|
1503
|
-
return path.join(gitPath, "HEAD");
|
|
1504
|
-
} catch (error) {
|
|
1505
|
-
if (signal.aborted) throw error;
|
|
1506
|
-
return null;
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
1485
|
async function resolveDeployFramework(context, options) {
|
|
1510
1486
|
if (options.requestedFramework) return frameworkFromUserFacingValue(options.requestedFramework, "set by --framework");
|
|
1511
1487
|
if (options.entrypoint) return {
|
|
@@ -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 };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/lib/git/local-branch.ts
|
|
4
|
+
async function readLocalGitBranch(cwd, signal) {
|
|
5
|
+
const headPath = await resolveGitHeadPath(path.join(cwd, ".git"), signal);
|
|
6
|
+
if (!headPath) return null;
|
|
7
|
+
try {
|
|
8
|
+
const head = (await readFile(headPath, {
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
signal
|
|
11
|
+
})).trim();
|
|
12
|
+
if (head.startsWith("ref: refs/heads/")) return head.slice(16);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (signal.aborted) throw error;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
async function resolveGitHeadPath(gitPath, signal) {
|
|
20
|
+
signal.throwIfAborted();
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readFile(gitPath, {
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
signal
|
|
25
|
+
});
|
|
26
|
+
if (raw.startsWith("gitdir:")) return path.join(path.resolve(path.dirname(gitPath), raw.slice(7).trim()), "HEAD");
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (signal.aborted) throw error;
|
|
29
|
+
}
|
|
30
|
+
signal.throwIfAborted();
|
|
31
|
+
try {
|
|
32
|
+
await access(path.join(gitPath, "HEAD"));
|
|
33
|
+
signal.throwIfAborted();
|
|
34
|
+
return path.join(gitPath, "HEAD");
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (signal.aborted) throw error;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { readLocalGitBranch };
|
|
@@ -2,8 +2,18 @@ import { renderList, renderShow, serializeList } from "../output/patterns.js";
|
|
|
2
2
|
//#region src/presenters/app-env.ts
|
|
3
3
|
function scopeLabel(scope) {
|
|
4
4
|
if (scope.kind === "role") return scope.role ?? "unknown";
|
|
5
|
+
if (scope.kind === "overview") return "overview";
|
|
5
6
|
return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
|
|
6
7
|
}
|
|
8
|
+
function listTargetLabel(result) {
|
|
9
|
+
const target = result.target;
|
|
10
|
+
if (target.source === "overview") return "overview";
|
|
11
|
+
if (target.branchName) {
|
|
12
|
+
const suffix = target.branchExists === false ? " (not created yet)" : "";
|
|
13
|
+
return `branch:${target.branchName} -> ${target.envMap}${suffix}`;
|
|
14
|
+
}
|
|
15
|
+
return scopeLabel(result.scope);
|
|
16
|
+
}
|
|
7
17
|
function renderEnvAdd(context, descriptor, result) {
|
|
8
18
|
return renderShow({
|
|
9
19
|
title: "Setting a new environment variable.",
|
|
@@ -75,8 +85,8 @@ function renderEnvList(context, descriptor, result) {
|
|
|
75
85
|
title: "Listing environment variables for the selected scope.",
|
|
76
86
|
descriptor,
|
|
77
87
|
parentContext: {
|
|
78
|
-
key: "
|
|
79
|
-
value:
|
|
88
|
+
key: "target",
|
|
89
|
+
value: listTargetLabel(result)
|
|
80
90
|
},
|
|
81
91
|
items: result.variables.map((variable) => ({
|
|
82
92
|
noun: "variable",
|
|
@@ -91,8 +101,9 @@ function serializeEnvList(result) {
|
|
|
91
101
|
return {
|
|
92
102
|
projectId: result.projectId,
|
|
93
103
|
scope: result.scope,
|
|
104
|
+
target: result.target,
|
|
94
105
|
...serializeList({
|
|
95
|
-
context: {
|
|
106
|
+
context: { target: listTargetLabel(result) },
|
|
96
107
|
items: result.variables.map((variable) => ({
|
|
97
108
|
noun: "variable",
|
|
98
109
|
label: `${variable.key} (${variable.source})`,
|
|
@@ -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
|
]
|
|
@@ -378,7 +379,7 @@ const DESCRIPTORS = [
|
|
|
378
379
|
],
|
|
379
380
|
description: "Manage environment variables for the active project",
|
|
380
381
|
examples: [
|
|
381
|
-
"prisma-cli project env list
|
|
382
|
+
"prisma-cli project env list",
|
|
382
383
|
"prisma-cli project env add STRIPE_KEY=sk_test_xxx --role production",
|
|
383
384
|
"prisma-cli project env add DATABASE_URL=postgresql://branch --branch feature/foo",
|
|
384
385
|
"prisma-cli project env remove STRIPE_KEY --role preview"
|
|
@@ -425,6 +426,7 @@ const DESCRIPTORS = [
|
|
|
425
426
|
],
|
|
426
427
|
description: "List environment variable metadata for a scope (no values).",
|
|
427
428
|
examples: [
|
|
429
|
+
"prisma-cli project env list",
|
|
428
430
|
"prisma-cli project env list --role production",
|
|
429
431
|
"prisma-cli project env list --role preview",
|
|
430
432
|
"prisma-cli project env list --branch feature/foo"
|