From 042717704e7a0637cc35bf74e36ac8c7be3812bb Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Sun, 14 Jun 2026 00:48:26 -0400 Subject: [PATCH] Publish Odoo install modules in artifact manifests --- docs/tooling/workspace-cli.md | 9 +++++++-- odoo_devkit/local_runtime.py | 3 +++ tests/test_runtime.py | 20 ++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/tooling/workspace-cli.md b/docs/tooling/workspace-cli.md index 943ed4b..a8cb847 100644 --- a/docs/tooling/workspace-cli.md +++ b/docs/tooling/workspace-cli.md @@ -255,7 +255,10 @@ Notes - When Launchplane supplies `ODOO_DEVKIT_RUNTIME_ENVIRONMENT_JSON`, publish can synthesize the selected manifest context from that explicit payload instead of requiring every Launchplane-owned product context to be listed in the shared - devkit stack. Unknown contexts still fail closed without the explicit payload. + devkit stack. Synthesized contexts do not inherit stack-level install-module + lists; their artifact install intent comes from the managed-instance required + modules plus any repo-owned `website-bootstrap.toml` modules. Unknown contexts + still fail closed without the explicit payload. - Publish-time GHCR credentials can be split by purpose. Private base image reads prefer `GHCR_READ_TOKEN`, artifact image pushes prefer `GHCR_TOKEN`, and private source checkout secrets still belong in the transient runtime @@ -274,7 +277,9 @@ Notes Repo-owned source selection belongs in the dedicated artifact-input manifest. - Artifact manifests preserve selector intent in `addon_selectors` while keeping `addon_sources` as the resolved exact-SHA runtime truth consumed by - control-plane release and deploy flows. + control-plane release and deploy flows. They also include the resolved + `odoo_install_modules` list so promotion/deploy orchestration can preserve + tenant module activation intent when it rewrites a live target environment. - `platform runtime odoo-shell` follows the same local-only rule. It can run interactively, consume a `--script` file, and optionally tee output into a `--log-file`, but it is still a manifest-backed local helper rather than a diff --git a/odoo_devkit/local_runtime.py b/odoo_devkit/local_runtime.py index 2c7677b..e80652d 100644 --- a/odoo_devkit/local_runtime.py +++ b/odoo_devkit/local_runtime.py @@ -552,6 +552,7 @@ def publish_runtime_artifact( runtime_repo_commit=runtime_commit, artifact_source_entries=artifact_source_entries, source_selector_entries=artifact_source_selectors, + odoo_install_modules=runtime_context.selection.effective_install_modules, openupgrade_addon_repository=runtime_values.get("OPENUPGRADE_ADDON_REPOSITORY", ""), openupgradelib_install_spec=runtime_values.get("OPENUPGRADELIB_INSTALL_SPEC", ""), addon_skip_flags=parse_csv_values(runtime_values.get("ODOO_PYTHON_SYNC_SKIP_ADDONS", "")), @@ -2378,6 +2379,7 @@ def build_runtime_artifact_manifest_payload( runtime_repo_commit: str, artifact_source_entries: tuple[dict[str, str], ...], source_selector_entries: tuple[dict[str, str], ...], + odoo_install_modules: tuple[str, ...], openupgrade_addon_repository: str, openupgradelib_install_spec: str, addon_skip_flags: tuple[str, ...], @@ -2402,6 +2404,7 @@ def build_runtime_artifact_manifest_payload( "enterprise_base_digest": enterprise_base_digest, "addon_sources": list(artifact_source_entries), "addon_selectors": list(source_selector_entries), + "odoo_install_modules": list(odoo_install_modules), "openupgrade_inputs": { "addon_repository": openupgrade_addon_repository, "install_spec": openupgradelib_install_spec, diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 9915543..d048341 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -1072,6 +1072,10 @@ def fake_run_command( self.assertEqual(payload["enterprise_base_digest"], "sha256:" + "2" * 64) self.assertEqual(payload["build_flags"]["values"]["build_target"], "production") self.assertEqual(payload["image"]["tags"], ["opw-20260416-abcdef"]) + self.assertEqual( + payload["odoo_install_modules"], + ["launchplane_settings", "disable_odoo_online", "opw_custom"], + ) self.assertEqual(payload["output_file"], str(output_file.resolve())) written_payload = json.loads(output_file.read_text(encoding="utf-8")) self.assertEqual(written_payload["artifact_id"], payload["artifact_id"]) @@ -1184,6 +1188,10 @@ def fake_run_command( } ], ) + self.assertEqual( + payload["odoo_install_modules"], + ["launchplane_settings", "disable_odoo_online", "opw_custom"], + ) self.assertNotIn("odoo_addon_repository_selectors", payload["build_flags"]["values"]) def test_registry_auth_splits_base_image_read_and_artifact_push_tokens(self) -> None: @@ -1384,6 +1392,10 @@ def fake_run_command( } ], ) + self.assertEqual( + payload["odoo_install_modules"], + ["launchplane_settings", "disable_odoo_online", "opw_custom"], + ) def test_native_runtime_publish_rejects_invalid_artifact_inputs_manifest(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: @@ -1685,6 +1697,10 @@ def fake_run_command( {"repository": "cbusillo/disable_odoo_online", "ref": exact_ref.rsplit("@", 1)[1]}, payload["addon_sources"], ) + self.assertEqual( + payload["odoo_install_modules"], + ["launchplane_settings", "disable_odoo_online", "opw_custom"], + ) def test_native_runtime_publish_rejects_unknown_context_without_explicit_runtime_payload(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: @@ -1836,6 +1852,10 @@ def fake_run_command( self.assertTrue(payload["artifact_id"].startswith("artifact-cm_website-")) self.assertEqual(payload["image"]["repository"], "ghcr.io/example/cm-website-runtime") self.assertEqual(payload["image"]["tags"], ["cm_website-20260606-abcdef"]) + self.assertEqual( + payload["odoo_install_modules"], + ["launchplane_settings", "disable_odoo_online", "cm_website"], + ) self.assertIn( "ODOO_ADDON_REPOSITORIES=cbusillo/disable_odoo_online@411f6b8e85cac72dc7aa2e2dc5540001043c327d", captured_build_args,