mirror of
https://github.com/okxlin/appstore.git
synced 2026-11-15 08:06:06 +00:00
fix(renovate): keep auxiliary image updates open for review
Auxiliary-only Renovate updates now stay open for manual review. Guard coverage and workflow lint passed.
This commit is contained in:
@@ -161,11 +161,38 @@ def compare_compose(
|
||||
service_count,
|
||||
changed_services,
|
||||
primary_services,
|
||||
"close",
|
||||
"review",
|
||||
"multi-service compose changed only auxiliary services",
|
||||
)
|
||||
|
||||
|
||||
def aggregate_decisions(decisions: list[ComposeDecision], *, seen_compose: bool) -> dict:
|
||||
if not seen_compose:
|
||||
return {
|
||||
"decision": "allow",
|
||||
"reason": "no compose files changed",
|
||||
"details": [],
|
||||
}
|
||||
|
||||
manual_review = [d for d in decisions if d.outcome == "review"]
|
||||
blocking_unknown = [d for d in decisions if d.outcome == "unknown"]
|
||||
if manual_review and not blocking_unknown:
|
||||
summary = "; ".join(
|
||||
f"{d.app}:{','.join(d.changed_services)}" for d in manual_review
|
||||
)
|
||||
return {
|
||||
"decision": "review",
|
||||
"reason": f"auxiliary-only update needs full-stack review ({summary})",
|
||||
"details": [d.__dict__ for d in decisions],
|
||||
}
|
||||
|
||||
return {
|
||||
"decision": "allow",
|
||||
"reason": "keep PR open",
|
||||
"details": [d.__dict__ for d in decisions],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--repo", required=True)
|
||||
@@ -221,31 +248,7 @@ def main() -> int:
|
||||
)
|
||||
)
|
||||
|
||||
if not seen_compose:
|
||||
result = {
|
||||
"decision": "allow",
|
||||
"reason": "no compose files changed",
|
||||
"details": [],
|
||||
}
|
||||
else:
|
||||
closable = [d for d in decisions if d.outcome == "close"]
|
||||
blocking_unknown = [d for d in decisions if d.outcome == "unknown"]
|
||||
primary_changed = [d for d in decisions if d.outcome == "allow"]
|
||||
if closable and not blocking_unknown and not primary_changed:
|
||||
summary = "; ".join(
|
||||
f"{d.app}:{','.join(d.changed_services)}" for d in closable
|
||||
)
|
||||
result = {
|
||||
"decision": "close",
|
||||
"reason": f"sidecar-only Renovate update detected ({summary})",
|
||||
"details": [d.__dict__ for d in decisions],
|
||||
}
|
||||
else:
|
||||
result = {
|
||||
"decision": "allow",
|
||||
"reason": "keep PR open",
|
||||
"details": [d.__dict__ for d in decisions],
|
||||
}
|
||||
result = aggregate_decisions(decisions, seen_compose=seen_compose)
|
||||
|
||||
json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
|
||||
sys.stdout.write("\n")
|
||||
|
||||
@@ -515,7 +515,7 @@ class RenovateAppVersionTests(unittest.TestCase):
|
||||
self.assertEqual("wait", too_soon["decision"])
|
||||
self.assertEqual("limit", too_many["decision"])
|
||||
|
||||
def test_retry_workflow_and_sidecar_branch_cleanup_are_guarded(self):
|
||||
def test_retry_workflow_and_sidecar_updates_are_reviewed_without_branch_cleanup(self):
|
||||
retry_workflow = (
|
||||
REPO_ROOT / ".github" / "workflows" / "renovate-rate-limit-retry.yml"
|
||||
).read_text(encoding="utf-8")
|
||||
@@ -527,9 +527,11 @@ class RenovateAppVersionTests(unittest.TestCase):
|
||||
self.assertIn("actions: write", retry_workflow)
|
||||
self.assertIn("renovate_retry_gate.py", retry_workflow)
|
||||
self.assertIn("automatic retry after Docker Hub 429", retry_workflow)
|
||||
self.assertIn('[[ "$head_repo" == "$REPO" ]]', sidecar_workflow)
|
||||
self.assertIn('selfhosted-renovate/*|renovate/*', sidecar_workflow)
|
||||
self.assertIn("git/refs/heads/$head_ref", sidecar_workflow)
|
||||
self.assertIn("decision == 'review'", sidecar_workflow)
|
||||
self.assertIn("gh pr comment", sidecar_workflow)
|
||||
self.assertNotIn("gh pr close", sidecar_workflow)
|
||||
self.assertNotIn("git/refs/heads/$head_ref", sidecar_workflow)
|
||||
|
||||
def test_image_tag_strips_digest_from_tagged_image(self):
|
||||
module = load_module()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ SPEC.loader.exec_module(GUARD)
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
||||
PRIMARY_SERVICES_PATH = REPOSITORY_ROOT / ".github" / "renovate-primary-services.json"
|
||||
RENOVATE_CONFIG_PATH = REPOSITORY_ROOT / ".github" / "renovate-docker.json"
|
||||
SIDECAR_GUARD_WORKFLOW_PATH = REPOSITORY_ROOT / ".github" / "workflows" / "renovate-sidecar-guard.yml"
|
||||
|
||||
|
||||
def compose(**images: str) -> dict:
|
||||
@@ -55,14 +56,14 @@ class ResolvePrimaryServicesTests(unittest.TestCase):
|
||||
|
||||
|
||||
class InferredPrimaryDecisionTests(unittest.TestCase):
|
||||
def test_closes_weblate_sidecar_only_update(self) -> None:
|
||||
def test_marks_weblate_sidecar_only_update_for_manual_review(self) -> None:
|
||||
base = compose(cache="valkey:9.1.0", database="postgres:18", weblate="weblate:latest")
|
||||
head = compose(cache="valkey:9.1.1", database="postgres:18", weblate="weblate:latest")
|
||||
primary = GUARD.resolve_primary_services("weblate", [], base, head)
|
||||
|
||||
decision = GUARD.compare_compose("weblate", "apps/weblate/latest/docker-compose.yml", base, head, primary)
|
||||
|
||||
self.assertEqual(decision.outcome, "close")
|
||||
self.assertEqual(decision.outcome, "review")
|
||||
self.assertEqual(decision.changed_services, ["cache"])
|
||||
|
||||
def test_allows_inferred_primary_update(self) -> None:
|
||||
@@ -84,6 +85,24 @@ class InferredPrimaryDecisionTests(unittest.TestCase):
|
||||
self.assertEqual(decision.detail, "single-service compose")
|
||||
|
||||
|
||||
class DecisionAggregationTests(unittest.TestCase):
|
||||
def test_auxiliary_only_update_emits_manual_review_decision(self) -> None:
|
||||
decision = GUARD.ComposeDecision(
|
||||
app="firecrawl",
|
||||
path="apps/firecrawl/2.11.359/docker-compose.yml",
|
||||
services_total=2,
|
||||
changed_services=["firecrawl-playwright"],
|
||||
primary_services=["firecrawl"],
|
||||
outcome="review",
|
||||
detail="multi-service compose changed only auxiliary services",
|
||||
)
|
||||
|
||||
result = GUARD.aggregate_decisions([decision], seen_compose=True)
|
||||
|
||||
self.assertEqual(result["decision"], "review")
|
||||
self.assertIn("firecrawl:firecrawl-playwright", result["reason"])
|
||||
|
||||
|
||||
class RepositoryPolicyTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
@@ -119,7 +138,7 @@ class RepositoryPolicyTests(unittest.TestCase):
|
||||
self.primary_services["dify"],
|
||||
)
|
||||
|
||||
self.assertEqual(decision.outcome, "close")
|
||||
self.assertEqual(decision.outcome, "review")
|
||||
|
||||
def test_safeline_components_are_grouped_behind_management_service(self) -> None:
|
||||
self.assertEqual(self.primary_services["safeline"], ["safeline-mgt"])
|
||||
@@ -173,7 +192,7 @@ class RepositoryPolicyTests(unittest.TestCase):
|
||||
self.primary_services["uuwaf"],
|
||||
)
|
||||
|
||||
self.assertEqual(decision.outcome, "close")
|
||||
self.assertEqual(decision.outcome, "review")
|
||||
|
||||
def test_langflow_updates_require_manual_compatibility_review(self) -> None:
|
||||
self.assertTrue(
|
||||
@@ -184,5 +203,15 @@ class RepositoryPolicyTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class WorkflowSafetyTests(unittest.TestCase):
|
||||
def test_sidecar_only_updates_are_reviewed_without_closing_or_deleting_branches(self) -> None:
|
||||
workflow = SIDECAR_GUARD_WORKFLOW_PATH.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("decision == 'review'", workflow)
|
||||
self.assertIn("gh pr comment", workflow)
|
||||
self.assertNotIn("gh pr close", workflow)
|
||||
self.assertNotIn("git/refs/heads", workflow)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user