Skip to content

Commit

Permalink
npx convex self-host deploy (#34039)
Browse files Browse the repository at this point in the history
create a distilled version of `npx convex deploy` that only has the flags and behavior necessary for self-hosting.

1. refactored deployment-agnostic options into the Command interface
2. refactor out `deployToDeployment`, which is the stuff that `npx convex deploy` runs after it knows what deployment it wants to talk to.
3. create command `npx convex self-host deploy` that deploys to a self-hosted deployment

note the `handleManuallySetUrlAndAdminKey` writes VITE_CONVEX_URL to .env.local, which I think only `npx convex self-host dev` wants to do.

The flow I imagine is similar to existing `npx convex dev` and `npx convex deploy`: while developing locally you run `npx convex self-host dev`, and it uses env variables from .env.local. Then when you git push, Vercel (or whatever hosting provider) runs `npx convex self-host deploy`, which uses env variables from vercel, and pushes to potentially a different self-hosted deployment. The point of these commands is they talk to a deployment directly via url and admin key, and never have to talk to big-brain.

GitOrigin-RevId: e0d5793528f1a7a61e05567fe916605744476d3a
  • Loading branch information
ldanilek authored and Convex, Inc. committed Feb 4, 2025
1 parent 51264c3 commit f31ed78
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 121 deletions.
118 changes: 3 additions & 115 deletions npm-packages/convex/src/cli/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
getConfiguredDeployment,
readAdminKeyFromEnvVar,
} from "./lib/utils/utils.js";
import { spawnSync } from "child_process";
import { runFunctionAndLog } from "./lib/run.js";
import { usageStateWarning } from "./lib/usage.js";
import {
Expand All @@ -35,6 +34,7 @@ import {
} from "./lib/deployment.js";
import { runPush } from "./lib/components.js";
import { promptYesNo } from "./lib/utils/prompts.js";
import { deployToDeployment, runCommand } from "./lib/deploy2.js";

export const deploy = new Command("deploy")
.summary("Deploy to your prod deployment")
Expand All @@ -43,45 +43,7 @@ export const deploy = new Command("deploy")
"Deploys to a preview deployment if the `CONVEX_DEPLOY_KEY` environment variable is set to a Preview Deploy Key.",
)
.allowExcessArguments(false)
.option("-v, --verbose", "Show full listing of changes")
.option(
"--dry-run",
"Print out the generated configuration without deploying to your Convex deployment",
)
.option("-y, --yes", "Skip confirmation prompt when running locally")
.addOption(
new Option(
"--typecheck <mode>",
`Whether to check TypeScript files with \`tsc --noEmit\` before deploying.`,
)
.choices(["enable", "try", "disable"] as const)
.default("try" as const),
)
.option(
"--typecheck-components",
"Check TypeScript files within component implementations with `tsc --noEmit`.",
false,
)
.addOption(
new Option(
"--codegen <mode>",
"Whether to regenerate code in `convex/_generated/` before pushing.",
)
.choices(["enable", "disable"] as const)
.default("enable" as const),
)
.addOption(
new Option(
"--cmd <command>",
"Command to run as part of deploying your app (e.g. `vite build`). This command can depend on the environment variables specified in `--cmd-url-env-var-name` being set.",
),
)
.addOption(
new Option(
"--cmd-url-env-var-name <name>",
"Environment variable name to set Convex deployment URL (e.g. `VITE_CONVEX_URL`) when using `--cmd`",
),
)
.addDeployOptions()
.addOption(
new Option(
"--preview-run <functionName>",
Expand All @@ -103,12 +65,9 @@ export const deploy = new Command("deploy")
.default("enable" as const)
.hideHelp(),
)
.addOption(new Option("--debug-bundle-path <path>").hideHelp())
.addOption(new Option("--debug").hideHelp())
// Hidden options to pass in admin key and url for tests and local development
.addOption(new Option("--admin-key <adminKey>").hideHelp())
.addOption(new Option("--url <url>").hideHelp())
.addOption(new Option("--write-push-request <writePushRequest>").hideHelp()) // Option used for tests in backend
.addOption(
new Option(
"--preview-name <name>",
Expand All @@ -117,7 +76,6 @@ export const deploy = new Command("deploy")
.hideHelp()
.conflicts("preview-create"),
)
.addOption(new Option("--live-component-sources").hideHelp())
.addOption(new Option("--partition-id <id>").hideHelp())
.showHelpAfterError()
.action(async (cmdOptions) => {
Expand Down Expand Up @@ -330,77 +288,7 @@ async function deployToExistingDeployment(
}
}

await runCommand(ctx, { ...options, url });

const pushOptions: PushOptions = {
adminKey,
verbose: !!options.verbose,
dryRun: !!options.dryRun,
typecheck: options.typecheck,
typecheckComponents: options.typecheckComponents,
debug: !!options.debug,
debugBundlePath: options.debugBundlePath,
codegen: options.codegen === "enable",
url,
writePushRequest: options.writePushRequest,
liveComponentSources: !!options.liveComponentSources,
};
showSpinner(
ctx,
`Deploying to ${url}...${options.dryRun ? " [dry run]" : ""}`,
);
await runPush(ctx, pushOptions);
logFinishedStep(
ctx,
`${
options.dryRun ? "Would have deployed" : "Deployed"
} Convex functions to ${url}`,
);
}

async function runCommand(
ctx: Context,
options: {
cmdUrlEnvVarName?: string | undefined;
cmd?: string | undefined;
dryRun?: boolean | undefined;
url: string;
},
) {
if (options.cmd === undefined) {
return;
}

const urlVar =
options.cmdUrlEnvVarName ?? (await suggestedEnvVarName(ctx)).envVar;
showSpinner(
ctx,
`Running '${options.cmd}' with environment variable "${urlVar}" set...${
options.dryRun ? " [dry run]" : ""
}`,
);
if (!options.dryRun) {
const env = { ...process.env };
env[urlVar] = options.url;
const result = spawnSync(options.cmd, {
env,
stdio: "inherit",
shell: true,
});
if (result.status !== 0) {
await ctx.crash({
exitCode: 1,
errorType: "invalid filesystem data",
printedMessage: `'${options.cmd}' failed`,
});
}
}
logFinishedStep(
ctx,
`${options.dryRun ? "Would have run" : "Ran"} "${
options.cmd
}" with environment variable "${urlVar}" set`,
);
await deployToDeployment(ctx, { url, adminKey }, options);
}

async function askToConfirmPush(
Expand Down
67 changes: 67 additions & 0 deletions npm-packages/convex/src/cli/lib/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ declare module "@commander-js/extra-typings" {
liveComponentSources?: boolean;
}
>;

/**
* Adds common options for deploy-related commands.
*/
addDeployOptions(): Command<
Args,
Opts & {
verbose?: boolean;
dryRun?: boolean;
yes?: boolean;
typecheck: "enable" | "try" | "disable";
typecheckComponents: boolean;
codegen: "enable" | "disable";
cmd?: string;
cmdUrlEnvVarName?: string;
debugBundlePath?: string;
debug?: boolean;
writePushRequest?: string;
liveComponentSources?: boolean;
}
>;
}
}

Expand Down Expand Up @@ -188,3 +209,49 @@ export async function normalizeDevOptions(
liveComponentSources: !!cmdOptions.liveComponentSources,
};
}

Command.prototype.addDeployOptions = function () {
return this.option("-v, --verbose", "Show full listing of changes")
.option(
"--dry-run",
"Print out the generated configuration without deploying to your Convex deployment",
)
.option("-y, --yes", "Skip confirmation prompt when running locally")
.addOption(
new Option(
"--typecheck <mode>",
`Whether to check TypeScript files with \`tsc --noEmit\` before deploying.`,
)
.choices(["enable", "try", "disable"] as const)
.default("try" as const),
)
.option(
"--typecheck-components",
"Check TypeScript files within component implementations with `tsc --noEmit`.",
false,
)
.addOption(
new Option(
"--codegen <mode>",
"Whether to regenerate code in `convex/_generated/` before pushing.",
)
.choices(["enable", "disable"] as const)
.default("enable" as const),
)
.addOption(
new Option(
"--cmd <command>",
"Command to run as part of deploying your app (e.g. `vite build`). This command can depend on the environment variables specified in `--cmd-url-env-var-name` being set.",
),
)
.addOption(
new Option(
"--cmd-url-env-var-name <name>",
"Environment variable name to set Convex deployment URL (e.g. `VITE_CONVEX_URL`) when using `--cmd`",
),
)
.addOption(new Option("--debug-bundle-path <path>").hideHelp())
.addOption(new Option("--debug").hideHelp())
.addOption(new Option("--write-push-request <writePushRequest>").hideHelp())
.addOption(new Option("--live-component-sources").hideHelp());
};
103 changes: 103 additions & 0 deletions npm-packages/convex/src/cli/lib/deploy2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import {
Context,
logError,
logFailure,
logFinishedStep,
logVerbose,
showSpinner,
} from "../../bundler/context.js";
import { spawnSync } from "child_process";
import {
deploymentFetch,
ErrorData,
Expand All @@ -29,6 +32,9 @@ import { finishPushDiff, FinishPushDiff } from "./deployApi/finishPush.js";
import { Reporter, Span } from "./tracing.js";
import { promisify } from "node:util";
import zlib from "node:zlib";
import { PushOptions } from "./push.js";
import { runPush } from "./components.js";
import { suggestedEnvVarName } from "./envvars.js";

const brotli = promisify(zlib.brotliCompress);

Expand Down Expand Up @@ -299,3 +305,100 @@ export async function reportPushCompleted(
);
}
}

export async function deployToDeployment(
ctx: Context,
credentials: {
url: string;
adminKey: string;
},
options: {
verbose?: boolean | undefined;
dryRun?: boolean | undefined;
yes?: boolean | undefined;
typecheck: "enable" | "try" | "disable";
typecheckComponents: boolean;
codegen: "enable" | "disable";
cmd?: string | undefined;
cmdUrlEnvVarName?: string | undefined;

debugBundlePath?: string | undefined;
debug?: boolean | undefined;
writePushRequest?: string | undefined;
liveComponentSources?: boolean | undefined;
partitionId?: string | undefined;
},
) {
const { url, adminKey } = credentials;
await runCommand(ctx, { ...options, url });

const pushOptions: PushOptions = {
adminKey,
verbose: !!options.verbose,
dryRun: !!options.dryRun,
typecheck: options.typecheck,
typecheckComponents: options.typecheckComponents,
debug: !!options.debug,
debugBundlePath: options.debugBundlePath,
codegen: options.codegen === "enable",
url,
writePushRequest: options.writePushRequest,
liveComponentSources: !!options.liveComponentSources,
};
showSpinner(
ctx,
`Deploying to ${url}...${options.dryRun ? " [dry run]" : ""}`,
);
await runPush(ctx, pushOptions);
logFinishedStep(
ctx,
`${
options.dryRun ? "Would have deployed" : "Deployed"
} Convex functions to ${url}`,
);
}

export async function runCommand(
ctx: Context,
options: {
cmdUrlEnvVarName?: string | undefined;
cmd?: string | undefined;
dryRun?: boolean | undefined;
url: string;
},
) {
if (options.cmd === undefined) {
return;
}

const urlVar =
options.cmdUrlEnvVarName ?? (await suggestedEnvVarName(ctx)).envVar;
showSpinner(
ctx,
`Running '${options.cmd}' with environment variable "${urlVar}" set...${
options.dryRun ? " [dry run]" : ""
}`,
);
if (!options.dryRun) {
const env = { ...process.env };
env[urlVar] = options.url;
const result = spawnSync(options.cmd, {
env,
stdio: "inherit",
shell: true,
});
if (result.status !== 0) {
await ctx.crash({
exitCode: 1,
errorType: "invalid filesystem data",
printedMessage: `'${options.cmd}' failed`,
});
}
}
logFinishedStep(
ctx,
`${options.dryRun ? "Would have run" : "Ran"} "${
options.cmd
}" with environment variable "${urlVar}" set`,
);
}
Loading

0 comments on commit f31ed78

Please sign in to comment.