DEVX-163: fix: use stderr=STDOUT to capture all docker push output in one stream
Post-merge / detect-and-configure (push) Successful in 13s
Post-merge / release-and-maintain (push) Successful in 1m18s

This commit was merged in pull request #309.
This commit is contained in:
2026-08-26 14:09:59 +00:00
parent a7cdebc0dc
commit 149e8846b8
2 changed files with 30 additions and 40 deletions
+19 -32
View File
@@ -198,7 +198,7 @@ class TestBuildImage:
class TestPushImage:
def test_success(self) -> None:
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
mock_result = MagicMock(returncode=0, stderr="", stdout="")
mock_result = MagicMock(returncode=0, stdout="")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
assert push_image(spec, "git.example.com") is True
assert mock_run.call_count == 2
@@ -206,8 +206,8 @@ class TestPushImage:
def test_partial_failure(self) -> None:
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
results = [
MagicMock(returncode=0, stderr="", stdout=""),
MagicMock(returncode=1, stderr="push failed", stdout=""),
MagicMock(returncode=0, stdout=""),
MagicMock(returncode=1, stdout="push failed"),
]
with patch("devx.tools.build_image.subprocess.run", side_effect=results):
assert push_image(spec, "git.example.com") is False
@@ -221,7 +221,7 @@ class TestPushImage:
def test_no_delete_on_success_with_creds(self) -> None:
"""Push-first: no delete needed when push succeeds."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
mock_result = MagicMock(returncode=0, stderr="", stdout="")
mock_result = MagicMock(returncode=0, stdout="")
with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
@@ -231,7 +231,7 @@ class TestPushImage:
def test_no_delete_without_creds(self) -> None:
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
mock_result = MagicMock(returncode=0, stderr="", stdout="")
mock_result = MagicMock(returncode=0, stdout="")
with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
@@ -243,8 +243,8 @@ 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="package version already exists", stdout=""),
MagicMock(returncode=0, stderr="", stdout=""),
MagicMock(returncode=1, stdout="package version already exists"),
MagicMock(returncode=0, stdout=""),
]
with (
patch("devx.tools.build_image.subprocess.run", side_effect=results),
@@ -263,7 +263,7 @@ class TestPushImage:
def test_no_delete_on_non_already_exists_failure(self) -> None:
"""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="denied: requested access to the resource is denied", stdout="")
mock_result = MagicMock(returncode=1, stdout="denied: requested access to the resource is denied")
with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
@@ -275,8 +275,8 @@ class TestPushImage:
"""Gitea #31964 retry also fails — both pushes fail."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
results = [
MagicMock(returncode=1, stderr="package version already exists", stdout=""),
MagicMock(returncode=1, stderr="push failed again", stdout=""),
MagicMock(returncode=1, stdout="package version already exists"),
MagicMock(returncode=1, stdout="push failed again"),
]
with (
patch("devx.tools.build_image.subprocess.run", side_effect=results),
@@ -288,8 +288,8 @@ class TestPushImage:
"""HTTP 500 from registry race condition — retry succeeds."""
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
results = [
MagicMock(returncode=1, stderr="", stdout="received unexpected HTTP status: 500 Internal Server Error"),
MagicMock(returncode=0, stderr="", stdout=""),
MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error"),
MagicMock(returncode=0, stdout=""),
]
with (
patch("devx.tools.build_image.subprocess.run", side_effect=results),
@@ -302,9 +302,7 @@ class TestPushImage:
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="", stdout="received unexpected HTTP status: 500 Internal Server Error"
)
mock_result = MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error")
with (
patch("devx.tools.build_image.subprocess.run", return_value=mock_result),
patch("devx.tools.build_image.delete_remote_manifest") as mock_del,
@@ -313,31 +311,20 @@ class TestPushImage:
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_stderr(self) -> None:
"""_run_push raises PushHTTP500Error when stderr contains 500."""
def test_run_push_raises_on_500(self) -> None:
"""_run_push raises PushHTTP500Error when stdout 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_raises_on_500_stdout(self) -> None:
"""_run_push raises PushHTTP500Error when stdout contains 500 (docker sends to stdout)."""
from devx.tools.build_image import _run_push
mock_result = MagicMock(
returncode=1, stderr="", stdout="received unexpected HTTP status: 500 Internal Server Error"
)
mock_result = MagicMock(returncode=1, stdout="received unexpected HTTP status: 500 Internal Server Error")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
with pytest.raises(PushHTTP500Error, match="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."""
"""_run_push returns result when stdout has no 500."""
from devx.tools.build_image import _run_push
mock_result = MagicMock(returncode=1, stderr="denied: access denied", stdout="")
mock_result = MagicMock(returncode=1, stdout="denied: access denied")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
result = _run_push(["docker", "push", "img:latest"])
assert result.returncode == 1
@@ -346,7 +333,7 @@ class TestPushImage:
"""_run_push returns result on success."""
from devx.tools.build_image import _run_push
mock_result = MagicMock(returncode=0, stderr="", stdout="")
mock_result = MagicMock(returncode=0, stdout="")
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
result = _run_push(["docker", "push", "img:latest"])
assert result.returncode == 0