11from __future__ import annotations
22
3+ from typing import NamedTuple
34from pathlib import Path
45
56from rich .console import Console
89from agentex .lib .cli .debug import DebugConfig
910from agentex .lib .utils .logging import make_logger
1011from agentex .lib .cli .handlers .run_handlers import RunError , run_agent as _run_agent
11- from agentex .lib .sdk .config .agent_manifest import AgentManifest
12+ from agentex .lib .sdk .config .agent_manifest import AgentManifest , BuildContextManager
1213
1314logger = make_logger (__name__ )
1415console = Console ()
@@ -18,6 +19,17 @@ class DockerBuildError(Exception):
1819 """An error occurred during docker build"""
1920
2021
22+ class CloudBuildContext (NamedTuple ):
23+ """Contains the prepared build context for cloud builds."""
24+
25+ archive_bytes : bytes
26+ dockerfile_path : str
27+ agent_name : str
28+ tag : str
29+ image_name : str
30+ build_context_size_kb : float
31+
32+
2133def build_agent (
2234 manifest_path : str ,
2335 registry_url : str ,
@@ -42,9 +54,7 @@ def build_agent(
4254 The image URL
4355 """
4456 agent_manifest = AgentManifest .from_yaml (file_path = manifest_path )
45- build_context_root = (
46- Path (manifest_path ).parent / agent_manifest .build .context .root
47- ).resolve ()
57+ build_context_root = (Path (manifest_path ).parent / agent_manifest .build .context .root ).resolve ()
4858
4959 repository_name = repository_name or agent_manifest .agent .name
5060
@@ -85,9 +95,7 @@ def build_agent(
8595 key , value = arg .split ("=" , 1 )
8696 docker_build_args [key ] = value
8797 else :
88- logger .warning (
89- f"Invalid build arg format: { arg } . Expected KEY=VALUE"
90- )
98+ logger .warning (f"Invalid build arg format: { arg } . Expected KEY=VALUE" )
9199
92100 if docker_build_args :
93101 docker_build_kwargs ["build_args" ] = docker_build_args
@@ -100,9 +108,7 @@ def build_agent(
100108 if push :
101109 # Build and push in one step for multi-platform builds
102110 logger .info ("Building and pushing image..." )
103- docker_build_kwargs ["push" ] = (
104- True # Push directly after build for multi-platform
105- )
111+ docker_build_kwargs ["push" ] = True # Push directly after build for multi-platform
106112 docker .buildx .build (** docker_build_kwargs )
107113
108114 logger .info (f"Successfully built and pushed { image_name } " )
@@ -146,15 +152,127 @@ def signal_handler(signum, _frame):
146152 shutting_down = True
147153 logger .info (f"Received signal { signum } , shutting down..." )
148154 raise KeyboardInterrupt ()
149-
155+
150156 # Set up signal handling for the main thread
151157 signal .signal (signal .SIGINT , signal_handler )
152158 signal .signal (signal .SIGTERM , signal_handler )
153-
159+
154160 try :
155161 asyncio .run (_run_agent (manifest_path , debug_config ))
156162 except KeyboardInterrupt :
157163 logger .info ("Shutdown completed." )
158164 sys .exit (0 )
159165 except RunError as e :
160166 raise RuntimeError (str (e )) from e
167+
168+
169+ def parse_build_args (build_args : list [str ] | None ) -> dict [str , str ]:
170+ """Parse build arguments from KEY=VALUE format to a dictionary.
171+
172+ Args:
173+ build_args: List of build arguments in KEY=VALUE format
174+
175+ Returns:
176+ Dictionary mapping keys to values
177+ """
178+ result : dict [str , str ] = {}
179+ if not build_args :
180+ return result
181+
182+ for arg in build_args :
183+ if "=" in arg :
184+ key , value = arg .split ("=" , 1 )
185+ result [key ] = value
186+ else :
187+ logger .warning (f"Invalid build arg format: { arg } . Expected KEY=VALUE" )
188+
189+ return result
190+
191+
192+ def prepare_cloud_build_context (
193+ manifest_path : str ,
194+ tag : str | None = None ,
195+ build_args : list [str ] | None = None ,
196+ ) -> CloudBuildContext :
197+ """Prepare the build context for cloud-based container builds.
198+
199+ Reads the manifest, prepares the build context by copying files according to
200+ the include_paths and dockerignore, then creates a compressed tar.gz archive
201+ ready for upload to a cloud build service.
202+
203+ Args:
204+ manifest_path: Path to the agent manifest file
205+ tag: Image tag override (if None, reads from manifest's deployment.image.tag)
206+ build_args: List of build arguments in KEY=VALUE format
207+
208+ Returns:
209+ CloudBuildContext containing the archive bytes, dockerfile path, and metadata
210+ """
211+ agent_manifest = AgentManifest .from_yaml (file_path = manifest_path )
212+ build_context_root = (Path (manifest_path ).parent / agent_manifest .build .context .root ).resolve ()
213+
214+ agent_name = agent_manifest .agent .name
215+ dockerfile_path = agent_manifest .build .context .dockerfile
216+
217+ # Validate that the Dockerfile exists
218+ full_dockerfile_path = build_context_root / dockerfile_path
219+ if not full_dockerfile_path .exists ():
220+ raise FileNotFoundError (
221+ f"Dockerfile not found at: { full_dockerfile_path } \n "
222+ f"Check that 'build.context.dockerfile' in your manifest points to an existing file."
223+ )
224+ if not full_dockerfile_path .is_file ():
225+ raise ValueError (
226+ f"Dockerfile path is not a file: { full_dockerfile_path } \n "
227+ f"'build.context.dockerfile' must point to a file, not a directory."
228+ )
229+
230+ # Get tag and repository from manifest if not provided
231+ if tag is None :
232+ if agent_manifest .deployment and agent_manifest .deployment .image :
233+ tag = agent_manifest .deployment .image .tag
234+ else :
235+ tag = "latest"
236+
237+ # Get repository name from manifest (just the repo name, not the full registry URL)
238+ if agent_manifest .deployment and agent_manifest .deployment .image :
239+ repository = agent_manifest .deployment .image .repository
240+ if repository :
241+ # Extract just the repo name (last part after any slashes)
242+ image_name = repository .split ("/" )[- 1 ]
243+ else :
244+ image_name = "<repository>"
245+ else :
246+ image_name = "<repository>"
247+
248+ logger .info (f"Agent: { agent_name } " )
249+ logger .info (f"Image name: { image_name } " )
250+ logger .info (f"Build context root: { build_context_root } " )
251+ logger .info (f"Dockerfile: { dockerfile_path } " )
252+ logger .info (f"Tag: { tag } " )
253+
254+ if agent_manifest .build .context .include_paths :
255+ logger .info (f"Include paths: { agent_manifest .build .context .include_paths } " )
256+
257+ parsed_build_args = parse_build_args (build_args )
258+ if parsed_build_args :
259+ logger .info (f"Build args: { list (parsed_build_args .keys ())} " )
260+
261+ logger .info ("Preparing build context..." )
262+
263+ with agent_manifest .context_manager (build_context_root ) as build_context :
264+ # Compress the prepared context using the static zipped method
265+ with BuildContextManager .zipped (root_path = build_context .path ) as archive_buffer :
266+ archive_bytes = archive_buffer .read ()
267+
268+ build_context_size_kb = len (archive_bytes ) / 1024
269+ logger .info (f"Build context size: { build_context_size_kb :.1f} KB" )
270+
271+ return CloudBuildContext (
272+ archive_bytes = archive_bytes ,
273+ dockerfile_path = build_context .dockerfile_path ,
274+ agent_name = agent_name ,
275+ tag = tag ,
276+ image_name = image_name ,
277+ build_context_size_kb = build_context_size_kb ,
278+ )
0 commit comments