fix(macos-container): tag the pinned base from its ref, not its ID
lint / lint (push) Successful in 59s
Update Quality Badges / update-badges (push) Successful in 1m6s
test / coverage (push) Successful in 22s
test / image-input-builds (push) Successful in 58s
test / integration-docker (push) Successful in 58s
test / unit (push) Successful in 2m51s

`container image tag` only accepts image-name[:tag] as its source and
rejects a bare 64-hex image ID ("cannot specify 64 byte hex string as
reference"), so pinning the agent image for the nested-containers layer
died before the build could start. Tag from the ref instead; the
existing post-tag inspection is what keeps the handoff fail-closed if
the ref moves between the two commands.

The unit test mocked subprocess and asserted the ID-as-source call
shape, so it never saw the CLI's rejection. It now pins the ref as the
source, and covers the bare-hex ID that `container image inspect`
actually reports plus the mid-flight tag move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 10:56:59 -04:00
parent cd9f023f3d
commit c89847b626
2 changed files with 38 additions and 3 deletions
+9 -2
View File
@@ -689,6 +689,13 @@ def pinned_local_image_ref(ref: str) -> str:
nested-containers layer. A content-derived tag prevents another concurrent
build from moving the provider's ordinary ``:latest`` tag between those
two builds.
Unlike Docker, `container image tag` only accepts ``image-name[:tag]`` as
its source and rejects a bare image ID ("cannot specify 64 byte hex string
as reference"), so the mutable ``ref`` is what gets tagged here. The
post-tag inspection below is what keeps that fail-closed: if ``ref`` moved
between the two commands, the new tag will not resolve to the ID this call
derived its name from.
"""
image = image_id(ref)
digest = image.removeprefix("sha256:")
@@ -701,14 +708,14 @@ def pinned_local_image_ref(ref: str) -> str:
repository = repository[:last_colon]
pinned_ref = f"{repository}:sha256-{digest}"
result = subprocess.run(
[_CONTAINER, "image", "tag", image, pinned_ref],
[_CONTAINER, "image", "tag", ref, pinned_ref],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
die(
f"could not tag exact local image {image}: "
f"could not tag exact local image {image} via {ref!r}: "
f"{(result.stderr or result.stdout or '').strip() or '<no detail>'}"
)
if image_id(pinned_ref) != image:
+29 -1
View File
@@ -158,8 +158,11 @@ resolver #2
f"registry:5000/agent:sha256-{'a' * 64}",
pinned,
)
# The tag source is the name:tag ref, not the image ID: Apple
# Container's `image tag` rejects a bare 64-byte hex string as a
# reference. The second image_id call is what keeps this fail-closed.
run.assert_called_once_with(
["container", "image", "tag", image, pinned],
["container", "image", "tag", "registry:5000/agent:latest", pinned],
capture_output=True,
text=True,
check=False,
@@ -169,6 +172,31 @@ resolver #2
[call.args for call in inspect.call_args_list],
)
def test_pinned_local_image_ref_accepts_bare_hex_image_id(self):
# `container image inspect` reports "id" without the sha256: prefix.
image = "b" * 64
completed = util.subprocess.CompletedProcess(
args=[], returncode=0, stdout="", stderr="",
)
with patch.object(util, "image_id", return_value=image), \
patch.object(util.subprocess, "run", return_value=completed) as run:
pinned = util.pinned_local_image_ref("agent:latest")
self.assertEqual(f"agent:sha256-{'b' * 64}", pinned)
self.assertEqual(
["container", "image", "tag", "agent:latest", pinned],
run.call_args.args[0],
)
def test_pinned_local_image_ref_dies_when_tag_moved_mid_flight(self):
completed = util.subprocess.CompletedProcess(
args=[], returncode=0, stdout="", stderr="",
)
with patch.object(
util, "image_id", side_effect=["c" * 64, "d" * 64],
), patch.object(util.subprocess, "run", return_value=completed), \
self.assertRaises(SystemExit):
util.pinned_local_image_ref("agent:latest")
def test_commit_container_execs_tar_and_builds_image(self):
# stderr is bytes because subprocess.run uses stderr=PIPE without text=True
completed = util.subprocess.CompletedProcess(