Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 10 additions & 6 deletions docs/tooling/workspace-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 21 additions & 11 deletions odoo_devkit/local_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,13 +524,15 @@ 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,
build_target_override="production",
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,
Expand All @@ -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",
)
Expand Down Expand Up @@ -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")
Expand All @@ -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:
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
)
Expand Down
17 changes: 12 additions & 5 deletions tests/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {})
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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",
Expand All @@ -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] = []
Expand Down