diff --git a/openhands-agent-server/openhands/agent_server/docker/build.py b/openhands-agent-server/openhands/agent_server/docker/build.py index 2389690ab5..4df53fed97 100755 --- a/openhands-agent-server/openhands/agent_server/docker/build.py +++ b/openhands-agent-server/openhands/agent_server/docker/build.py @@ -226,7 +226,10 @@ def _base_slug(image: str, max_len: int = 64) -> str: # Parse components from the slug form if "_tag_" in base_slug: - left, tag = base_slug.split("_tag_", 1) + # Ports also become "_tag_" (e.g., "ghcr.io:443/repo:tag" + # -> "ghcr.io_tag_443_s_repo_tag_tag"). Use rsplit so we split on the real + # image tag (the last _tag_), not the port. + left, tag = base_slug.rsplit("_tag_", 1) else: left, tag = base_slug, "" @@ -234,7 +237,8 @@ def _base_slug(image: str, max_len: int = 64) -> str: repo = parts[-1] if parts else left # last path segment is the repo # Reconstruct a compact, identifiable core: "[_tag_]" - ident = repo + (f"_tag_{tag}" if tag else "") + tag_piece = f"_tag_{tag}" if tag else "" + ident = repo + tag_piece # Fit within budget, reserving space for the digest suffix visible_budget = max_len - len(suffix) @@ -242,8 +246,22 @@ def _base_slug(image: str, max_len: int = 64) -> str: f"max_len too small to fit digest suffix with length {len(suffix)}" ) - kept = ident[:visible_budget] - return kept + suffix + if len(ident) > visible_budget: + if tag_piece and len(tag_piece) < visible_budget: + # Full tag fits: trim repo to make room + ident = repo[: visible_budget - len(tag_piece)] + tag_piece + else: + # Either no tag or tag too long: truncate from the start + ident = (tag_piece or ident)[:visible_budget] + + truncated = ident + suffix + logger.debug( + "[base-slug] Truncated base image '%s' to slug '%s' (max_len=%d)", + image, + truncated, + max_len, + ) + return truncated def _git_info() -> tuple[str, str]: diff --git a/tests/agent_server/test_docker_build.py b/tests/agent_server/test_docker_build.py index 2415493b64..b48ead839f 100644 --- a/tests/agent_server/test_docker_build.py +++ b/tests/agent_server/test_docker_build.py @@ -212,6 +212,39 @@ def test_base_slug_truncation_no_tag(): assert "very-long-repository-name" in result +def test_base_slug_preserves_latest_tag_suffix(): + """Ensure tag_latest suffix is not mangled when truncating long slugs.""" + from openhands.agent_server.docker.build import _base_slug + + image = ( + "docker.io/swebench/sweb.eval.x86_64.astropy_1776_astropy-8872:" + "tag_latest-0a797356ebce" + ) + + result = _base_slug(image, max_len=64) + + assert len(result) <= 64 + assert "tag_latest" in result + # Keep cache keys stable with digest suffix + assert result[-13:-12] == "-" + assert all(c in "0123456789abcdef" for c in result[-12:]) + + +def test_base_slug_preserves_tag_with_registry_port(): + """Handle registries with ports without losing the tag segment.""" + from openhands.agent_server.docker.build import _base_slug + + image = ( + "localhost:5001/swebench/sweb.eval.x86_64.astropy_1776_astropy-8872:" + "tag_latest-0a797356ebce" + ) + + result = _base_slug(image, max_len=64) + + assert len(result) <= 64 + assert "tag_latest" in result + + def test_base_slug_custom_max_len(): """Test base_slug with custom max_len parameter.""" from openhands.agent_server.docker.build import _base_slug