Public Access
DEVX-162: fix: serialize registry uploads and retry on HTTP 500
This commit was merged in pull request #304.
This commit is contained in:
@@ -13,6 +13,7 @@ from click.testing import CliRunner
|
||||
import devx.tools.build_image as build_image
|
||||
from devx.tools.build_image import (
|
||||
ImageSpec,
|
||||
PushHTTP500Error,
|
||||
build_full_tag,
|
||||
delete_remote_manifest,
|
||||
load_manifest,
|
||||
@@ -242,7 +243,7 @@ class TestPushImage:
|
||||
"""Gitea #31964: push fails with 'already exists', delete + retry."""
|
||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||
results = [
|
||||
MagicMock(returncode=1, stderr="500 Internal Server Error: already exists", stdout=""),
|
||||
MagicMock(returncode=1, stderr="package version already exists", stdout=""),
|
||||
MagicMock(returncode=0, stderr="", stdout=""),
|
||||
]
|
||||
with (
|
||||
@@ -260,11 +261,9 @@ class TestPushImage:
|
||||
)
|
||||
|
||||
def test_no_delete_on_non_already_exists_failure(self) -> None:
|
||||
"""Push fails for other reasons (HTTP 500) — old manifest preserved."""
|
||||
"""Push fails for other reasons (non-500) — old manifest preserved."""
|
||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||
mock_result = MagicMock(
|
||||
returncode=1, stderr="received unexpected HTTP status: 500 Internal Server Error", stdout=""
|
||||
)
|
||||
mock_result = MagicMock(returncode=1, stderr="denied: requested access to the resource is denied", stdout="")
|
||||
with (
|
||||
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
||||
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
||||
@@ -276,7 +275,7 @@ class TestPushImage:
|
||||
"""Gitea #31964 retry also fails — both pushes fail."""
|
||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||
results = [
|
||||
MagicMock(returncode=1, stderr="500 Internal Server Error: already exists", stdout=""),
|
||||
MagicMock(returncode=1, stderr="package version already exists", stdout=""),
|
||||
MagicMock(returncode=1, stderr="push failed again", stdout=""),
|
||||
]
|
||||
with (
|
||||
@@ -285,6 +284,62 @@ class TestPushImage:
|
||||
):
|
||||
assert push_image(spec, "git.example.com", username="user", token="tok") is False
|
||||
|
||||
def test_http_500_retries_then_succeeds(self) -> None:
|
||||
"""HTTP 500 from registry race condition — retry succeeds."""
|
||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||
results = [
|
||||
MagicMock(returncode=1, stderr="received unexpected HTTP status: 500 Internal Server Error", stdout=""),
|
||||
MagicMock(returncode=0, stderr="", stdout=""),
|
||||
]
|
||||
with (
|
||||
patch("devx.tools.build_image.subprocess.run", side_effect=results),
|
||||
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
||||
patch("time.sleep"),
|
||||
):
|
||||
assert push_image(spec, "git.example.com", username="user", token="tok") is True
|
||||
mock_del.assert_not_called()
|
||||
|
||||
def test_http_500_retries_all_fail(self) -> None:
|
||||
"""HTTP 500 retries exhausted — push fails, no delete attempted."""
|
||||
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
||||
mock_result = MagicMock(
|
||||
returncode=1, stderr="received unexpected HTTP status: 500 Internal Server Error", stdout=""
|
||||
)
|
||||
with (
|
||||
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
|
||||
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
|
||||
patch("time.sleep"),
|
||||
):
|
||||
assert push_image(spec, "git.example.com", username="user", token="tok") is False
|
||||
mock_del.assert_not_called()
|
||||
|
||||
def test_run_push_raises_on_500(self) -> None:
|
||||
"""_run_push raises PushHTTP500Error when stderr contains 500."""
|
||||
from devx.tools.build_image import _run_push
|
||||
|
||||
mock_result = MagicMock(returncode=1, stderr="HTTP 500 Internal Server Error", stdout="")
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||
with pytest.raises(PushHTTP500Error, match="HTTP 500"):
|
||||
_run_push(["docker", "push", "img:latest"])
|
||||
|
||||
def test_run_push_no_raise_on_non_500(self) -> None:
|
||||
"""_run_push returns result when stderr has no 500."""
|
||||
from devx.tools.build_image import _run_push
|
||||
|
||||
mock_result = MagicMock(returncode=1, stderr="denied: access denied", stdout="")
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||
result = _run_push(["docker", "push", "img:latest"])
|
||||
assert result.returncode == 1
|
||||
|
||||
def test_run_push_no_raise_on_success(self) -> None:
|
||||
"""_run_push returns result on success."""
|
||||
from devx.tools.build_image import _run_push
|
||||
|
||||
mock_result = MagicMock(returncode=0, stderr="", stdout="")
|
||||
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
||||
result = _run_push(["docker", "push", "img:latest"])
|
||||
assert result.returncode == 0
|
||||
|
||||
|
||||
class TestDeleteRemoteManifest:
|
||||
def test_dry_run(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user