Skip to content
Open
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
6 changes: 3 additions & 3 deletions docs/product/command-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ prisma-cli app run --build-type nextjs
prisma-cli app run --build-type bun --entry server.ts --port 3000
```

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

Purpose:

Expand Down Expand Up @@ -630,7 +630,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`
- accepts repeated `--env NAME=VALUE` flags
- accepts repeated `--env NAME=VALUE` flags and dotenv file paths such as `--env .env`
- supports `--db` for preview Branches to create a new empty Prisma Postgres database, apply a supported local Prisma schema source when one exists, and write branch-scoped `DATABASE_URL` and `DIRECT_URL` overrides through the existing `project env` storage
- supports `--no-db` to suppress automatic database prompting for the deploy
- `--db` and `--no-db` are mutually exclusive; passing both is rejected
Expand All @@ -645,7 +645,7 @@ Behavior:
- when no supported Prisma schema source is found, `--db` still creates the database and env overrides but skips schema setup
- known non-Postgres Prisma sources do not trigger automatic database prompting; explicit `--db` is rejected because the created database is Prisma Postgres
- if schema setup fails, deploy stops before the app build/deploy starts
- inline `--env DATABASE_URL=...` or `--env DIRECT_URL=...` suppresses automatic database prompting; combining those inline env vars with `--db` is rejected
- `--env DATABASE_URL=...`, `--env DIRECT_URL=...`, or the same keys loaded from an env file suppress automatic database prompting; combining those database env vars with `--db` is rejected
- maps user-facing framework names to deploy build strategies
- uses `src/index.ts` as the Hono deploy entrypoint when the app has no `package.json#main` or `package.json#module` and that file exists
- supports vanilla Bun apps with `--framework bun` using `package.json#main` or `package.json#module`, or with `--entry <path>`
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ function createDeployCommand(runtime: CliRuntime): Command {
.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")
new Option("--env <name=value|file>", "Environment variable assignment or dotenv file")
.argParser(collectRepeatableValues),
)
.addOption(new Option("--db", "Create and wire an isolated database for the preview Branch"))
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/controllers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import type { ProjectResolution, ProjectSummary } from "../types/project";
import { requireComputeAuth } from "../lib/auth/guard";
import { readAuthState } from "../lib/auth/auth-ops";
import { getApiBaseUrl, SERVICE_TOKEN_ENV_VAR } from "../lib/auth/client";
import { envVarNames, parseEnvAssignments } from "../lib/app/env-vars";
import { envVarNames, parseEnvInputs } from "../lib/app/env-vars";
import { renderDeployOutputRows, renderDeploySettingsPreview } from "../lib/app/deploy-output";
import {
DEFAULT_LOCAL_DEV_PORT,
Expand Down Expand Up @@ -280,7 +280,7 @@ export async function runAppDeploy(
let runtime = resolveDeployRuntime(options?.httpPort, framework);
assertSupportedEntrypoint(framework.buildType, options?.entrypoint, "deploy");
const envVars = toOptionalEnvVars(
parseEnvAssignments(options?.envAssignments, {
await parseEnvInputs(context.runtime.cwd, options?.envAssignments, {
commandName: "deploy",
}),
);
Expand Down Expand Up @@ -325,7 +325,7 @@ export async function runAppDeploy(
const portMapping = parseDeployPortMapping(String(runtime.port));
const branchDatabaseSetup = await maybeSetupBranchDatabase(context, provider, projectId, toBranchDatabaseDeployBranch(target.branch), {
db: options?.db,
inlineEnvVars: envVars,
providedEnvVars: envVars,
});

const progressState = createPreviewDeployProgressState();
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/src/lib/app/branch-database-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,19 @@ export async function maybeSetupBranchDatabase(
branch: BranchDatabaseDeployBranch,
options: {
db: boolean | undefined;
inlineEnvVars: Record<string, string> | undefined;
providedEnvVars: Record<string, string> | undefined;
},
): Promise<BranchDatabaseSetupOutcome> {
if (options.db === false) {
return emptyBranchDatabaseSetupOutcome();
}

if (hasInlineDatabaseEnvVars(options.inlineEnvVars)) {
if (hasProvidedDatabaseEnvVars(options.providedEnvVars)) {
if (options.db === true) {
throw usageError(
"Branch database setup cannot be combined with inline database env vars",
"The deploy command received --db and an inline DATABASE_URL or DIRECT_URL value.",
"Remove the inline --env database value to let --db create a branch override, or remove --db to deploy with the provided value.",
"Branch 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 a branch override, or remove --db to deploy with the provided value.",
[
"prisma-cli app deploy --db",
"prisma-cli app deploy --env DATABASE_URL=postgresql://example",
Expand Down Expand Up @@ -336,7 +336,7 @@ function findEnvVar(
return rows.find((row) => row.branchId === options.branchId) ?? null;
}

function hasInlineDatabaseEnvVars(envVars: Record<string, string> | undefined): boolean {
function hasProvidedDatabaseEnvVars(envVars: Record<string, string> | undefined): boolean {
return Boolean(envVars && ("DATABASE_URL" in envVars || "DIRECT_URL" in envVars));
}

Expand Down
16 changes: 11 additions & 5 deletions packages/cli/src/lib/app/env-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface EnvFileAssignment {
value: string;
}

type EnvFileCommand = "add" | "update" | "deploy";

interface ParsedEnvFileKey {
key: string;
line: number;
Expand All @@ -20,7 +22,7 @@ const ASSIGNMENT_KEY_PATTERN = /^\s*(?:export\s+)?([^#=\s]+)\s*=/;
export async function readEnvFileAssignments(
cwd: string,
filePath: string,
command: "add" | "update",
command: EnvFileCommand,
): Promise<EnvFileAssignment[]> {
const resolvedPath = path.resolve(cwd, filePath);
let contents: string;
Expand All @@ -31,7 +33,11 @@ export async function readEnvFileAssignments(
`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`],
[
command === "deploy"
? "prisma-cli app deploy --env .env"
: `prisma-cli project env ${command} --file .env --role preview`,
],
"app",
);
}
Expand All @@ -42,7 +48,7 @@ export async function readEnvFileAssignments(
export function parseEnvFileContents(
contents: string,
filePath: string,
command: "add" | "update",
command: EnvFileCommand,
): EnvFileAssignment[] {
const parsedKeys = extractParsedKeys(contents);
if (parsedKeys.length === 0) {
Expand Down Expand Up @@ -135,10 +141,10 @@ function validateEnvFileKey(
key: string,
line: number,
filePath: string,
command: "add" | "update",
command: EnvFileCommand,
): void {
try {
validateKey(key, command);
validateKey(key, command === "deploy" ? "add" : command);
} catch (error) {
const reason = error instanceof Error && error.message.length > 0
? error.message
Expand Down
66 changes: 61 additions & 5 deletions packages/cli/src/lib/app/env-vars.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { usageError } from "../../shell/errors";
import { validateKey } from "./env-config";
import { readEnvFileAssignments } from "./env-file";

type EnvAssignmentOptions = {
commandName: "deploy";
requireAtLeastOne?: boolean;
};

export function parseEnvAssignments(
assignments: string[] | undefined,
options: {
commandName: "deploy";
requireAtLeastOne?: boolean;
},
options: EnvAssignmentOptions,
): Record<string, string> {
const values = assignments ?? [];

Expand Down Expand Up @@ -44,6 +48,7 @@ export function parseEnvAssignments(
"app",
);
}
validateEnvAssignmentName(name, options.commandName);

if (seen.has(name)) {
throw usageError(
Expand All @@ -55,13 +60,64 @@ export function parseEnvAssignments(
);
}

const value = assignment.slice(separatorIndex + 1);
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",
);
}

seen.add(name);
parsed[name] = assignment.slice(separatorIndex + 1);
parsed[name] = value;
}

return parsed;
}

export async function parseEnvInputs(
cwd: string,
inputs: string[] | undefined,
options: EnvAssignmentOptions,
): Promise<Record<string, string>> {
const values = inputs ?? [];
const expandedAssignments: string[] = [];

for (const value of values) {
if (value.includes("=")) {
expandedAssignments.push(value);
continue;
}

const fileAssignments = await readEnvFileAssignments(cwd, value, options.commandName);
expandedAssignments.push(
...fileAssignments.map((assignment) => `${assignment.key}=${assignment.value}`),
);
}

return parseEnvAssignments(expandedAssignments, options);
}

function validateEnvAssignmentName(name: string, commandName: EnvAssignmentOptions["commandName"]): void {
try {
validateKey(name, "add");
} catch (error) {
const reason = error instanceof Error && error.message.length > 0
? error.message
: "Invalid environment variable name.";
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",
);
}
}

export function envVarNames(
envVars: Record<string, string | null> | undefined,
): string[] {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/shell/command-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ const DESCRIPTORS: CommandDescriptor[] = [
"prisma-cli app deploy --project proj_123",
"prisma-cli app deploy --create-project my-app --yes",
"prisma-cli app deploy --app my-app --env DATABASE_URL=postgresql://example",
"prisma-cli app deploy --env .env",
"prisma-cli app deploy --db",
"prisma-cli app deploy --db --yes",
"prisma-cli app deploy --app my-app --framework nextjs --http-port 3000",
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/tests/app-branch-database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,7 @@ describe("app deploy branch database setup", () => {
expect(createBranchDatabase).toHaveBeenCalled();
});

it("rejects --db when deploy also passes inline database env vars", async () => {
it("rejects --db when deploy also passes database env vars", async () => {
const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient());
const createBranchDatabase = vi.fn();
const deployApp = vi.fn();
Expand All @@ -914,6 +914,7 @@ describe("app deploy branch database setup", () => {
const { createTempCwd, createTestCommandContext } = await import("./helpers");
const { runAppDeploy } = await import("../src/controllers/app");
const cwd = await createTempCwd();
await writeFile(path.join(cwd, ".env"), "DATABASE_URL=postgresql://example\n");
const { context } = await createTestCommandContext({
cwd,
stateDir: path.join(cwd, ".state"),
Expand All @@ -930,12 +931,12 @@ describe("app deploy branch database setup", () => {
projectRef: "proj_123",
branchName: "feature/db",
framework: "hono",
envAssignments: ["DATABASE_URL=postgresql://example"],
envAssignments: [".env"],
db: true,
})).rejects.toMatchObject({
code: "USAGE_ERROR",
domain: "app",
summary: "Branch database setup cannot be combined with inline database env vars",
summary: "Branch database setup cannot be combined with provided database env vars",
});
expect(createBranchDatabase).not.toHaveBeenCalled();
expect(deployApp).not.toHaveBeenCalled();
Expand Down
45 changes: 40 additions & 5 deletions packages/cli/tests/app-env-vars.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,20 +158,41 @@ describe("app env vars", () => {
expect(JSON.stringify(emptyValueError)).not.toContain("secret");
});

it("parses repeated env assignments and allows empty values", async () => {
it("parses repeated env assignments", async () => {
const { parseEnvAssignments } = await import("../src/lib/app/env-vars");

expect(
parseEnvAssignments(
[
"DATABASE_URL=postgresql://example",
"EMPTY=",
"TOKEN=value=with=equals",
],
{ commandName: "deploy" },
),
).toEqual({
DATABASE_URL: "postgresql://example",
EMPTY: "",
TOKEN: "value=with=equals",
});
});

it("parses deploy env inputs from assignments and dotenv files", async () => {
const { createTempCwd } = await import("./helpers");
const { parseEnvInputs } = await import("../src/lib/app/env-vars");
const cwd = await createTempCwd();
await writeFile(
path.join(cwd, ".env"),
[
"DATABASE_URL=postgresql://example",
"FEATURE_FLAG=enabled",
].join("\n"),
);

await expect(
parseEnvInputs(cwd, [".env", "INLINE_FLAG=enabled"], { commandName: "deploy" }),
).resolves.toEqual({
DATABASE_URL: "postgresql://example",
FEATURE_FLAG: "enabled",
INLINE_FLAG: "enabled",
});
});

Expand All @@ -190,6 +211,19 @@ describe("app env vars", () => {
summary: "Environment variable name is required",
}),
);
expect(() => parseEnvAssignments(["lowercase-key=secret"], { commandName: "deploy" })).toThrowError(
expect.objectContaining({
code: "USAGE_ERROR",
summary: 'Invalid environment variable "lowercase-key"',
why: expect.stringContaining("must match the POSIX env-var shape"),
}),
);
expect(() => parseEnvAssignments(["EMPTY="], { commandName: "deploy" })).toThrowError(
expect.objectContaining({
code: "USAGE_ERROR",
summary: 'Environment variable "EMPTY" has an empty value',
}),
);

try {
parseEnvAssignments(
Expand Down Expand Up @@ -446,6 +480,7 @@ describe("app env vars", () => {
const { createTempCwd, createTestCommandContext } = await import("./helpers");
const { runAppDeploy } = await import("../src/controllers/app");
const cwd = await createTempCwd();
await writeFile(path.join(cwd, ".env"), "FEATURE_FLAG=enabled\n");
const stateDir = path.join(cwd, ".state");
const { context } = await createTestCommandContext({
cwd,
Expand All @@ -462,7 +497,7 @@ describe("app env vars", () => {
{
projectRef: "proj_123",
framework: "hono",
envAssignments: ["DATABASE_URL=postgresql://example", "FEATURE_FLAG=enabled", "EMPTY="],
envAssignments: ["DATABASE_URL=postgresql://example", ".env", "INLINE_FLAG=enabled"],
},
);

Expand All @@ -473,7 +508,7 @@ describe("app env vars", () => {
envVars: {
DATABASE_URL: "postgresql://example",
FEATURE_FLAG: "enabled",
EMPTY: "",
INLINE_FLAG: "enabled",
},
}),
);
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/tests/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ describe("app commands", () => {
expect(deployHelp.stderr).toContain("$ prisma-cli app deploy");
expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --project proj_123");
expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --create-project my-app --yes");
expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --env .env");
expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --db");
expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --db --yes");
expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --app my-app --framework nextjs --http-port 3000");
Expand All @@ -159,7 +160,7 @@ describe("app commands", () => {
expect(deployHelp.stderr).toContain("--framework <name>");
expect(deployHelp.stderr).not.toContain("--build-type <type>");
expect(deployHelp.stderr).toContain("--http-port <port>");
expect(deployHelp.stderr).toContain("--env <name=value>");
expect(deployHelp.stderr).toContain("--env <name=value|file>");
expect(deployHelp.stderr).toContain("--db");
expect(deployHelp.stderr).toContain("--no-db");

Expand Down
Loading