convex
Version:
Client for the Convex Cloud
415 lines (391 loc) • 11.9 kB
text/typescript
import chalk from "chalk";
import { Command, Option } from "@commander-js/extra-typings";
import inquirer from "inquirer";
import {
Context,
logError,
logFailure,
logFinishedStep,
logMessage,
oneoffContext,
showSpinner,
} from "../bundler/context.js";
import {
fetchDeploymentCredentialsWithinCurrentProject,
deploymentSelectionFromOptions,
projectSelection,
storeAdminKeyEnvVar,
} from "./lib/api.js";
import {
gitBranchFromEnvironment,
isNonProdBuildEnvironment,
suggestedEnvVarName,
} from "./lib/envvars.js";
import { PushOptions, runPush } from "./lib/push.js";
import {
CONVEX_DEPLOY_KEY_ENV_VAR_NAME,
bigBrainAPI,
getConfiguredDeploymentName,
readAdminKeyFromEnvVar,
} from "./lib/utils.js";
import { spawnSync } from "child_process";
import { runFunctionAndLog } from "./lib/run.js";
import { usageStateWarning } from "./lib/usage.js";
import {
deploymentTypeFromAdminKey,
getConfiguredDeploymentFromEnvVar,
isPreviewDeployKey,
} from "./lib/deployment.js";
export const deploy = new Command("deploy")
.summary("Deploy to your prod deployment")
.description(
"Deploy to your deployment. By default, this deploys to your prod deployment.\n\n" +
"Deploys to a preview deployment if the `CONVEX_DEPLOY_KEY` environment variable is set to a Preview Deploy Key.",
)
.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),
)
.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(
"--preview-run <functionName>",
"Function to run if deploying to a preview deployment. This is ignored if deploying to a production deployment.",
),
)
.addOption(
new Option(
"--preview-create <name>",
"The name to associate with this deployment if deploying to a newly created preview deployment. Defaults to the current Git branch name in Vercel, Netlify and Github CI. This is ignored if deploying to a production deployment.",
).conflicts("preview-name"),
)
.addOption(
new Option(
"--check-build-environment <mode>",
"Whether to check for a non-production build environment before deploying to a production Convex deployment.",
)
.choices(["enable", "disable"] as const)
.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(
"--preview-name <name>",
"[deprecated] Use `--preview-create` instead. The name to associate with this deployment if deploying to a preview deployment.",
)
.hideHelp()
.conflicts("preview-create"),
)
.showHelpAfterError()
.action(async (cmdOptions) => {
const ctx = oneoffContext;
storeAdminKeyEnvVar(cmdOptions.adminKey);
const configuredDeployKey = readAdminKeyFromEnvVar() ?? null;
if (
cmdOptions.checkBuildEnvironment === "enable" &&
isNonProdBuildEnvironment() &&
configuredDeployKey !== null &&
deploymentTypeFromAdminKey(configuredDeployKey) === "prod"
) {
logError(
ctx,
`Detected a non-production build environment and "${CONVEX_DEPLOY_KEY_ENV_VAR_NAME}" for a production Convex deployment.\n
This is probably unintentional.
`,
);
await ctx.crash(1);
}
await usageStateWarning(ctx);
if (
configuredDeployKey !== null &&
isPreviewDeployKey(configuredDeployKey)
) {
if (cmdOptions.previewName !== undefined) {
logError(
ctx,
"The `--preview-name` flag has been deprecated in favor of `--preview-create`. Please re-run the command using `--preview-create` instead.",
);
await ctx.crash(1);
}
await deployToNewPreviewDeployment(ctx, {
...cmdOptions,
configuredDeployKey,
});
} else {
await deployToExistingDeployment(ctx, cmdOptions);
}
});
async function deployToNewPreviewDeployment(
ctx: Context,
options: {
configuredDeployKey: string;
dryRun?: boolean | undefined;
previewCreate?: string | undefined;
previewRun?: string | undefined;
cmdUrlEnvVarName?: string | undefined;
cmd?: string | undefined;
verbose?: boolean | undefined;
typecheck: "enable" | "try" | "disable";
codegen: "enable" | "disable";
debug?: boolean | undefined;
debugBundlePath?: string | undefined;
},
) {
const previewName = options.previewCreate ?? gitBranchFromEnvironment();
if (previewName === null) {
logError(
ctx,
"`npx convex deploy` to a preview deployment could not determine the preview name. Provide one using `--preview-create`",
);
await ctx.crash(1);
}
if (options.dryRun) {
logFinishedStep(
ctx,
`Would have claimed preview deployment for "${previewName}"`,
);
await runCommand(ctx, {
cmdUrlEnvVarName: options.cmdUrlEnvVarName,
cmd: options.cmd,
dryRun: !!options.dryRun,
url: "https://<PREVIEW DEPLOYMENT>.convex.cloud",
});
logFinishedStep(
ctx,
`Would have deployed Convex functions to preview deployment for "${previewName}"`,
);
if (options.previewRun !== undefined) {
logMessage(ctx, `Would have run function "${options.previewRun}"`);
}
return;
}
const data = await bigBrainAPI({
ctx,
method: "POST",
url: "claim_preview_deployment",
data: {
projectSelection: await projectSelection(
ctx,
await getConfiguredDeploymentName(ctx),
options.configuredDeployKey,
),
identifier: previewName,
},
});
const previewAdminKey = data.adminKey;
const previewUrl = data.instanceUrl;
await runCommand(ctx, { ...options, url: previewUrl });
const pushOptions: PushOptions = {
adminKey: previewAdminKey,
verbose: !!options.verbose,
dryRun: false,
typecheck: options.typecheck,
debug: !!options.debug,
debugBundlePath: options.debugBundlePath,
codegen: options.codegen === "enable",
url: previewUrl,
enableComponents: false,
};
showSpinner(ctx, `Deploying to ${previewUrl}...`);
await runPush(oneoffContext, pushOptions);
logFinishedStep(ctx, `Deployed Convex functions to ${previewUrl}`);
if (options.previewRun !== undefined) {
await runFunctionAndLog(
ctx,
previewUrl,
previewAdminKey,
options.previewRun,
{},
{
onSuccess: () => {
logFinishedStep(
ctx,
`Finished running function "${options.previewRun}"`,
);
},
},
);
}
}
async function deployToExistingDeployment(
ctx: Context,
options: {
verbose?: boolean | undefined;
dryRun?: boolean | undefined;
yes?: boolean | undefined;
typecheck: "enable" | "try" | "disable";
codegen: "enable" | "disable";
cmd?: string | undefined;
cmdUrlEnvVarName?: string | undefined;
debugBundlePath?: string | undefined;
debug?: boolean | undefined;
adminKey?: string | undefined;
url?: string | undefined;
},
) {
const deploymentSelection = deploymentSelectionFromOptions({
...options,
prod: true,
});
const { name: configuredDeploymentName, type: configuredDeploymentType } =
getConfiguredDeploymentFromEnvVar();
const { adminKey, url, deploymentName, deploymentType } =
await fetchDeploymentCredentialsWithinCurrentProject(
ctx,
deploymentSelection,
);
if (
deploymentSelection.kind !== "deployKey" &&
deploymentName !== undefined &&
deploymentType !== undefined &&
configuredDeploymentName !== null
) {
const shouldPushToProd =
deploymentName === configuredDeploymentName ||
(options.yes ??
(await askToConfirmPush(
ctx,
{
configuredName: configuredDeploymentName,
configuredType: configuredDeploymentType,
requestedName: deploymentName,
requestedType: deploymentType,
},
url,
)));
if (!shouldPushToProd) {
await ctx.crash(1);
}
}
await runCommand(ctx, { ...options, url });
const pushOptions: PushOptions = {
adminKey,
verbose: !!options.verbose,
dryRun: !!options.dryRun,
typecheck: options.typecheck,
debug: !!options.debug,
debugBundlePath: options.debugBundlePath,
codegen: options.codegen === "enable",
url,
enableComponents: false,
};
showSpinner(
ctx,
`Deploying to ${url}...${options.dryRun ? " [dry run]" : ""}`,
);
await runPush(oneoffContext, 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) {
logFailure(ctx, `'${options.cmd}' failed`);
await ctx.crash(1);
}
}
logFinishedStep(
ctx,
`${options.dryRun ? "Would have run" : "Ran"} "${
options.cmd
}" with environment variable "${urlVar}" set`,
);
}
async function askToConfirmPush(
ctx: Context,
deployment: {
configuredName: string;
configuredType: string | null;
requestedName: string;
requestedType: string;
},
prodUrl: string,
) {
logMessage(
ctx,
`\
You're currently developing against your ${chalk.bold(
deployment.configuredType ?? "dev",
)} deployment
${deployment.configuredName} (set in CONVEX_DEPLOYMENT)
Your ${chalk.bold(deployment.requestedType)} deployment ${chalk.bold(
deployment.requestedName,
)} serves traffic at:
${(await suggestedEnvVarName(ctx)).envVar}=${chalk.bold(prodUrl)}
Make sure that your published client is configured with this URL (for instructions see https://docs.convex.dev/hosting)\n`,
);
return (
await inquirer.prompt([
{
type: "confirm",
name: "shouldPush",
message: `Do you want to push your code to your ${deployment.requestedType} deployment ${deployment.requestedName} now?`,
default: true,
},
])
).shouldPush;
}