odoo-devkit bootstraps tenant-focused Odoo workspaces from a tracked
workspace.toml manifest.
The owner's overall DIRECTION.md
sets priorities and stop boundaries; this repository has no separate direction
file. For repository work, read AGENTS.md and the
shared docs index. AGENTS.md is the sole agent-instruction
file format, including tenant overlays and nested addon guides. The operating
guide routes issue claims, model review, and PR landing through the shared
skills, and validation through .github/github.json.
The current workspace flow:
- assembles a long-lived but rebuildable workspace under
~/Developer/odoo-workspaces/<tenant>by default, - treats the active tenant checkout as the source of truth for handwritten code,
- emits a
workspace.lock.tomlfile with the exact assembled refs, - inspects combined tenant/shared-addon uv workspaces before artifact publish,
- generates a minimal runtime config scaffold under
.generated/, and - generates workspace-root
AGENTS.md,docs/README.md, anddocs/session-prompt.mdso Codex, Claude Code, and other coding-agent consumers can use the assembled workspace without turning each tenant repo into a copy of the shared operating guide, and - owns the pure PyCharm Odoo-conf helper and the starter templates for thin tenant overlays, and
- writes PyCharm-visible shared run configurations for rare-but-important commands.
For remote environments, the stable lane model is testing plus prod.
Launchplane-managed PR previews are a separate control-plane concern rather
than a third durable runtime lane exposed through platform runtime.
odoo-devkit can publish artifact images for handoff, but remote ship,
promote, gate, restore, bootstrap, update, and preview lifecycle flow belongs
in launchplane, not in branch-oriented odoo-devkit commands.
uv run platform workspace sync --manifest /path/to/workspace.toml
uv run platform workspace status --manifest /path/to/workspace.toml
uv run platform workspace status --manifest /path/to/workspace.toml --check
uv run platform workspace check-source --manifest /path/to/workspace.toml
uv run platform workspace scaffold-tenant-overlay \
--output-dir /path/to/repo --tenant opw
uv run platform workspace clean --manifest /path/to/workspace.toml
uv run platform workspace run --manifest /path/to/workspace.toml -- pwd
uv run platform dependencies inspect --manifest /path/to/workspace.toml
uv run platform dependencies check --manifest /path/to/workspace.toml
uv run platform dependencies normalize --manifest /path/to/workspace.toml
uv run platform runtime select --manifest /path/to/workspace.toml
uv run platform runtime build --manifest /path/to/workspace.toml --no-cache
uv run platform runtime check-artifact --manifest /path/to/workspace.toml \
--instance artifact --devkit-commit <40-character-devkit-commit> --platform linux/amd64
uv run platform runtime up --manifest /path/to/workspace.toml --build
uv run platform runtime down --manifest /path/to/workspace.toml --volumes
uv run platform runtime workflow --manifest /path/to/workspace.toml --workflow update
uv run platform runtime restore --manifest /path/to/workspace.toml
uv run platform runtime inspect --manifest /path/to/workspace.toml
uv run platform runtime logs --manifest /path/to/workspace.toml --service web --no-follow
uv run platform runtime psql --manifest /path/to/workspace.toml -- -c 'select 1'
uv run platform runtime odoo-shell --manifest /path/to/workspace.toml \
--script tmp/scripts/example.pyIf --manifest is omitted, the CLI looks for workspace.toml in the current
directory.
All platform runtime commands consume runtime values through one typed input:
ODOO_DEVKIT_RUNTIME_ENVIRONMENT_JSON. The JSON object must contain the exact
selected context, the exact selected instance, and a non-empty
environment object whose keys and values are strings:
{
"context": "replace-me",
"instance": "local",
"environment": {
"ODOO_MASTER_PASSWORD": "<operator-local-value>",
"ODOO_DB_USER": "odoo",
"ODOO_DB_PASSWORD": "<operator-local-value>"
}
}Inject the payload from an operator-owned shell, password manager, or a
mode-0600 file outside the repository before running runtime select,
inspect, up, or local workflows. For example:
export ODOO_DEVKIT_RUNTIME_ENVIRONMENT_JSON="$(cat ~/.config/odoo-devkit/runtime-environment.json)"
uv run platform runtime inspect --manifest /path/to/workspace.toml --instance localDo 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
artifact builds 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.
This repo is intentionally small. It owns the manifest/runtime contract for tenant workspaces and the local runtime commands needed to develop against those workspaces.
workspace syncmaterializes repo-addressable shared-addons inputs from[repos.shared_addons].url+refintosources/shared-addonswhen the manifest does not point at a pre-existing local path.- The active tenant checkout remains path-based and is still the source of truth for handwritten tenant code.
- Local runtime assets live in
odoo-devkititself. Tenant scaffolds keep[repos.runtime]pointed at the siblingodoo-devkitcheckout so the same tracked tenant manifest can target local runtime work and artifact handoff without growing a repo-local remote mutation surface. - Runtime repo ownership remains explicit. When
[repos.runtime]is present it may be path-based or repo-addressable,workspace syncmaterializes repo-addressed runtime inputs intosources/runtime, and runtime commands fail closed untilworkspace synchas materialized that checkout. - Runtime Odoo core is inherited from the image/tooling chain. For IDE analysis,
workspace prepare-ideattaches an explicit, pinned community source checkout to the exact tenant project; see the workspace CLI guide. workspace statuscompares deterministic generated surfaces, the manifest hash, source materialization, and the local lock snapshot.--checkexits nonzero for stale guidance or materialization while reporting ordinary source commit/dirty changes on editable path-linked sources separately as informational baseline drift. Baseline drift on managed read-only checkouts fails the check.- Status exposes tenant, devkit, shared-addons, and distinct runtime roles with workspace-relative entrypoints, resolved paths, materialization type, and editability. Path-linked sources are edit roots; managed checkouts are not.
- Use an optional regular-file
workspace.local.mdfor supplemental non-secret notes; symlinks and non-files fail the workspace check.AGENTS.override.mdis reserved full-replacement input and its presence fails the normal workspace status check.
Current runtime ownership is intentionally narrow and explicit:
- local runtime targets run natively inside
odoo-devkitagainst the repo owned byodoo-devkititself:select,build,publish,up,down,inspect,logs,psql,odoo-shell,restore,workflow bootstrap,workflow init,workflow update, andworkflow openupgrade. - Non-local runtime mutation is not a devkit command surface. Stable remote
lanes (
testing,prod) route through Launchplane service APIs, operator UI, or reusable Launchplane workflows. PR preview lifecycle also stays outsideplatform runtime. - Release actions such as ship, promote, and gate execution belong in
launchplane, not underplatform runtime. - non-local
workflow initandworkflow openupgraderemain local-only and fail closed with an explicit--instance localrequirement instead of falling through to an implicit remote path.
- Post-deploy maintenance, update-only runs and restored copies use a committed SQL credential boundary before the first Odoo registry or module hook. See the data-workflow contract for managed overrides, lane handling and failure behavior.
- The startup wrapper maps explicitly supplied
ODOO_SMTP_SERVER,ODOO_SMTP_PORT,ODOO_SMTP_USER,ODOO_SMTP_PASSWORD,ODOO_SMTP_SSL,ODOO_EMAIL_FROM, andODOO_FROM_FILTERto Odoo's native mail options.ODOO_SMTP_SSL=Trueselects STARTTLS; it is not implicit TLS on port 465. Launchplane owns hosted values and secret bindings. Empty supplied values clear inherited mail options; omitted values preserve the base config. The generated config containing credentials is readable only by its owner. Existing Odoo outgoing-server records take precedence over the config fallback. Restored databases that undergo sanitization get an active dummy outgoing server, following Odoo's neutralization behavior, so even configured SMTP fallback cannot send copied customer mail. Copied SMTP usernames/passwords are cleared. Non-production instances (anything other thanprod/production, including an emptyPLATFORM_INSTANCE) get the same dummy server on bootstrap, every post-deploy maintenance run, and every restore, even with--no-sanitize, so testing and preview lanes cannot send mail whatever SMTP settings they receive. Production bootstrap does not insert the dummy server, so a new production database can use the operator's supplied mail configuration. - Restores onto a non-production instance clear the copy's production
integration credentials and signing keys. This includes Shopify, secret-shaped
ebay.*parameters, PrintNode, Fishbowl, RepairShopr and cm_data connections, payment providers, incoming mail servers and Gmail/Outlook OAuth app client secrets, IAP account tokens, map and media tokens, web push keys and devices, user API keys, anddatabase.secret. The restore preserves the copy's channel identity graphs and export history. Only an explicitly selected managed Shopify apply withtest_store=trueontestingresets Shopify IDs and export state for the existing development-store reset flow. The restore keeps only the integrations named inODOO_RESTORE_KEPT_INTEGRATIONS; keepingincoming_mailalso keeps those OAuth app client secrets. Failures during database preparation drop the restored database. This includes the Launchplane settings apply and the outgoing-mail block immediately afterward. Seedocs/tooling/workspace-cli.mdfor the full contract. - Isolated restored and saved migration copies set
ODOO_COPY_PURPOSE=migrationin the existing run manifest. Their instance label never permits live keys: the SQL boundary clears credentials, cancels copied Shopify work, disables all crons and blocks outgoing mail even with--no-sanitize. It preserves Shopify/eBay bindings and export/import history for reconciliation. Managed credentials, kept integrations and service-key provisioning are refused. Non-credential addon settings are validated and left unapplied. Startup repeats the boundary before Odoo and refuses a separate scheduler. The exact caller and OPW handback are in the workspace CLI guide. - An optional
company_emailin Launchplane's website-bootstrap payload sets the selected website company's sender address and verifies it was saved. Omission preserves the existing company email. This fixes the company sender used by native website contact forms without changing form submissions or sending email during bootstrap. - The shared tenant compose database service stays pinned to
postgres:17while existing tenant DB volumes still use the legacy/var/lib/postgresql/datalayout. - The shared local compose contract includes the image-owned Launchplane runtime
addon root
/opt/launchplane/addonsand loadsbase,web,launchplane_runtime_healthas server-wide modules by default. Keep that addon root in the renderedODOO_ADDONS_PATHso startup scripts, generated Odoo config, and wrapper-normalized server commands agree. Startup shell phases normalize/opt/launchplane/addonsinto their generated config before running database updates so server-wide runtime health stays loadable even when downstream image layers overrideODOO_ADDONS_PATH./web/healthremains the local container liveness check; Launchplane runtime identity evidence is exposed by the base image at/launchplane/health. - Startup treats every instance as public except an explicit
local,dev, ordevelopmentPLATFORM_INSTANCE. An empty or unset value is public, so the credential checks anddbfilterbelow apply to it. - Public single-database runtimes pin both
db_nameanddbfilterto the configuredODOO_DB_NAME, and keep database listing disabled. This keeps normal website requests on the public hostname bound to the tenant database instead of falling through to Odoo's database selector. - Public runtimes require
ODOO_ADMIN_PASSWORD, but startup skips admin hardening when the configuredODOO_ADMIN_LOGINis absent in a restored tenant database. This preserves boot for tenant databases that renamed or removed the defaultadminlogin while still checking active default admin passwords when matching users exist. - A non-production restore preserves people's login passwords, including
admin, even when the lane has a differentODOO_ADMIN_PASSWORD. Before the first registry load the restore commitsodoo_devkit.preserve_restored_loginsin the copied database. Startup, localinitand later maintenance honor it, so a restart or deploy cannot replace a restored login with the lane's bootstrap password. Infrastructure secrets and service API keys still follow the isolated-lane credential boundary. Keep the bootstrap binding populated; public-runtime preflight still requires it. Fresh databases use it normally, and production ignores the copied preservation marker. Changing the bootstrap binding therefore does not reset a restored person's login; use Odoo's account password controls for that intentional change. - Startup, bootstrap and post-deploy maintenance on databases without that
preservation marker verify an existing administrator
password before writing it. A matching configured password is left
unchanged, avoiding password-change emails on ordinary restarts and deploys.
Actual configured password changes still use Odoo's normal write path and
security notifications. Quotes and backslashes in configured passwords are
preserved when passed into the Odoo shell.
Restores still normalize the configured administrator's email to a test
address, including with
--no-sanitize. An activeadmin/adminaccount fails restore preparation and drops the restored database, including on a local instance. A non-production copy whoseadminor configured administrator still usesadminfails rather than silently replacing that login. The verified upstream dump remains available for recovery when preparation fails. For a disposable local source that still uses the default password, change it through Odoo's password settings before taking another copy. Alternatively, bootstrap an empty disposable database with its configured password. The same holds for a configuredODOO_ADMIN_LOGINchecked by the default-password policy. Leading/trailing spaces and tabs inODOO_ADMIN_PASSWORDare also literal: local initialization, data-workflow admin hardening, and startup apply the supplied value unchanged. Blank/omitted values still mean no password update. The shared image scripts apply this nonblank-password rule in hosted lanes too. For directly supplied admin passwords, image startup and data workflows enforce the single-line input and correction contract in the workspace CLI guide before touching Odoo state. Odoo's config-file reader trims surrounding whitespace fromdb_password; devkit-run local Odoo commands use the container's literalPGPASSWORDenvironment input, which takes precedence over that file in Odoo 19. Reading the generated conf alone does not qualify database-password preservation. Container startup supplies the configured database password through nativePGPASSWORDfor initialization, maintenance shells, and the server process too, replacing any inheritedPGPASSWORD. These launches omit the password from process arguments. - A Postgres major-version bump is not a routine dependency refresh on this surface. Treat it as explicit migration work with a documented upgrade path for existing tenant data volumes.
- Renovate does not propose Postgres major upgrades for the root
docker-compose.yml(renovate.json); those changes are intentional operator work.
For secretless tenant PRs, use the source-only validation command.
Use the local/CI test contract. Locally, select the test file covering the changed behavior:
uv run python -m unittest discover -s tests -p 'test_runtime.py'Replace test_runtime.py with the relevant file; repeat for each affected
module. Runtime environment, startup and data-workflow changes use their
complete test_runtime_environment_contract.py, test_runtime_env_serialization.py,
test_odoo_startup.py, test_odoo_data_workflows.py and
test_docker_script_override_guards.py modules as applicable. Addon CI credential
handling uses test_addon_ci.py. Include modules found through the changed
code's callers and tests importing those peers. Keep the metadata's format/lint
gates and required inspection. CI runs qualityGate.test.ci from
.github/github.json; live Odoo and tenant consumer-build checks retain their
separate documented gates.
For tenant repos that keep instance = "local" in the tracked manifest,
--instance testing or --instance prod is not a shortcut for remote
mutation. Release and non-local data actions should run through launchplane.
Local workflow example:
uv --directory ../odoo-devkit run platform runtime workflow \
--manifest ./workspace.toml \
--workflow bootstrapNon-local artifacts are built by the product's own build workflow, which
calls platform runtime publish with the runtime-environment payload; see
launchplane#2602.
platform runtime publish stages the tenant addons plus shared addons into a
real build context, requires clean git worktrees for the repos it captures,
pushes the resulting image, resolves the pushed digest, and emits a
control-plane-compatible artifact manifest JSON file.
Dependency consumers read the published base's baked Python catalog and derive
staged support/tenant locks automatically, preserving source locks and genuine
version conflicts. Local builds, addon CI and explicit host preparation use the same
resolver; see image-owned versions.
Runtime startup, upgrade and shell commands expose packaged addon groups under
the configured project addon root, including the shared addons staged in
/opt/project/addons/shared. An inherited explicit addon path therefore sees
the same packaged modules as the artifact build. Explicit roots retain their
precedence; discovered groups follow their project root, matching the build.
Without an explicit addon environment path, AUTO updates use the generated
configuration path, falling back to the packaged project addons when no
configuration exists.
When the repo-owned artifact inputs file exists (artifact-inputs.toml
beside workspace.toml by default, or the path in [artifacts].inputs_file),
runtime and publish treat that file as the repo-owned source-input contract.
Runtime and publish no longer fall back to stack.toml source selector
fields.
The shared renovate/default.json preset writes each update's type, versions,
values and digests into a Renovate-Updates JSON commit trailer. Launchplane
uses that immutable signed evidence for its existing dependency-update train
admission. Renovate does not merge changes; majors and incomplete metadata
remain for agent review.