Skip to content

Commit 03b92cc

Browse files
Merge pull request #15 from cbusillo/code/2026-04-27-local-typed-odoo-settings
Generate typed local Odoo settings payloads
2 parents 7e8c0c8 + 472bfb2 commit 03b92cc

4 files changed

Lines changed: 164 additions & 1 deletion

File tree

docs/tooling/workspace-cli.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,14 @@ Notes
180180
- Local `platform runtime up` emits manifest-backed host addon mount
181181
paths for compose, so tenant checkouts can bind-mount `sources/tenant/addons`
182182
plus `sources/shared-addons` into the devkit-owned local runtime bundle.
183+
- Local runtime selection converts legacy setting-shaped inputs such as
184+
`ENV_OVERRIDE_CONFIG_PARAM__*`, `ENV_OVERRIDE_AUTHENTIK__*`, and
185+
`ENV_OVERRIDE_SHOPIFY__*` into the typed
186+
`ODOO_INSTANCE_OVERRIDES_PAYLOAD_B64` payload consumed by
187+
`launchplane_settings`. The generated runtime env no longer emits those
188+
legacy setting keys, while unrelated devkit control keys such as
189+
`ENV_OVERRIDE_DISABLE_CRON` remain available until they get their own typed
190+
local contract.
183191
- When `ODOO_CONTROL_PLANE_ROOT` points at a valid `launchplane`
184192
checkout, local runtime env resolution comes from the control-plane-owned
185193
environment contract. Devkit-local `.env` / `platform/secrets.toml` runtime

odoo_devkit/local_runtime.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@
3131
GIT_SHA_PATTERN = re.compile(r"[0-9a-fA-F]{7,40}")
3232
ARTIFACT_SOURCE_ENV_KEYS = ("ODOO_ADDON_REPOSITORIES", "OPENUPGRADE_ADDON_REPOSITORY")
3333
RUNTIME_ENVIRONMENT_PAYLOAD_ENV_VAR = "ODOO_DEVKIT_RUNTIME_ENVIRONMENT_JSON"
34+
ODOO_INSTANCE_OVERRIDES_PAYLOAD_ENV_KEY = "ODOO_INSTANCE_OVERRIDES_PAYLOAD_B64"
35+
LEGACY_CONFIG_PARAM_PREFIX = "ENV_OVERRIDE_CONFIG_PARAM__"
36+
LEGACY_AUTHENTIK_PREFIX = "ENV_OVERRIDE_AUTHENTIK__"
37+
LEGACY_SHOPIFY_PREFIX = "ENV_OVERRIDE_SHOPIFY__"
38+
LEGACY_SETTING_OVERRIDE_PREFIXES = (
39+
LEGACY_CONFIG_PARAM_PREFIX,
40+
LEGACY_AUTHENTIK_PREFIX,
41+
LEGACY_SHOPIFY_PREFIX,
42+
)
3443

3544
PLATFORM_RUNTIME_ENV_KEYS = (
3645
"PLATFORM_CONTEXT",
@@ -1545,9 +1554,93 @@ def build_runtime_env_values(
15451554
runtime_values[environment_key] = source_environment[environment_key]
15461555
for runtime_key, runtime_value in runtime_selection.effective_runtime_env.items():
15471556
runtime_values[runtime_key] = runtime_value
1557+
apply_typed_odoo_instance_override_payload(
1558+
runtime_values=runtime_values,
1559+
context_name=runtime_selection.context_name,
1560+
instance_name=runtime_selection.instance_name,
1561+
)
15481562
return runtime_values
15491563

15501564

1565+
def apply_typed_odoo_instance_override_payload(
1566+
*,
1567+
runtime_values: dict[str, str],
1568+
context_name: str,
1569+
instance_name: str,
1570+
) -> None:
1571+
payload = build_typed_odoo_instance_override_payload(
1572+
runtime_values=runtime_values,
1573+
context_name=context_name,
1574+
instance_name=instance_name,
1575+
)
1576+
if payload is None:
1577+
return
1578+
if runtime_values.get(ODOO_INSTANCE_OVERRIDES_PAYLOAD_ENV_KEY, "").strip():
1579+
raise RuntimeCommandError(
1580+
f"{ODOO_INSTANCE_OVERRIDES_PAYLOAD_ENV_KEY} cannot be combined with legacy ENV_OVERRIDE_* setting inputs."
1581+
)
1582+
encoded_payload = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
1583+
runtime_values[ODOO_INSTANCE_OVERRIDES_PAYLOAD_ENV_KEY] = base64.b64encode(encoded_payload).decode("ascii")
1584+
for runtime_key in tuple(runtime_values):
1585+
if runtime_key.startswith(LEGACY_SETTING_OVERRIDE_PREFIXES):
1586+
runtime_values.pop(runtime_key, None)
1587+
1588+
1589+
def build_typed_odoo_instance_override_payload(
1590+
*,
1591+
runtime_values: dict[str, str],
1592+
context_name: str,
1593+
instance_name: str,
1594+
) -> dict[str, object] | None:
1595+
config_parameters: list[dict[str, object]] = []
1596+
addon_settings: list[dict[str, object]] = []
1597+
for runtime_key in sorted(runtime_values):
1598+
runtime_value = runtime_values[runtime_key]
1599+
if runtime_key.startswith(LEGACY_CONFIG_PARAM_PREFIX):
1600+
suffix = runtime_key[len(LEGACY_CONFIG_PARAM_PREFIX) :].strip().lower()
1601+
if not suffix:
1602+
continue
1603+
config_parameters.append(
1604+
{
1605+
"key": suffix.replace("__", "."),
1606+
"value": {"source": "literal", "value": runtime_value},
1607+
}
1608+
)
1609+
continue
1610+
if runtime_key.startswith(LEGACY_AUTHENTIK_PREFIX):
1611+
suffix = runtime_key[len(LEGACY_AUTHENTIK_PREFIX) :].strip().lower()
1612+
if not suffix:
1613+
continue
1614+
addon_settings.append(
1615+
{
1616+
"addon": "authentik_sso",
1617+
"setting": suffix,
1618+
"value": {"source": "literal", "value": runtime_value},
1619+
}
1620+
)
1621+
continue
1622+
if runtime_key.startswith(LEGACY_SHOPIFY_PREFIX):
1623+
suffix = runtime_key[len(LEGACY_SHOPIFY_PREFIX) :].strip().lower()
1624+
if not suffix:
1625+
continue
1626+
addon_settings.append(
1627+
{
1628+
"addon": "shopify",
1629+
"setting": suffix,
1630+
"value": {"source": "literal", "value": runtime_value},
1631+
}
1632+
)
1633+
if not config_parameters and not addon_settings:
1634+
return None
1635+
return {
1636+
"schema_version": 1,
1637+
"context": context_name,
1638+
"instance": instance_name,
1639+
"config_parameters": config_parameters,
1640+
"addon_settings": addon_settings,
1641+
}
1642+
1643+
15511644
def apply_publish_artifact_input_manifest(
15521645
*,
15531646
runtime_context: RuntimeContext,

platform/stack.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ install_modules_add = [
9999
database = "opw"
100100
install_modules = [
101101
"opw_custom",
102-
"environment_overrides",
103102
]
104103

105104
[contexts.opw.runtime_env]

tests/test_runtime.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import argparse
4+
import base64
45
import contextlib
56
import io
67
import json
@@ -108,6 +109,68 @@ def test_resolve_runtime_repo_path_prefers_explicit_repo(self) -> None:
108109

109110
self.assertEqual(resolve_runtime_repo_path(manifest), runtime_repo_path.resolve())
110111

112+
def test_typed_odoo_instance_override_payload_from_legacy_setting_env(self) -> None:
113+
runtime_values = {
114+
"ENV_OVERRIDE_CONFIG_PARAM__WEB__BASE__URL": "https://opw-local.example.com",
115+
"ENV_OVERRIDE_AUTHENTIK__BASE_URL": "https://authentik.example.com",
116+
"ENV_OVERRIDE_SHOPIFY__TEST_STORE": "true",
117+
"ENV_OVERRIDE_DISABLE_CRON": "1",
118+
}
119+
120+
local_runtime.apply_typed_odoo_instance_override_payload(
121+
runtime_values=runtime_values,
122+
context_name="opw",
123+
instance_name="local",
124+
)
125+
126+
encoded_payload = runtime_values[local_runtime.ODOO_INSTANCE_OVERRIDES_PAYLOAD_ENV_KEY]
127+
payload = json.loads(base64.b64decode(encoded_payload).decode("utf-8"))
128+
129+
self.assertEqual(payload["context"], "opw")
130+
self.assertEqual(payload["instance"], "local")
131+
self.assertEqual(
132+
payload["config_parameters"],
133+
[
134+
{
135+
"key": "web.base.url",
136+
"value": {"source": "literal", "value": "https://opw-local.example.com"},
137+
}
138+
],
139+
)
140+
self.assertIn(
141+
{
142+
"addon": "authentik_sso",
143+
"setting": "base_url",
144+
"value": {"source": "literal", "value": "https://authentik.example.com"},
145+
},
146+
payload["addon_settings"],
147+
)
148+
self.assertIn(
149+
{
150+
"addon": "shopify",
151+
"setting": "test_store",
152+
"value": {"source": "literal", "value": "true"},
153+
},
154+
payload["addon_settings"],
155+
)
156+
self.assertNotIn("ENV_OVERRIDE_CONFIG_PARAM__WEB__BASE__URL", runtime_values)
157+
self.assertNotIn("ENV_OVERRIDE_AUTHENTIK__BASE_URL", runtime_values)
158+
self.assertNotIn("ENV_OVERRIDE_SHOPIFY__TEST_STORE", runtime_values)
159+
self.assertEqual(runtime_values["ENV_OVERRIDE_DISABLE_CRON"], "1")
160+
161+
def test_typed_odoo_instance_override_payload_rejects_mixed_authority(self) -> None:
162+
runtime_values = {
163+
local_runtime.ODOO_INSTANCE_OVERRIDES_PAYLOAD_ENV_KEY: "already-set",
164+
"ENV_OVERRIDE_CONFIG_PARAM__WEB__BASE__URL": "https://opw-local.example.com",
165+
}
166+
167+
with self.assertRaisesRegex(local_runtime.RuntimeCommandError, "cannot be combined"):
168+
local_runtime.apply_typed_odoo_instance_override_payload(
169+
runtime_values=runtime_values,
170+
context_name="opw",
171+
instance_name="local",
172+
)
173+
111174
def test_resolve_runtime_repo_path_requires_workspace_sync_for_repo_addressable_runtime(self) -> None:
112175
with tempfile.TemporaryDirectory() as temporary_directory:
113176
temp_root = Path(temporary_directory)

0 commit comments

Comments
 (0)