@@ -102,31 +102,76 @@ def pump(stream, sink: list[str], log_fn, prefix: str) -> None:
102102 return result
103103
104104
105- def _base_slug (image : str ) -> str :
106- return image .replace ("/" , "_s_" ).replace (":" , "_tag_" )
107-
108-
109105def _sanitize_branch (ref : str ) -> str :
110106 ref = re .sub (r"^refs/heads/" , "" , ref or "unknown" )
111107 return re .sub (r"[^a-zA-Z0-9.-]+" , "-" , ref ).lower ()
112108
113109
114- def _sdk_version () -> str :
115- from importlib .metadata import version
110+ def _base_slug (image : str , max_len : int = 64 ) -> str :
111+ """
112+ If the slug is too long, keep the most identifiable parts:
113+ - repository name (last path segment)
114+ - tag (if present)
115+ Then append a short digest for uniqueness.
116+ Format preserved with existing separators: '_s_' for '/', '_tag_' for ':'.
117+
118+ Example:
119+ 'ghcr.io_s_org_s/very-long-repo_tag_v1.2.3-extra'
120+ -> 'very-long-repo_tag_v1.2.3-<digest>'
121+ """
122+ base_slug = image .replace ("/" , "_s_" ).replace (":" , "_tag_" )
123+
124+ if len (base_slug ) <= max_len :
125+ return base_slug
126+
127+ digest = hashlib .sha256 (base_slug .encode ()).hexdigest ()[:12 ]
128+ suffix = f"-{ digest } "
129+
130+ # Parse components from the slug form
131+ if "_tag_" in base_slug :
132+ left , tag = base_slug .split ("_tag_" , 1 )
133+ else :
134+ left , tag = base_slug , ""
135+
136+ parts = left .split ("_s_" ) if left else []
137+ repo = parts [- 1 ] if parts else left # last path segment is the repo
138+
139+ # Reconstruct a compact, identifiable core: "<repo>[_tag_<tag>]"
140+ ident = repo + (f"_tag_{ tag } " if tag else "" )
141+
142+ # Fit within budget, reserving space for the digest suffix
143+ visible_budget = max_len - len (suffix )
144+ assert visible_budget > 0 , (
145+ f"max_len too small to fit digest suffix with length { len (suffix )} "
146+ )
116147
117- return version ("openhands-sdk" )
148+ kept = ident [:visible_budget ]
149+ return kept + suffix
118150
119151
120152def _git_info () -> tuple [str , str , str ]:
121- git_sha = os .environ .get ("GITHUB_SHA" )
153+ """
154+ Get git info (ref, sha, short_sha) for the current working directory.
155+
156+ Priority order for SHA:
157+ 1. SDK_SHA - Explicit override (e.g., for submodule builds)
158+ 2. GITHUB_SHA - GitHub Actions environment
159+ 3. git rev-parse HEAD - Local development
160+
161+ Priority order for REF:
162+ 1. SDK_REF - Explicit override (e.g., for submodule builds)
163+ 2. GITHUB_REF - GitHub Actions environment
164+ 3. git symbolic-ref HEAD - Local development
165+ """
166+ git_sha = os .environ .get ("SDK_SHA" ) or os .environ .get ("GITHUB_SHA" )
122167 if not git_sha :
123168 try :
124169 git_sha = _run (["git" , "rev-parse" , "--verify" , "HEAD" ]).stdout .strip ()
125170 except subprocess .CalledProcessError :
126171 git_sha = "unknown"
127172 short_sha = git_sha [:7 ] if git_sha != "unknown" else "unknown"
128173
129- git_ref = os .environ .get ("GITHUB_REF" )
174+ git_ref = os .environ .get ("SDK_REF" ) or os . environ . get ( " GITHUB_REF" )
130175 if not git_ref :
131176 try :
132177 git_ref = _run (
@@ -137,8 +182,30 @@ def _git_info() -> tuple[str, str, str]:
137182 return git_ref , git_sha , short_sha
138183
139184
185+ def _package_version () -> str :
186+ """
187+ Get the semantic version from the openhands-sdk package.
188+ This is used for versioned tags during releases.
189+ """
190+ try :
191+ from importlib .metadata import version
192+
193+ return version ("openhands-sdk" )
194+ except Exception :
195+ # If package is not installed, try reading from pyproject.toml
196+ try :
197+ sdk_root = _default_sdk_project_root ()
198+ pyproject_path = sdk_root / "openhands-sdk" / "pyproject.toml"
199+ if pyproject_path .exists ():
200+ cfg = tomllib .loads (pyproject_path .read_text (encoding = "utf-8" ))
201+ return cfg .get ("project" , {}).get ("version" , "unknown" )
202+ except Exception :
203+ pass
204+ return "unknown"
205+
206+
140207GIT_REF , GIT_SHA , SHORT_SHA = _git_info ()
141- SDK_VERSION = _sdk_version ()
208+ PACKAGE_VERSION = _package_version ()
142209
143210
144211# --- options ---
@@ -263,6 +330,13 @@ class BuildOptions(BaseModel):
263330 default = None ,
264331 description = "Architecture suffix (e.g., 'amd64', 'arm64') to append to tags" ,
265332 )
333+ include_versioned_tag : bool = Field (
334+ default = False ,
335+ description = (
336+ "Whether to include the versioned tag (e.g., v1.0.0_...) in all_tags "
337+ "output. Should only be True for release builds."
338+ ),
339+ )
266340
267341 @field_validator ("target" )
268342 @classmethod
@@ -280,46 +354,16 @@ def base_image_slug(self) -> str:
280354 return _base_slug (self .base_image )
281355
282356 @property
283- def is_dev (self ) -> bool :
284- return self .target in ( "source" , "source-minimal" )
357+ def versioned_tag (self ) -> str :
358+ return f"v { PACKAGE_VERSION } _ { self .base_image_slug } "
285359
286360 @property
287- def versioned_tag (self ) -> str :
288- return f"v { SDK_VERSION } _ { self .base_image_slug } _ { self . target } "
361+ def base_tag (self ) -> str :
362+ return f"{ SHORT_SHA } - { self .base_image_slug } "
289363
290364 @property
291365 def cache_tags (self ) -> tuple [str , str ]:
292- # Docker image tags have a 128-character limit.
293- # If the base slug is too long, hash it to create a shorter unique identifier.
294- MAX_TAG_LENGTH = 128
295- base_slug = self .base_image_slug
296-
297- # Reserve space for prefix, branch, and separators
298- prefix = f"buildcache-{ self .target } -"
299- branch_suffix = (
300- f"-{ _sanitize_branch (GIT_REF )} "
301- if GIT_REF not in ("main" , "refs/heads/main" , "unknown" )
302- else ""
303- )
304- main_suffix = "-main" if GIT_REF in ("main" , "refs/heads/main" ) else ""
305-
306- # Calculate available space for base_slug
307- reserved = len (prefix ) + max (len (branch_suffix ), len (main_suffix ))
308- available = MAX_TAG_LENGTH - reserved
309-
310- # If base_slug is too long, use a hash
311- if len (base_slug ) > available :
312- # Use first 8 chars of SHA256 hash for uniqueness while keeping it short
313- hash_digest = hashlib .sha256 (base_slug .encode ()).hexdigest ()[:12 ]
314- base_slug_short = hash_digest
315- logger .debug (
316- f"[build] Base image slug too long ({ len (base_slug )} chars), "
317- f"using hash: { base_slug_short } "
318- )
319- else :
320- base_slug_short = base_slug
321-
322- base = f"{ prefix } { base_slug_short } "
366+ base = f"buildcache-{ self .target } -{ self .base_image_slug } "
323367 if GIT_REF in ("main" , "refs/heads/main" ):
324368 return f"{ base } -main" , base
325369 elif GIT_REF != "unknown" :
@@ -332,14 +376,24 @@ def all_tags(self) -> list[str]:
332376 tags : list [str ] = []
333377 arch_suffix = f"-{ self .arch } " if self .arch else ""
334378
379+ # Use git commit SHA for commit-based tags
335380 for t in self .custom_tag_list :
336381 tags .append (f"{ self .image } :{ SHORT_SHA } -{ t } { arch_suffix } " )
382+
337383 if GIT_REF in ("main" , "refs/heads/main" ):
338384 for t in self .custom_tag_list :
339385 tags .append (f"{ self .image } :main-{ t } { arch_suffix } " )
340- tags .append (f"{ self .image } :{ self .versioned_tag } { arch_suffix } " )
341- if self .is_dev :
342- tags = [f"{ t } -dev" for t in tags ]
386+
387+ # Always include base tag as default
388+ tags .append (f"{ self .image } :{ self .base_tag } { arch_suffix } " )
389+
390+ # Only include versioned tag if requested (for releases)
391+ if self .include_versioned_tag :
392+ tags .append (f"{ self .image } :{ self .versioned_tag } { arch_suffix } " )
393+
394+ # Append target suffix for clarity (binary is default, no suffix needed)
395+ if self .target != "binary" :
396+ tags = [f"{ t } -{ self .target } " for t in tags ]
343397 return tags
344398
345399
@@ -519,7 +573,10 @@ def build(opts: BuildOptions) -> list[str]:
519573 f"custom_tags='{ opts .custom_tags } ' from base='{ opts .base_image } ' "
520574 f"for platforms='{ opts .platforms if push else 'local-arch' } '"
521575 )
522- logger .info (f"[build] Git ref='{ GIT_REF } ' sha='{ GIT_SHA } ' version='{ SDK_VERSION } '" )
576+ logger .info (
577+ f"[build] Git ref='{ GIT_REF } ' sha='{ GIT_SHA } ' "
578+ f"package_version='{ PACKAGE_VERSION } '"
579+ )
523580 logger .info (f"[build] Cache tag: { cache_tag } " )
524581
525582 try :
@@ -609,6 +666,14 @@ def main(argv: list[str]) -> int:
609666 action = "store_true" ,
610667 help = "Only create the clean build context directory and print its path." ,
611668 )
669+ parser .add_argument (
670+ "--versioned-tag" ,
671+ action = "store_true" ,
672+ help = (
673+ "Include versioned tag (e.g., v1.0.0_...) in output. "
674+ "Should only be used for release builds."
675+ ),
676+ )
612677
613678 args = parser .parse_args (argv )
614679
@@ -636,6 +701,7 @@ def main(argv: list[str]) -> int:
636701 push = None , # Not relevant for build-ctx-only
637702 sdk_project_root = sdk_project_root ,
638703 arch = args .arch or None ,
704+ include_versioned_tag = args .versioned_tag ,
639705 )
640706
641707 # If running in GitHub Actions, write outputs directly to GITHUB_OUTPUT
@@ -678,6 +744,7 @@ def main(argv: list[str]) -> int:
678744 push = push ,
679745 sdk_project_root = sdk_project_root ,
680746 arch = args .arch or None ,
747+ include_versioned_tag = args .versioned_tag ,
681748 )
682749 tags = build (opts )
683750
0 commit comments