fix(release): harden review gate diagnostics
This commit is contained in:
@@ -6,8 +6,10 @@ from datetime import UTC, datetime, timedelta
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
import unittest
|
||||
|
||||
@@ -17,8 +19,10 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
|
||||
if str(ASSESSMENT_TOOLS_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ASSESSMENT_TOOLS_ROOT))
|
||||
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
|
||||
for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
|
||||
if str(tools_root) not in sys.path:
|
||||
sys.path.insert(0, str(tools_root))
|
||||
|
||||
from govoplan_assessment.capability_fit import ( # noqa: E402
|
||||
local_tag_provenance,
|
||||
@@ -87,6 +91,156 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
"composition_version_changed", {item["code"] for item in report["findings"]}
|
||||
)
|
||||
|
||||
def test_contradictory_signed_selected_unit_metadata_blocks_rerun(self) -> None:
|
||||
core = next(
|
||||
item
|
||||
for item in self.assessment["composition"]
|
||||
if item["module_id"] == "core"
|
||||
)
|
||||
catalog, keyring = signed_catalog(
|
||||
self.assessment,
|
||||
selected_units=[
|
||||
{
|
||||
"repo": core["repository"],
|
||||
"version": core["manifest_version"],
|
||||
"tag": "v999.0.0",
|
||||
"commit_sha": "0" * 40,
|
||||
"tag_object_sha": "1" * 40,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
report = review_capability_fit(
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", report["status"])
|
||||
codes = {item["code"] for item in report["findings"]}
|
||||
self.assertIn("catalog_release_metadata", codes)
|
||||
self.assertIn("composition_commit_changed", codes)
|
||||
self.assertTrue(report["proof_scope"]["catalog_signature_and_keyring"]["valid"])
|
||||
self.assertFalse(report["proof_scope"]["release_metadata"]["valid"])
|
||||
|
||||
def test_signed_selected_unit_commit_drift_requires_review(self) -> None:
|
||||
core = next(
|
||||
item
|
||||
for item in self.assessment["composition"]
|
||||
if item["module_id"] == "core"
|
||||
)
|
||||
catalog, keyring = signed_catalog(
|
||||
self.assessment,
|
||||
selected_units=[
|
||||
{
|
||||
"repo": core["repository"],
|
||||
"version": core["manifest_version"],
|
||||
"tag": f"v{core['manifest_version']}",
|
||||
"commit_sha": "0" * 40,
|
||||
"tag_object_sha": "1" * 40,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
report = review_capability_fit(
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
)
|
||||
|
||||
self.assertEqual("review_required", report["status"])
|
||||
self.assertIn(
|
||||
"composition_commit_changed", {item["code"] for item in report["findings"]}
|
||||
)
|
||||
self.assertIn(
|
||||
"composition.core", {item["id"] for item in report["review_targets"]}
|
||||
)
|
||||
self.assertFalse(report["proof_scope"]["release_metadata"]["valid"])
|
||||
|
||||
def test_missing_duplicate_and_malformed_selected_unit_provenance_blocks(
|
||||
self,
|
||||
) -> None:
|
||||
core = next(
|
||||
item
|
||||
for item in self.assessment["composition"]
|
||||
if item["module_id"] == "core"
|
||||
)
|
||||
valid_unit = {
|
||||
"repo": core["repository"],
|
||||
"version": core["manifest_version"],
|
||||
"tag": f"v{core['manifest_version']}",
|
||||
"commit_sha": (str(core["commit"]) + "0" * 40)[:40],
|
||||
"tag_object_sha": "1" * 40,
|
||||
}
|
||||
cases = (
|
||||
("missing", None, False, "catalog_source_provenance"),
|
||||
(
|
||||
"duplicate",
|
||||
[valid_unit, deepcopy(valid_unit)],
|
||||
True,
|
||||
"catalog_release_metadata",
|
||||
),
|
||||
(
|
||||
"malformed",
|
||||
[{**valid_unit, "commit_sha": "not-a-git-object"}],
|
||||
True,
|
||||
"catalog_source_provenance",
|
||||
),
|
||||
)
|
||||
for label, selected_units, include_selected_units, expected_code in cases:
|
||||
with self.subTest(label=label):
|
||||
catalog, keyring = signed_catalog(
|
||||
self.assessment,
|
||||
selected_units=selected_units,
|
||||
include_selected_units=include_selected_units,
|
||||
)
|
||||
|
||||
report = review_capability_fit(
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", report["status"])
|
||||
self.assertIn(
|
||||
expected_code, {item["code"] for item in report["findings"]}
|
||||
)
|
||||
self.assertFalse(report["proof_scope"]["release_metadata"]["valid"])
|
||||
|
||||
def test_catalog_validation_ignores_and_preserves_installer_replay_state(
|
||||
self,
|
||||
) -> None:
|
||||
catalog, keyring = signed_catalog(self.assessment)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
state_path = Path(temp_dir) / "catalog-sequence.json"
|
||||
state_path.write_text(
|
||||
json.dumps(
|
||||
{"channels": {"stable": {"last_sequence": catalog["sequence"]}}}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
replay_environment = {
|
||||
"GOVOPLAN_MODULE_PACKAGE_CATALOG_SEQUENCE_STATE": str(state_path),
|
||||
"GOVOPLAN_MODULE_PACKAGE_CATALOG_ENFORCE_SEQUENCE": "true",
|
||||
}
|
||||
with mock.patch.dict(os.environ, replay_environment, clear=False):
|
||||
report = review_capability_fit(
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
)
|
||||
self.assertEqual(
|
||||
replay_environment,
|
||||
{key: os.environ[key] for key in replay_environment},
|
||||
)
|
||||
|
||||
self.assertEqual("current", report["status"])
|
||||
self.assertTrue(report["proof_scope"]["catalog_signature_and_keyring"]["valid"])
|
||||
|
||||
def test_untrusted_or_substituted_keyring_blocks_rerun(self) -> None:
|
||||
catalog, _ = signed_catalog(self.assessment)
|
||||
_, other_keyring = signed_catalog(self.assessment)
|
||||
@@ -186,6 +340,8 @@ def signed_catalog(
|
||||
*,
|
||||
versions: dict[str, str] | None = None,
|
||||
sequence: int = 202607220843,
|
||||
selected_units: list[dict[str, object]] | None = None,
|
||||
include_selected_units: bool = True,
|
||||
) -> tuple[dict[str, object], dict[str, object]]:
|
||||
versions = versions or {}
|
||||
private_key = Ed25519PrivateKey.generate()
|
||||
@@ -220,6 +376,27 @@ def signed_catalog(
|
||||
else:
|
||||
modules.append(entry)
|
||||
assert core_release is not None
|
||||
if selected_units is None:
|
||||
selected_units = [
|
||||
{
|
||||
"repo": component["repository"],
|
||||
"version": versions.get(
|
||||
str(component["module_id"]), str(component["manifest_version"])
|
||||
),
|
||||
"tag": "v"
|
||||
+ versions.get(
|
||||
str(component["module_id"]), str(component["manifest_version"])
|
||||
),
|
||||
"commit_sha": (str(component["commit"]) + "0" * 40)[:40],
|
||||
"tag_object_sha": hashlib.sha256(
|
||||
f"{component['repository']}:{component['manifest_version']}:tag".encode()
|
||||
).hexdigest()[:40],
|
||||
}
|
||||
for component in assessment["composition"]
|
||||
]
|
||||
release: dict[str, object] = {"keyring_sha256": canonical_hash(keyring)}
|
||||
if include_selected_units:
|
||||
release["selected_units"] = selected_units
|
||||
catalog: dict[str, object] = {
|
||||
"catalog_version": "1",
|
||||
"channel": "stable",
|
||||
@@ -228,7 +405,7 @@ def signed_catalog(
|
||||
"expires_at": (now + timedelta(days=30)).isoformat().replace("+00:00", "Z"),
|
||||
"core_release": core_release,
|
||||
"modules": modules,
|
||||
"release": {"keyring_sha256": canonical_hash(keyring)},
|
||||
"release": release,
|
||||
}
|
||||
signature_payload = json.dumps(
|
||||
catalog, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
||||
|
||||
@@ -74,6 +74,40 @@ class ReleasePlanGuidanceTests(unittest.TestCase):
|
||||
self.assertEqual("preview_source_release", plan.recommended_action.id)
|
||||
self.assertIn("Preview Tag + Publish", plan.recommended_action.remediation)
|
||||
|
||||
def test_malformed_version_metadata_is_a_structured_blocker(self) -> None:
|
||||
cases = (
|
||||
("package.json", "{not-json\n", "JSONDecodeError"),
|
||||
("pyproject.toml", "[project\n", "TOMLDecodeError"),
|
||||
)
|
||||
for relative_path, malformed, error_name in cases:
|
||||
with self.subTest(relative_path=relative_path):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
workspace = Path(temp_dir)
|
||||
repo_path = workspace / "govoplan-files"
|
||||
repo_path.mkdir()
|
||||
(repo_path / "pyproject.toml").write_text(
|
||||
'[project]\nname = "govoplan-files"\nversion = "1.2.4"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(repo_path / relative_path).write_text(malformed, encoding="utf-8")
|
||||
|
||||
plan = build_selective_release_plan(
|
||||
dashboard(workspace=workspace, version="1.2.4"),
|
||||
selected_repos=("govoplan-files",),
|
||||
repo_versions={"govoplan-files": "1.2.4"},
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", plan.status)
|
||||
finding = next(
|
||||
item
|
||||
for item in plan.gate_findings
|
||||
if item.code == "repository_version_metadata_unreadable"
|
||||
)
|
||||
self.assertEqual("govoplan-files", finding.repo)
|
||||
self.assertIn(error_name, finding.message)
|
||||
self.assertIn("Repair the malformed TOML or JSON", finding.remediation)
|
||||
self.assertEqual("resolve_release_gate", plan.recommended_action.id)
|
||||
|
||||
def test_webui_renders_recommendation_and_remediation(self) -> None:
|
||||
webui = (RELEASE_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user