@@ -2,9 +2,10 @@ import { logger } from "../utilities/logger.js";
22import { depot } from "@depot/cli" ;
33import { x } from "tinyexec" ;
44import type { BuildManifest , BuildRuntime } from "@trigger.dev/core/v3/schemas" ;
5- import { networkInterfaces } from "os" ;
6- import { join } from "path" ;
5+ import { homedir , networkInterfaces , tmpdir } from "os" ;
6+ import { join , resolve } from "path" ;
77import { safeReadJSONFile } from "../utilities/fileSystem.js" ;
8+ import { mkdtemp , rm , symlink , writeFile } from "node:fs/promises" ;
89import { readFileSync } from "fs" ;
910
1011import { isLinux } from "std-env" ;
@@ -474,6 +475,10 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
474475
475476 const errors : string [ ] = [ ] ;
476477
478+ const writeRegistryAuthFile = process . env . TRIGGER_REGISTRY_AUTH === "file" ;
479+ let authConfigDir : string | undefined ;
480+ let buildEnv : NodeJS . ProcessEnv | undefined ;
481+
477482 let cloudRegistryHost : string | undefined ;
478483 if ( push && options . authenticateToRegistry ) {
479484 cloudRegistryHost =
@@ -499,32 +504,56 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
499504 } ;
500505 }
501506
502- logger . debug ( `Logging in to docker registry: ${ cloudRegistryHost } ` ) ;
507+ if ( writeRegistryAuthFile ) {
508+ const [ authDirError , result ] = await tryCatch (
509+ createRegistryAuthConfigDir ( cloudRegistryHost , credentials )
510+ ) ;
503511
504- const loginProcess = x (
505- "docker" ,
506- [ "login" , "--username" , credentials . username , "--password-stdin" , cloudRegistryHost ] ,
507- {
508- nodeOptions : {
509- cwd : options . cwd ,
510- } ,
512+ if ( authDirError ) {
513+ return {
514+ ok : false as const ,
515+ error : `Failed to write registry auth for ${ cloudRegistryHost } : ${ authDirError . message } ` ,
516+ logs : "" ,
517+ } ;
511518 }
512- ) ;
513519
514- loginProcess . process ?. stdin ?. write ( credentials . password ) ;
515- loginProcess . process ?. stdin ?. end ( ) ;
520+ authConfigDir = result ;
521+ // buildx keeps its builder state under $DOCKER_CONFIG/buildx, so point it back at
522+ // the real location while the ephemeral config dir supplies only the registry auth.
523+ const realConfigDir = resolve ( process . env . DOCKER_CONFIG ?? join ( homedir ( ) , ".docker" ) ) ;
524+ buildEnv = {
525+ ...process . env ,
526+ DOCKER_CONFIG : authConfigDir ,
527+ BUILDX_CONFIG : process . env . BUILDX_CONFIG ?? join ( realConfigDir , "buildx" ) ,
528+ } ;
529+ } else {
530+ logger . debug ( `Logging in to docker registry: ${ cloudRegistryHost } ` ) ;
531+
532+ const loginProcess = x (
533+ "docker" ,
534+ [ "login" , "--username" , credentials . username , "--password-stdin" , cloudRegistryHost ] ,
535+ {
536+ nodeOptions : {
537+ cwd : options . cwd ,
538+ } ,
539+ }
540+ ) ;
516541
517- for await ( const line of loginProcess ) {
518- errors . push ( line ) ;
519- logger . debug ( line ) ;
520- }
542+ loginProcess . process ?. stdin ?. write ( credentials . password ) ;
543+ loginProcess . process ?. stdin ?. end ( ) ;
521544
522- if ( loginProcess . exitCode !== 0 ) {
523- return {
524- ok : false as const ,
525- error : `Failed to login to registry: ${ cloudRegistryHost } ` ,
526- logs : extractLogs ( errors ) ,
527- } ;
545+ for await ( const line of loginProcess ) {
546+ errors . push ( line ) ;
547+ logger . debug ( line ) ;
548+ }
549+
550+ if ( loginProcess . exitCode !== 0 ) {
551+ return {
552+ ok : false as const ,
553+ error : `Failed to login to registry: ${ cloudRegistryHost } ` ,
554+ logs : extractLogs ( errors ) ,
555+ } ;
556+ }
528557 }
529558
530559 options . onLog ?.( `Successfully logged in to the remote registry` ) ;
@@ -598,6 +627,7 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
598627 const buildProcess = x ( "docker" , args , {
599628 nodeOptions : {
600629 cwd : options . cwd ,
630+ env : buildEnv ,
601631 } ,
602632 } ) ;
603633
@@ -610,8 +640,7 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
610640
611641 if ( buildProcess . exitCode !== 0 ) {
612642 if ( cloudRegistryHost ) {
613- logger . debug ( `Logging out from docker registry: ${ cloudRegistryHost } ` ) ;
614- await x ( "docker" , [ "logout" , cloudRegistryHost ] ) ;
643+ await clearRegistryAuth ( cloudRegistryHost , authConfigDir ) ;
615644 }
616645
617646 return {
@@ -660,8 +689,7 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
660689 }
661690
662691 if ( cloudRegistryHost ) {
663- logger . debug ( `Logging out from docker registry: ${ cloudRegistryHost } ` ) ;
664- await x ( "docker" , [ "logout" , cloudRegistryHost ] ) ;
692+ await clearRegistryAuth ( cloudRegistryHost , authConfigDir ) ;
665693 }
666694
667695 return {
@@ -672,6 +700,60 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
672700 } ;
673701}
674702
703+ // An ephemeral docker config dir holding only this deploy's registry auth entry, passed
704+ // to the build via DOCKER_CONFIG. Nothing shared is read or mutated, and a fresh config
705+ // has no credsStore, so docker resolves the entry from the file.
706+ async function createRegistryAuthConfigDir (
707+ host : string ,
708+ credentials : { username : string ; password : string }
709+ ) {
710+ const dir = await mkdtemp ( join ( tmpdir ( ) , "trigger-docker-" ) ) ;
711+ const realConfigDir = resolve ( process . env . DOCKER_CONFIG ?? join ( homedir ( ) , ".docker" ) ) ;
712+
713+ // docker login stores Docker Hub credentials under the legacy index key
714+ const bareHost = host . replace ( / ^ h t t p s ? : \/ \/ / , "" ) . replace ( / \/ .* $ / , "" ) ;
715+ const authKey =
716+ bareHost === "docker.io" || bareHost === "registry-1.docker.io"
717+ ? "https://index.docker.io/v1/"
718+ : bareHost ;
719+
720+ // Carry over the context selection (e.g. OrbStack/colima) so endpoint resolution
721+ // still works; the contexts store and CLI plugins are shared read-only via symlink.
722+ const realConfig = ( await safeReadJSONFile ( join ( realConfigDir , "config.json" ) ) ) as
723+ | { currentContext ?: string }
724+ | undefined ;
725+
726+ await writeFile (
727+ join ( dir , "config.json" ) ,
728+ JSON . stringify ( {
729+ ...( realConfig ?. currentContext ? { currentContext : realConfig . currentContext } : { } ) ,
730+ auths : {
731+ [ authKey ] : {
732+ auth : Buffer . from ( `${ credentials . username } :${ credentials . password } ` ) . toString ( "base64" ) ,
733+ } ,
734+ } ,
735+ } ) ,
736+ { mode : 0o600 }
737+ ) ;
738+
739+ await tryCatch ( symlink ( join ( realConfigDir , "contexts" ) , join ( dir , "contexts" ) ) ) ;
740+ await tryCatch ( symlink ( join ( realConfigDir , "cli-plugins" ) , join ( dir , "cli-plugins" ) ) ) ;
741+
742+ logger . debug ( `Wrote registry auth for ${ host } to ${ dir } ` ) ;
743+
744+ return dir ;
745+ }
746+
747+ async function clearRegistryAuth ( host : string , authConfigDir : string | undefined ) {
748+ if ( ! authConfigDir ) {
749+ logger . debug ( `Logging out from docker registry: ${ host } ` ) ;
750+ await x ( "docker" , [ "logout" , host ] ) ;
751+ return ;
752+ }
753+
754+ await tryCatch ( rm ( authConfigDir , { recursive : true , force : true } ) ) ;
755+ }
756+
675757function extractLogs ( outputs : string [ ] ) {
676758 // Remove empty lines
677759 const cleanedOutputs = outputs . map ( ( line ) => line . trim ( ) ) . filter ( ( line ) => line !== "" ) ;
0 commit comments