Skip to content

Commit a405ca7

Browse files
authored
Merge pull request #661 from posit-dev/fix/publish-retry
fix(soci): retry transient registry errors on SOCI push
2 parents a410eea + 95b2b49 commit a405ca7

2 files changed

Lines changed: 47 additions & 3 deletions

File tree

posit-bakery/posit_bakery/plugins/builtin/imagetools/soci.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,14 +221,23 @@ def run(self, dry_run: bool = False, runner: "CommandRunner | None" = None) -> S
221221
).run(dry_run=dry_run, runner=runner)
222222

223223
# 3. push converted layout -> registry, referenced by digest since
224-
# the converted layout carries no tag.
224+
# the converted layout carries no tag. Retry transient registry
225+
# errors: pushing by digest can race the same eventual-consistency
226+
# window as the pull above (e.g. cross-repo blob mounts of
227+
# just-pushed source layers).
225228
digest = "sha256:<dry-run>" if dry_run else self._read_converted_digest(out_layout)
226-
OrasCopy(
229+
push = OrasCopy(
227230
oras_bin=self.oras_bin,
228231
source=f"{out_layout}@{digest}",
229232
destination=self.destination_ref,
230233
from_oci_layout=True,
231-
).run(dry_run=dry_run, runner=runner)
234+
)
235+
retry_on_transient(
236+
lambda: push.run(dry_run=dry_run, runner=runner),
237+
policy=self.retry_policy,
238+
description=f"soci push for '{self.image_target.uid}'",
239+
sleep=runner.sleep if runner is not None else None,
240+
)
232241
except BakeryToolRuntimeError as e:
233242
return SociConvertWorkflowResult(
234243
success=False,

posit-bakery/test/plugins/builtin/imagetools/test_workflow.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,41 @@ def fake_run(cmd, capture_output):
190190
sleep.assert_called_once()
191191

192192

193+
def test_push_retries_transient_then_succeeds(mock_target):
194+
"""A transient registry error on the SOCI push is retried, not fatal."""
195+
from posit_bakery.retry import RetryPolicy
196+
197+
workflow = SociConvertWorkflow(
198+
soci_bin="soci",
199+
oras_bin="oras",
200+
image_target=mock_target,
201+
options=SociOptions(enabled=True),
202+
source_ref="ghcr.io/posit-dev/test-image/tmp:merged",
203+
retry_policy=RetryPolicy(max_attempts=5, initial_backoff=1.0),
204+
)
205+
206+
attempts = {"push": 0}
207+
208+
def fake_run(cmd, capture_output):
209+
# Second oras cp is the converted layout -> registry push; fail it transiently once.
210+
if cmd[:2] == ["oras", "cp"] and "--from-oci-layout" in cmd:
211+
attempts["push"] += 1
212+
if attempts["push"] == 1:
213+
return subprocess.CompletedProcess(args=cmd, returncode=1, stdout=b"", stderr=b"sha256:x: not found")
214+
return _ok_proc(cmd)
215+
216+
with (
217+
patch("subprocess.run", side_effect=fake_run),
218+
patch("posit_bakery.retry.time.sleep") as sleep,
219+
patch.object(SociConvertWorkflow, "_read_converted_digest", return_value="sha256:abc123"),
220+
):
221+
result = workflow.run()
222+
223+
assert result.success is True
224+
assert attempts["push"] == 2
225+
sleep.assert_called_once()
226+
227+
193228
def test_pull_retry_uses_runner_sleep_when_runner_provided(mock_target):
194229
workflow = SociConvertWorkflow(
195230
soci_bin="soci",

0 commit comments

Comments
 (0)