From 164b25982c2e5b3791c0e583a1d856422794209c Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Sat, 13 Jun 2026 12:29:32 -0400 Subject: [PATCH] Verify Odoo post-deploy override handoff --- .../odoo_stable_target_replacement.py | 6 +++++ control_plane/dokploy.py | 22 ++++++++++++++++-- .../odoo_stable_target_replacement.py | 23 +++++++++++++++++++ tests/test_dokploy.py | 7 ++++++ tests/test_odoo_stable_target_replacement.py | 14 +++++++++++ 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/control_plane/contracts/odoo_stable_target_replacement.py b/control_plane/contracts/odoo_stable_target_replacement.py index 75ac3de4..cceb139c 100644 --- a/control_plane/contracts/odoo_stable_target_replacement.py +++ b/control_plane/contracts/odoo_stable_target_replacement.py @@ -66,6 +66,12 @@ class OdooStableTargetReplacementApplyResult(BaseModel): release_tuple_id: str = "" deploy_status: Literal["pass", "fail"] post_deploy_status: Literal["pass", "fail", "skipped"] = "skipped" + post_deploy_override_status: Literal["pending", "pass", "fail", "skipped"] = "skipped" + post_deploy_override_record_found: bool = False + post_deploy_override_payload_rendered: bool = False + post_deploy_override_count: int = 0 + post_deploy_website_bootstrap_included: bool = False + post_deploy_override_evidence: dict[str, str] = Field(default_factory=dict) health_status: Literal["pass", "fail", "skipped"] = "skipped" canonical_status: Literal["pass", "fail", "skipped"] = "skipped" logo_status: Literal["pass", "fail", "skipped"] = "skipped" diff --git a/control_plane/dokploy.py b/control_plane/dokploy.py index 13654cdd..3f9aafe9 100644 --- a/control_plane/dokploy.py +++ b/control_plane/dokploy.py @@ -2257,8 +2257,20 @@ def _build_dokploy_data_workflow_script( workflow_environment_lines = _render_docker_exec_environment_lines( workflow_environment_overrides or {} ) + effective_required_workflow_environment_keys = tuple( + sorted( + { + *required_workflow_environment_keys, + *( + tuple(workflow_environment_overrides or {}) + if workflow_environment_overrides + else () + ), + } + ) + ) required_workflow_environment_lines = _render_required_environment_key_lines( - required_workflow_environment_keys + effective_required_workflow_environment_keys ) protected_shopify_store_key_lines = _render_bash_array_assignment_lines( "protected_shopify_store_keys", @@ -2360,7 +2372,10 @@ def _build_dokploy_data_workflow_script( fi if [ "${{#required_workflow_environment_keys[@]}}" -gt 0 ]; then - docker exec "${{script_runner_container_id}}" /bin/bash -lc ' + docker exec \ + "${{workflow_environment[@]}}" \ + "${{script_runner_container_id}}" \ + /bin/bash -lc ' set -euo pipefail for key_name in "$@"; do if [ -z "${{!key_name+x}}" ]; then @@ -2368,6 +2383,9 @@ def _build_dokploy_data_workflow_script( exit 1 fi done + if [ -n "${{ODOO_INSTANCE_OVERRIDES_PAYLOAD_B64:-}}" ]; then + echo "odoo_instance_overrides_payload_present=true" + fi ' _ "${{required_workflow_environment_keys[@]}}" fi diff --git a/control_plane/workflows/odoo_stable_target_replacement.py b/control_plane/workflows/odoo_stable_target_replacement.py index 88d4d5ad..f449074d 100644 --- a/control_plane/workflows/odoo_stable_target_replacement.py +++ b/control_plane/workflows/odoo_stable_target_replacement.py @@ -146,6 +146,7 @@ def result( deploy_status: Literal["pass", "fail"], release_tuple_id: str = "", post_deploy_status: Literal["pass", "fail", "skipped"] = "skipped", + post_deploy_result: object | None = None, health_status: Literal["pass", "fail", "skipped"] = "skipped", canonical_status: Literal["pass", "fail", "skipped"] = "skipped", logo_status: Literal["pass", "fail", "skipped"] = "skipped", @@ -157,6 +158,7 @@ def result( runtime_source: dict[str, str] | None = None, error_message: str = "", ) -> OdooStableTargetReplacementApplyResult: + post_deploy_payload = post_deploy_result return OdooStableTargetReplacementApplyResult( product=self.product, context=self.context, @@ -166,6 +168,24 @@ def result( release_tuple_id=release_tuple_id, deploy_status=deploy_status, post_deploy_status=post_deploy_status, + post_deploy_override_status=getattr( + post_deploy_payload, "override_status", "skipped" + ), + post_deploy_override_record_found=bool( + getattr(post_deploy_payload, "override_record_found", False) + ), + post_deploy_override_payload_rendered=bool( + getattr(post_deploy_payload, "override_payload_rendered", False) + ), + post_deploy_override_count=int( + getattr(post_deploy_payload, "override_count", 0) or 0 + ), + post_deploy_website_bootstrap_included=bool( + getattr(post_deploy_payload, "website_bootstrap_included", False) + ), + post_deploy_override_evidence=dict( + getattr(post_deploy_payload, "override_evidence", {}) or {} + ), health_status=health_status, canonical_status=canonical_status, logo_status=logo_status, @@ -1361,6 +1381,7 @@ def execute_odoo_stable_target_replacement_apply( return base_result.result( deploy_status="fail", post_deploy_status=post_deploy_result.post_deploy_status, + post_deploy_result=post_deploy_result, runtime_identity_injected=True, runtime_source=runtime_source, error_message=post_deploy_result.error_message or "Odoo post-deploy failed.", @@ -1401,6 +1422,7 @@ def execute_odoo_stable_target_replacement_apply( return base_result.result( deploy_status="fail", post_deploy_status="pass", + post_deploy_result=post_deploy_result, health_status=health_status if health_status == "pass" else "fail", canonical_status=canonical_status if canonical_status == "pass" else "fail", logo_status=logo_status if logo_status == "pass" else "fail", @@ -1448,6 +1470,7 @@ def execute_odoo_stable_target_replacement_apply( deploy_status="pass", release_tuple_id=release_tuple_id, post_deploy_status="pass", + post_deploy_result=post_deploy_result, health_status=health_status, canonical_status=canonical_status, logo_status=logo_status, diff --git a/tests/test_dokploy.py b/tests/test_dokploy.py index 8a3477c5..5dfe2075 100644 --- a/tests/test_dokploy.py +++ b/tests/test_dokploy.py @@ -3215,6 +3215,13 @@ def test_build_dokploy_data_workflow_script_injects_workflow_environment(self) - "required_workflow_environment_keys+=(ODOO_OVERRIDE_SECRET__ADDON__SHOPIFY__API_TOKEN)", script, ) + self.assertIn( + f"required_workflow_environment_keys+=({ODOO_INSTANCE_OVERRIDES_PAYLOAD_ENV_KEY})", + script, + ) + self.assertIn('docker exec "${workflow_environment[@]}"', script) + self.assertIn('"${script_runner_container_id}" /bin/bash -lc', script) + self.assertIn("odoo_instance_overrides_payload_present=true", script) self.assertIn("protected_shopify_store_keys+=(yps-your-part-supplier)", script) self.assertIn("Missing required Odoo override environment key", script) self.assertIn("Protected Shopify store key is not allowed on this Dokploy lane.", script) diff --git a/tests/test_odoo_stable_target_replacement.py b/tests/test_odoo_stable_target_replacement.py index c4ecb5de..8610126b 100644 --- a/tests/test_odoo_stable_target_replacement.py +++ b/tests/test_odoo_stable_target_replacement.py @@ -821,6 +821,12 @@ def _wait_for_deploy(**_: object) -> None: instance="testing", phase="deploy", post_deploy_status="pass", + override_status="pass", + override_record_found=True, + override_payload_rendered=True, + override_count=2, + website_bootstrap_included=True, + override_evidence={"config_parameter_count": "1", "website_bootstrap_included": "true"}, ) rendered_compose_file = control_plane_dokploy.render_odoo_raw_compose_file( image_reference="ghcr.io/cbusillo/odoo-tenant-cm@sha256:artifact", @@ -901,6 +907,14 @@ def _sync_source(*, compose_file: str, **_: object) -> dict[str, str]: self.assertEqual(result.deploy_status, "pass") self.assertEqual(result.post_deploy_status, "pass") + self.assertEqual(result.post_deploy_override_status, "pass") + self.assertTrue(result.post_deploy_override_record_found) + self.assertTrue(result.post_deploy_override_payload_rendered) + self.assertEqual(result.post_deploy_override_count, 2) + self.assertTrue(result.post_deploy_website_bootstrap_included) + self.assertEqual( + result.post_deploy_override_evidence["config_parameter_count"], "1" + ) self.assertEqual(result.health_status, "pass") self.assertEqual(result.canonical_status, "pass") self.assertEqual(result.logo_status, "pass")