Public Access
DEVX-165: feat(ci): manifest-aware dependency PRs and cleanup protection
This commit was merged in pull request #327.
This commit is contained in:
@@ -485,6 +485,38 @@ class TestSelectForDeletion:
|
||||
to_delete = select_for_deletion(versions, keep=5)
|
||||
assert len(to_delete) == 0
|
||||
|
||||
def test_protect_exempts_version(self) -> None:
|
||||
"""REQ-2: a manifest-pinned version survives cleanup beyond --keep."""
|
||||
versions = [
|
||||
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||
{"version": "0.3.0", "created_at": "2025-03-01"},
|
||||
{"version": "0.4.0", "created_at": "2025-04-01"},
|
||||
]
|
||||
to_delete = select_for_deletion(versions, keep=2, protect=frozenset({"0.1.0"}))
|
||||
deleted = {v["version"] for v in to_delete}
|
||||
assert "0.1.0" not in deleted
|
||||
assert deleted == {"0.2.0"}
|
||||
|
||||
def test_protect_multiple_versions(self) -> None:
|
||||
versions = [
|
||||
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||
{"version": "0.3.0", "created_at": "2025-03-01"},
|
||||
{"version": "0.4.0", "created_at": "2025-04-01"},
|
||||
]
|
||||
to_delete = select_for_deletion(versions, keep=1, protect=frozenset({"0.1.0", "0.2.0"}))
|
||||
assert {v["version"] for v in to_delete} == {"0.3.0"}
|
||||
|
||||
def test_protect_default_empty_behaves_as_before(self) -> None:
|
||||
versions = [
|
||||
{"version": "0.1.0", "created_at": "2025-01-01"},
|
||||
{"version": "0.2.0", "created_at": "2025-02-01"},
|
||||
{"version": "0.3.0", "created_at": "2025-03-01"},
|
||||
]
|
||||
to_delete = select_for_deletion(versions, keep=2)
|
||||
assert {v["version"] for v in to_delete} == {"0.1.0"}
|
||||
|
||||
|
||||
class TestCleanImagesAPI:
|
||||
"""Tests for the clean_images module's API functions."""
|
||||
|
||||
@@ -244,14 +244,119 @@ class TestResolveContainerDigest:
|
||||
resolve_container_digest("https://git.example.com/api/v1", "oblachno", "sso-bridge", "0.9.1", "tok")
|
||||
|
||||
|
||||
class TestCliVerifyContainer:
|
||||
"""REQ-1: artifact verification gates the dependency PR."""
|
||||
class TestManifestHelpers:
|
||||
"""REQ-1: manifest read/update helpers."""
|
||||
|
||||
def test_read_version(self, tmp_path: Path) -> None:
|
||||
import json
|
||||
|
||||
from devx.ci.create_dependency_pr import read_manifest_version
|
||||
|
||||
p = tmp_path / "m.json"
|
||||
p.write_text(json.dumps({"schema_version": 1, "sso_bridge": {"version": "0.9.0"}}))
|
||||
assert read_manifest_version(str(p), "sso_bridge") == "0.9.0"
|
||||
|
||||
def test_read_version_missing_file(self, tmp_path: Path) -> None:
|
||||
from devx.ci.create_dependency_pr import read_manifest_version
|
||||
|
||||
assert read_manifest_version(str(tmp_path / "nope.json"), "sso_bridge") is None
|
||||
|
||||
def test_read_version_bad_json(self, tmp_path: Path) -> None:
|
||||
from devx.ci.create_dependency_pr import read_manifest_version
|
||||
|
||||
p = tmp_path / "m.json"
|
||||
p.write_text("not json{")
|
||||
assert read_manifest_version(str(p), "sso_bridge") is None
|
||||
|
||||
def test_read_version_missing_section(self, tmp_path: Path) -> None:
|
||||
import json
|
||||
|
||||
from devx.ci.create_dependency_pr import read_manifest_version
|
||||
|
||||
p = tmp_path / "m.json"
|
||||
p.write_text(json.dumps({"schema_version": 1, "other": "x"}))
|
||||
assert read_manifest_version(str(p), "sso_bridge") is None
|
||||
|
||||
def test_update_manifest_fields(self, tmp_path: Path) -> None:
|
||||
import json
|
||||
|
||||
from devx.ci.create_dependency_pr import update_manifest
|
||||
|
||||
p = tmp_path / "m.json"
|
||||
p.write_text(json.dumps({"schema_version": 1, "sso_bridge": {"version": "0.9.0"}}))
|
||||
changed = update_manifest(
|
||||
str(p),
|
||||
"sso_bridge",
|
||||
{"version": "0.9.1", "git_ref": "v0.9.1", "image_digest": "sha256:x"},
|
||||
)
|
||||
assert changed is True
|
||||
data = json.loads(p.read_text())
|
||||
assert data["sso_bridge"]["version"] == "0.9.1"
|
||||
assert data["sso_bridge"]["git_ref"] == "v0.9.1"
|
||||
assert data["sso_bridge"]["image_digest"] == "sha256:x"
|
||||
|
||||
def test_update_manifest_no_change(self, tmp_path: Path) -> None:
|
||||
import json
|
||||
|
||||
from devx.ci.create_dependency_pr import update_manifest
|
||||
|
||||
p = tmp_path / "m.json"
|
||||
p.write_text(json.dumps({"sso_bridge": {"version": "0.9.1"}}))
|
||||
assert update_manifest(str(p), "sso_bridge", {"version": "0.9.1"}) is False
|
||||
|
||||
def test_update_manifest_missing_file(self, tmp_path: Path) -> None:
|
||||
from devx.ci.create_dependency_pr import update_manifest
|
||||
|
||||
with pytest.raises(click.ClickException, match="not found"):
|
||||
update_manifest(str(tmp_path / "nope.json"), "sso_bridge", {"version": "1"})
|
||||
|
||||
def test_update_manifest_bad_json(self, tmp_path: Path) -> None:
|
||||
from devx.ci.create_dependency_pr import update_manifest
|
||||
|
||||
p = tmp_path / "m.json"
|
||||
p.write_text("broken{")
|
||||
with pytest.raises(click.ClickException, match="not valid JSON"):
|
||||
update_manifest(str(p), "sso_bridge", {"version": "1"})
|
||||
|
||||
def test_update_manifest_missing_section(self, tmp_path: Path) -> None:
|
||||
import json
|
||||
|
||||
from devx.ci.create_dependency_pr import update_manifest
|
||||
|
||||
p = tmp_path / "m.json"
|
||||
p.write_text(json.dumps({"schema_version": 1}))
|
||||
with pytest.raises(click.ClickException, match="no object section"):
|
||||
update_manifest(str(p), "sso_bridge", {"version": "1"})
|
||||
|
||||
|
||||
class TestCliManifestMode:
|
||||
@patch("devx.ci.create_dependency_pr.read_manifest_version")
|
||||
@patch("devx.ci.create_dependency_pr.get_ci_token")
|
||||
def test_manifest_same_version_no_pr(self, mock_token: MagicMock, mock_read: MagicMock) -> None:
|
||||
mock_token.return_value = "fake-token"
|
||||
mock_read.return_value = "0.9.1"
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"--package",
|
||||
"sso_bridge",
|
||||
"--new-version",
|
||||
"0.9.1",
|
||||
"--source-repo",
|
||||
"oblachno/sso-bridge",
|
||||
"--manifest",
|
||||
"deploy/sso-bridge-release.json",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "no pr needed" in result.output.lower()
|
||||
|
||||
@patch("devx.ci.create_dependency_pr.resolve_container_digest")
|
||||
@patch("devx.ci.create_dependency_pr.find_pinned_version")
|
||||
@patch("devx.ci.create_dependency_pr.read_manifest_version")
|
||||
@patch("devx.ci.create_dependency_pr.get_ci_token")
|
||||
def test_verify_container_runs_before_lookup(
|
||||
self, mock_token: MagicMock, mock_find: MagicMock, mock_digest: MagicMock
|
||||
self, mock_token: MagicMock, mock_read: MagicMock, mock_digest: MagicMock
|
||||
) -> None:
|
||||
"""Verification failure aborts before the version lookup/PR steps."""
|
||||
mock_token.return_value = "fake-token"
|
||||
@@ -266,21 +371,23 @@ class TestCliVerifyContainer:
|
||||
"0.9.1",
|
||||
"--source-repo",
|
||||
"oblachno/sso-bridge",
|
||||
"--manifest",
|
||||
"deploy/m.json",
|
||||
"--verify-container",
|
||||
"oblachno/sso-bridge",
|
||||
],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
mock_find.assert_not_called()
|
||||
mock_read.assert_not_called()
|
||||
|
||||
@patch("devx.ci.create_dependency_pr.resolve_container_digest")
|
||||
@patch("devx.ci.create_dependency_pr.find_pinned_version")
|
||||
@patch("devx.ci.create_dependency_pr.read_manifest_version")
|
||||
@patch("devx.ci.create_dependency_pr.get_ci_token")
|
||||
def test_verify_container_resolves_digest(
|
||||
self, mock_token: MagicMock, mock_find: MagicMock, mock_digest: MagicMock
|
||||
self, mock_token: MagicMock, mock_read: MagicMock, mock_digest: MagicMock
|
||||
) -> None:
|
||||
mock_token.return_value = "fake-token"
|
||||
mock_find.return_value = "0.9.1"
|
||||
mock_read.return_value = "0.9.1"
|
||||
mock_digest.return_value = "sha256:abc"
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
@@ -292,6 +399,8 @@ class TestCliVerifyContainer:
|
||||
"0.9.1",
|
||||
"--source-repo",
|
||||
"oblachno/sso-bridge",
|
||||
"--manifest",
|
||||
"deploy/m.json",
|
||||
"--verify-container",
|
||||
"oblachno/sso-bridge",
|
||||
],
|
||||
@@ -321,14 +430,14 @@ class TestCliVerifyContainer:
|
||||
assert result.exit_code != 0
|
||||
|
||||
@patch("devx.ci.create_dependency_pr.resolve_container_digest")
|
||||
@patch("devx.ci.create_dependency_pr.find_pinned_version")
|
||||
@patch("devx.ci.create_dependency_pr.read_manifest_version")
|
||||
@patch("devx.ci.create_dependency_pr.get_ci_token")
|
||||
def test_container_tag_overrides_new_version(
|
||||
self, mock_token: MagicMock, mock_find: MagicMock, mock_digest: MagicMock
|
||||
self, mock_token: MagicMock, mock_read: MagicMock, mock_digest: MagicMock
|
||||
) -> None:
|
||||
"""--container-tag selects the image tag when it differs from version."""
|
||||
mock_token.return_value = "fake-token"
|
||||
mock_find.return_value = "0.9.1"
|
||||
mock_read.return_value = "0.9.1"
|
||||
mock_digest.return_value = "sha256:abc"
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
@@ -340,6 +449,8 @@ class TestCliVerifyContainer:
|
||||
"0.9.1",
|
||||
"--source-repo",
|
||||
"oblachno/sso-bridge",
|
||||
"--manifest",
|
||||
"deploy/m.json",
|
||||
"--verify-container",
|
||||
"oblachno/sso-bridge",
|
||||
"--container-tag",
|
||||
|
||||
Reference in New Issue
Block a user