From 8324c99e87e90545caf40b14fe2e4d8c922e0ba0 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Sat, 13 Jun 2026 22:49:48 -0400 Subject: [PATCH] Require Launchplane Odoo modules for managed lanes --- docs/tooling/workspace-cli.md | 7 ++- odoo_devkit/local_runtime.py | 8 ++++ tests/test_runtime.py | 80 +++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/docs/tooling/workspace-cli.md b/docs/tooling/workspace-cli.md index fa53178..943ed4b 100644 --- a/docs/tooling/workspace-cli.md +++ b/docs/tooling/workspace-cli.md @@ -180,6 +180,11 @@ Notes `launchplane_settings`. `config_parameters` tables write Odoo `ir.config_parameter` keys, while `addon_settings.` tables write supported addon settings such as `authentik_sso` values. +- Non-local Launchplane-managed instances (`dev`, `testing`, and `prod`) always + prepend `launchplane_settings` and `disable_odoo_online` to the resolved Odoo + install module list. Artifact inputs or base images make addon files + available, but this install list is what activates those modules in each + database. - When a tenant repo contains `website-bootstrap.toml` beside `workspace.toml`, runtime selection also folds that non-secret website intent into the same typed payload. The bootstrap contract can add install modules, provide the @@ -198,7 +203,7 @@ Notes continue. `LAUNCHPLANE_WEBSITE_BOOTSTRAP_REQUIRED=true` additionally requires a non-empty `website_bootstrap` object in that payload. These flags are runtime assertions supplied by Launchplane-managed records or operator input; - local/dev runtimes remain optional unless a caller explicitly sets them. + local runtimes remain optional unless a caller explicitly sets them. - Legacy setting-shaped inputs such as `ENV_OVERRIDE_CONFIG_PARAM__*`, `ENV_OVERRIDE_AUTHENTIK__*`, and `ENV_OVERRIDE_SHOPIFY__*` are still accepted as a compatibility input and converted into the same typed payload, but they diff --git a/odoo_devkit/local_runtime.py b/odoo_devkit/local_runtime.py index 5c99bfb..2c7677b 100644 --- a/odoo_devkit/local_runtime.py +++ b/odoo_devkit/local_runtime.py @@ -191,6 +191,8 @@ DEFAULT_ODOO_BASE_RUNTIME_IMAGE = "registry.invalid/private-enterprise-runtime:19.0-runtime" DEFAULT_ODOO_BASE_DEVTOOLS_IMAGE = "registry.invalid/private-enterprise-devtools:19.0-devtools" CONTROL_PLANE_ROOT_ENV_VAR = "ODOO_CONTROL_PLANE_ROOT" +LAUNCHPLANE_MANAGED_INSTANCE_NAMES = {"dev", "testing", "prod"} +LAUNCHPLANE_REQUIRED_ODOO_MODULES = ("launchplane_settings", "disable_odoo_online") _REGISTRY_LOGINS_DONE: set[tuple[str, str, str]] = set() _VERIFIED_IMAGE_ACCESS: set[str] = set() @@ -1530,6 +1532,8 @@ def resolve_runtime_selection( effective_install_modules = merge_effective_modules( context_definition=context_definition, instance_definition=instance_definition ) + if launchplane_managed_instance(instance_name): + effective_install_modules = dedupe_module_names((*LAUNCHPLANE_REQUIRED_ODOO_MODULES, *effective_install_modules)) if website_bootstrap is not None: effective_install_modules = dedupe_module_names((*effective_install_modules, *website_bootstrap.install_modules)) effective_source_repositories = resolve_runtime_source_repositories( @@ -1588,6 +1592,10 @@ def merge_effective_modules(*, context_definition: ContextDefinition, instance_d return tuple(effective_install_modules) +def launchplane_managed_instance(instance_name: str) -> bool: + return instance_name.strip().lower() in LAUNCHPLANE_MANAGED_INSTANCE_NAMES + + def dedupe_module_names(module_names: Iterable[str]) -> tuple[str, ...]: effective_module_names: list[str] = [] for module_name in module_names: diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 421a213..9915543 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -1534,6 +1534,86 @@ def test_resolve_runtime_selection_tracks_effective_source_selectors(self) -> No "example/testing_selector@release-19", ), ) + self.assertEqual( + selection.effective_install_modules, + ("launchplane_settings", "disable_odoo_online", "opw_custom"), + ) + + def test_resolve_runtime_selection_keeps_local_modules_explicit(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + temp_root = Path(temporary_directory) + stack_definition = local_runtime.parse_stack_definition( + { + "schema_version": 1, + "odoo_version": "19.0", + "addons_path": ["/odoo/addons", "/opt/project/addons"], + "contexts": { + "opw": { + "database": "opw", + "install_modules": ["opw_custom"], + "instances": { + "local": {}, + }, + } + }, + }, + stack_file_path=temp_root / "platform" / "stack.toml", + ) + + selection = local_runtime.resolve_runtime_selection( + stack_definition=stack_definition, + artifact_inputs_definition=None, + context_name="opw", + instance_name="local", + repo_root=temp_root, + ) + + self.assertEqual(selection.effective_install_modules, ("opw_custom",)) + + def test_resolve_runtime_selection_orders_managed_and_bootstrap_modules(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + temp_root = Path(temporary_directory) + stack_definition = local_runtime.parse_stack_definition( + { + "schema_version": 1, + "odoo_version": "19.0", + "addons_path": ["/odoo/addons", "/opt/project/addons"], + "contexts": { + "opw": { + "database": "opw", + "install_modules": ["opw_custom"], + "instances": { + "testing": {}, + }, + } + }, + }, + stack_file_path=temp_root / "platform" / "stack.toml", + ) + website_bootstrap = local_runtime.parse_website_bootstrap_definition( + { + "schema_version": 1, + "tenant": "opw", + "odoo": {"install_modules": ["opw_custom", "website_sale"]}, + "website": {"name": "OPW"}, + }, + bootstrap_path=temp_root / "website-bootstrap.toml", + context_name="opw", + ) + + selection = local_runtime.resolve_runtime_selection( + stack_definition=stack_definition, + artifact_inputs_definition=None, + context_name="opw", + instance_name="testing", + repo_root=temp_root, + website_bootstrap=website_bootstrap, + ) + + self.assertEqual( + selection.effective_install_modules, + ("launchplane_settings", "disable_odoo_online", "opw_custom", "website_sale"), + ) def test_native_runtime_publish_prefers_exact_control_plane_refs_over_stack_defaults(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: