@prisma/cli 3.0.0-beta.3 → 3.0.0-beta.5
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 +4 -2
- package/dist/commands/app/index.js +22 -10
- package/dist/commands/branch/index.js +2 -27
- package/dist/commands/env.js +8 -4
- package/dist/controllers/app-env-api.js +54 -0
- package/dist/controllers/app-env-file.js +179 -0
- package/dist/controllers/app-env.js +200 -86
- package/dist/controllers/app.js +51 -53
- package/dist/controllers/branch.js +73 -47
- package/dist/lib/app/branch-database-deploy.js +323 -0
- package/dist/lib/app/branch-database.js +316 -0
- package/dist/lib/app/env-config.js +1 -1
- package/dist/lib/app/env-file.js +82 -0
- package/dist/lib/app/preview-branch-database.js +102 -0
- package/dist/lib/app/preview-provider.js +45 -6
- package/dist/lib/app/production-deploy-gate.js +160 -0
- package/dist/lib/git/local-branch.js +41 -0
- package/dist/lib/project/resolution.js +49 -4
- package/dist/presenters/app-env.js +44 -3
- package/dist/presenters/app.js +25 -0
- package/dist/presenters/branch.js +29 -101
- package/dist/presenters/project.js +15 -16
- package/dist/shell/command-meta.js +12 -24
- package/dist/shell/ui.js +4 -1
- package/dist/use-cases/branch.js +20 -68
- package/dist/use-cases/create-cli-gateways.js +2 -17
- package/package.json +2 -1
|
@@ -1,38 +1,38 @@
|
|
|
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";
|
|
8
|
+
import { readEnvFileAssignments } from "../lib/app/env-file.js";
|
|
9
|
+
import { apiCallError, findVariableByNaturalKey, toMetadata } from "./app-env-api.js";
|
|
10
|
+
import { runEnvAddFile, runEnvUpdateFile } from "./app-env-file.js";
|
|
7
11
|
//#region src/controllers/app-env.ts
|
|
8
|
-
function defaultRoleScope() {
|
|
9
|
-
return {
|
|
10
|
-
kind: "role",
|
|
11
|
-
role: "production"
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
12
|
async function runEnvAdd(context, rawAssignment, flags) {
|
|
15
|
-
const
|
|
13
|
+
const source = resolveEnvWriteSource(rawAssignment, flags.filePath, "add");
|
|
16
14
|
const scope = resolveEnvScope(flags, {
|
|
17
15
|
requireExplicit: true,
|
|
18
16
|
command: "add"
|
|
19
17
|
});
|
|
20
|
-
if (!scope) throw usageError(`prisma-cli project env add requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [
|
|
18
|
+
if (!scope) throw usageError(`prisma-cli project env add requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", ["prisma-cli project env add KEY=value --role production"], "app");
|
|
19
|
+
const input = await resolveEnvWriteInput(context, source, "add");
|
|
21
20
|
const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env add");
|
|
22
21
|
const resolved = await resolveScopeToApi(client, projectId, scope, {
|
|
23
22
|
createBranchIfMissing: true,
|
|
24
23
|
signal: context.runtime.signal
|
|
25
24
|
});
|
|
26
|
-
if (
|
|
25
|
+
if (input.kind === "file") return runEnvAddFile(context, client, projectId, resolved, input.filePath, input.assignments);
|
|
26
|
+
if (await findVariableByNaturalKey(client, projectId, input.key, resolved, context.runtime.signal)) throw new CliError({
|
|
27
27
|
code: "ENV_VARIABLE_ALREADY_EXISTS",
|
|
28
28
|
domain: "app",
|
|
29
|
-
summary: `Variable "${key}" already exists in ${formatScopeLabel(scope)}`,
|
|
29
|
+
summary: `Variable "${input.key}" already exists in ${formatScopeLabel(scope)}`,
|
|
30
30
|
why: "A variable with this key already exists in the targeted scope.",
|
|
31
31
|
fix: "Use `prisma-cli project env update` to change an existing variable's value.",
|
|
32
32
|
exitCode: 1,
|
|
33
|
-
nextSteps: [`prisma-cli project env update ${key}=<new-value> ${formatScopeFlag(scope)}`]
|
|
33
|
+
nextSteps: [`prisma-cli project env update ${input.key}=<new-value> ${formatScopeFlag(scope)}`]
|
|
34
34
|
});
|
|
35
|
-
const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(client, projectId, key, {
|
|
35
|
+
const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(client, projectId, input.key, {
|
|
36
36
|
scope: {
|
|
37
37
|
kind: "role",
|
|
38
38
|
role: "preview"
|
|
@@ -45,18 +45,18 @@ async function runEnvAdd(context, rawAssignment, flags) {
|
|
|
45
45
|
class: "preview",
|
|
46
46
|
branchId: null
|
|
47
47
|
}
|
|
48
|
-
}, context.runtime.signal) ? [`Variable "${key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
|
|
48
|
+
}, context.runtime.signal) ? [`Variable "${input.key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
|
|
49
49
|
const { data, error, response } = await client.POST("/v1/environment-variables", {
|
|
50
50
|
body: {
|
|
51
51
|
projectId,
|
|
52
52
|
class: resolved.apiTarget.class,
|
|
53
53
|
...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
|
|
54
|
-
key,
|
|
55
|
-
value
|
|
54
|
+
key: input.key,
|
|
55
|
+
value: input.value
|
|
56
56
|
},
|
|
57
57
|
signal: context.runtime.signal
|
|
58
58
|
});
|
|
59
|
-
if (error || !data) throw apiCallError(`Failed to add ${key}`, response, error);
|
|
59
|
+
if (error || !data) throw apiCallError(`Failed to add ${input.key}`, response, error);
|
|
60
60
|
return {
|
|
61
61
|
command: "project.env.add",
|
|
62
62
|
result: {
|
|
@@ -69,33 +69,35 @@ async function runEnvAdd(context, rawAssignment, flags) {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
async function runEnvUpdate(context, rawAssignment, flags) {
|
|
72
|
-
const
|
|
72
|
+
const source = resolveEnvWriteSource(rawAssignment, flags.filePath, "update");
|
|
73
73
|
const scope = resolveEnvScope(flags, {
|
|
74
74
|
requireExplicit: true,
|
|
75
75
|
command: "update"
|
|
76
76
|
});
|
|
77
|
-
if (!scope) throw usageError(`prisma-cli project env update requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [
|
|
77
|
+
if (!scope) throw usageError(`prisma-cli project env update requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", ["prisma-cli project env update KEY=value --role production"], "app");
|
|
78
|
+
const input = await resolveEnvWriteInput(context, source, "update");
|
|
78
79
|
const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env update");
|
|
79
80
|
const resolved = await resolveScopeToApi(client, projectId, scope, {
|
|
80
81
|
createBranchIfMissing: false,
|
|
81
82
|
signal: context.runtime.signal
|
|
82
83
|
});
|
|
83
|
-
|
|
84
|
+
if (input.kind === "file") return runEnvUpdateFile(context, client, projectId, resolved, input.filePath, input.assignments);
|
|
85
|
+
const existing = await findVariableByNaturalKey(client, projectId, input.key, resolved, context.runtime.signal);
|
|
84
86
|
if (!existing) throw new CliError({
|
|
85
87
|
code: "ENV_VARIABLE_NOT_FOUND",
|
|
86
88
|
domain: "app",
|
|
87
|
-
summary: `Variable "${key}" not found in ${formatScopeLabel(scope)}`,
|
|
89
|
+
summary: `Variable "${input.key}" not found in ${formatScopeLabel(scope)}`,
|
|
88
90
|
why: "No variable with this key exists in the targeted scope.",
|
|
89
91
|
fix: "Use `prisma-cli project env add` to create a new variable.",
|
|
90
92
|
exitCode: 1,
|
|
91
|
-
nextSteps: [`prisma-cli project env add ${key}=<value> ${formatScopeFlag(scope)}`]
|
|
93
|
+
nextSteps: [`prisma-cli project env add ${input.key}=<value> ${formatScopeFlag(scope)}`]
|
|
92
94
|
});
|
|
93
95
|
const { data, error, response } = await client.PATCH("/v1/environment-variables/{envVarId}", {
|
|
94
96
|
params: { path: { envVarId: existing.id } },
|
|
95
|
-
body: { value },
|
|
97
|
+
body: { value: input.value },
|
|
96
98
|
signal: context.runtime.signal
|
|
97
99
|
});
|
|
98
|
-
if (error || !data) throw apiCallError(`Failed to update value for ${key}`, response, error);
|
|
100
|
+
if (error || !data) throw apiCallError(`Failed to update value for ${input.key}`, response, error);
|
|
99
101
|
return {
|
|
100
102
|
command: "project.env.update",
|
|
101
103
|
result: {
|
|
@@ -107,26 +109,57 @@ async function runEnvUpdate(context, rawAssignment, flags) {
|
|
|
107
109
|
nextSteps: []
|
|
108
110
|
};
|
|
109
111
|
}
|
|
112
|
+
function resolveEnvWriteSource(rawAssignment, filePath, command) {
|
|
113
|
+
if (filePath !== void 0 && rawAssignment !== void 0) throw usageError(`prisma-cli project env ${command} accepts either KEY=VALUE or --file`, "The command received both a positional assignment and a dotenv file path.", "Pass one input source.", [`prisma-cli project env ${command} KEY=value --role preview`, `prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
114
|
+
if (filePath !== void 0) {
|
|
115
|
+
if (filePath.length === 0) throw usageError(`prisma-cli project env ${command} --file requires a path`, "The --file flag was passed without a file path.", "Pass a readable dotenv file path.", [`prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
116
|
+
return {
|
|
117
|
+
kind: "file",
|
|
118
|
+
filePath
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (rawAssignment === void 0) throw usageError(`prisma-cli project env ${command} requires KEY=VALUE or --file`, "No environment variable input was supplied.", "Pass a single KEY=VALUE assignment or a dotenv file path.", [`prisma-cli project env ${command} KEY=value --role preview`, `prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
122
|
+
return {
|
|
123
|
+
kind: "single",
|
|
124
|
+
rawAssignment
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function resolveEnvWriteInput(context, source, command) {
|
|
128
|
+
if (source.kind === "file") return {
|
|
129
|
+
kind: "file",
|
|
130
|
+
filePath: source.filePath,
|
|
131
|
+
assignments: await readEnvFileAssignments(context.runtime.cwd, source.filePath, command)
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
kind: "single",
|
|
135
|
+
...parseKeyValuePositional(source.rawAssignment, command, context.runtime.env)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
110
138
|
async function runEnvList(context, flags) {
|
|
111
|
-
const
|
|
139
|
+
const explicit = resolveEnvScope(flags, {
|
|
112
140
|
requireExplicit: false,
|
|
113
141
|
command: "list"
|
|
114
|
-
})
|
|
142
|
+
});
|
|
115
143
|
const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env list");
|
|
116
|
-
const resolved = await
|
|
117
|
-
|
|
144
|
+
const resolved = await resolveListScopeToApi(client, projectId, explicit, {
|
|
145
|
+
cwd: context.runtime.cwd,
|
|
118
146
|
signal: context.runtime.signal
|
|
119
147
|
});
|
|
120
|
-
const variables = await listVariables(client, projectId,
|
|
148
|
+
const variables = resolved.kind === "scoped" ? await listVariables(client, projectId, {
|
|
149
|
+
scope: resolved.addScope,
|
|
150
|
+
descriptor: resolved.descriptor,
|
|
151
|
+
apiTarget: resolved.apiTarget
|
|
152
|
+
}, context.runtime.signal) : await listOverviewVariables(client, projectId, context.runtime.signal);
|
|
121
153
|
return {
|
|
122
154
|
command: "project.env.list",
|
|
123
155
|
result: {
|
|
124
156
|
projectId,
|
|
125
157
|
scope: resolved.descriptor,
|
|
158
|
+
target: resolved.target,
|
|
126
159
|
variables: variables.map((row) => toMetadata(row, resolved.descriptor))
|
|
127
160
|
},
|
|
128
161
|
warnings: [],
|
|
129
|
-
nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value ${formatScopeFlag(
|
|
162
|
+
nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value ${formatScopeFlag(resolved.addScope)}`] : []
|
|
130
163
|
};
|
|
131
164
|
}
|
|
132
165
|
async function runEnvRemove(context, key, flags) {
|
|
@@ -196,12 +229,12 @@ async function resolveScopeToApi(client, projectId, scope, options) {
|
|
|
196
229
|
}
|
|
197
230
|
};
|
|
198
231
|
const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName, options.signal) : await resolveExistingBranch(client, projectId, scope.branchName, options.signal);
|
|
199
|
-
if (branch.
|
|
232
|
+
if (branch.role === "production") throw new CliError({
|
|
200
233
|
code: "ENV_BRANCH_SCOPE_IS_PRODUCTION",
|
|
201
234
|
domain: "app",
|
|
202
|
-
summary: `Branch "${scope.branchName}" is the
|
|
235
|
+
summary: `Branch "${scope.branchName}" is the production branch`,
|
|
203
236
|
why: "Production variables are project-level only; branch overrides apply to preview branches.",
|
|
204
|
-
fix: "Use --role production for the
|
|
237
|
+
fix: "Use --role production for the production branch.",
|
|
205
238
|
exitCode: 1,
|
|
206
239
|
nextSteps: ["prisma-cli project env list --role production"]
|
|
207
240
|
});
|
|
@@ -218,6 +251,123 @@ async function resolveScopeToApi(client, projectId, scope, options) {
|
|
|
218
251
|
}
|
|
219
252
|
};
|
|
220
253
|
}
|
|
254
|
+
async function resolveListScopeToApi(client, projectId, explicit, options) {
|
|
255
|
+
if (explicit) {
|
|
256
|
+
const resolved = await resolveScopeToApi(client, projectId, explicit, {
|
|
257
|
+
createBranchIfMissing: false,
|
|
258
|
+
signal: options.signal
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
kind: "scoped",
|
|
262
|
+
descriptor: resolved.descriptor,
|
|
263
|
+
target: targetFromExplicitScope(resolved.descriptor),
|
|
264
|
+
apiTarget: resolved.apiTarget,
|
|
265
|
+
addScope: resolved.scope
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const gitBranch = await readLocalGitBranch(options.cwd, options.signal);
|
|
269
|
+
if (gitBranch) {
|
|
270
|
+
const branch = (await listBranchesByName(client, projectId, gitBranch, options.signal))[0];
|
|
271
|
+
if (!branch) return {
|
|
272
|
+
kind: "scoped",
|
|
273
|
+
descriptor: {
|
|
274
|
+
kind: "role",
|
|
275
|
+
role: "preview"
|
|
276
|
+
},
|
|
277
|
+
target: {
|
|
278
|
+
source: "local-git",
|
|
279
|
+
branchName: gitBranch,
|
|
280
|
+
branchExists: false,
|
|
281
|
+
envMap: "preview"
|
|
282
|
+
},
|
|
283
|
+
apiTarget: {
|
|
284
|
+
class: "preview",
|
|
285
|
+
branchId: null
|
|
286
|
+
},
|
|
287
|
+
addScope: {
|
|
288
|
+
kind: "branch",
|
|
289
|
+
branchName: gitBranch
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
if (branch.role === "production") return {
|
|
293
|
+
kind: "scoped",
|
|
294
|
+
descriptor: {
|
|
295
|
+
kind: "role",
|
|
296
|
+
role: "production"
|
|
297
|
+
},
|
|
298
|
+
target: {
|
|
299
|
+
source: "local-git",
|
|
300
|
+
branchName: branch.gitName,
|
|
301
|
+
branchId: branch.id,
|
|
302
|
+
branchRole: branch.role,
|
|
303
|
+
branchExists: true,
|
|
304
|
+
envMap: "production"
|
|
305
|
+
},
|
|
306
|
+
apiTarget: {
|
|
307
|
+
class: "production",
|
|
308
|
+
branchId: null
|
|
309
|
+
},
|
|
310
|
+
addScope: {
|
|
311
|
+
kind: "role",
|
|
312
|
+
role: "production"
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
return {
|
|
316
|
+
kind: "scoped",
|
|
317
|
+
descriptor: {
|
|
318
|
+
kind: "branch",
|
|
319
|
+
branchName: branch.gitName,
|
|
320
|
+
branchId: branch.id
|
|
321
|
+
},
|
|
322
|
+
target: {
|
|
323
|
+
source: "local-git",
|
|
324
|
+
branchName: branch.gitName,
|
|
325
|
+
branchId: branch.id,
|
|
326
|
+
branchRole: branch.role,
|
|
327
|
+
branchExists: true,
|
|
328
|
+
envMap: "preview"
|
|
329
|
+
},
|
|
330
|
+
apiTarget: {
|
|
331
|
+
class: "preview",
|
|
332
|
+
branchId: branch.id
|
|
333
|
+
},
|
|
334
|
+
addScope: {
|
|
335
|
+
kind: "branch",
|
|
336
|
+
branchName: branch.gitName
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
kind: "overview",
|
|
342
|
+
descriptor: { kind: "overview" },
|
|
343
|
+
target: {
|
|
344
|
+
source: "overview",
|
|
345
|
+
envMap: "overview"
|
|
346
|
+
},
|
|
347
|
+
addScope: {
|
|
348
|
+
kind: "role",
|
|
349
|
+
role: "preview"
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function targetFromExplicitScope(scope) {
|
|
354
|
+
if (scope.kind === "branch") return {
|
|
355
|
+
source: "explicit",
|
|
356
|
+
branchName: scope.branchName,
|
|
357
|
+
branchId: scope.branchId,
|
|
358
|
+
branchRole: "preview",
|
|
359
|
+
branchExists: true,
|
|
360
|
+
envMap: "preview"
|
|
361
|
+
};
|
|
362
|
+
if (scope.kind === "role") return {
|
|
363
|
+
source: "explicit",
|
|
364
|
+
envMap: scope.role
|
|
365
|
+
};
|
|
366
|
+
return {
|
|
367
|
+
source: "overview",
|
|
368
|
+
envMap: "overview"
|
|
369
|
+
};
|
|
370
|
+
}
|
|
221
371
|
function formatScopeFlag(scope) {
|
|
222
372
|
if (scope.kind === "role") return `--role ${scope.role}`;
|
|
223
373
|
return `--branch ${scope.branchName}`;
|
|
@@ -293,47 +443,45 @@ async function projectHasDefaultBranch(client, projectId, signal) {
|
|
|
293
443
|
cursor = result.data.pagination.nextCursor;
|
|
294
444
|
}
|
|
295
445
|
}
|
|
296
|
-
async function
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
446
|
+
async function listVariables(client, projectId, resolved, signal) {
|
|
447
|
+
return materializeEffectiveRows(await collectEnvironmentVariables(client, projectId, signal, {
|
|
448
|
+
className: resolved.apiTarget.class,
|
|
449
|
+
filter: (row) => rowMatchesScope(row, resolved)
|
|
450
|
+
}), resolved);
|
|
451
|
+
}
|
|
452
|
+
async function listOverviewVariables(client, projectId, signal) {
|
|
453
|
+
return (await collectEnvironmentVariables(client, projectId, signal, { filter: (row) => row.branchId === null && (row.class === "production" || row.class === "preview") })).sort((left, right) => {
|
|
454
|
+
const roleOrder = roleSortOrder(left.class) - roleSortOrder(right.class);
|
|
455
|
+
return roleOrder !== 0 ? roleOrder : left.key.localeCompare(right.key);
|
|
304
456
|
});
|
|
305
|
-
if (error || !data) throw apiCallError(`Failed to look up ${key}`, response, error);
|
|
306
|
-
return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
|
|
307
457
|
}
|
|
308
|
-
async function
|
|
458
|
+
async function collectEnvironmentVariables(client, projectId, signal, options) {
|
|
309
459
|
const collected = [];
|
|
310
460
|
let cursor;
|
|
311
461
|
while (true) {
|
|
312
|
-
const query = {
|
|
313
|
-
|
|
314
|
-
class: resolved.apiTarget.class
|
|
315
|
-
};
|
|
462
|
+
const query = { projectId };
|
|
463
|
+
if (options.className !== void 0) query.class = options.className;
|
|
316
464
|
if (cursor !== void 0) query.cursor = cursor;
|
|
317
465
|
const result = await client.GET("/v1/environment-variables", {
|
|
318
466
|
params: { query },
|
|
319
467
|
signal
|
|
320
468
|
});
|
|
321
469
|
if (result.error || !result.data) throw apiCallError(`Failed to list environment variables`, result.response, result.error);
|
|
322
|
-
const page = result.data.data.filter(
|
|
470
|
+
const page = result.data.data.filter(options.filter);
|
|
323
471
|
collected.push(...page);
|
|
324
472
|
if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
|
|
325
473
|
cursor = result.data.pagination.nextCursor;
|
|
326
474
|
}
|
|
327
|
-
return
|
|
475
|
+
return collected;
|
|
476
|
+
}
|
|
477
|
+
function roleSortOrder(role) {
|
|
478
|
+
return role === "production" ? 0 : 1;
|
|
328
479
|
}
|
|
329
480
|
function rowMatchesScope(row, resolved) {
|
|
330
481
|
if (row.class !== resolved.apiTarget.class) return false;
|
|
331
482
|
if (resolved.apiTarget.branchId === null) return row.branchId === null;
|
|
332
483
|
return row.branchId === null || row.branchId === resolved.apiTarget.branchId;
|
|
333
484
|
}
|
|
334
|
-
function rowMatchesExactScope(row, resolved) {
|
|
335
|
-
return row.class === resolved.apiTarget.class && row.branchId === resolved.apiTarget.branchId;
|
|
336
|
-
}
|
|
337
485
|
function materializeEffectiveRows(rows, resolved) {
|
|
338
486
|
if (resolved.apiTarget.branchId === null) return rows;
|
|
339
487
|
const byKey = /* @__PURE__ */ new Map();
|
|
@@ -341,39 +489,5 @@ function materializeEffectiveRows(rows, resolved) {
|
|
|
341
489
|
for (const row of rows) if (row.branchId === resolved.apiTarget.branchId) byKey.set(row.key, row);
|
|
342
490
|
return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
|
|
343
491
|
}
|
|
344
|
-
function toMetadata(row, requestedScope) {
|
|
345
|
-
const rowScope = row.branchId === null ? {
|
|
346
|
-
kind: "role",
|
|
347
|
-
role: row.class
|
|
348
|
-
} : requestedScope;
|
|
349
|
-
return {
|
|
350
|
-
id: row.id,
|
|
351
|
-
key: row.key,
|
|
352
|
-
scope: rowScope,
|
|
353
|
-
source: formatDescriptorLabel(rowScope),
|
|
354
|
-
isManagedBySystem: row.isManagedBySystem,
|
|
355
|
-
updatedAt: row.updatedAt
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
function formatDescriptorLabel(scope) {
|
|
359
|
-
if (scope.kind === "role") return scope.role ?? "unknown";
|
|
360
|
-
return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
|
|
361
|
-
}
|
|
362
|
-
function apiCallError(summary, response, error) {
|
|
363
|
-
const status = response?.status ?? 0;
|
|
364
|
-
const apiCode = error?.error?.code;
|
|
365
|
-
const apiMessage = error?.error?.message;
|
|
366
|
-
const apiHint = error?.error?.hint;
|
|
367
|
-
if (status === 401 || status === 403) return authRequiredError(["prisma auth login"]);
|
|
368
|
-
return new CliError({
|
|
369
|
-
code: apiCode ?? "ENV_API_ERROR",
|
|
370
|
-
domain: "app",
|
|
371
|
-
summary,
|
|
372
|
-
why: apiMessage ?? `The Management API returned status ${status || "unknown"}.`,
|
|
373
|
-
fix: apiHint ?? "Re-run with --trace for the underlying API response details.",
|
|
374
|
-
exitCode: 1,
|
|
375
|
-
nextSteps: []
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
492
|
//#endregion
|
|
379
493
|
export { runEnvAdd, runEnvList, runEnvRemove, runEnvUpdate };
|
package/dist/controllers/app.js
CHANGED
|
@@ -16,10 +16,13 @@ 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";
|
|
22
|
+
import { maybeSetupBranchDatabase } from "../lib/app/branch-database-deploy.js";
|
|
21
23
|
import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress } from "../lib/app/preview-progress.js";
|
|
22
24
|
import { PreviewDomainApiError, createPreviewAppProvider } from "../lib/app/preview-provider.js";
|
|
25
|
+
import { enforceProductionDeployGate } from "../lib/app/production-deploy-gate.js";
|
|
23
26
|
import { formatDomainFailureFix } from "../lib/app/domain-guidance.js";
|
|
24
27
|
import { createSelectPromptPort } from "./select-prompt-port.js";
|
|
25
28
|
import { requireAuthenticatedAuthState } from "./auth.js";
|
|
@@ -158,10 +161,20 @@ async function runAppDeploy(context, appName, options) {
|
|
|
158
161
|
});
|
|
159
162
|
framework = customized.framework;
|
|
160
163
|
runtime = customized.runtime;
|
|
164
|
+
await enforceProductionDeployGate(context, provider, {
|
|
165
|
+
appId: selectedApp.appId,
|
|
166
|
+
appName: selectedApp.displayName,
|
|
167
|
+
branchKind: target.branch.kind,
|
|
168
|
+
prod: options?.prod === true
|
|
169
|
+
});
|
|
161
170
|
const buildType = framework.buildType;
|
|
162
171
|
assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
|
|
163
172
|
const entrypoint = await resolveDeployEntrypoint(context.runtime.cwd, framework, options?.entrypoint, context.runtime.signal);
|
|
164
173
|
const portMapping = parseDeployPortMapping(String(runtime.port));
|
|
174
|
+
const branchDatabaseSetup = await maybeSetupBranchDatabase(context, provider, projectId, toBranchDatabaseDeployBranch(target.branch), {
|
|
175
|
+
db: options?.db,
|
|
176
|
+
inlineEnvVars: envVars
|
|
177
|
+
});
|
|
165
178
|
const progressState = createPreviewDeployProgressState();
|
|
166
179
|
const deployStartedAt = Date.now();
|
|
167
180
|
const deployResult = await provider.deployApp({
|
|
@@ -192,8 +205,9 @@ async function runAppDeploy(context, appName, options) {
|
|
|
192
205
|
result: {
|
|
193
206
|
workspace: target.workspace,
|
|
194
207
|
project: target.project,
|
|
195
|
-
branch: target.branch,
|
|
208
|
+
branch: toResultBranch(target.branch),
|
|
196
209
|
resolution: target.resolution,
|
|
210
|
+
branchDatabase: branchDatabaseSetup.result,
|
|
197
211
|
app: {
|
|
198
212
|
id: deployResult.app.id,
|
|
199
213
|
name: deployResult.app.name
|
|
@@ -202,7 +216,7 @@ async function runAppDeploy(context, appName, options) {
|
|
|
202
216
|
durationMs: deployDurationMs,
|
|
203
217
|
localPin: localPinResult
|
|
204
218
|
},
|
|
205
|
-
warnings:
|
|
219
|
+
warnings: branchDatabaseSetup.warnings,
|
|
206
220
|
nextSteps: ["prisma-cli app list-deploys", `prisma-cli app show-deploy ${deployResult.deployment.id}`]
|
|
207
221
|
};
|
|
208
222
|
}
|
|
@@ -749,7 +763,7 @@ async function resolveAppDomainTarget(context, options, commandName = "app domai
|
|
|
749
763
|
resultTarget: {
|
|
750
764
|
workspace: target.workspace,
|
|
751
765
|
project: target.project,
|
|
752
|
-
branch: target.branch,
|
|
766
|
+
branch: toResultBranch(target.branch),
|
|
753
767
|
app: {
|
|
754
768
|
id: selectedApp.id,
|
|
755
769
|
name: selectedApp.name
|
|
@@ -1312,6 +1326,7 @@ async function resolveProjectContext(context, client, explicitProject, options)
|
|
|
1312
1326
|
return {
|
|
1313
1327
|
...resolved,
|
|
1314
1328
|
branch: {
|
|
1329
|
+
id: null,
|
|
1315
1330
|
name: branch.name,
|
|
1316
1331
|
kind: toBranchKind(branch.name)
|
|
1317
1332
|
}
|
|
@@ -1322,7 +1337,7 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1322
1337
|
if (!workspace) throw workspaceRequiredError();
|
|
1323
1338
|
const branch = options.branch ?? await resolveDeployBranch(context, void 0);
|
|
1324
1339
|
const projects = await listRealWorkspaceProjects(client, workspace, context.runtime.signal);
|
|
1325
|
-
if (explicitProject) return
|
|
1340
|
+
if (explicitProject) return withRemoteDeployBranch(provider, {
|
|
1326
1341
|
workspace,
|
|
1327
1342
|
project: toProjectSummary(resolveProjectForSetup(explicitProject, projects, workspace)),
|
|
1328
1343
|
resolution: {
|
|
@@ -1331,11 +1346,11 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1331
1346
|
targetNameSource: "explicit"
|
|
1332
1347
|
},
|
|
1333
1348
|
localPinAction: "linked"
|
|
1334
|
-
}, branch);
|
|
1349
|
+
}, branch, context.runtime.signal);
|
|
1335
1350
|
if (options.createProjectName) {
|
|
1336
1351
|
const projectName = options.createProjectName.trim();
|
|
1337
1352
|
if (!projectName) throw projectSetupNameRequiredError("app deploy --create-project");
|
|
1338
|
-
return
|
|
1353
|
+
return withRemoteDeployBranch(provider, {
|
|
1339
1354
|
workspace,
|
|
1340
1355
|
project: toProjectSummary(await createProjectForDeploySetup(provider, projectName, workspace, context.runtime.signal)),
|
|
1341
1356
|
resolution: {
|
|
@@ -1344,12 +1359,12 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1344
1359
|
targetNameSource: "explicit"
|
|
1345
1360
|
},
|
|
1346
1361
|
localPinAction: "created"
|
|
1347
|
-
}, branch);
|
|
1362
|
+
}, branch, context.runtime.signal);
|
|
1348
1363
|
}
|
|
1349
1364
|
if (options.envProjectId) {
|
|
1350
1365
|
const project = projects.find((candidate) => candidate.id === options.envProjectId);
|
|
1351
1366
|
if (!project) throw projectNotFoundError(options.envProjectId, workspace);
|
|
1352
|
-
return
|
|
1367
|
+
return withRemoteDeployBranch(provider, {
|
|
1353
1368
|
workspace,
|
|
1354
1369
|
project: toProjectSummary(project),
|
|
1355
1370
|
resolution: {
|
|
@@ -1357,14 +1372,14 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1357
1372
|
targetName: options.envProjectId,
|
|
1358
1373
|
targetNameSource: "env"
|
|
1359
1374
|
}
|
|
1360
|
-
}, branch);
|
|
1375
|
+
}, branch, context.runtime.signal);
|
|
1361
1376
|
}
|
|
1362
1377
|
const localPin = options.localPin;
|
|
1363
1378
|
if (localPin.kind === "present") {
|
|
1364
1379
|
if (localPin.pin.workspaceId !== workspace.id) throw localResolutionPinStaleError();
|
|
1365
1380
|
const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
|
|
1366
1381
|
if (!project) throw localResolutionPinStaleError();
|
|
1367
|
-
return
|
|
1382
|
+
return withRemoteDeployBranch(provider, {
|
|
1368
1383
|
workspace,
|
|
1369
1384
|
project: toProjectSummary(project),
|
|
1370
1385
|
resolution: {
|
|
@@ -1372,10 +1387,10 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1372
1387
|
targetName: project.name,
|
|
1373
1388
|
targetNameSource: "local-pin"
|
|
1374
1389
|
}
|
|
1375
|
-
}, branch);
|
|
1390
|
+
}, branch, context.runtime.signal);
|
|
1376
1391
|
}
|
|
1377
1392
|
const platformMapping = await resolveDurablePlatformMapping();
|
|
1378
|
-
if (platformMapping && platformMapping.workspace.id === workspace.id) return
|
|
1393
|
+
if (platformMapping && platformMapping.workspace.id === workspace.id) return withRemoteDeployBranch(provider, {
|
|
1379
1394
|
workspace,
|
|
1380
1395
|
project: toProjectSummary(platformMapping),
|
|
1381
1396
|
resolution: {
|
|
@@ -1383,8 +1398,8 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
|
|
|
1383
1398
|
targetName: platformMapping.name,
|
|
1384
1399
|
targetNameSource: "platform-mapping"
|
|
1385
1400
|
}
|
|
1386
|
-
}, branch);
|
|
1387
|
-
if (canPrompt(context) && !context.flags.yes) return
|
|
1401
|
+
}, branch, context.runtime.signal);
|
|
1402
|
+
if (canPrompt(context) && !context.flags.yes) return withRemoteDeployBranch(provider, await resolveInteractiveDeployProjectSetup(context, provider, workspace, projects), branch, context.runtime.signal);
|
|
1388
1403
|
throw projectSetupRequiredError(projects, await inferTargetName(context.runtime.cwd, context.runtime.signal));
|
|
1389
1404
|
}
|
|
1390
1405
|
async function resolveInteractiveDeployProjectSetup(context, provider, workspace, projects) {
|
|
@@ -1430,18 +1445,37 @@ async function createProjectForDeploySetup(provider, projectName, workspace, sig
|
|
|
1430
1445
|
workspace
|
|
1431
1446
|
};
|
|
1432
1447
|
}
|
|
1433
|
-
function
|
|
1448
|
+
async function withRemoteDeployBranch(provider, target, branch, signal) {
|
|
1449
|
+
const remoteBranch = await provider.resolveBranch(target.project.id, {
|
|
1450
|
+
branchName: branch.name,
|
|
1451
|
+
signal
|
|
1452
|
+
});
|
|
1434
1453
|
return {
|
|
1435
1454
|
...target,
|
|
1436
1455
|
branch: {
|
|
1437
|
-
|
|
1438
|
-
|
|
1456
|
+
id: remoteBranch.id,
|
|
1457
|
+
name: remoteBranch.name,
|
|
1458
|
+
kind: remoteBranch.role
|
|
1439
1459
|
}
|
|
1440
1460
|
};
|
|
1441
1461
|
}
|
|
1442
1462
|
function toBranchKind(name) {
|
|
1443
1463
|
return name === "production" || name === "main" ? "production" : "preview";
|
|
1444
1464
|
}
|
|
1465
|
+
function toResultBranch(branch) {
|
|
1466
|
+
return {
|
|
1467
|
+
name: branch.name,
|
|
1468
|
+
kind: branch.kind
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
function toBranchDatabaseDeployBranch(branch) {
|
|
1472
|
+
if (!branch.id) throw new Error(`Deploy branch "${branch.name}" was not resolved remotely.`);
|
|
1473
|
+
return {
|
|
1474
|
+
id: branch.id,
|
|
1475
|
+
name: branch.name,
|
|
1476
|
+
kind: branch.kind
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1445
1479
|
function assertExclusiveDeployProjectInputs(options) {
|
|
1446
1480
|
const provided = [
|
|
1447
1481
|
options.projectRef ? "--project" : null,
|
|
@@ -1470,42 +1504,6 @@ async function resolveDeployBranch(context, explicitBranchName) {
|
|
|
1470
1504
|
annotation: "default"
|
|
1471
1505
|
};
|
|
1472
1506
|
}
|
|
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
1507
|
async function resolveDeployFramework(context, options) {
|
|
1510
1508
|
if (options.requestedFramework) return frameworkFromUserFacingValue(options.requestedFramework, "set by --framework");
|
|
1511
1509
|
if (options.entrypoint) return {
|