@prisma/cli 3.0.0-dev.70.1 → 3.0.0-dev.73.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/dist/commands/app/index.js +2 -2
- package/dist/controllers/app.js +19 -5
- package/dist/controllers/project.js +16 -2
- package/dist/lib/app/branch-database-deploy.js +114 -64
- package/dist/lib/app/production-deploy-gate.js +5 -4
- package/dist/lib/project/local-pin.js +64 -14
- package/dist/lib/project/resolution.js +16 -2
- package/package.json +2 -1
|
@@ -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|file>", "Environment variable assignment or dotenv file").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,
|
package/dist/controllers/app.js
CHANGED
|
@@ -31,6 +31,7 @@ import { listRealWorkspaceProjects } from "./project.js";
|
|
|
31
31
|
import { access, readFile } from "node:fs/promises";
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import open from "open";
|
|
34
|
+
import { Result, matchError } from "better-result";
|
|
34
35
|
//#region src/controllers/app.ts
|
|
35
36
|
const DEPLOY_FRAMEWORKS = [
|
|
36
37
|
"nextjs",
|
|
@@ -112,9 +113,9 @@ async function runAppDeploy(context, appName, options) {
|
|
|
112
113
|
createProjectName: options?.createProjectName,
|
|
113
114
|
envProjectId
|
|
114
115
|
});
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const localPinReadResult = Boolean(envProjectId || options?.projectRef || options?.createProjectName) ? Result.ok({ kind: "missing" }) : await readLocalResolutionPin(context.runtime.cwd, context.runtime.signal);
|
|
117
|
+
if (localPinReadResult.isErr()) throw localPinReadErrorToDeployError(localPinReadResult.error);
|
|
118
|
+
const localPin = localPinReadResult.value;
|
|
118
119
|
const branch = await resolveDeployBranch(context, options?.branchName);
|
|
119
120
|
if (options?.httpPort) parseDeployHttpPort(options.httpPort);
|
|
120
121
|
assertSupportedEntrypointForRequestedDeployShape({
|
|
@@ -162,7 +163,7 @@ async function runAppDeploy(context, appName, options) {
|
|
|
162
163
|
});
|
|
163
164
|
framework = customized.framework;
|
|
164
165
|
runtime = customized.runtime;
|
|
165
|
-
await enforceProductionDeployGate(context, provider, {
|
|
166
|
+
const productionDeployGate = await enforceProductionDeployGate(context, provider, {
|
|
166
167
|
appId: selectedApp.appId,
|
|
167
168
|
appName: selectedApp.displayName,
|
|
168
169
|
branchKind: target.branch.kind,
|
|
@@ -180,7 +181,8 @@ async function runAppDeploy(context, appName, options) {
|
|
|
180
181
|
const portMapping = parseDeployPortMapping(String(runtime.port));
|
|
181
182
|
const branchDatabaseSetup = await maybeSetupBranchDatabase(context, provider, projectId, toBranchDatabaseDeployBranch(target.branch), {
|
|
182
183
|
db: options?.db,
|
|
183
|
-
providedEnvVars: envVars
|
|
184
|
+
providedEnvVars: envVars,
|
|
185
|
+
firstProductionDeploy: productionDeployGate.firstProductionDeploy
|
|
184
186
|
});
|
|
185
187
|
const progressState = createPreviewDeployProgressState();
|
|
186
188
|
const deployStartedAt = Date.now();
|
|
@@ -1975,6 +1977,18 @@ function localResolutionPinStaleError() {
|
|
|
1975
1977
|
]
|
|
1976
1978
|
});
|
|
1977
1979
|
}
|
|
1980
|
+
function localPinReadErrorToDeployError(error) {
|
|
1981
|
+
return matchError(error, {
|
|
1982
|
+
LocalResolutionPinInvalidJsonError: () => localResolutionPinStaleError(),
|
|
1983
|
+
LocalResolutionPinInvalidShapeError: () => localResolutionPinStaleError(),
|
|
1984
|
+
LocalResolutionPinReadAbortedError: (error) => {
|
|
1985
|
+
throw error;
|
|
1986
|
+
},
|
|
1987
|
+
UnhandledException: (error) => {
|
|
1988
|
+
throw error;
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1978
1992
|
function readDeployEnvOverride(context, name) {
|
|
1979
1993
|
const value = context.runtime.env[name]?.trim();
|
|
1980
1994
|
return value ? value : void 0;
|
|
@@ -13,6 +13,7 @@ import { requireAuthenticatedAuthState } from "./auth.js";
|
|
|
13
13
|
import { parseGitHubRepositoryUrl, readGitOriginRemote } from "../adapters/git.js";
|
|
14
14
|
import { createProjectUseCases } from "../use-cases/project.js";
|
|
15
15
|
import open from "open";
|
|
16
|
+
import { matchError } from "better-result";
|
|
16
17
|
//#region src/controllers/project.ts
|
|
17
18
|
const GITHUB_INSTALL_POLL_INTERVAL_MS = 2e3;
|
|
18
19
|
const GITHUB_INSTALL_POLL_TIMEOUT_MS = 12e4;
|
|
@@ -20,11 +21,24 @@ function isRealMode(context) {
|
|
|
20
21
|
return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
|
|
21
22
|
}
|
|
22
23
|
async function readProjectListLocalBinding(cwd, workspace, projects, signal) {
|
|
23
|
-
const
|
|
24
|
+
const pinResult = await readLocalResolutionPin(cwd, signal);
|
|
25
|
+
if (pinResult.isErr()) return localPinReadErrorToInvalidLocalBinding(pinResult.error);
|
|
26
|
+
const pin = pinResult.value;
|
|
24
27
|
if (pin.kind === "present") return pin.pin.workspaceId === workspace.id && projects.some((project) => project.id === pin.pin.projectId) ? { status: "linked" } : { status: "invalid" };
|
|
25
|
-
if (pin.kind === "invalid") return { status: "invalid" };
|
|
26
28
|
return { status: "not-linked" };
|
|
27
29
|
}
|
|
30
|
+
function localPinReadErrorToInvalidLocalBinding(error) {
|
|
31
|
+
return matchError(error, {
|
|
32
|
+
LocalResolutionPinInvalidJsonError: () => ({ status: "invalid" }),
|
|
33
|
+
LocalResolutionPinInvalidShapeError: () => ({ status: "invalid" }),
|
|
34
|
+
LocalResolutionPinReadAbortedError: (error) => {
|
|
35
|
+
throw error;
|
|
36
|
+
},
|
|
37
|
+
UnhandledException: (error) => {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
28
42
|
async function runProjectList(context) {
|
|
29
43
|
const authState = await requireAuthenticatedAuthState(context);
|
|
30
44
|
const workspace = authState.workspace;
|
|
@@ -3,71 +3,72 @@ import { renderSummaryLine } from "../../shell/ui.js";
|
|
|
3
3
|
import { canPrompt } from "../../shell/runtime.js";
|
|
4
4
|
import { confirmPrompt } from "../../shell/prompt.js";
|
|
5
5
|
import { formatCommandArgument } from "../../shell/command-arguments.js";
|
|
6
|
+
import "../project/setup.js";
|
|
6
7
|
import { hasBranchDatabaseSignal, inspectBranchDatabaseSignal, runBranchDatabaseSchemaSetup } from "./branch-database.js";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
//#region src/lib/app/branch-database-deploy.ts
|
|
9
10
|
async function maybeSetupBranchDatabase(context, provider, projectId, branch, options) {
|
|
10
11
|
if (options.db === false) return emptyBranchDatabaseSetupOutcome();
|
|
11
12
|
if (hasProvidedDatabaseEnvVars(options.providedEnvVars)) {
|
|
12
|
-
if (options.db === true) throw usageError("
|
|
13
|
+
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
14
|
return emptyBranchDatabaseSetupOutcome();
|
|
14
15
|
}
|
|
15
|
-
if (branch.kind === "production") {
|
|
16
|
-
if (options.db === true) throw
|
|
16
|
+
if (branch.kind === "production" && !options.firstProductionDeploy) {
|
|
17
|
+
if (options.db === true) throw productionDatabaseSetupAfterFirstDeployError();
|
|
17
18
|
return emptyBranchDatabaseSetupOutcome();
|
|
18
19
|
}
|
|
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;
|
|
20
|
+
const envState = await inspectBranchDatabaseEnv(provider, projectId, branch, context.runtime.signal);
|
|
21
|
+
const targetEnvVars = getTargetDatabaseEnvVarKeys(envState);
|
|
22
|
+
if (hasExistingDatabaseEnvForTarget(branch, envState)) {
|
|
23
|
+
const warning = options.db === true ? existingDatabaseEnvWarning(branch, targetEnvVars) : null;
|
|
24
24
|
if (warning) emitBranchDatabaseWarning(context, warning);
|
|
25
25
|
return {
|
|
26
26
|
result: options.db === true ? {
|
|
27
27
|
status: "skipped",
|
|
28
|
-
reason:
|
|
29
|
-
envVars:
|
|
28
|
+
reason: existingDatabaseEnvReason(branch),
|
|
29
|
+
envVars: targetEnvVars,
|
|
30
30
|
schema: null
|
|
31
31
|
} : void 0,
|
|
32
32
|
warnings: warning ? [warning] : []
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal);
|
|
35
36
|
if (localSignal.unsupportedSchema) {
|
|
36
|
-
if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch
|
|
37
|
+
if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch, context);
|
|
37
38
|
return emptyBranchDatabaseSetupOutcome();
|
|
38
39
|
}
|
|
39
|
-
const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.
|
|
40
|
+
const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.inheritedPreviewDatabaseUrl);
|
|
40
41
|
if (options.db !== true) {
|
|
41
42
|
if (!hasSignal) return emptyBranchDatabaseSetupOutcome();
|
|
42
43
|
if (!canPrompt(context) || context.flags.yes) {
|
|
43
|
-
const warning =
|
|
44
|
+
const warning = databasePromptSuppressedWarning(branch);
|
|
44
45
|
emitBranchDatabaseWarning(context, warning);
|
|
45
46
|
return {
|
|
46
47
|
result: void 0,
|
|
47
48
|
warnings: [warning]
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
|
-
maybeRenderBranchDatabaseSignal(context, branch
|
|
51
|
+
maybeRenderBranchDatabaseSignal(context, branch, localSignal, envState);
|
|
51
52
|
if (!await confirmPrompt({
|
|
52
53
|
input: context.runtime.stdin,
|
|
53
54
|
output: context.output.stderr,
|
|
54
|
-
message:
|
|
55
|
+
message: databasePromptMessage(branch),
|
|
55
56
|
initialValue: false
|
|
56
57
|
})) return emptyBranchDatabaseSetupOutcome();
|
|
57
|
-
}
|
|
58
|
+
} else if (!canPrompt(context) && !context.flags.yes) throw nonInteractiveDatabaseSetupRequiresYesError(branch);
|
|
58
59
|
return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState);
|
|
59
60
|
}
|
|
60
61
|
async function setupBranchDatabase(context, provider, projectId, branch, signal, envState) {
|
|
61
|
-
emitBranchDatabaseProgress(context, "pending", "Creating
|
|
62
|
+
emitBranchDatabaseProgress(context, "pending", "Creating database");
|
|
62
63
|
const database = await provider.createBranchDatabase({
|
|
63
64
|
projectId,
|
|
64
65
|
branchId: branch.id,
|
|
65
66
|
branchName: branch.name,
|
|
66
67
|
signal: context.runtime.signal
|
|
67
68
|
}).catch((error) => {
|
|
68
|
-
throw branchDatabaseSetupFailedError("Failed to create
|
|
69
|
+
throw branchDatabaseSetupFailedError("Failed to create database", error, branch);
|
|
69
70
|
});
|
|
70
|
-
emitBranchDatabaseProgress(context, "success", "Created
|
|
71
|
+
emitBranchDatabaseProgress(context, "success", "Created database");
|
|
71
72
|
try {
|
|
72
73
|
let schemaSetup = null;
|
|
73
74
|
const warnings = [];
|
|
@@ -80,12 +81,12 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
|
|
|
80
81
|
databaseUrl: database.databaseUrl,
|
|
81
82
|
directUrl: database.directUrl
|
|
82
83
|
}).catch((error) => {
|
|
83
|
-
throw schemaSetupFailedError(error, signal.schema, branch
|
|
84
|
+
throw schemaSetupFailedError(error, signal.schema, branch, context.runtime.cwd);
|
|
84
85
|
});
|
|
85
86
|
emitBranchDatabaseProgress(context, "success", "Applied database schema");
|
|
86
|
-
} else skippedSchemaWarning = "No supported Prisma schema source was found.
|
|
87
|
+
} else skippedSchemaWarning = "No supported Prisma schema source was found. Database env vars were created, but schema setup was skipped.";
|
|
87
88
|
const envVars = await upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState);
|
|
88
|
-
emitBranchDatabaseProgress(context, "success", `Added branch env
|
|
89
|
+
emitBranchDatabaseProgress(context, "success", `Added ${envScopeLabel(branch)} env var${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`);
|
|
89
90
|
if (skippedSchemaWarning) {
|
|
90
91
|
emitBranchDatabaseWarning(context, skippedSchemaWarning);
|
|
91
92
|
warnings.push(skippedSchemaWarning);
|
|
@@ -107,37 +108,36 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
|
|
|
107
108
|
warnings
|
|
108
109
|
};
|
|
109
110
|
} catch (error) {
|
|
110
|
-
throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch
|
|
111
|
+
throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error);
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
async function upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState) {
|
|
115
|
+
const scope = envScopeForBranch(branch);
|
|
114
116
|
const written = [];
|
|
115
117
|
await upsertBranchDatabaseEnvVar(context, provider, {
|
|
116
118
|
projectId,
|
|
117
|
-
|
|
118
|
-
className: "preview",
|
|
119
|
+
...scope,
|
|
119
120
|
key: "DATABASE_URL",
|
|
120
121
|
value: database.databaseUrl,
|
|
121
|
-
existing: envState.
|
|
122
|
-
|
|
122
|
+
existing: envState.targetDatabaseUrl,
|
|
123
|
+
branch
|
|
123
124
|
});
|
|
124
125
|
written.push("DATABASE_URL");
|
|
125
126
|
if (database.directUrl) {
|
|
126
127
|
await upsertBranchDatabaseEnvVar(context, provider, {
|
|
127
128
|
projectId,
|
|
128
|
-
|
|
129
|
-
className: "preview",
|
|
129
|
+
...scope,
|
|
130
130
|
key: "DIRECT_URL",
|
|
131
131
|
value: database.directUrl,
|
|
132
|
-
existing: envState.
|
|
133
|
-
|
|
132
|
+
existing: envState.targetDirectUrl,
|
|
133
|
+
branch
|
|
134
134
|
});
|
|
135
135
|
written.push("DIRECT_URL");
|
|
136
|
-
} else if (envState.
|
|
137
|
-
envVarId: envState.
|
|
136
|
+
} else if (branch.kind === "preview" && envState.targetDirectUrl) await provider.deleteEnvironmentVariable({
|
|
137
|
+
envVarId: envState.targetDirectUrl.id,
|
|
138
138
|
signal: context.runtime.signal
|
|
139
139
|
}).catch((error) => {
|
|
140
|
-
throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch
|
|
140
|
+
throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch);
|
|
141
141
|
});
|
|
142
142
|
return written;
|
|
143
143
|
}
|
|
@@ -148,37 +148,39 @@ async function upsertBranchDatabaseEnvVar(context, provider, options) {
|
|
|
148
148
|
value: options.value,
|
|
149
149
|
signal: context.runtime.signal
|
|
150
150
|
}).catch((error) => {
|
|
151
|
-
throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.
|
|
151
|
+
throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.branch);
|
|
152
152
|
});
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
155
|
await provider.createEnvironmentVariable({
|
|
156
156
|
projectId: options.projectId,
|
|
157
|
-
branchId: options.branchId,
|
|
158
157
|
className: options.className,
|
|
159
158
|
key: options.key,
|
|
160
159
|
value: options.value,
|
|
160
|
+
...options.branchId ? { branchId: options.branchId } : {},
|
|
161
161
|
signal: context.runtime.signal
|
|
162
162
|
}).catch((error) => {
|
|
163
|
-
throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.
|
|
163
|
+
throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.branch);
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
|
-
async function inspectBranchDatabaseEnv(provider, projectId,
|
|
166
|
+
async function inspectBranchDatabaseEnv(provider, projectId, branch, signal) {
|
|
167
|
+
const scope = envScopeForBranch(branch);
|
|
167
168
|
const [databaseUrlRows, directUrlRows] = await Promise.all([provider.listEnvironmentVariables({
|
|
168
169
|
projectId,
|
|
169
|
-
className:
|
|
170
|
+
className: scope.className,
|
|
170
171
|
key: "DATABASE_URL",
|
|
171
172
|
signal
|
|
172
173
|
}), provider.listEnvironmentVariables({
|
|
173
174
|
projectId,
|
|
174
|
-
className:
|
|
175
|
+
className: scope.className,
|
|
175
176
|
key: "DIRECT_URL",
|
|
176
177
|
signal
|
|
177
178
|
})]);
|
|
179
|
+
const targetBranchId = branch.kind === "preview" ? branch.id : null;
|
|
178
180
|
return {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
181
|
+
targetDatabaseUrl: findEnvVar(databaseUrlRows, { branchId: targetBranchId }),
|
|
182
|
+
targetDirectUrl: findEnvVar(directUrlRows, { branchId: targetBranchId }),
|
|
183
|
+
inheritedPreviewDatabaseUrl: branch.kind === "preview" ? findEnvVar(databaseUrlRows, { branchId: null }) : null
|
|
182
184
|
};
|
|
183
185
|
}
|
|
184
186
|
function findEnvVar(rows, options) {
|
|
@@ -187,14 +189,47 @@ function findEnvVar(rows, options) {
|
|
|
187
189
|
function hasProvidedDatabaseEnvVars(envVars) {
|
|
188
190
|
return Boolean(envVars && ("DATABASE_URL" in envVars || "DIRECT_URL" in envVars));
|
|
189
191
|
}
|
|
190
|
-
function
|
|
192
|
+
function envScopeForBranch(branch) {
|
|
193
|
+
return branch.kind === "production" ? { className: "production" } : {
|
|
194
|
+
className: "preview",
|
|
195
|
+
branchId: branch.id
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function envScopeLabel(branch) {
|
|
199
|
+
return branch.kind === "production" ? "production" : "branch";
|
|
200
|
+
}
|
|
201
|
+
function getTargetDatabaseEnvVarKeys(envState) {
|
|
202
|
+
return [envState.targetDatabaseUrl, envState.targetDirectUrl].filter((variable) => Boolean(variable)).map((variable) => variable.key).sort();
|
|
203
|
+
}
|
|
204
|
+
function hasExistingDatabaseEnvForTarget(branch, envState) {
|
|
205
|
+
if (branch.kind === "production") return Boolean(envState.targetDatabaseUrl || envState.targetDirectUrl);
|
|
206
|
+
return Boolean(envState.targetDatabaseUrl);
|
|
207
|
+
}
|
|
208
|
+
function existingDatabaseEnvReason(branch) {
|
|
209
|
+
return branch.kind === "production" ? "production-env-exists" : "branch-env-exists";
|
|
210
|
+
}
|
|
211
|
+
function existingDatabaseEnvWarning(branch, envVars) {
|
|
212
|
+
if (branch.kind === "production") return `Production already has ${envVars.join(" and ")}. Treating it as BYO database configuration and leaving env vars unchanged.`;
|
|
213
|
+
return `Branch "${branch.name}" already has DATABASE_URL. Leaving branch database env vars unchanged.`;
|
|
214
|
+
}
|
|
215
|
+
function databasePromptSuppressedWarning(branch) {
|
|
216
|
+
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.";
|
|
217
|
+
return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db to create an isolated database for this preview branch.";
|
|
218
|
+
}
|
|
219
|
+
function databasePromptMessage(branch) {
|
|
220
|
+
return branch.kind === "production" ? "Create a Prisma Postgres database for production?" : `Create an isolated database for branch "${branch.name}"?`;
|
|
221
|
+
}
|
|
222
|
+
function maybeRenderBranchDatabaseSignal(context, branch, signal, envState) {
|
|
191
223
|
if (context.flags.json || context.flags.quiet) return;
|
|
192
224
|
const rows = [
|
|
193
225
|
signal.schema ? ` Schema ${path.relative(context.runtime.cwd, signal.schema.path) || defaultSchemaSourcePath(signal.schema)}` : null,
|
|
194
226
|
signal.databaseUrlReferences.length > 0 ? ` Code ${signal.databaseUrlReferences.slice(0, 3).join(", ")}` : null,
|
|
195
|
-
envState.
|
|
227
|
+
envState.inheritedPreviewDatabaseUrl ? " Env preview DATABASE_URL is inherited by this branch" : null
|
|
196
228
|
].filter((row) => Boolean(row));
|
|
197
|
-
context.output.stderr.write(`Database signal found for
|
|
229
|
+
context.output.stderr.write(`Database signal found for ${databaseTargetLabel(branch)}\n${rows.join("\n")}\n\n`);
|
|
230
|
+
}
|
|
231
|
+
function databaseTargetLabel(branch) {
|
|
232
|
+
return branch.kind === "production" ? `production branch "${branch.name}"` : `branch "${branch.name}"`;
|
|
198
233
|
}
|
|
199
234
|
function emitBranchDatabaseProgress(context, status, message) {
|
|
200
235
|
if (context.flags.json || context.flags.quiet) return;
|
|
@@ -211,6 +246,12 @@ function emptyBranchDatabaseSetupOutcome() {
|
|
|
211
246
|
warnings: []
|
|
212
247
|
};
|
|
213
248
|
}
|
|
249
|
+
function productionDatabaseSetupAfterFirstDeployError() {
|
|
250
|
+
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");
|
|
251
|
+
}
|
|
252
|
+
function nonInteractiveDatabaseSetupRequiresYesError(branch) {
|
|
253
|
+
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");
|
|
254
|
+
}
|
|
214
255
|
function formatSchemaSetupCommand(command) {
|
|
215
256
|
switch (command) {
|
|
216
257
|
case "migrate-deploy": return "prisma migrate deploy";
|
|
@@ -218,46 +259,46 @@ function formatSchemaSetupCommand(command) {
|
|
|
218
259
|
case "prisma-next-db-init": return "prisma-next db init";
|
|
219
260
|
}
|
|
220
261
|
}
|
|
221
|
-
function branchDatabaseSetupFailedError(summary, error,
|
|
262
|
+
function branchDatabaseSetupFailedError(summary, error, branch) {
|
|
222
263
|
return new CliError({
|
|
223
264
|
code: "BRANCH_DATABASE_SETUP_FAILED",
|
|
224
265
|
domain: "app",
|
|
225
266
|
summary,
|
|
226
267
|
why: error instanceof Error ? error.message : String(error),
|
|
227
|
-
fix: "Retry the command, or create the
|
|
268
|
+
fix: "Retry the command, or create the database and env vars manually with project env commands.",
|
|
228
269
|
debug: formatDebugDetails(error),
|
|
229
|
-
meta: { branch:
|
|
270
|
+
meta: { branch: branch.name },
|
|
230
271
|
exitCode: 1,
|
|
231
|
-
nextSteps: [
|
|
272
|
+
nextSteps: [formatAppDeployWithDbNextStep(branch), formatProjectEnvListNextStep(branch)]
|
|
232
273
|
});
|
|
233
274
|
}
|
|
234
|
-
async function cleanupCreatedBranchDatabaseAfterFailure(context, provider, database,
|
|
235
|
-
const setupError = error instanceof CliError ? error : branchDatabaseSetupFailedError("
|
|
236
|
-
emitBranchDatabaseProgress(context, "pending", "Removing
|
|
275
|
+
async function cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error) {
|
|
276
|
+
const setupError = error instanceof CliError ? error : branchDatabaseSetupFailedError("Database setup failed", error, branch);
|
|
277
|
+
emitBranchDatabaseProgress(context, "pending", "Removing database after setup failed");
|
|
237
278
|
try {
|
|
238
279
|
await provider.deleteBranchDatabase({
|
|
239
280
|
databaseId: database.id,
|
|
240
281
|
signal: context.runtime.signal
|
|
241
282
|
});
|
|
242
|
-
emitBranchDatabaseProgress(context, "success", "Removed
|
|
283
|
+
emitBranchDatabaseProgress(context, "success", "Removed database after setup failed");
|
|
243
284
|
} catch (cleanupError) {
|
|
244
|
-
return branchDatabaseCleanupFailedError(setupError, cleanupError, database,
|
|
285
|
+
return branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch);
|
|
245
286
|
}
|
|
246
287
|
return setupError;
|
|
247
288
|
}
|
|
248
|
-
function branchDatabaseCleanupFailedError(setupError, cleanupError, database,
|
|
289
|
+
function branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch) {
|
|
249
290
|
const cleanupWhy = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
250
|
-
const setupWhy = setupError.why ?? "
|
|
291
|
+
const setupWhy = setupError.why ?? "Database setup failed.";
|
|
251
292
|
return new CliError({
|
|
252
293
|
code: setupError.code,
|
|
253
294
|
domain: setupError.domain,
|
|
254
295
|
summary: setupError.summary,
|
|
255
296
|
why: `${setupWhy} Prisma could not delete the created database "${database.name}" (${database.id}): ${cleanupWhy}`,
|
|
256
|
-
fix: "Delete the created
|
|
297
|
+
fix: "Delete the created database from Console or contact Prisma support, then rerun deploy with --db.",
|
|
257
298
|
debug: formatCombinedDebugDetails(setupError, cleanupError),
|
|
258
299
|
meta: {
|
|
259
300
|
...setupError.meta,
|
|
260
|
-
branch:
|
|
301
|
+
branch: branch.name,
|
|
261
302
|
databaseId: database.id,
|
|
262
303
|
databaseName: database.name,
|
|
263
304
|
cleanupFailed: true
|
|
@@ -266,7 +307,7 @@ function branchDatabaseCleanupFailedError(setupError, cleanupError, database, br
|
|
|
266
307
|
nextSteps: []
|
|
267
308
|
});
|
|
268
309
|
}
|
|
269
|
-
function schemaSetupFailedError(error, schema,
|
|
310
|
+
function schemaSetupFailedError(error, schema, branch, cwd) {
|
|
270
311
|
return new CliError({
|
|
271
312
|
code: "SCHEMA_SETUP_FAILED",
|
|
272
313
|
domain: "app",
|
|
@@ -275,17 +316,26 @@ function schemaSetupFailedError(error, schema, branchName, cwd) {
|
|
|
275
316
|
fix: "Fix the Prisma schema or migrations, then rerun deploy with --db.",
|
|
276
317
|
debug: formatDebugDetails(error),
|
|
277
318
|
meta: {
|
|
278
|
-
branch:
|
|
319
|
+
branch: branch.name,
|
|
279
320
|
schemaPath: schema.path,
|
|
280
321
|
source: schema.kind,
|
|
281
322
|
command: schema.command
|
|
282
323
|
},
|
|
283
324
|
exitCode: 1,
|
|
284
|
-
nextSteps: [...formatSchemaSetupNextSteps(schema, cwd),
|
|
325
|
+
nextSteps: [...formatSchemaSetupNextSteps(schema, cwd), formatAppDeployWithDbNextStep(branch)]
|
|
285
326
|
});
|
|
286
327
|
}
|
|
287
|
-
function unsupportedBranchDatabaseSchemaError(schema,
|
|
288
|
-
return usageError("
|
|
328
|
+
function unsupportedBranchDatabaseSchemaError(schema, branch, context) {
|
|
329
|
+
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");
|
|
330
|
+
}
|
|
331
|
+
function formatAppDeployWithDbNextStep(branch) {
|
|
332
|
+
return `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db`;
|
|
333
|
+
}
|
|
334
|
+
function formatProjectEnvListNextStep(branch) {
|
|
335
|
+
return branch.kind === "production" ? "prisma-cli project env list --role production" : `prisma-cli project env list --branch ${formatCommandArgument(branch.name)}`;
|
|
336
|
+
}
|
|
337
|
+
function formatProjectEnvAddNextStep(branch) {
|
|
338
|
+
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
339
|
}
|
|
290
340
|
function formatSchemaSetupNextSteps(schema, cwd) {
|
|
291
341
|
const sourcePath = path.relative(cwd, schema.path) || defaultSchemaSourcePath(schema);
|
|
@@ -3,22 +3,22 @@ import { canPrompt } from "../../shell/runtime.js";
|
|
|
3
3
|
import { confirmPrompt } from "../../shell/prompt.js";
|
|
4
4
|
//#region src/lib/app/production-deploy-gate.ts
|
|
5
5
|
async function enforceProductionDeployGate(context, provider, options) {
|
|
6
|
-
if (options.branchKind !== "production") return;
|
|
6
|
+
if (options.branchKind !== "production") return { firstProductionDeploy: false };
|
|
7
7
|
if (!options.appId) {
|
|
8
8
|
renderFirstProductionDeployLine(context, options.appName);
|
|
9
|
-
return;
|
|
9
|
+
return { firstProductionDeploy: true };
|
|
10
10
|
}
|
|
11
11
|
const currentLiveDeployment = resolveCurrentProductionDeployment(await provider.listDeployments(options.appId).catch((error) => {
|
|
12
12
|
throw productionDeployInspectionFailedError(error);
|
|
13
13
|
}));
|
|
14
14
|
if (!currentLiveDeployment) {
|
|
15
15
|
renderFirstProductionDeployLine(context, options.appName);
|
|
16
|
-
return;
|
|
16
|
+
return { firstProductionDeploy: true };
|
|
17
17
|
}
|
|
18
18
|
if (!options.prod) throw productionDeployRequiresFlagError();
|
|
19
19
|
if (context.flags.yes) {
|
|
20
20
|
renderProductionDeployYesLine(context);
|
|
21
|
-
return;
|
|
21
|
+
return { firstProductionDeploy: false };
|
|
22
22
|
}
|
|
23
23
|
if (!canPrompt(context)) throw productionDeployConfirmationRequiredError(options.appName);
|
|
24
24
|
renderProductionDeployConfirmation(context, currentLiveDeployment);
|
|
@@ -28,6 +28,7 @@ async function enforceProductionDeployGate(context, provider, options) {
|
|
|
28
28
|
message: "Deploy to production?",
|
|
29
29
|
initialValue: false
|
|
30
30
|
})) throw productionDeployCancelledError();
|
|
31
|
+
return { firstProductionDeploy: false };
|
|
31
32
|
}
|
|
32
33
|
function resolveCurrentProductionDeployment(result) {
|
|
33
34
|
if (result.deployments.length === 0) return null;
|
|
@@ -1,25 +1,75 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { Result, TaggedError, UnhandledException } from "better-result";
|
|
3
4
|
//#region src/lib/project/local-pin.ts
|
|
4
5
|
const LOCAL_RESOLUTION_PIN_RELATIVE_PATH = ".prisma/local.json";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
var LocalResolutionPinInvalidJsonError = class extends TaggedError("LocalResolutionPinInvalidJsonError")() {
|
|
7
|
+
constructor(cause) {
|
|
8
|
+
super({
|
|
9
|
+
message: `${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} contains invalid JSON.`,
|
|
10
|
+
cause,
|
|
11
|
+
pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var LocalResolutionPinInvalidShapeError = class extends TaggedError("LocalResolutionPinInvalidShapeError")() {
|
|
16
|
+
constructor() {
|
|
17
|
+
super({
|
|
18
|
+
message: `${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} must contain workspaceId and projectId string fields only.`,
|
|
19
|
+
pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var LocalResolutionPinReadAbortedError = class extends TaggedError("LocalResolutionPinReadAbortedError")() {
|
|
24
|
+
constructor(cause) {
|
|
25
|
+
super({
|
|
26
|
+
message: `Reading ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} was aborted.`,
|
|
27
|
+
cause,
|
|
28
|
+
pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
|
|
11
29
|
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
async function readLocalResolutionPin(cwd, signal) {
|
|
33
|
+
return Result.gen(async function* () {
|
|
34
|
+
yield* ensureLocalResolutionPinReadNotAborted(signal);
|
|
35
|
+
const file = yield* Result.await(readLocalResolutionPinFile(cwd, signal));
|
|
36
|
+
if (file.kind === "missing") return Result.ok({ kind: "missing" });
|
|
37
|
+
const parsed = yield* parseLocalResolutionPin(file.raw);
|
|
38
|
+
if (!isLocalResolutionPin(parsed)) return Result.err(new LocalResolutionPinInvalidShapeError());
|
|
39
|
+
return Result.ok({
|
|
15
40
|
kind: "present",
|
|
16
41
|
pin: parsed
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function ensureLocalResolutionPinReadNotAborted(signal) {
|
|
46
|
+
return Result.try({
|
|
47
|
+
try: () => signal?.throwIfAborted(),
|
|
48
|
+
catch: (cause) => new LocalResolutionPinReadAbortedError(cause)
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async function readLocalResolutionPinFile(cwd, signal) {
|
|
52
|
+
const readResult = await Result.tryPromise({
|
|
53
|
+
try: () => readFile(path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH), {
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
signal
|
|
56
|
+
}),
|
|
57
|
+
catch: (cause) => signal?.aborted ? new LocalResolutionPinReadAbortedError(cause) : new UnhandledException({ cause })
|
|
58
|
+
});
|
|
59
|
+
if (readResult.isErr()) {
|
|
60
|
+
if (readResult.error instanceof UnhandledException && readResult.error.cause.code === "ENOENT") return Result.ok({ kind: "missing" });
|
|
61
|
+
return Result.err(readResult.error);
|
|
22
62
|
}
|
|
63
|
+
return Result.ok({
|
|
64
|
+
kind: "present",
|
|
65
|
+
raw: readResult.value
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function parseLocalResolutionPin(raw) {
|
|
69
|
+
return Result.try({
|
|
70
|
+
try: () => JSON.parse(raw),
|
|
71
|
+
catch: (cause) => cause instanceof SyntaxError ? new LocalResolutionPinInvalidJsonError(cause) : new UnhandledException({ cause })
|
|
72
|
+
});
|
|
23
73
|
}
|
|
24
74
|
async function writeLocalResolutionPin(cwd, pin, signal) {
|
|
25
75
|
const prismaDir = path.join(cwd, ".prisma");
|
|
@@ -3,6 +3,7 @@ import { formatCommandArgument } from "../../shell/command-arguments.js";
|
|
|
3
3
|
import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "./local-pin.js";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { matchError } from "better-result";
|
|
6
7
|
//#region src/lib/project/resolution.ts
|
|
7
8
|
async function resolveProjectTarget(options) {
|
|
8
9
|
const localPin = await readImplicitLocalPin(options, { allowEnvProjectId: true });
|
|
@@ -224,7 +225,6 @@ async function resolveBoundProjectTarget(options, projects, settings) {
|
|
|
224
225
|
}
|
|
225
226
|
const localPin = settings.localPin;
|
|
226
227
|
if (!localPin) return null;
|
|
227
|
-
if (localPin.kind === "invalid") throw localStateStaleError();
|
|
228
228
|
if (localPin.kind === "present") {
|
|
229
229
|
if (localPin.pin.workspaceId !== options.workspace.id) throw localProjectWorkspaceMismatchError({
|
|
230
230
|
pinnedWorkspaceId: localPin.pin.workspaceId,
|
|
@@ -247,7 +247,9 @@ async function resolveBoundProjectTarget(options, projects, settings) {
|
|
|
247
247
|
}
|
|
248
248
|
async function readImplicitLocalPin(options, settings) {
|
|
249
249
|
if (options.explicitProject || settings.allowEnvProjectId && options.envProjectId) return null;
|
|
250
|
-
const
|
|
250
|
+
const localPinResult = await readLocalResolutionPin(options.context.runtime.cwd, options.context.runtime.signal);
|
|
251
|
+
if (localPinResult.isErr()) throw localPinReadErrorToProjectError(localPinResult.error);
|
|
252
|
+
const localPin = localPinResult.value;
|
|
251
253
|
if (localPin.kind === "present" && localPin.pin.workspaceId !== options.workspace.id) throw localProjectWorkspaceMismatchError({
|
|
252
254
|
pinnedWorkspaceId: localPin.pin.workspaceId,
|
|
253
255
|
pinnedProjectId: localPin.pin.projectId,
|
|
@@ -255,6 +257,18 @@ async function readImplicitLocalPin(options, settings) {
|
|
|
255
257
|
});
|
|
256
258
|
return localPin;
|
|
257
259
|
}
|
|
260
|
+
function localPinReadErrorToProjectError(error) {
|
|
261
|
+
return matchError(error, {
|
|
262
|
+
LocalResolutionPinInvalidJsonError: () => localStateStaleError(),
|
|
263
|
+
LocalResolutionPinInvalidShapeError: () => localStateStaleError(),
|
|
264
|
+
LocalResolutionPinReadAbortedError: (error) => {
|
|
265
|
+
throw error;
|
|
266
|
+
},
|
|
267
|
+
UnhandledException: (error) => {
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
258
272
|
function resolvedTarget(workspace, project, projectSource, resolutionDetails) {
|
|
259
273
|
return {
|
|
260
274
|
workspace,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma/cli",
|
|
3
|
-
"version": "3.0.0-dev.
|
|
3
|
+
"version": "3.0.0-dev.73.1",
|
|
4
4
|
"description": "Command-line interface for the Prisma Developer Platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"@prisma/compute-sdk": "^0.22.0",
|
|
40
40
|
"@prisma/credentials-store": "^7.8.0",
|
|
41
41
|
"@prisma/management-api-sdk": "^1.37.0",
|
|
42
|
+
"better-result": "^2.9.2",
|
|
42
43
|
"c12": "4.0.0-beta.5",
|
|
43
44
|
"colorette": "^2.0.20",
|
|
44
45
|
"commander": "^14.0.3",
|