diff --git a/README.md b/README.md index e49ff82..a86116e 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,11 @@ Do not put the payload or its values in `workspace.toml`, generated workspace docs, checked-in config, `.env`, `platform/.env`, or `platform/secrets.toml`. Missing input, context/instance mismatches, non-string values, and legacy devkit-local env/secrets files fail closed. Non-local -Launchplane artifact workflows may inject the same typed payload boundary; -non-local mutation remains Launchplane-owned. +Launchplane artifact workflows use the same typed payload envelope but include +only artifact build inputs; they do not require or transmit database, master +password, or other deployment-only secrets. A stack-required key is enforced +during publish only when it is itself an artifact build input. Non-local +mutation remains Launchplane-owned. ## Scope diff --git a/docs/tooling/workspace-cli.md b/docs/tooling/workspace-cli.md index 60e205b..d8ed707 100644 --- a/docs/tooling/workspace-cli.md +++ b/docs/tooling/workspace-cli.md @@ -339,12 +339,16 @@ Notes workspace instead. - Non-local publish requires Launchplane to supply `ODOO_DEVKIT_RUNTIME_ENVIRONMENT_JSON`. The payload is authoritative for - artifact build runtime keys and can synthesize a missing context or instance - instead of requiring hosted lanes in the shared devkit stack. Synthesized - contexts do not inherit stack-level install-module lists; their artifact - install intent comes from managed-instance required modules plus any - repo-owned `website-bootstrap.toml` modules. Unknown contexts and non-local - instances fail closed without the explicit payload. + artifact build runtime keys and deliberately excludes deployment-only + database/master secrets. Publish enforces stack-required values only when + those keys are part of the artifact-build input contract; local and remote + runtime mutation commands retain the full stack required-key gate. The + payload can synthesize a missing context or instance instead of requiring + hosted lanes in the shared devkit stack. Synthesized contexts do not inherit + stack-level install-module lists; their artifact install intent comes from + managed-instance required modules plus any repo-owned + `website-bootstrap.toml` modules. Unknown contexts and non-local instances + 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 diff --git a/odoo_devkit/local_runtime.py b/odoo_devkit/local_runtime.py index 37f8ada..e3437be 100644 --- a/odoo_devkit/local_runtime.py +++ b/odoo_devkit/local_runtime.py @@ -524,6 +524,7 @@ def publish_runtime_artifact( manifest=manifest, runtime_repo_path=runtime_repo_path, require_local_instance=False, + enforce_required_environment=False, ) runtime_values = build_runtime_env_values( runtime_context=runtime_context, @@ -531,6 +532,7 @@ def publish_runtime_artifact( image_repository_override=normalized_image_repository, image_tag_override=normalized_image_tag, include_selection_sources=False, + required_environment_keys=(), ) runtime_values = apply_publish_artifact_input_manifest( runtime_context=runtime_context, @@ -541,7 +543,11 @@ def publish_runtime_artifact( runtime_values=runtime_values, ) ensure_required_environment_mapping( - required_keys=runtime_context.stack.stack_definition.required_env_keys, + required_keys=tuple( + required_key + for required_key in runtime_context.stack.stack_definition.required_env_keys + if required_key in ARTIFACT_PUBLISH_RUNTIME_ENV_KEYS + ), environment_values=runtime_values, source_description="Resolved publish runtime environment", ) @@ -918,6 +924,7 @@ def load_runtime_context( manifest: WorkspaceManifest, runtime_repo_path: Path, require_local_instance: bool = True, + enforce_required_environment: bool = True, ) -> RuntimeContext: if require_local_instance: assert_local_instance(instance_name=manifest.runtime.instance, operation_name="platform runtime") @@ -928,10 +935,11 @@ def load_runtime_context( context_name=manifest.runtime.context, instance_name=manifest.runtime.instance, ) - ensure_required_environment_values( - stack_definition=loaded_stack.stack_definition, - loaded_environment=loaded_environment, - ) + if enforce_required_environment: + ensure_required_environment_values( + stack_definition=loaded_stack.stack_definition, + loaded_environment=loaded_environment, + ) try: artifact_inputs_definition = load_artifact_inputs_definition(manifest=manifest) except ArtifactInputsError as error: @@ -954,11 +962,12 @@ def load_runtime_context( repo_root=runtime_repo_path, website_bootstrap=website_bootstrap, ) - ensure_required_runtime_selection_values( - stack_definition=effective_stack_definition, - loaded_environment=loaded_environment, - runtime_selection=runtime_selection, - ) + if enforce_required_environment: + ensure_required_runtime_selection_values( + stack_definition=effective_stack_definition, + loaded_environment=loaded_environment, + runtime_selection=runtime_selection, + ) runtime_env_file = runtime_env_file_for_scope( repo_root=runtime_repo_path, context_name=manifest.runtime.context, @@ -1863,6 +1872,7 @@ def build_runtime_env_values( image_repository_override: str | None = None, image_tag_override: str | None = None, include_selection_sources: bool = True, + required_environment_keys: tuple[str, ...] | None = None, ) -> dict[str, str]: stack_definition = runtime_context.stack.stack_definition runtime_selection = runtime_context.selection @@ -1960,7 +1970,7 @@ def build_runtime_env_values( website_bootstrap=runtime_selection.website_bootstrap, ) ensure_required_environment_mapping( - required_keys=stack_definition.required_env_keys, + required_keys=stack_definition.required_env_keys if required_environment_keys is None else required_environment_keys, environment_values=runtime_values, source_description="Resolved runtime environment", ) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index f119a61..50d0c15 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -166,15 +166,21 @@ def _configure_publish_runtime_payload( instance: str = "testing", odoo_version: str | None = "19.0", environment: dict[str, str] | None = None, + include_deployment_secrets: bool = True, ) -> None: payload_environment = { - "ODOO_MASTER_PASSWORD": "runtime-payload-master", - "ODOO_DB_USER": "odoo", - "ODOO_DB_PASSWORD": "runtime-payload-database", "GITHUB_TOKEN": "gh-token", "ODOO_BASE_RUNTIME_IMAGE": "ghcr.io/example/runtime:19.0-runtime", "ODOO_BASE_DEVTOOLS_IMAGE": "ghcr.io/example/devtools:19.0-devtools", } + if include_deployment_secrets: + payload_environment.update( + { + "ODOO_MASTER_PASSWORD": "runtime-payload-master", + "ODOO_DB_USER": "odoo", + "ODOO_DB_PASSWORD": "runtime-payload-database", + } + ) if odoo_version is not None: payload_environment["ODOO_VERSION"] = odoo_version payload_environment.update(environment or {}) @@ -2281,7 +2287,7 @@ def test_runtime_payload_synthesizes_missing_instance_in_existing_context(self) self.assertEqual(synthesized.contexts["opw"].install_modules, ("opw_custom",)) self.assertEqual(synthesized.contexts["opw"].instances["testing"].database, "opw") - def test_native_runtime_publish_prefers_exact_payload_refs_over_stack_defaults(self) -> None: + def test_native_runtime_publish_accepts_build_only_payload_and_prefers_exact_refs(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: temp_root = Path(temporary_directory) tenant_repo_path = self._create_git_repo(temp_root / "tenant-repo") @@ -2312,6 +2318,7 @@ def test_native_runtime_publish_prefers_exact_payload_refs_over_stack_defaults(s subprocess.run(["git", "commit", "-m", "workspace manifest"], cwd=tenant_repo_path, check=True, capture_output=True) manifest = load_workspace_manifest(manifest_path) self._configure_publish_runtime_payload( + include_deployment_secrets=False, environment={ "ODOO_VERSION": "20.0", "ODOO_BASE_RUNTIME_IMAGE": "ghcr.io/example/runtime:20.0-runtime", @@ -2322,7 +2329,7 @@ def test_native_runtime_publish_prefers_exact_payload_refs_over_stack_defaults(s "git+https://github.com/OCA/openupgradelib.git@89e649728027a8ab656b3aa4be18f4bd364db417" ), "ODOO_PYTHON_SYNC_SKIP_ADDONS": "", - } + }, ) captured_build_args: list[str] = []