From 752f3f3d252122ee8864e29fed20ff5d73f68fb7 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:24:32 +0000 Subject: [PATCH] add push to deploy command for self-hosting --- packages/cli-v3/src/commands/deploy.ts | 135 ++++++++++++++++++------- 1 file changed, 97 insertions(+), 38 deletions(-) diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index c06937852..836e3e26a 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -33,6 +33,7 @@ import { detectPackageNameFromImportPath } from "../utilities/installPackages"; import { logger } from "../utilities/logger.js"; import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles"; import { login } from "./login"; +import { SetOptional } from "type-fest"; const DeployCommandOptions = CommonCommandOptions.extend({ skipTypecheck: z.boolean().default(false), @@ -42,7 +43,7 @@ const DeployCommandOptions = CommonCommandOptions.extend({ buildPlatform: z.enum(["linux/amd64", "linux/arm64"]).default("linux/amd64"), selfHosted: z.boolean().default(false), registry: z.string().optional(), - pushImage: z.boolean().default(false), + push: z.boolean().default(false), config: z.string().optional(), projectRef: z.string().optional(), outputMetafile: z.string().optional(), @@ -81,14 +82,14 @@ export function configureDeployCommand(program: Command) { ) .addOption( new CommandOption( - "--push-image", - "(Coming soon) When using the --self-hosted flag, push the image to the default registry. (defaults to false when not using --registry)" + "--push", + "When using the --self-hosted flag, push the image to the default registry. (defaults to false when not using --registry)" ).hideHelp() ) .addOption( new CommandOption( "--registry ", - "(Coming soon) The registry to push the image to when using --self-hosted" + "The registry to push the image to when using --self-hosted" ).hideHelp() ) .addOption( @@ -134,7 +135,11 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { intro("Deploying project"); - const authorization = await login({ embedded: true, defaultApiUrl: options.apiUrl, profile: options.profile }); + const authorization = await login({ + embedded: true, + defaultApiUrl: options.apiUrl, + profile: options.profile, + }); if (!authorization.ok) { if (authorization.error === "fetch failed") { @@ -228,11 +233,13 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { const deploymentSpinner = spinner(); deploymentSpinner.start(`Deploying version ${version}`); - const registryHost = deploymentResponse.data.registryHost ?? options.registry ?? "registry.trigger.dev"; + const selfHostedRegistryHost = deploymentResponse.data.registryHost ?? options.registry; + const registryHost = selfHostedRegistryHost ?? "registry.trigger.dev"; const buildImage = async () => { if (options.selfHosted) { return buildAndPushSelfHostedImage({ + registryHost: selfHostedRegistryHost, imageTag: deploymentResponse.data.imageTag, cwd: compilation.path, projectId: resolvedConfig.config.project, @@ -241,6 +248,8 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { contentHash: deploymentResponse.data.contentHash, projectRef: resolvedConfig.config.project, buildPlatform: options.buildPlatform, + pushImage: options.push, + selfHostedRegistry: !!options.registry, }); } @@ -277,7 +286,9 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { } const imageReference = options.selfHosted - ? `${image.image}${image.digest ? `@${image.digest}` : ""}` + ? `${selfHostedRegistryHost ? `${selfHostedRegistryHost}/` : ""}${image.image}${ + image.digest ? `@${image.digest}` : "" + }` : `${registryHost}/${image.image}${image.digest ? `@${image.digest}` : ""}`; span?.setAttributes({ @@ -330,7 +341,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { const deploymentLink = terminalLink( "View deployment", - `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.config.project}/deployments/${finishedDeployment.id}` + `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.config.project}/deployments/${finishedDeployment.shortCode}` ); switch (finishedDeployment.status) { @@ -345,7 +356,8 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { ); } else { outro( - `Version ${version} deployed with ${taskCount} detected task${taskCount === 1 ? "" : "s" + `Version ${version} deployed with ${taskCount} detected task${ + taskCount === 1 ? "" : "s" } ${deploymentLink}` ); } @@ -509,14 +521,14 @@ type BuildAndPushImageOptions = { type BuildAndPushImageResults = | { - ok: true; - image: string; - digest?: string; - } + ok: true; + image: string; + digest?: string; + } | { - ok: false; - error: string; - }; + ok: false; + error: string; + }; async function buildAndPushImage( options: BuildAndPushImageOptions @@ -624,10 +636,16 @@ async function buildAndPushImage( }); } -type BuildAndPushSelfHostedImageOptions = Omit< - BuildAndPushImageOptions, - "registryHost" | "buildId" | "buildToken" | "buildProjectId" | "auth" | "loadImage" ->; +type BuildAndPushSelfHostedImageOptions = SetOptional< + Omit< + BuildAndPushImageOptions, + "buildId" | "buildToken" | "buildProjectId" | "auth" | "loadImage" + >, + "registryHost" +> & { + pushImage: boolean; + selfHostedRegistry: boolean; +}; async function buildAndPushSelfHostedImage( options: BuildAndPushSelfHostedImageOptions @@ -643,7 +661,9 @@ async function buildAndPushSelfHostedImage( "options.projectRef": options.projectRef, }); - const args = [ + const imageRef = `${options.registryHost ? `${options.registryHost}/` : ""}${options.imageTag}`; + + const buildArgs = [ "build", "-f", "Containerfile", @@ -660,48 +680,41 @@ async function buildAndPushSelfHostedImage( "--build-arg", `TRIGGER_PROJECT_REF=${options.projectRef}`, "-t", - `${options.imageTag}`, + imageRef, ".", // The build context ].filter(Boolean) as string[]; - logger.debug(`docker ${args.join(" ")}`); + logger.debug(`docker ${buildArgs.join(" ")}`); - span.setAttribute("docker.command", `docker ${args.join(" ")}`); + span.setAttribute("docker.command.build", `docker ${buildArgs.join(" ")}`); - // Step 4: Build and push the image - const childProcess = execa("docker", args, { + // Build the image + const buildProcess = execa("docker", buildArgs, { cwd: options.cwd, }); const errors: string[] = []; + let digest: string | undefined; try { await new Promise((res, rej) => { // For some reason everything is output on stderr, not stdout - childProcess.stderr?.on("data", (data: Buffer) => { + buildProcess.stderr?.on("data", (data: Buffer) => { const text = data.toString(); errors.push(text); logger.debug(text); }); - childProcess.on("error", (e) => rej(e)); - childProcess.on("close", () => res()); + buildProcess.on("error", (e) => rej(e)); + buildProcess.on("close", () => res()); }); - const digest = extractImageDigest(errors); + digest = extractImageDigest(errors); span.setAttributes({ "image.digest": digest, }); - - span.end(); - - return { - ok: true as const, - image: options.imageTag, - digest, - }; } catch (e) { recordSpanException(span, e); @@ -712,6 +725,52 @@ async function buildAndPushSelfHostedImage( error: e instanceof Error ? e.message : JSON.stringify(e), }; } + + const pushArgs = ["push", imageRef].filter(Boolean) as string[]; + + logger.debug(`docker ${pushArgs.join(" ")}`); + + span.setAttribute("docker.command.push", `docker ${pushArgs.join(" ")}`); + + if (options.selfHostedRegistry || options.pushImage) { + // Push the image + const pushProcess = execa("docker", pushArgs, { + cwd: options.cwd, + }); + + try { + await new Promise((res, rej) => { + // For some reason everything is output on stderr, not stdout + pushProcess.stderr?.on("data", (data: Buffer) => { + const text = data.toString(); + + logger.debug(text); + }); + + pushProcess.on("error", (e) => rej(e)); + pushProcess.on("close", () => res()); + }); + + span.end(); + } catch (e) { + recordSpanException(span, e); + + span.end(); + + return { + ok: false as const, + error: e instanceof Error ? e.message : JSON.stringify(e), + }; + } + } + + span.end(); + + return { + ok: true as const, + image: options.imageTag, + digest, + }; }); }