Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/product/command-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1012,7 +1012,7 @@ prisma-cli app run --build-type bun --entry server.ts --port 3000
prisma-cli app run api
```

## `prisma-cli app deploy [app] --project <id-or-name> --create-project <name> --app <name> --branch <name> --framework <nextjs|nuxt|astro|hono|nestjs|tanstack-start|custom|bun> --entry <path> --http-port <port> --region <region> --env <name=value|file> --db --no-db --prod`
## `prisma-cli app deploy [app] --project <id-or-name> --create-project <name> --app <name> --branch <name> --framework <nextjs|nuxt|astro|hono|nestjs|tanstack-start|custom|bun> --entry <path> --http-port <port> --region <region> --env <name=value|file> --db --no-db --prod --no-promote`

Purpose:

Expand Down Expand Up @@ -1053,7 +1053,7 @@ same reasons they are excluded today.
- the `[app]` argument selects an `apps` target by key:
- without an `[app]` argument, a command run from inside a target's `root` selects that target, so `cd apps/api && prisma-cli app deploy` deploys `api`; the deepest matching root wins and an ambiguous tie selects nothing
- with multiple `apps` entries, no `[app]` argument, and no target inferred from the invocation directory, deploy deploys every target sequentially in declaration order — the config declares the system, and a bare `prisma-cli app deploy` ships it
- deploying all targets rejects per-app inputs (`--app`, `--framework`, `--entry`, `--http-port`, `--region`, `--env`, `PRISMA_APP_ID`) with a usage error; project- and branch-level flags (`--project`, `--create-project`, `--branch`, `--db`, `--no-db`, `--prod`, `--yes`) apply to the whole run, with `--create-project` creating and binding the Project once before the first target
- deploying all targets rejects per-app inputs (`--app`, `--framework`, `--entry`, `--http-port`, `--region`, `--env`, `PRISMA_APP_ID`) with a usage error; project- and branch-level flags (`--project`, `--create-project`, `--branch`, `--db`, `--no-db`, `--prod`, `--no-promote`, `--yes`) apply to the whole run, with `--create-project` creating and binding the Project once before the first target
- a deploy-all run stops at the first failure and reports the targets already live; `--json` output aggregates one full deploy result per target
- `app build` and `app run` still require a target in multi-app configs and fail with `COMPUTE_CONFIG_TARGET_REQUIRED` (a dev server cannot run N apps at once; build keeps the same shape)
- an `[app]` argument that matches no target fails with `COMPUTE_CONFIG_TARGET_UNKNOWN`
Expand Down Expand Up @@ -1120,6 +1120,8 @@ Behavior:
- resolves or creates app context inside the resolved branch from `--app`, `PRISMA_APP_ID`, `package.json#name`, or current directory name
- auto-promotes the first production deploy for an App without `--prod`
- requires `--prod` for subsequent deploys to a production Branch; `--yes` only skips the confirmation prompt when `--prod` is also present
- supports `--no-promote` to build a new deployment without promoting it to live: the previous deployment keeps serving, and the new deployment is reachable only at its own candidate URL until a later `app promote <deployment-id>` makes it live
- `--no-promote` never replaces the live deployment, so it bypasses the production gate: `app deploy --no-promote` on a production Branch builds a candidate without `--prod` and without confirmation. It is the CI build-then-verify path — build the candidate, health-check its URL, then `app promote`
- does not prompt when there is no real choice; zero matching apps creates the inferred app
- writes `.prisma/local.json` after Project binding succeeds and before build/deploy starts, so retries after a failed deploy do not repeat setup
- before asking `Customize build settings? (y/N)`, previews the detected framework and runtime so the user can see the defaults they are accepting or changing
Expand All @@ -1135,6 +1137,7 @@ Behavior:
- after setup, deploy prints `Deploying to <Project> / <Branch> / <App>`; later deploys print a compact target header such as `Deploying ./j1 to j1 / main / j1`
- deploy progress uses short stage copy (`Building locally...`, `Built <size>`, `Uploading...`, `Uploaded`, `Deploying...`, `Deployed`) and never prints `Status: running` or `Deployment is running at ...`
- success human output prints `Live in <duration>`, the URL on its own line, and `Logs prisma-cli app logs`
- with `--no-promote`, success human output instead prints `Built <deployment-id> in <duration> (not promoted)`, the candidate URL on its own line, a note that the live deployment is unchanged, and a `Promote prisma-cli app promote <deployment-id>` next step
- accepts repeated `--env NAME=VALUE` flags and dotenv file paths such as `--env .env`
- supports `--db` to create a new empty Prisma Postgres database and write `DATABASE_URL` and `DIRECT_URL` through the existing `project env` storage; the CLI never runs schema or migration commands — applying the schema stays with the user's own tooling
- `--db` is the opinionated single-database path: it creates **one** branch database and exposes `DATABASE_URL`/`DIRECT_URL` as branch-scoped env vars, so every app on the branch shares it. In a multi-app deploy-all run the database is created once (on the first target) and reused by the rest; the run never creates a database per app. Per-app or per-service database topologies are explicit-configuration territory — set each app's database env vars yourself rather than relying on `--db` to infer app-to-database ownership
Expand Down Expand Up @@ -1176,6 +1179,7 @@ prisma-cli app deploy --no-db
prisma-cli app deploy --framework nextjs --http-port 3000
prisma-cli app deploy --branch feat-login --framework hono --http-port 3000
prisma-cli app deploy --prod --yes
prisma-cli app deploy --no-promote
prisma-cli app deploy --framework bun --entry src/server.ts --http-port 3000
prisma-cli app deploy --entry src/server.ts --http-port 3000
prisma-cli app deploy web
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/src/commands/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,13 @@ function createDeployCommand(runtime: CliRuntime): Command {
),
)
.addOption(new Option("--no-db", "Skip database setup"))
.addOption(new Option("--prod", "Confirm intent to deploy to production"));
.addOption(new Option("--prod", "Confirm intent to deploy to production"))
.addOption(
new Option(
"--no-promote",
"Build the new deployment without promoting it to live (promote later with app promote <id>)",
),
);
addGlobalFlags(command);

command.action(async (configTarget: string | undefined, options) => {
Expand All @@ -262,6 +268,7 @@ function createDeployCommand(runtime: CliRuntime): Command {
const createProjectName = (options as { createProject?: string })
.createProject;
const prod = (options as { prod?: boolean }).prod;
const noPromote = (options as { promote?: boolean }).promote === false;
const db = (options as { db?: boolean }).db;
const hasDbConflict =
hasFlag(runtime.argv, "--db") && hasFlag(runtime.argv, "--no-db");
Expand Down Expand Up @@ -291,6 +298,7 @@ function createDeployCommand(runtime: CliRuntime): Command {
region,
envAssignments,
prod: prod === true,
noPromote,
db,
configTarget,
});
Expand Down
55 changes: 36 additions & 19 deletions packages/cli/src/controllers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@
region?: string;
envAssignments?: string[];
prod?: boolean;
noPromote?: boolean;
db?: boolean;
configTarget?: string;
}
Expand Down Expand Up @@ -582,7 +583,7 @@
});
}

async function runSingleAppDeploy(

Check notice on line 586 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/complexity/noExcessiveCognitiveComplexity

Excessive complexity of 17 detected (max: 15).
context: CommandContext,
appName: string | undefined,
options: AppDeployOptions | undefined,
Expand Down Expand Up @@ -739,16 +740,18 @@
framework = customized.framework;
runtime = customized.runtime;

const productionDeployGate = await enforceProductionDeployGate(
context,
provider,
{
appId: selectedApp.appId,
appName: selectedApp.displayName,
branchKind: target.branch.kind,
prod: options?.prod === true,
},
);
const noPromote = options?.noPromote === true;
// A promotionless deploy never replaces the live deployment, so the
// production-confirmation gate does not apply: --no-promote on the production
// branch builds a candidate without --prod.
const productionDeployGate = noPromote
? { firstProductionDeploy: false }
: await enforceProductionDeployGate(context, provider, {
appId: selectedApp.appId,
appName: selectedApp.displayName,
branchKind: target.branch.kind,
prod: options?.prod === true,
});

// Customization can switch from a Bun-compatible framework to one that
// derives its entrypoint from build output, so validate --entry again after it.
Expand Down Expand Up @@ -801,6 +804,7 @@
buildSettings: buildSettingsResolution.settings,
portMapping,
envVars,
skipPromote: noPromote,
interaction: undefined,
signal: context.runtime.signal,
progress: createDeployProgress(
Expand All @@ -819,11 +823,18 @@
id: deployResult.app.id,
name: deployResult.app.name,
});
await context.stateStore.setKnownLiveDeployment(
projectId,
deployResult.app.id,
deployResult.deployment.id,
);
// With --no-promote the live deployment is unchanged, so cache the actually-live
// id (never the un-promoted candidate); skip when the app has nothing live yet.
const knownLiveDeploymentId = deployResult.promoted
? deployResult.deployment.id
: deployResult.app.liveDeploymentId;
if (knownLiveDeploymentId) {
await context.stateStore.setKnownLiveDeployment(
projectId,
deployResult.app.id,
knownLiveDeploymentId,
);
}

return {
command: "app.deploy",
Expand All @@ -838,6 +849,7 @@
name: deployResult.app.name,
},
deployment: deployResult.deployment,
promoted: deployResult.promoted,
deploySettings: {
config: {
path: buildSettingsResolution.relativeConfigPath,
Expand Down Expand Up @@ -871,10 +883,15 @@
...legacyWarnings,
...branchDatabaseSetup.warnings,
],
nextSteps: [
"prisma-cli app list-deploys",
`prisma-cli app show-deploy ${deployResult.deployment.id}`,
],
nextSteps: deployResult.promoted
? [
"prisma-cli app list-deploys",
`prisma-cli app show-deploy ${deployResult.deployment.id}`,
]
: [
`prisma-cli app promote ${deployResult.deployment.id}`,
`prisma-cli app show-deploy ${deployResult.deployment.id}`,
],
};
}

Expand Down Expand Up @@ -1157,9 +1174,9 @@
...deployment.deployment,
live: providerLiveDeploymentId
? deployment.deployment.id === providerLiveDeploymentId
: knownLiveDeploymentId
? deployment.deployment.id === knownLiveDeploymentId
: deployment.deployment.live,

Check notice on line 1179 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.
},
},
warnings: [],
Expand Down Expand Up @@ -1538,10 +1555,10 @@
});
}

await sleep(
Math.min(pollIntervalMs, Math.max(deadline - Date.now(), 0)),
context.runtime.signal,
);

Check notice on line 1561 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/noAwaitInLoops

Avoid using await inside loops.
current = await target.provider
.showDomain(current.id, { signal: context.runtime.signal })
.catch((error) => {
Expand Down Expand Up @@ -2103,7 +2120,7 @@
const compute = await resolveComputeManagementContext(
context,
options?.configTarget,
commandName.replace(/^app /, ""),

Check warning on line 2123 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
);
const branch = resolveDomainBranch(options?.branchName);
if (toBranchKind(branch.name) !== "production") {
Expand Down Expand Up @@ -2236,7 +2253,7 @@
}

function normalizeDomainHostname(hostname: string): string {
const normalized = hostname.trim().replace(/\.$/, "").toLowerCase();

Check warning on line 2256 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
if (!isValidDomainHostname(normalized)) {
throw new CliError({
code: "DOMAIN_HOSTNAME_INVALID",
Expand Down Expand Up @@ -2271,14 +2288,14 @@
}

return labels.every((label) =>
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label),

Check warning on line 2291 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
);
}

function sameDomainHostname(left: string, right: string): boolean {
return (
left.trim().replace(/\.$/, "").toLowerCase() ===

Check warning on line 2297 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
right.trim().replace(/\.$/, "").toLowerCase()

Check warning on line 2298 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
);
}

Expand Down Expand Up @@ -2366,7 +2383,7 @@
}
}

function domainCommandError(

Check notice on line 2386 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/complexity/noExcessiveCognitiveComplexity

Excessive complexity of 26 detected (max: 15).
command: AppDomainCommand,
error: unknown,
hostname: string,
Expand Down Expand Up @@ -2503,7 +2520,7 @@
text.includes("no cname") ||
text.includes("cname record") ||
text.includes("no a/aaaa") ||
/\bcname(?:s)?\s+to\b/.test(text)

Check warning on line 2523 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
);
}

Expand Down Expand Up @@ -2532,7 +2549,7 @@

function extractDomainDnsTarget(error: DomainApiError): string | null {
const text = `${error.hint ?? ""} ${error.message}`;
const match = /\b((?:[a-z0-9-]+\.)+prisma\.build)\b/i.exec(text);

Check warning on line 2552 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
return match?.[1]?.toLowerCase() ?? null;
}

Expand Down Expand Up @@ -2584,11 +2601,11 @@
const multiplier =
unit === "h"
? 60 * 60 * 1000
: unit === "m"
? 60 * 1000
: unit === "s"
? 1000
: 1;

Check notice on line 2608 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.

Check notice on line 2608 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.
return amount * multiplier;
}

Expand Down Expand Up @@ -3362,7 +3379,7 @@
listProjects: () =>
listRealWorkspaceProjects(
client,
authState.workspace!,

Check warning on line 3382 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
context.runtime.signal,
),
commandName: options?.commandName,
Expand Down Expand Up @@ -3396,7 +3413,7 @@
};
}

async function resolveDeployProjectContext(

Check notice on line 3416 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/complexity/noExcessiveCognitiveComplexity

Excessive complexity of 18 detected (max: 15).
context: CommandContext,
client: ManagementApiClient,
provider: ReturnType<typeof createAppProvider>,
Expand Down Expand Up @@ -4071,7 +4088,7 @@
continue;
}

const configFile = await detectFrameworkConfigFile(cwd, framework, signal);

Check notice on line 4091 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/noAwaitInLoops

Avoid using await inside loops.
if (
!configFile.exists &&
!hasAnyPackageDependency(packageJson, framework.detectPackages)
Expand All @@ -4084,8 +4101,8 @@
const annotation =
framework.key === "nextjs" && configFile.standalone
? "standalone output detected"
: configFile.exists
? `detected from ${path.basename(configFile.path!)}`

Check warning on line 4105 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
: "detected from package.json";

Check notice on line 4106 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.

return {
Expand All @@ -4108,10 +4125,10 @@
const filePath = path.join(cwd, candidate);
signal.throwIfAborted();
try {
const content = await readFile(filePath, { encoding: "utf8", signal });

Check notice on line 4128 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/noAwaitInLoops

Avoid using await inside loops.
return {
exists: true,
standalone: /\boutput\s*:\s*["'`]standalone["'`]/.test(content),

Check warning on line 4131 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
path: filePath,
};
} catch (error) {
Expand Down
13 changes: 12 additions & 1 deletion packages/cli/src/lib/app/app-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ export interface DeployRecord {
id: string;
status: string;
url: string | null;
live: boolean;
};
promoted: boolean;
}

export interface EnvRecord {
Expand Down Expand Up @@ -229,6 +231,7 @@ export interface AppProvider {
buildSettings?: AppBuildSettings;
portMapping?: PortMapping;
envVars?: Record<string, string>;
skipPromote?: boolean;
interaction?: unknown;
signal?: AbortSignal;
progress?: unknown;
Expand Down Expand Up @@ -511,6 +514,7 @@ export function createAppProvider(
region: resolvedApp.region,
portMapping: options.portMapping,
envVars: options.envVars,
skipPromote: options.skipPromote,
timeoutSeconds: 120,
pollIntervalMs: 2000,
interaction: options.interaction as never,
Expand All @@ -524,13 +528,18 @@ export function createAppProvider(

const deployed = deployResult.value;

// On a promotionless deploy the SDK leaves appEndpointDomain null and the
// previous deployment serving live, so the live pointer stays on the old
// deployment and both URL expressions resolve to the candidate endpoint.
return {
projectId: deployed.projectId,
app: {
id: deployed.appId,
name: deployed.appName,
region: deployed.region ?? null,
liveDeploymentId: deployed.deploymentId,
liveDeploymentId: deployed.promoted
? deployed.deploymentId
: deployed.previousDeploymentId,
liveUrl: toAbsoluteUrl(deployed.appEndpointDomain ?? null),
},
deployment: {
Expand All @@ -541,7 +550,9 @@ export function createAppProvider(
deployed.deploymentEndpointDomain ??
null,
),
live: deployed.promoted,
},
promoted: deployed.promoted,
};
},

Expand Down
25 changes: 23 additions & 2 deletions packages/cli/src/presenters/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,33 @@ export function renderAppDeploy(
const logsCommand = options?.logsTarget
? `prisma-cli app logs ${options.logsTarget}`
: "prisma-cli app logs";
const headline = result.promoted
? [
`Live in ${formatDuration(result.durationMs)}`,
...(result.deployment.url
? [context.ui.link(result.deployment.url)]
: []),
]
: [
`Built ${result.deployment.id} in ${formatDuration(result.durationMs)} (not promoted)`,
...(result.deployment.url
? [context.ui.link(result.deployment.url)]
: []),
context.ui.dim("The live deployment is unchanged."),
];
const lines = [
`Live in ${formatDuration(result.durationMs)}`,
...(result.deployment.url ? [context.ui.link(result.deployment.url)] : []),
...headline,
...renderBranchDatabaseDeploySummary(context, result),
"",
...renderDeployOutputRows(context.ui, [
...(result.promoted
? []
: [
{
label: "Promote",
value: `prisma-cli app promote ${result.deployment.id}`,
},
]),
{ label: "Logs", value: logsCommand },
]),
...renderDeployResolvedContextBlock(context, result),
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/types/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ export interface AppDeployResult {
id: string;
status: string;
url: string | null;
live: boolean;
};
/** Whether the new deployment was promoted to live. False for --no-promote. */
promoted: boolean;
deploySettings: AppDeploySettings;
durationMs: number;
localPin?: {
Expand Down
Loading
Loading