Skip to content

Commit 8c85535

Browse files
committed
use metadata for digest, fix local multi-platform builds
1 parent ffa725f commit 8c85535

File tree

3 files changed

+107
-29
lines changed

3 files changed

+107
-29
lines changed

packages/cli-v3/src/commands/deploy.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const DeployCommandOptions = CommonCommandOptions.extend({
4545
skipSyncEnvVars: z.boolean().default(false),
4646
env: z.enum(["prod", "staging", "preview"]),
4747
branch: z.string().optional(),
48-
loadImage: z.boolean().default(false),
48+
load: z.boolean().default(false),
4949
config: z.string().optional(),
5050
projectRef: z.string().optional(),
5151
saveLogs: z.boolean().default(false),
@@ -109,7 +109,7 @@ export function configureDeployCommand(program: Command) {
109109
).hideHelp()
110110
)
111111
.addOption(
112-
new CommandOption("--load-image", "Load the built image into your local docker").hideHelp()
112+
new CommandOption("--load", "Load the built image into your local docker").hideHelp()
113113
)
114114
.addOption(
115115
new CommandOption(
@@ -392,7 +392,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
392392
deploymentVersion: deployment.version,
393393
imageTag: deployment.imageTag,
394394
imagePlatform: deployment.imagePlatform,
395-
loadImage: options.loadImage,
395+
loadImage: options.load,
396396
contentHash: deployment.contentHash,
397397
externalBuildId: deployment.externalBuildData?.buildId,
398398
externalBuildToken: deployment.externalBuildData?.buildToken,

packages/cli-v3/src/deploy/buildImage.ts

Lines changed: 103 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { depot } from "@depot/cli";
33
import { x } from "tinyexec";
44
import { BuildManifest, BuildRuntime } from "@trigger.dev/core/v3/schemas";
55
import { networkInterfaces } from "os";
6+
import { join } from "path";
7+
import { safeReadJSONFile } from "../utilities/fileSystem.js";
8+
import { readFileSync } from "fs";
9+
import { isLinux } from "std-env";
10+
import { z } from "zod";
611

712
export interface BuildImageOptions {
813
// Common options
@@ -171,6 +176,8 @@ async function remoteBuildImage(options: DepotBuildImageOptions): Promise<BuildI
171176
options.imagePlatform,
172177
"--provenance",
173178
"false",
179+
"--metadata-file",
180+
"metadata.json",
174181
"--build-arg",
175182
`TRIGGER_PROJECT_ID=${options.projectId}`,
176183
"--build-arg",
@@ -196,7 +203,7 @@ async function remoteBuildImage(options: DepotBuildImageOptions): Promise<BuildI
196203
options.loadImage ? "--load" : undefined,
197204
].filter(Boolean) as string[];
198205

199-
logger.debug(`depot ${args.join(" ")}`);
206+
logger.debug(`depot ${args.join(" ")}`, { cwd: options.cwd });
200207

201208
// Step 4: Build and push the image
202209
const childProcess = depot(args, {
@@ -243,7 +250,21 @@ async function remoteBuildImage(options: DepotBuildImageOptions): Promise<BuildI
243250
};
244251
}
245252

246-
const digest = extractImageDigest(errors);
253+
const metadataPath = join(options.cwd, "metadata.json");
254+
const rawMetadata = await safeReadJSONFile(metadataPath);
255+
256+
const meta = BuildKitMetadata.safeParse(rawMetadata);
257+
258+
let digest: string | undefined;
259+
if (!meta.success) {
260+
logger.error("Failed to parse metadata.json", {
261+
errors: meta.error.message,
262+
path: metadataPath,
263+
});
264+
} else {
265+
logger.debug("Parsed metadata.json", { metadata: meta.data, path: metadataPath });
266+
digest = meta.data["containerimage.digest"];
267+
}
247268

248269
return {
249270
ok: true as const,
@@ -290,13 +311,12 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
290311
const lsLogs: string[] = [];
291312

292313
// List existing builders
293-
const lsProcess = x("docker", ["buildx", "ls"]);
314+
const lsProcess = x("docker", ["buildx", "ls", "--format", "{{.Name}}"]);
294315
for await (const line of lsProcess) {
295316
lsLogs.push(line);
296317
logger.debug(line);
297-
options.onLog?.(line);
298318

299-
if (line.startsWith(builder + " ")) {
319+
if (line === builder) {
300320
builderExists = true;
301321
}
302322
}
@@ -398,6 +418,8 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
398418
? false
399419
: true;
400420

421+
await ensureQemuRegistered(options.imagePlatform);
422+
401423
const args = [
402424
"buildx",
403425
"build",
@@ -412,6 +434,10 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
412434
addHost ? `--add-host=${addHost}` : undefined,
413435
shouldPush ? "--push" : undefined,
414436
options.loadImage ? "--load" : undefined,
437+
"--provenance",
438+
"false",
439+
"--metadata-file",
440+
"metadata.json",
415441
"--build-arg",
416442
`TRIGGER_PROJECT_ID=${options.projectId}`,
417443
"--build-arg",
@@ -437,9 +463,7 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
437463
".", // The build context
438464
].filter(Boolean) as string[];
439465

440-
logger.debug(`docker ${args.join(" ")}`, {
441-
cwd: options.cwd,
442-
});
466+
logger.debug(`docker ${args.join(" ")}`, { cwd: options.cwd });
443467

444468
const errors: string[] = [];
445469

@@ -463,7 +487,21 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
463487
};
464488
}
465489

466-
const digest = extractImageDigest(errors);
490+
const metadataPath = join(options.cwd, "metadata.json");
491+
const rawMetadata = await safeReadJSONFile(metadataPath);
492+
493+
const meta = BuildKitMetadata.safeParse(rawMetadata);
494+
495+
let digest: string | undefined;
496+
if (!meta.success) {
497+
logger.error("Failed to parse metadata.json", {
498+
errors: meta.error.message,
499+
path: metadataPath,
500+
});
501+
} else {
502+
logger.debug("Parsed metadata.json", { metadata: meta.data, path: metadataPath });
503+
digest = meta.data["containerimage.digest"];
504+
}
467505

468506
// Get the image size
469507
const sizeProcess = x("docker", ["image", "inspect", options.imageTag, "--format={{.Size}}"], {
@@ -500,22 +538,6 @@ function extractLogs(outputs: string[]) {
500538
return cleanedOutputs.map((line) => line.trim()).join("\n");
501539
}
502540

503-
function extractImageDigest(outputs: string[]): string | undefined {
504-
const imageDigestRegex = /pushing manifest for .+(?<digest>sha256:[a-f0-9]{64})/;
505-
506-
for (const line of outputs) {
507-
const imageDigestMatch = line.match(imageDigestRegex);
508-
509-
const digest = imageDigestMatch?.groups?.digest;
510-
511-
if (digest) {
512-
return digest;
513-
}
514-
}
515-
516-
return;
517-
}
518-
519541
export type GenerateContainerfileOptions = {
520542
runtime: BuildRuntime;
521543
build: BuildManifest["build"];
@@ -819,3 +841,59 @@ function getAddHost(apiUrl: string) {
819841

820842
return;
821843
}
844+
845+
function isQemuRegistered() {
846+
try {
847+
// Check a single QEMU handler
848+
const binfmt = readFileSync("/proc/sys/fs/binfmt_misc/qemu-aarch64", "utf8");
849+
return binfmt.includes("enabled");
850+
} catch (e) {
851+
return false;
852+
}
853+
}
854+
855+
function isMultiPlatform(imagePlatform: string) {
856+
return imagePlatform.split(",").length > 1;
857+
}
858+
859+
async function ensureQemuRegistered(imagePlatform: string) {
860+
if (isLinux && isMultiPlatform(imagePlatform) && !isQemuRegistered()) {
861+
logger.debug("Registering QEMU for multi-platform build...");
862+
863+
const ensureQemuProcess = x("docker", [
864+
"run",
865+
"--rm",
866+
"--privileged",
867+
"multiarch/qemu-user-static",
868+
"--reset",
869+
"-p",
870+
"yes",
871+
]);
872+
873+
const logs: string[] = [];
874+
for await (const line of ensureQemuProcess) {
875+
logger.debug(line);
876+
logs.push(line);
877+
}
878+
879+
if (ensureQemuProcess.exitCode !== 0) {
880+
logger.error("Failed to register QEMU for multi-platform build", {
881+
exitCode: ensureQemuProcess.exitCode,
882+
logs: logs.join("\n"),
883+
});
884+
}
885+
}
886+
}
887+
888+
const BuildKitMetadata = z.object({
889+
"buildx.build.ref": z.string().optional(),
890+
"containerimage.descriptor": z
891+
.object({
892+
mediaType: z.string(),
893+
digest: z.string(),
894+
size: z.number(),
895+
})
896+
.optional(),
897+
"containerimage.digest": z.string().optional(),
898+
"image.name": z.string().optional(),
899+
});

packages/cli-v3/src/utilities/fileSystem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export async function readJSONFile(path: string) {
5656
return JSON.parse(fileContents);
5757
}
5858

59-
export async function safeFeadJSONFile(path: string) {
59+
export async function safeReadJSONFile(path: string) {
6060
try {
6161
const fileExists = await pathExists(path);
6262

0 commit comments

Comments
 (0)