From f31ed78bee3246df8640dc5ebadc3537cca6c5e1 Mon Sep 17 00:00:00 2001 From: Lee Danilek Date: Tue, 4 Feb 2025 18:54:36 -0500 Subject: [PATCH] npx convex self-host deploy (#34039) 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 --- npm-packages/convex/src/cli/deploy.ts | 118 +-------------------- npm-packages/convex/src/cli/lib/command.ts | 67 ++++++++++++ npm-packages/convex/src/cli/lib/deploy2.ts | 103 ++++++++++++++++++ npm-packages/convex/src/cli/selfHost.ts | 41 +++++-- 4 files changed, 208 insertions(+), 121 deletions(-) diff --git a/npm-packages/convex/src/cli/deploy.ts b/npm-packages/convex/src/cli/deploy.ts index 737f4cc0..ac030c3e 100644 --- a/npm-packages/convex/src/cli/deploy.ts +++ b/npm-packages/convex/src/cli/deploy.ts @@ -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 { @@ -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") @@ -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 ", - `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 ", - "Whether to regenerate code in `convex/_generated/` before pushing.", - ) - .choices(["enable", "disable"] as const) - .default("enable" as const), - ) - .addOption( - new Option( - "--cmd ", - "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 ", - "Environment variable name to set Convex deployment URL (e.g. `VITE_CONVEX_URL`) when using `--cmd`", - ), - ) + .addDeployOptions() .addOption( new Option( "--preview-run ", @@ -103,12 +65,9 @@ export const deploy = new Command("deploy") .default("enable" as const) .hideHelp(), ) - .addOption(new Option("--debug-bundle-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 ").hideHelp()) .addOption(new Option("--url ").hideHelp()) - .addOption(new Option("--write-push-request ").hideHelp()) // Option used for tests in backend .addOption( new Option( "--preview-name ", @@ -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 ").hideHelp()) .showHelpAfterError() .action(async (cmdOptions) => { @@ -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( diff --git a/npm-packages/convex/src/cli/lib/command.ts b/npm-packages/convex/src/cli/lib/command.ts index 2e6cee61..4d4cbf53 100644 --- a/npm-packages/convex/src/cli/lib/command.ts +++ b/npm-packages/convex/src/cli/lib/command.ts @@ -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; + } + >; } } @@ -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 ", + `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 ", + "Whether to regenerate code in `convex/_generated/` before pushing.", + ) + .choices(["enable", "disable"] as const) + .default("enable" as const), + ) + .addOption( + new Option( + "--cmd ", + "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 ", + "Environment variable name to set Convex deployment URL (e.g. `VITE_CONVEX_URL`) when using `--cmd`", + ), + ) + .addOption(new Option("--debug-bundle-path ").hideHelp()) + .addOption(new Option("--debug").hideHelp()) + .addOption(new Option("--write-push-request ").hideHelp()) + .addOption(new Option("--live-component-sources").hideHelp()); +}; diff --git a/npm-packages/convex/src/cli/lib/deploy2.ts b/npm-packages/convex/src/cli/lib/deploy2.ts index e1c43218..4041a3b4 100644 --- a/npm-packages/convex/src/cli/lib/deploy2.ts +++ b/npm-packages/convex/src/cli/lib/deploy2.ts @@ -3,8 +3,11 @@ import { Context, logError, logFailure, + logFinishedStep, logVerbose, + showSpinner, } from "../../bundler/context.js"; +import { spawnSync } from "child_process"; import { deploymentFetch, ErrorData, @@ -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); @@ -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`, + ); +} diff --git a/npm-packages/convex/src/cli/selfHost.ts b/npm-packages/convex/src/cli/selfHost.ts index b5add2de..16f951f6 100644 --- a/npm-packages/convex/src/cli/selfHost.ts +++ b/npm-packages/convex/src/cli/selfHost.ts @@ -4,6 +4,8 @@ import { handleManuallySetUrlAndAdminKey } from "./configure.js"; import { devAgainstDeployment } from "./lib/dev.js"; import { normalizeDevOptions } from "./lib/command.js"; import { getConfiguredCredentialsFromEnvVar } from "./lib/deployment.js"; +import { storeAdminKeyEnvVar } from "./lib/api.js"; +import { deployToDeployment } from "./lib/deploy2.js"; export const selfHost = new Command("self-host"); @@ -17,6 +19,7 @@ selfHost " 3. Runs the provided function (if `--run` is used)\n" + " 4. Watches for file changes, and repeats step 2\n", ) + .allowExcessArguments(false) .addDevOptions() .option( "--admin-key ", @@ -35,13 +38,37 @@ selfHost const devOptions = await normalizeDevOptions(ctx, cmdOptions); - const credentials = await selfHostCredentials(ctx, cmdOptions); + const credentials = await selfHostCredentials(ctx, true, cmdOptions); await devAgainstDeployment(ctx, credentials, devOptions); }); +selfHost + .command("deploy") + .summary("Deploy to your deployment") + .description("Deploy to your deployment.") + .allowExcessArguments(false) + .addDeployOptions() + .option( + "--admin-key ", + "An admin key for the deployment. Can alternatively be set as `CONVEX_DEPLOY_KEY` environment variable.", + ) + .option( + "--url ", + "The url of the deployment. Can alternatively be set as `CONVEX_SELF_HOST_DEPLOYMENT_URL` environment variable.", + ) + .action(async (cmdOptions) => { + const ctx = oneoffContext(); + + storeAdminKeyEnvVar(cmdOptions.adminKey); + const credentials = await selfHostCredentials(ctx, false, cmdOptions); + + await deployToDeployment(ctx, credentials, cmdOptions); + }); + async function selfHostCredentials( ctx: OneoffCtx, + writeEnvVarsToFile: boolean, cmdOptions: { adminKey?: string; url?: string; @@ -51,11 +78,13 @@ async function selfHostCredentials( const urlOverride = cmdOptions.url ?? envVarCredentials.url; const adminKeyOverride = cmdOptions.adminKey ?? envVarCredentials.adminKey; if (urlOverride !== undefined && adminKeyOverride !== undefined) { - const credentials = await handleManuallySetUrlAndAdminKey(ctx, { - url: urlOverride, - adminKey: adminKeyOverride, - }); - return { ...credentials }; + if (writeEnvVarsToFile) { + await handleManuallySetUrlAndAdminKey(ctx, { + url: urlOverride, + adminKey: adminKeyOverride, + }); + } + return { url: urlOverride, adminKey: adminKeyOverride }; } return await ctx.crash({ exitCode: 1,