Public Access
fix(ci): resolve dep-PR container digest via registry v2 API
The packages-API manifest.json blob sha256 is not pullable via repo@sha256:... — production deploy 6251 failed pulling the recorded digest (404 not found). Existence check stays on the packages API (keeps the 404 retry), but the digest now comes from the registry v2 Docker-Content-Digest header. Implements DEVX-173 REQ-1/REQ-2.
This commit is contained in:
@@ -200,23 +200,39 @@ class TestCreateVikunjaTask:
|
||||
assert mock_client.create_task.call_args.args[0] == 3
|
||||
|
||||
|
||||
class TestResolveContainerDigest:
|
||||
"""REQ-1: pre-PR artifact verification via the packages API."""
|
||||
def _v2_mocks(digest: str = "sha256:deadbeef") -> list[MagicMock]:
|
||||
"""Token + manifest responses for the registry v2 digest lookup."""
|
||||
tok = MagicMock()
|
||||
tok.raise_for_status = MagicMock()
|
||||
tok.json.return_value = {"token": "bearer-tok"}
|
||||
man = MagicMock()
|
||||
man.raise_for_status = MagicMock()
|
||||
man.headers = {"Docker-Content-Digest": digest}
|
||||
return [tok, man]
|
||||
|
||||
def test_returns_digest_from_manifest_blob(self) -> None:
|
||||
|
||||
class TestResolveContainerDigest:
|
||||
"""REQ-1: existence check via packages API; digest via registry v2."""
|
||||
|
||||
def test_returns_digest_from_registry_v2(self) -> None:
|
||||
from devx.ci.create_dependency_pr import resolve_container_digest
|
||||
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
mock_resp.json.return_value = [
|
||||
{"name": "sha256_layer", "sha256": "abc"},
|
||||
{"name": "manifest.json", "sha256": "deadbeef"},
|
||||
]
|
||||
with patch("devx.ci.create_dependency_pr.requests.get", return_value=mock_resp):
|
||||
files = MagicMock()
|
||||
files.raise_for_status = MagicMock()
|
||||
files.json.return_value = [{"name": "manifest.json", "sha256": "blob-not-pullable"}]
|
||||
with patch(
|
||||
"devx.ci.create_dependency_pr.requests.get",
|
||||
side_effect=[files, *_v2_mocks()],
|
||||
) as mock_get:
|
||||
digest = resolve_container_digest(
|
||||
"https://git.example.com/api/v1", "oblachno", "sso-bridge", "0.9.1", "tok"
|
||||
)
|
||||
assert digest == "sha256:deadbeef"
|
||||
man_call = mock_get.call_args_list[2]
|
||||
assert "manifests/0.9.1" in man_call.args[0]
|
||||
assert "oci.image.index" in man_call.kwargs["headers"]["Accept"]
|
||||
tok_call = mock_get.call_args_list[1]
|
||||
assert tok_call.kwargs["params"]["scope"] == "repository:oblachno/sso-bridge:pull"
|
||||
|
||||
def test_raises_when_version_missing(self) -> None:
|
||||
import requests
|
||||
@@ -231,14 +247,38 @@ class TestResolveContainerDigest:
|
||||
with pytest.raises(click.ClickException, match="unpublished artifact"):
|
||||
resolve_container_digest("https://git.example.com/api/v1", "oblachno", "sso-bridge", "9.9.9", "tok")
|
||||
|
||||
def test_raises_when_no_manifest_blob(self) -> None:
|
||||
def test_raises_when_v2_digest_missing(self) -> None:
|
||||
from devx.ci.create_dependency_pr import resolve_container_digest
|
||||
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
mock_resp.json.return_value = [{"name": "sha256_layer", "sha256": "abc"}]
|
||||
with patch("devx.ci.create_dependency_pr.requests.get", return_value=mock_resp):
|
||||
with pytest.raises(click.ClickException, match="no manifest blob"):
|
||||
files = MagicMock()
|
||||
files.raise_for_status = MagicMock()
|
||||
files.json.return_value = []
|
||||
tok, man = _v2_mocks(digest="")
|
||||
with patch(
|
||||
"devx.ci.create_dependency_pr.requests.get",
|
||||
side_effect=[files, tok, man],
|
||||
):
|
||||
with pytest.raises(click.ClickException, match="no Docker-Content-Digest"):
|
||||
resolve_container_digest("https://git.example.com/api/v1", "oblachno", "sso-bridge", "0.9.1", "tok")
|
||||
|
||||
def test_raises_when_v2_manifest_request_fails(self) -> None:
|
||||
import requests
|
||||
|
||||
from devx.ci.create_dependency_pr import resolve_container_digest
|
||||
|
||||
files = MagicMock()
|
||||
files.raise_for_status = MagicMock()
|
||||
files.json.return_value = []
|
||||
tok, _ = _v2_mocks()
|
||||
err = requests.HTTPError("500")
|
||||
err.response = MagicMock(status_code=500)
|
||||
man = MagicMock()
|
||||
man.raise_for_status.side_effect = err
|
||||
with patch(
|
||||
"devx.ci.create_dependency_pr.requests.get",
|
||||
side_effect=[files, tok, man],
|
||||
):
|
||||
with pytest.raises(click.ClickException, match="v2 digest lookup failed"):
|
||||
resolve_container_digest("https://git.example.com/api/v1", "oblachno", "sso-bridge", "0.9.1", "tok")
|
||||
|
||||
def test_raises_on_connection_error(self) -> None:
|
||||
@@ -635,7 +675,7 @@ class TestBranchCreation:
|
||||
ok.raise_for_status = MagicMock()
|
||||
ok.json.return_value = [{"name": "manifest.json", "sha256": "cafe"}]
|
||||
with (
|
||||
patch("devx.ci.create_dependency_pr.requests.get", side_effect=[fail, ok]),
|
||||
patch("devx.ci.create_dependency_pr.requests.get", side_effect=[fail, ok, *_v2_mocks("sha256:cafe")]),
|
||||
patch("devx.ci.create_dependency_pr.time.sleep"),
|
||||
):
|
||||
digest = resolve_container_digest(
|
||||
|
||||
Reference in New Issue
Block a user