@prisma/cli 3.0.0-beta.6 → 3.0.0-beta.8
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/dist/commands/app/index.js +2 -2
- package/dist/controllers/app-env.js +2 -2
- package/dist/controllers/app.js +39 -5
- package/dist/lib/app/branch-database-deploy.js +115 -66
- package/dist/lib/app/env-file.js +2 -2
- package/dist/lib/app/env-vars.js +28 -2
- package/dist/lib/app/preview-build-settings.js +385 -0
- package/dist/lib/app/preview-build.js +196 -33
- package/dist/lib/app/preview-provider.js +2 -1
- package/dist/lib/app/production-deploy-gate.js +5 -4
- package/dist/presenters/app.js +7 -2
- package/dist/shell/command-meta.js +1 -0
- package/package.json +2 -2
|
@@ -65,7 +65,7 @@ function createDeployCommand(runtime) {
|
|
|
65
65
|
"hono",
|
|
66
66
|
"tanstack-start",
|
|
67
67
|
"bun"
|
|
68
|
-
])).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("--db", "Create and wire
|
|
68
|
+
])).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|file>", "Environment variable assignment or dotenv file").argParser(collectRepeatableValues)).addOption(new Option("--db", "Create and wire a Prisma Postgres database for this deploy target")).addOption(new Option("--no-db", "Skip database setup")).addOption(new Option("--prod", "Confirm intent to deploy to production"));
|
|
69
69
|
addGlobalFlags(command);
|
|
70
70
|
command.action(async (options) => {
|
|
71
71
|
const appName = options.app;
|
|
@@ -80,7 +80,7 @@ function createDeployCommand(runtime) {
|
|
|
80
80
|
const db = options.db;
|
|
81
81
|
const hasDbConflict = hasFlag(runtime.argv, "--db") && hasFlag(runtime.argv, "--no-db");
|
|
82
82
|
await runCommand(runtime, "app.deploy", options, (context) => {
|
|
83
|
-
if (hasDbConflict) throw usageError("app deploy accepts either --db or --no-db", "--db requests
|
|
83
|
+
if (hasDbConflict) throw usageError("app deploy accepts either --db or --no-db", "--db requests database setup, while --no-db disables it.", "Pass exactly one database setup flag.", ["prisma-cli app deploy --db", "prisma-cli app deploy --no-db"], "app");
|
|
84
84
|
return runAppDeploy(context, appName, {
|
|
85
85
|
projectRef,
|
|
86
86
|
createProjectName,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
|
|
2
2
|
import { requireComputeAuth } from "../lib/auth/guard.js";
|
|
3
|
+
import { formatScopeLabel, parseKeyValuePositional, resolveEnvScope } from "../lib/app/env-config.js";
|
|
4
|
+
import { readEnvFileAssignments } from "../lib/app/env-file.js";
|
|
3
5
|
import { resolveProjectTarget } from "../lib/project/resolution.js";
|
|
4
6
|
import { readLocalGitBranch } from "../lib/git/local-branch.js";
|
|
5
7
|
import { requireAuthenticatedAuthState } from "./auth.js";
|
|
6
8
|
import { listRealWorkspaceProjects } from "./project.js";
|
|
7
|
-
import { formatScopeLabel, parseKeyValuePositional, resolveEnvScope } from "../lib/app/env-config.js";
|
|
8
|
-
import { readEnvFileAssignments } from "../lib/app/env-file.js";
|
|
9
9
|
import { apiCallError, findVariableByNaturalKey, toMetadata } from "./app-env-api.js";
|
|
10
10
|
import { runEnvAddFile, runEnvUpdateFile } from "./app-env-file.js";
|
|
11
11
|
//#region src/controllers/app-env.ts
|
package/dist/controllers/app.js
CHANGED
|
@@ -7,7 +7,7 @@ import { canPrompt } from "../shell/runtime.js";
|
|
|
7
7
|
import { confirmPrompt, selectPrompt, textPrompt } from "../shell/prompt.js";
|
|
8
8
|
import { requireComputeAuth } from "../lib/auth/guard.js";
|
|
9
9
|
import { readAuthState } from "../lib/auth/auth-ops.js";
|
|
10
|
-
import { envVarNames,
|
|
10
|
+
import { envVarNames, parseEnvInputs } from "../lib/app/env-vars.js";
|
|
11
11
|
import { renderDeployOutputRows, renderDeploySettingsPreview } from "../lib/app/deploy-output.js";
|
|
12
12
|
import { readBunPackageEntrypoint, readBunPackageJson } from "../lib/app/bun-project.js";
|
|
13
13
|
import { DEFAULT_LOCAL_DEV_PORT, resolveLocalBuildType, runLocalApp } from "../lib/app/local-dev.js";
|
|
@@ -17,6 +17,7 @@ import { buildProjectSetupNextActions, inferTargetName, projectNotFoundError, re
|
|
|
17
17
|
import { bindProjectToDirectory, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
|
|
18
18
|
import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
|
|
19
19
|
import { readLocalGitBranch } from "../lib/git/local-branch.js";
|
|
20
|
+
import { resolveOrCreatePreviewBuildSettings } from "../lib/app/preview-build-settings.js";
|
|
20
21
|
import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild } from "../lib/app/preview-build.js";
|
|
21
22
|
import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
|
|
22
23
|
import { maybeSetupBranchDatabase } from "../lib/app/branch-database-deploy.js";
|
|
@@ -138,7 +139,7 @@ async function runAppDeploy(context, appName, options) {
|
|
|
138
139
|
});
|
|
139
140
|
let runtime = resolveDeployRuntime(options?.httpPort, framework);
|
|
140
141
|
assertSupportedEntrypoint(framework.buildType, options?.entrypoint, "deploy");
|
|
141
|
-
const envVars = toOptionalEnvVars(
|
|
142
|
+
const envVars = toOptionalEnvVars(await parseEnvInputs(context.runtime.cwd, options?.envAssignments, { commandName: "deploy" }));
|
|
142
143
|
const selectedApp = await resolveDeployAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), {
|
|
143
144
|
explicitAppName: appName,
|
|
144
145
|
explicitAppId: envAppId,
|
|
@@ -161,7 +162,7 @@ async function runAppDeploy(context, appName, options) {
|
|
|
161
162
|
});
|
|
162
163
|
framework = customized.framework;
|
|
163
164
|
runtime = customized.runtime;
|
|
164
|
-
await enforceProductionDeployGate(context, provider, {
|
|
165
|
+
const productionDeployGate = await enforceProductionDeployGate(context, provider, {
|
|
165
166
|
appId: selectedApp.appId,
|
|
166
167
|
appName: selectedApp.displayName,
|
|
167
168
|
branchKind: target.branch.kind,
|
|
@@ -170,10 +171,17 @@ async function runAppDeploy(context, appName, options) {
|
|
|
170
171
|
const buildType = framework.buildType;
|
|
171
172
|
assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
|
|
172
173
|
const entrypoint = await resolveDeployEntrypoint(context.runtime.cwd, framework, options?.entrypoint, context.runtime.signal);
|
|
174
|
+
const buildSettingsResolution = await resolveOrCreatePreviewBuildSettings({
|
|
175
|
+
appPath: context.runtime.cwd,
|
|
176
|
+
buildType,
|
|
177
|
+
signal: context.runtime.signal
|
|
178
|
+
});
|
|
179
|
+
maybeRenderDeployBuildSettings(context, buildSettingsResolution);
|
|
173
180
|
const portMapping = parseDeployPortMapping(String(runtime.port));
|
|
174
181
|
const branchDatabaseSetup = await maybeSetupBranchDatabase(context, provider, projectId, toBranchDatabaseDeployBranch(target.branch), {
|
|
175
182
|
db: options?.db,
|
|
176
|
-
|
|
183
|
+
providedEnvVars: envVars,
|
|
184
|
+
firstProductionDeploy: productionDeployGate.firstProductionDeploy
|
|
177
185
|
});
|
|
178
186
|
const progressState = createPreviewDeployProgressState();
|
|
179
187
|
const deployStartedAt = Date.now();
|
|
@@ -186,6 +194,7 @@ async function runAppDeploy(context, appName, options) {
|
|
|
186
194
|
region: selectedApp.region,
|
|
187
195
|
entrypoint,
|
|
188
196
|
buildType,
|
|
197
|
+
buildSettings: buildSettingsResolution.settings,
|
|
189
198
|
portMapping,
|
|
190
199
|
envVars,
|
|
191
200
|
interaction: void 0,
|
|
@@ -214,6 +223,18 @@ async function runAppDeploy(context, appName, options) {
|
|
|
214
223
|
},
|
|
215
224
|
deployment: deployResult.deployment,
|
|
216
225
|
deploySettings: {
|
|
226
|
+
config: {
|
|
227
|
+
path: buildSettingsResolution.relativeConfigPath,
|
|
228
|
+
status: buildSettingsResolution.status
|
|
229
|
+
},
|
|
230
|
+
buildCommand: {
|
|
231
|
+
value: buildSettingsResolution.settings.buildCommand,
|
|
232
|
+
source: buildSettingsResolution.settings.buildCommandSource
|
|
233
|
+
},
|
|
234
|
+
outputDirectory: {
|
|
235
|
+
value: buildSettingsResolution.settings.outputDirectory,
|
|
236
|
+
source: buildSettingsResolution.settings.outputDirectorySource
|
|
237
|
+
},
|
|
217
238
|
framework: {
|
|
218
239
|
key: framework.key,
|
|
219
240
|
buildType,
|
|
@@ -1603,7 +1624,6 @@ async function detectNextConfig(cwd, signal) {
|
|
|
1603
1624
|
for (const candidate of [
|
|
1604
1625
|
"next.config.js",
|
|
1605
1626
|
"next.config.mjs",
|
|
1606
|
-
"next.config.cjs",
|
|
1607
1627
|
"next.config.ts",
|
|
1608
1628
|
"next.config.mts"
|
|
1609
1629
|
]) {
|
|
@@ -1696,6 +1716,20 @@ async function maybeRenderDeploySetupBlock(context, details) {
|
|
|
1696
1716
|
const prefix = details.includeDirectory ? `Deploying ${directory} to` : "Deploying to";
|
|
1697
1717
|
context.output.stderr.write(`${prefix} ${details.projectName} / ${details.branchName} / ${details.appName}\n\n`);
|
|
1698
1718
|
}
|
|
1719
|
+
function maybeRenderDeployBuildSettings(context, resolution) {
|
|
1720
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
1721
|
+
const settings = resolution.settings;
|
|
1722
|
+
const title = resolution.status === "created" ? `Created ${resolution.relativeConfigPath}` : `Using ${resolution.relativeConfigPath}`;
|
|
1723
|
+
context.output.stderr.write(`${title}\n${renderDeployOutputRows(context.ui, [{
|
|
1724
|
+
label: "Build Command",
|
|
1725
|
+
value: settings.buildCommand ?? "none",
|
|
1726
|
+
origin: settings.buildCommandSource ?? void 0
|
|
1727
|
+
}, {
|
|
1728
|
+
label: "Output Directory",
|
|
1729
|
+
value: settings.outputDirectory,
|
|
1730
|
+
origin: settings.outputDirectorySource ?? void 0
|
|
1731
|
+
}]).join("\n")}\n\n`);
|
|
1732
|
+
}
|
|
1699
1733
|
function maybeRenderProjectLinked(context, directory, projectName, localPinPath) {
|
|
1700
1734
|
if (context.flags.json || context.flags.quiet) return;
|
|
1701
1735
|
context.output.stderr.write(`${context.ui.success("✔")} Linked "${directory}" to Project "${projectName}"\nSaved ${localPinPath}\n\n`);
|
|
@@ -8,66 +8,66 @@ import path from "node:path";
|
|
|
8
8
|
//#region src/lib/app/branch-database-deploy.ts
|
|
9
9
|
async function maybeSetupBranchDatabase(context, provider, projectId, branch, options) {
|
|
10
10
|
if (options.db === false) return emptyBranchDatabaseSetupOutcome();
|
|
11
|
-
if (
|
|
12
|
-
if (options.db === true) throw usageError("
|
|
11
|
+
if (hasProvidedDatabaseEnvVars(options.providedEnvVars)) {
|
|
12
|
+
if (options.db === true) throw usageError("Database setup cannot be combined with provided database env vars", "The deploy command received --db and a DATABASE_URL or DIRECT_URL value from --env.", "Remove the --env database value to let --db create and wire a database, or remove --db to deploy with the provided value.", ["prisma-cli app deploy --db", "prisma-cli app deploy --env DATABASE_URL=postgresql://example"], "app");
|
|
13
13
|
return emptyBranchDatabaseSetupOutcome();
|
|
14
14
|
}
|
|
15
|
-
if (branch.kind === "production") {
|
|
16
|
-
if (options.db === true) throw
|
|
15
|
+
if (branch.kind === "production" && !options.firstProductionDeploy) {
|
|
16
|
+
if (options.db === true) throw productionDatabaseSetupAfterFirstDeployError();
|
|
17
17
|
return emptyBranchDatabaseSetupOutcome();
|
|
18
18
|
}
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const warning = options.db === true ? `Branch "${branch.name}" already has DATABASE_URL. Leaving branch database env vars unchanged.` : null;
|
|
19
|
+
const envState = await inspectBranchDatabaseEnv(provider, projectId, branch, context.runtime.signal);
|
|
20
|
+
const targetEnvVars = getTargetDatabaseEnvVarKeys(envState);
|
|
21
|
+
if (hasExistingDatabaseEnvForTarget(branch, envState)) {
|
|
22
|
+
const warning = options.db === true ? existingDatabaseEnvWarning(branch, targetEnvVars) : null;
|
|
24
23
|
if (warning) emitBranchDatabaseWarning(context, warning);
|
|
25
24
|
return {
|
|
26
25
|
result: options.db === true ? {
|
|
27
26
|
status: "skipped",
|
|
28
|
-
reason:
|
|
29
|
-
envVars:
|
|
27
|
+
reason: existingDatabaseEnvReason(branch),
|
|
28
|
+
envVars: targetEnvVars,
|
|
30
29
|
schema: null
|
|
31
30
|
} : void 0,
|
|
32
31
|
warnings: warning ? [warning] : []
|
|
33
32
|
};
|
|
34
33
|
}
|
|
34
|
+
const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal);
|
|
35
35
|
if (localSignal.unsupportedSchema) {
|
|
36
|
-
if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch
|
|
36
|
+
if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch, context);
|
|
37
37
|
return emptyBranchDatabaseSetupOutcome();
|
|
38
38
|
}
|
|
39
|
-
const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.
|
|
39
|
+
const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.inheritedPreviewDatabaseUrl);
|
|
40
40
|
if (options.db !== true) {
|
|
41
41
|
if (!hasSignal) return emptyBranchDatabaseSetupOutcome();
|
|
42
42
|
if (!canPrompt(context) || context.flags.yes) {
|
|
43
|
-
const warning =
|
|
43
|
+
const warning = databasePromptSuppressedWarning(branch);
|
|
44
44
|
emitBranchDatabaseWarning(context, warning);
|
|
45
45
|
return {
|
|
46
46
|
result: void 0,
|
|
47
47
|
warnings: [warning]
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
maybeRenderBranchDatabaseSignal(context, branch
|
|
50
|
+
maybeRenderBranchDatabaseSignal(context, branch, localSignal, envState);
|
|
51
51
|
if (!await confirmPrompt({
|
|
52
52
|
input: context.runtime.stdin,
|
|
53
53
|
output: context.output.stderr,
|
|
54
|
-
message:
|
|
54
|
+
message: databasePromptMessage(branch),
|
|
55
55
|
initialValue: false
|
|
56
56
|
})) return emptyBranchDatabaseSetupOutcome();
|
|
57
|
-
}
|
|
57
|
+
} else if (!canPrompt(context) && !context.flags.yes) throw nonInteractiveDatabaseSetupRequiresYesError(branch);
|
|
58
58
|
return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState);
|
|
59
59
|
}
|
|
60
60
|
async function setupBranchDatabase(context, provider, projectId, branch, signal, envState) {
|
|
61
|
-
emitBranchDatabaseProgress(context, "pending", "Creating
|
|
61
|
+
emitBranchDatabaseProgress(context, "pending", "Creating database");
|
|
62
62
|
const database = await provider.createBranchDatabase({
|
|
63
63
|
projectId,
|
|
64
64
|
branchId: branch.id,
|
|
65
65
|
branchName: branch.name,
|
|
66
66
|
signal: context.runtime.signal
|
|
67
67
|
}).catch((error) => {
|
|
68
|
-
throw branchDatabaseSetupFailedError("Failed to create
|
|
68
|
+
throw branchDatabaseSetupFailedError("Failed to create database", error, branch);
|
|
69
69
|
});
|
|
70
|
-
emitBranchDatabaseProgress(context, "success", "Created
|
|
70
|
+
emitBranchDatabaseProgress(context, "success", "Created database");
|
|
71
71
|
try {
|
|
72
72
|
let schemaSetup = null;
|
|
73
73
|
const warnings = [];
|
|
@@ -80,12 +80,12 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
|
|
|
80
80
|
databaseUrl: database.databaseUrl,
|
|
81
81
|
directUrl: database.directUrl
|
|
82
82
|
}).catch((error) => {
|
|
83
|
-
throw schemaSetupFailedError(error, signal.schema, branch
|
|
83
|
+
throw schemaSetupFailedError(error, signal.schema, branch, context.runtime.cwd);
|
|
84
84
|
});
|
|
85
85
|
emitBranchDatabaseProgress(context, "success", "Applied database schema");
|
|
86
|
-
} else skippedSchemaWarning = "No supported Prisma schema source was found.
|
|
86
|
+
} else skippedSchemaWarning = "No supported Prisma schema source was found. Database env vars were created, but schema setup was skipped.";
|
|
87
87
|
const envVars = await upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState);
|
|
88
|
-
emitBranchDatabaseProgress(context, "success", `Added branch env
|
|
88
|
+
emitBranchDatabaseProgress(context, "success", `Added ${envScopeLabel(branch)} env var${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`);
|
|
89
89
|
if (skippedSchemaWarning) {
|
|
90
90
|
emitBranchDatabaseWarning(context, skippedSchemaWarning);
|
|
91
91
|
warnings.push(skippedSchemaWarning);
|
|
@@ -107,37 +107,36 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
|
|
|
107
107
|
warnings
|
|
108
108
|
};
|
|
109
109
|
} catch (error) {
|
|
110
|
-
throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch
|
|
110
|
+
throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
async function upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState) {
|
|
114
|
+
const scope = envScopeForBranch(branch);
|
|
114
115
|
const written = [];
|
|
115
116
|
await upsertBranchDatabaseEnvVar(context, provider, {
|
|
116
117
|
projectId,
|
|
117
|
-
|
|
118
|
-
className: "preview",
|
|
118
|
+
...scope,
|
|
119
119
|
key: "DATABASE_URL",
|
|
120
120
|
value: database.databaseUrl,
|
|
121
|
-
existing: envState.
|
|
122
|
-
|
|
121
|
+
existing: envState.targetDatabaseUrl,
|
|
122
|
+
branch
|
|
123
123
|
});
|
|
124
124
|
written.push("DATABASE_URL");
|
|
125
125
|
if (database.directUrl) {
|
|
126
126
|
await upsertBranchDatabaseEnvVar(context, provider, {
|
|
127
127
|
projectId,
|
|
128
|
-
|
|
129
|
-
className: "preview",
|
|
128
|
+
...scope,
|
|
130
129
|
key: "DIRECT_URL",
|
|
131
130
|
value: database.directUrl,
|
|
132
|
-
existing: envState.
|
|
133
|
-
|
|
131
|
+
existing: envState.targetDirectUrl,
|
|
132
|
+
branch
|
|
134
133
|
});
|
|
135
134
|
written.push("DIRECT_URL");
|
|
136
|
-
} else if (envState.
|
|
137
|
-
envVarId: envState.
|
|
135
|
+
} else if (branch.kind === "preview" && envState.targetDirectUrl) await provider.deleteEnvironmentVariable({
|
|
136
|
+
envVarId: envState.targetDirectUrl.id,
|
|
138
137
|
signal: context.runtime.signal
|
|
139
138
|
}).catch((error) => {
|
|
140
|
-
throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch
|
|
139
|
+
throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch);
|
|
141
140
|
});
|
|
142
141
|
return written;
|
|
143
142
|
}
|
|
@@ -148,53 +147,88 @@ async function upsertBranchDatabaseEnvVar(context, provider, options) {
|
|
|
148
147
|
value: options.value,
|
|
149
148
|
signal: context.runtime.signal
|
|
150
149
|
}).catch((error) => {
|
|
151
|
-
throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.
|
|
150
|
+
throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.branch);
|
|
152
151
|
});
|
|
153
152
|
return;
|
|
154
153
|
}
|
|
155
154
|
await provider.createEnvironmentVariable({
|
|
156
155
|
projectId: options.projectId,
|
|
157
|
-
branchId: options.branchId,
|
|
158
156
|
className: options.className,
|
|
159
157
|
key: options.key,
|
|
160
158
|
value: options.value,
|
|
159
|
+
...options.branchId ? { branchId: options.branchId } : {},
|
|
161
160
|
signal: context.runtime.signal
|
|
162
161
|
}).catch((error) => {
|
|
163
|
-
throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.
|
|
162
|
+
throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.branch);
|
|
164
163
|
});
|
|
165
164
|
}
|
|
166
|
-
async function inspectBranchDatabaseEnv(provider, projectId,
|
|
165
|
+
async function inspectBranchDatabaseEnv(provider, projectId, branch, signal) {
|
|
166
|
+
const scope = envScopeForBranch(branch);
|
|
167
167
|
const [databaseUrlRows, directUrlRows] = await Promise.all([provider.listEnvironmentVariables({
|
|
168
168
|
projectId,
|
|
169
|
-
className:
|
|
169
|
+
className: scope.className,
|
|
170
170
|
key: "DATABASE_URL",
|
|
171
171
|
signal
|
|
172
172
|
}), provider.listEnvironmentVariables({
|
|
173
173
|
projectId,
|
|
174
|
-
className:
|
|
174
|
+
className: scope.className,
|
|
175
175
|
key: "DIRECT_URL",
|
|
176
176
|
signal
|
|
177
177
|
})]);
|
|
178
|
+
const targetBranchId = branch.kind === "preview" ? branch.id : null;
|
|
178
179
|
return {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
targetDatabaseUrl: findEnvVar(databaseUrlRows, { branchId: targetBranchId }),
|
|
181
|
+
targetDirectUrl: findEnvVar(directUrlRows, { branchId: targetBranchId }),
|
|
182
|
+
inheritedPreviewDatabaseUrl: branch.kind === "preview" ? findEnvVar(databaseUrlRows, { branchId: null }) : null
|
|
182
183
|
};
|
|
183
184
|
}
|
|
184
185
|
function findEnvVar(rows, options) {
|
|
185
186
|
return rows.find((row) => row.branchId === options.branchId) ?? null;
|
|
186
187
|
}
|
|
187
|
-
function
|
|
188
|
+
function hasProvidedDatabaseEnvVars(envVars) {
|
|
188
189
|
return Boolean(envVars && ("DATABASE_URL" in envVars || "DIRECT_URL" in envVars));
|
|
189
190
|
}
|
|
190
|
-
function
|
|
191
|
+
function envScopeForBranch(branch) {
|
|
192
|
+
return branch.kind === "production" ? { className: "production" } : {
|
|
193
|
+
className: "preview",
|
|
194
|
+
branchId: branch.id
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function envScopeLabel(branch) {
|
|
198
|
+
return branch.kind === "production" ? "production" : "branch";
|
|
199
|
+
}
|
|
200
|
+
function getTargetDatabaseEnvVarKeys(envState) {
|
|
201
|
+
return [envState.targetDatabaseUrl, envState.targetDirectUrl].filter((variable) => Boolean(variable)).map((variable) => variable.key).sort();
|
|
202
|
+
}
|
|
203
|
+
function hasExistingDatabaseEnvForTarget(branch, envState) {
|
|
204
|
+
if (branch.kind === "production") return Boolean(envState.targetDatabaseUrl || envState.targetDirectUrl);
|
|
205
|
+
return Boolean(envState.targetDatabaseUrl);
|
|
206
|
+
}
|
|
207
|
+
function existingDatabaseEnvReason(branch) {
|
|
208
|
+
return branch.kind === "production" ? "production-env-exists" : "branch-env-exists";
|
|
209
|
+
}
|
|
210
|
+
function existingDatabaseEnvWarning(branch, envVars) {
|
|
211
|
+
if (branch.kind === "production") return `Production already has ${envVars.join(" and ")}. Treating it as BYO database configuration and leaving env vars unchanged.`;
|
|
212
|
+
return `Branch "${branch.name}" already has DATABASE_URL. Leaving branch database env vars unchanged.`;
|
|
213
|
+
}
|
|
214
|
+
function databasePromptSuppressedWarning(branch) {
|
|
215
|
+
if (branch.kind === "production") return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db --yes to create and wire a Prisma Postgres database for this first production deploy.";
|
|
216
|
+
return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db to create an isolated database for this preview branch.";
|
|
217
|
+
}
|
|
218
|
+
function databasePromptMessage(branch) {
|
|
219
|
+
return branch.kind === "production" ? "Create a Prisma Postgres database for production?" : `Create an isolated database for branch "${branch.name}"?`;
|
|
220
|
+
}
|
|
221
|
+
function maybeRenderBranchDatabaseSignal(context, branch, signal, envState) {
|
|
191
222
|
if (context.flags.json || context.flags.quiet) return;
|
|
192
223
|
const rows = [
|
|
193
224
|
signal.schema ? ` Schema ${path.relative(context.runtime.cwd, signal.schema.path) || defaultSchemaSourcePath(signal.schema)}` : null,
|
|
194
225
|
signal.databaseUrlReferences.length > 0 ? ` Code ${signal.databaseUrlReferences.slice(0, 3).join(", ")}` : null,
|
|
195
|
-
envState.
|
|
226
|
+
envState.inheritedPreviewDatabaseUrl ? " Env preview DATABASE_URL is inherited by this branch" : null
|
|
196
227
|
].filter((row) => Boolean(row));
|
|
197
|
-
context.output.stderr.write(`Database signal found for
|
|
228
|
+
context.output.stderr.write(`Database signal found for ${databaseTargetLabel(branch)}\n${rows.join("\n")}\n\n`);
|
|
229
|
+
}
|
|
230
|
+
function databaseTargetLabel(branch) {
|
|
231
|
+
return branch.kind === "production" ? `production branch "${branch.name}"` : `branch "${branch.name}"`;
|
|
198
232
|
}
|
|
199
233
|
function emitBranchDatabaseProgress(context, status, message) {
|
|
200
234
|
if (context.flags.json || context.flags.quiet) return;
|
|
@@ -211,6 +245,12 @@ function emptyBranchDatabaseSetupOutcome() {
|
|
|
211
245
|
warnings: []
|
|
212
246
|
};
|
|
213
247
|
}
|
|
248
|
+
function productionDatabaseSetupAfterFirstDeployError() {
|
|
249
|
+
return usageError("Database setup is only available during the first production deploy", "The selected production app already has a live deployment.", "Use project env commands to manage production DATABASE_URL, or deploy a preview branch with --db.", ["prisma-cli project env add DATABASE_URL=<value> --role production", "prisma-cli app deploy --branch feature/db --db"], "app");
|
|
250
|
+
}
|
|
251
|
+
function nonInteractiveDatabaseSetupRequiresYesError(branch) {
|
|
252
|
+
return usageError("Database setup requires --yes in non-interactive mode", "The deploy command received --db, but prompts are not available and --yes was not passed.", "Pass --yes together with --db to confirm non-interactive database creation.", [branch.kind === "production" ? "prisma-cli app deploy --prod --db --yes" : `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db --yes`], "app");
|
|
253
|
+
}
|
|
214
254
|
function formatSchemaSetupCommand(command) {
|
|
215
255
|
switch (command) {
|
|
216
256
|
case "migrate-deploy": return "prisma migrate deploy";
|
|
@@ -218,46 +258,46 @@ function formatSchemaSetupCommand(command) {
|
|
|
218
258
|
case "prisma-next-db-init": return "prisma-next db init";
|
|
219
259
|
}
|
|
220
260
|
}
|
|
221
|
-
function branchDatabaseSetupFailedError(summary, error,
|
|
261
|
+
function branchDatabaseSetupFailedError(summary, error, branch) {
|
|
222
262
|
return new CliError({
|
|
223
263
|
code: "BRANCH_DATABASE_SETUP_FAILED",
|
|
224
264
|
domain: "app",
|
|
225
265
|
summary,
|
|
226
266
|
why: error instanceof Error ? error.message : String(error),
|
|
227
|
-
fix: "Retry the command, or create the
|
|
267
|
+
fix: "Retry the command, or create the database and env vars manually with project env commands.",
|
|
228
268
|
debug: formatDebugDetails(error),
|
|
229
|
-
meta: { branch:
|
|
269
|
+
meta: { branch: branch.name },
|
|
230
270
|
exitCode: 1,
|
|
231
|
-
nextSteps: [
|
|
271
|
+
nextSteps: [formatAppDeployWithDbNextStep(branch), formatProjectEnvListNextStep(branch)]
|
|
232
272
|
});
|
|
233
273
|
}
|
|
234
|
-
async function cleanupCreatedBranchDatabaseAfterFailure(context, provider, database,
|
|
235
|
-
const setupError = error instanceof CliError ? error : branchDatabaseSetupFailedError("
|
|
236
|
-
emitBranchDatabaseProgress(context, "pending", "Removing
|
|
274
|
+
async function cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error) {
|
|
275
|
+
const setupError = error instanceof CliError ? error : branchDatabaseSetupFailedError("Database setup failed", error, branch);
|
|
276
|
+
emitBranchDatabaseProgress(context, "pending", "Removing database after setup failed");
|
|
237
277
|
try {
|
|
238
278
|
await provider.deleteBranchDatabase({
|
|
239
279
|
databaseId: database.id,
|
|
240
280
|
signal: context.runtime.signal
|
|
241
281
|
});
|
|
242
|
-
emitBranchDatabaseProgress(context, "success", "Removed
|
|
282
|
+
emitBranchDatabaseProgress(context, "success", "Removed database after setup failed");
|
|
243
283
|
} catch (cleanupError) {
|
|
244
|
-
return branchDatabaseCleanupFailedError(setupError, cleanupError, database,
|
|
284
|
+
return branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch);
|
|
245
285
|
}
|
|
246
286
|
return setupError;
|
|
247
287
|
}
|
|
248
|
-
function branchDatabaseCleanupFailedError(setupError, cleanupError, database,
|
|
288
|
+
function branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch) {
|
|
249
289
|
const cleanupWhy = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
250
|
-
const setupWhy = setupError.why ?? "
|
|
290
|
+
const setupWhy = setupError.why ?? "Database setup failed.";
|
|
251
291
|
return new CliError({
|
|
252
292
|
code: setupError.code,
|
|
253
293
|
domain: setupError.domain,
|
|
254
294
|
summary: setupError.summary,
|
|
255
295
|
why: `${setupWhy} Prisma could not delete the created database "${database.name}" (${database.id}): ${cleanupWhy}`,
|
|
256
|
-
fix: "Delete the created
|
|
296
|
+
fix: "Delete the created database from Console or contact Prisma support, then rerun deploy with --db.",
|
|
257
297
|
debug: formatCombinedDebugDetails(setupError, cleanupError),
|
|
258
298
|
meta: {
|
|
259
299
|
...setupError.meta,
|
|
260
|
-
branch:
|
|
300
|
+
branch: branch.name,
|
|
261
301
|
databaseId: database.id,
|
|
262
302
|
databaseName: database.name,
|
|
263
303
|
cleanupFailed: true
|
|
@@ -266,7 +306,7 @@ function branchDatabaseCleanupFailedError(setupError, cleanupError, database, br
|
|
|
266
306
|
nextSteps: []
|
|
267
307
|
});
|
|
268
308
|
}
|
|
269
|
-
function schemaSetupFailedError(error, schema,
|
|
309
|
+
function schemaSetupFailedError(error, schema, branch, cwd) {
|
|
270
310
|
return new CliError({
|
|
271
311
|
code: "SCHEMA_SETUP_FAILED",
|
|
272
312
|
domain: "app",
|
|
@@ -275,17 +315,26 @@ function schemaSetupFailedError(error, schema, branchName, cwd) {
|
|
|
275
315
|
fix: "Fix the Prisma schema or migrations, then rerun deploy with --db.",
|
|
276
316
|
debug: formatDebugDetails(error),
|
|
277
317
|
meta: {
|
|
278
|
-
branch:
|
|
318
|
+
branch: branch.name,
|
|
279
319
|
schemaPath: schema.path,
|
|
280
320
|
source: schema.kind,
|
|
281
321
|
command: schema.command
|
|
282
322
|
},
|
|
283
323
|
exitCode: 1,
|
|
284
|
-
nextSteps: [...formatSchemaSetupNextSteps(schema, cwd),
|
|
324
|
+
nextSteps: [...formatSchemaSetupNextSteps(schema, cwd), formatAppDeployWithDbNextStep(branch)]
|
|
285
325
|
});
|
|
286
326
|
}
|
|
287
|
-
function unsupportedBranchDatabaseSchemaError(schema,
|
|
288
|
-
return usageError("
|
|
327
|
+
function unsupportedBranchDatabaseSchemaError(schema, branch, context) {
|
|
328
|
+
return usageError("Database setup is not available for this Prisma schema", `${path.relative(context.runtime.cwd, schema.path) || defaultUnsupportedSchemaSourcePath(schema)} targets ${formatUnsupportedSchemaTarget(schema.target)}, but --db creates Prisma Postgres databases.`, "Use project env commands to provide a database URL, or switch the Prisma schema source to PostgreSQL before using --db.", [formatProjectEnvAddNextStep(branch), `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)}`], "app");
|
|
329
|
+
}
|
|
330
|
+
function formatAppDeployWithDbNextStep(branch) {
|
|
331
|
+
return `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db`;
|
|
332
|
+
}
|
|
333
|
+
function formatProjectEnvListNextStep(branch) {
|
|
334
|
+
return branch.kind === "production" ? "prisma-cli project env list --role production" : `prisma-cli project env list --branch ${formatCommandArgument(branch.name)}`;
|
|
335
|
+
}
|
|
336
|
+
function formatProjectEnvAddNextStep(branch) {
|
|
337
|
+
return branch.kind === "production" ? "prisma-cli project env add DATABASE_URL=<value> --role production" : `prisma-cli project env add DATABASE_URL=<value> --branch ${formatCommandArgument(branch.name)}`;
|
|
289
338
|
}
|
|
290
339
|
function formatSchemaSetupNextSteps(schema, cwd) {
|
|
291
340
|
const sourcePath = path.relative(cwd, schema.path) || defaultSchemaSourcePath(schema);
|
package/dist/lib/app/env-file.js
CHANGED
|
@@ -11,7 +11,7 @@ async function readEnvFileAssignments(cwd, filePath, command) {
|
|
|
11
11
|
try {
|
|
12
12
|
contents = await readFile(resolvedPath, "utf8");
|
|
13
13
|
} catch (error) {
|
|
14
|
-
throw usageError(`Failed to read env file "${filePath}"`, error instanceof Error ? error.message : "The file could not be read.", "Pass a readable dotenv file path.", [`prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
14
|
+
throw usageError(`Failed to read env file "${filePath}"`, error instanceof Error ? error.message : "The file could not be read.", "Pass a readable dotenv file path.", [command === "deploy" ? "prisma-cli app deploy --env .env" : `prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
15
15
|
}
|
|
16
16
|
return parseEnvFileContents(contents, filePath, command);
|
|
17
17
|
}
|
|
@@ -63,7 +63,7 @@ function extractParsedKeys(contents) {
|
|
|
63
63
|
}
|
|
64
64
|
function validateEnvFileKey(key, line, filePath, command) {
|
|
65
65
|
try {
|
|
66
|
-
validateKey(key, command);
|
|
66
|
+
validateKey(key, command === "deploy" ? "add" : command);
|
|
67
67
|
} catch (error) {
|
|
68
68
|
const reason = error instanceof Error && error.message.length > 0 ? error.message : "Invalid environment variable key.";
|
|
69
69
|
throw usageError(`Invalid environment variable "${key}" in "${filePath}"`, `Line ${line}: ${reason}`, "Use a valid env-var key and retry the import.", [], "app");
|
package/dist/lib/app/env-vars.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { usageError } from "../../shell/errors.js";
|
|
2
|
+
import { validateKey } from "./env-config.js";
|
|
3
|
+
import { readEnvFileAssignments } from "./env-file.js";
|
|
2
4
|
//#region src/lib/app/env-vars.ts
|
|
3
5
|
function parseEnvAssignments(assignments, options) {
|
|
4
6
|
const values = assignments ?? [];
|
|
@@ -10,15 +12,39 @@ function parseEnvAssignments(assignments, options) {
|
|
|
10
12
|
if (separatorIndex === -1) throw usageError("Environment variable assignment must use NAME=VALUE", "A provided --env flag is missing the = separator.", `Pass repeated --env NAME=VALUE flags, for example prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
|
|
11
13
|
const name = assignment.slice(0, separatorIndex);
|
|
12
14
|
if (name.length === 0) throw usageError("Environment variable name is required", "A provided --env flag has an empty variable name.", `Pass repeated --env NAME=VALUE flags, for example prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
|
|
15
|
+
validateEnvAssignmentName(name, options.commandName);
|
|
13
16
|
if (seen.has(name)) throw usageError(`Environment variable "${name}" was provided more than once`, "Each environment variable name may be set only once per command invocation.", `Remove the duplicate "${name}" assignment and rerun prisma-cli app ${options.commandName}.`, [`prisma-cli app ${options.commandName} --env ${name}=value`], "app");
|
|
17
|
+
const value = assignment.slice(separatorIndex + 1);
|
|
18
|
+
if (value.length === 0) throw usageError(`Environment variable "${name}" has an empty value`, `A provided --env flag defines ${name} with no value.`, "Pass a non-empty value, or omit the key from the deploy command.", [`prisma-cli app ${options.commandName} --env ${name}=value`], "app");
|
|
14
19
|
seen.add(name);
|
|
15
|
-
parsed[name] =
|
|
20
|
+
parsed[name] = value;
|
|
16
21
|
}
|
|
17
22
|
return parsed;
|
|
18
23
|
}
|
|
24
|
+
async function parseEnvInputs(cwd, inputs, options) {
|
|
25
|
+
const values = inputs ?? [];
|
|
26
|
+
const expandedAssignments = [];
|
|
27
|
+
for (const value of values) {
|
|
28
|
+
if (value.includes("=")) {
|
|
29
|
+
expandedAssignments.push(value);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const fileAssignments = await readEnvFileAssignments(cwd, value, options.commandName);
|
|
33
|
+
expandedAssignments.push(...fileAssignments.map((assignment) => `${assignment.key}=${assignment.value}`));
|
|
34
|
+
}
|
|
35
|
+
return parseEnvAssignments(expandedAssignments, options);
|
|
36
|
+
}
|
|
37
|
+
function validateEnvAssignmentName(name, commandName) {
|
|
38
|
+
try {
|
|
39
|
+
validateKey(name, "add");
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const reason = error instanceof Error && error.message.length > 0 ? error.message : "Invalid environment variable name.";
|
|
42
|
+
throw usageError(`Invalid environment variable "${name}"`, reason, "Use a valid env-var name and retry the deploy.", [`prisma-cli app ${commandName} --env DATABASE_URL=postgresql://example`], "app");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
19
45
|
function envVarNames(envVars) {
|
|
20
46
|
if (!envVars) return [];
|
|
21
47
|
return Object.entries(envVars).filter(([, value]) => value !== null).map(([name]) => name).sort((left, right) => left.localeCompare(right));
|
|
22
48
|
}
|
|
23
49
|
//#endregion
|
|
24
|
-
export { envVarNames,
|
|
50
|
+
export { envVarNames, parseEnvInputs };
|