fix: return None from image_created_at when timestamp is absent
test / integration (pull_request) Successful in 14s
test / coverage (pull_request) Successful in 41s
lint / lint (push) Failing after 45s
test / unit (pull_request) Successful in 1m37s

FROM-scratch images (commit_container output) and registry images built
for reproducibility often omit the created field entirely. Both backends
were calling die() in that case, crashing the cached-image quickstart
before any container started.

image_created_at now returns datetime | None — None when the field is
absent or unparseable, die() only on real inspect failures (non-zero
exit, malformed JSON). stale_checks in both backends skips the staleness
check when None is returned.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 01:04:25 +00:00
parent fa3e45cc54
commit 6082e92b46
6 changed files with 45 additions and 34 deletions
+6 -2
View File
@@ -231,7 +231,11 @@ def stale_checks(plan: DockerBottlePlan) -> None:
return
committed = read_committed_image(plan.slug)
if committed and docker_mod.image_exists(committed):
check_stale(f"agent image {committed!r}", docker_mod.image_created_at(committed))
ts = docker_mod.image_created_at(committed)
if ts is not None:
check_stale(f"agent image {committed!r}", ts)
return
if docker_mod.image_exists(plan.image):
check_stale(f"agent image {plan.image!r}", docker_mod.image_created_at(plan.image))
ts = docker_mod.image_created_at(plan.image)
if ts is not None:
check_stale(f"agent image {plan.image!r}", ts)
+7 -3
View File
@@ -200,8 +200,10 @@ def commit_container(container_name: str, image_tag: str) -> None:
info(f"committed {container_name!r}{image_tag!r}")
def image_created_at(ref: str) -> datetime:
"""Return Docker's image Created timestamp as an aware UTC datetime."""
def image_created_at(ref: str) -> datetime | None:
"""Return Docker's image Created timestamp as an aware UTC datetime, or
None when the field is absent or unparseable. Callers should skip the
stale check when None is returned."""
r = subprocess.run(
["docker", "image", "inspect", "--format", "{{.Created}}", ref],
capture_output=True,
@@ -214,10 +216,12 @@ def image_created_at(ref: str) -> datetime:
f"{(r.stderr or '').strip() or '<no stderr>'}"
)
raw = r.stdout.strip()
if not raw:
return None
try:
return _parse_docker_timestamp(raw)
except ValueError:
die(f"docker image inspect for {ref!r} returned invalid Created timestamp: {raw!r}")
return None
def _parse_docker_timestamp(raw: str) -> datetime:
+6 -2
View File
@@ -211,10 +211,14 @@ def stale_checks(plan: MacosContainerBottlePlan) -> None:
return
committed = read_committed_image(plan.slug)
if committed and container_mod.image_exists(committed):
check_stale(f"agent image {committed!r}", container_mod.image_created_at(committed))
ts = container_mod.image_created_at(committed)
if ts is not None:
check_stale(f"agent image {committed!r}", ts)
return
if container_mod.image_exists(plan.image):
check_stale(f"agent image {plan.image!r}", container_mod.image_created_at(plan.image))
ts = container_mod.image_created_at(plan.image)
if ts is not None:
check_stale(f"agent image {plan.image!r}", ts)
def _provision_git_gate_keys(
+6 -6
View File
@@ -612,10 +612,11 @@ def image_id(ref: str) -> str:
raise AssertionError("unreachable")
def image_created_at(ref: str) -> datetime:
"""Return the image creation timestamp as an aware UTC datetime.
Parses the `created` field from `container image inspect` JSON output."""
def image_created_at(ref: str) -> datetime | None:
"""Return the image creation timestamp as an aware UTC datetime, or None
when the field is absent or unparseable (e.g. FROM-scratch images, images
pulled from registries that omit the field). Callers should skip the stale
check when None is returned rather than treating it as an error."""
result = subprocess.run(
[_CONTAINER, "image", "inspect", ref],
capture_output=True,
@@ -641,8 +642,7 @@ def image_created_at(ref: str) -> datetime:
return datetime.fromisoformat(ts).replace(tzinfo=timezone.utc)
except ValueError:
pass
die(f"container image inspect for {ref!r} did not include a creation timestamp")
raise AssertionError("unreachable")
return None
def save(ref: str, output: str) -> None:
+12 -8
View File
@@ -54,17 +54,21 @@ class TestImageCreatedAt(unittest.TestCase):
die.assert_called_once()
self.assertIn("missing:tag", die.call_args.args[0])
def test_dies_on_invalid_timestamp(self):
def test_returns_none_on_invalid_timestamp(self):
with patch.object(
docker_mod.subprocess, "run",
return_value=_ok(stdout="not-a-timestamp\n"),
), patch.object(
docker_mod, "die", side_effect=SystemExit("die"),
) as die:
with self.assertRaises(SystemExit):
docker_mod.image_created_at("some:tag")
die.assert_called_once()
self.assertIn("some:tag", die.call_args.args[0])
):
result = docker_mod.image_created_at("some:tag")
self.assertIsNone(result)
def test_returns_none_on_empty_stdout(self):
with patch.object(
docker_mod.subprocess, "run",
return_value=_ok(stdout=""),
):
result = docker_mod.image_created_at("some:tag")
self.assertIsNone(result)
def test_parse_docker_timestamp_no_tzinfo_defaults_to_utc(self):
# A bare datetime with no tz offset should be treated as UTC.
+8 -13
View File
@@ -371,22 +371,17 @@ class TestMacosContainerImageCreatedAt(unittest.TestCase):
util.image_created_at("some:tag")
die.assert_called_once()
def test_dies_when_no_created_field(self):
def test_returns_none_when_no_created_field(self):
payload = '[{"id": "sha256:abc123"}]'
with patch.object(util.subprocess, "run", return_value=self._ok(payload)), \
patch.object(util, "die", side_effect=SystemExit("die")) as die:
with self.assertRaises(SystemExit):
util.image_created_at("some:tag")
die.assert_called_once()
self.assertIn("creation timestamp", die.call_args.args[0])
with patch.object(util.subprocess, "run", return_value=self._ok(payload)):
result = util.image_created_at("some:tag")
self.assertIsNone(result)
def test_dies_on_invalid_timestamp_format(self):
def test_returns_none_on_invalid_timestamp_format(self):
payload = '[{"created": "not-a-date"}]'
with patch.object(util.subprocess, "run", return_value=self._ok(payload)), \
patch.object(util, "die", side_effect=SystemExit("die")) as die:
with self.assertRaises(SystemExit):
util.image_created_at("some:tag")
die.assert_called_once()
with patch.object(util.subprocess, "run", return_value=self._ok(payload)):
result = util.image_created_at("some:tag")
self.assertIsNone(result)
if __name__ == "__main__":