feat(ci): manifest-aware dependency PRs and cleanup protection
CI / validate (pull_request) Successful in 51s
CI / auto-merge (pull_request) Successful in 19s

This commit is contained in:
Emil Simeonov
2026-09-19 02:47:06 +00:00
committed by kireto
parent 7268c2b4ac
commit 21f5f39165
7 changed files with 388 additions and 47 deletions
+122 -11
View File
@@ -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",