Environments#
Branches per environment, comparing them and promoting addons between them:
env and promote.
[tool.odx.environments] says which branches are which environment, the
series each runs, and which promotions are allowed. A preset supplies the
usual layouts; env.<name> overrides a preset environment key by key or
adds one, and promotion replaces the preset’s edges:
[tool.odx.environments]
preset = "major-branches"
branch_specific = [".template-answers.yml"] # never promoted
[tool.odx.environments.env.test]
series = "20.0" # test already runs the next series
[tool.odx.environments.env.prod]
repo = "https://git.example.com/odoo/project-prod.git" # a separate repository
Preset |
Environments (branch) |
Promotions |
|---|---|---|
|
|
test → uat, test → prod, uat → prod, prod → test |
|
|
test → prod, prod → test |
|
|
test → prod, prod → test |
|
|
dev → uat, uat → prod, prod → uat |
|
only |
only |
{series} is the environment’s series (19.0), {major} its major version
(19), and * in a branch matches anything. An environment’s series
defaults to series, then to odoo_version; its branch defaults to its
name; uat environments are disposable unless disposable = false (a
disposable environment is never the default source of a promotion). Without
a preset, a table that declares environments is explicit.
$ odx env list
preset major-branches
prod prod 19.0 19-prod https://git.example.com/odoo/project-prod.git
* test test 20.0 20-test
uat uat 19.0 19-uat (disposable)
promotion: test → uat, test → prod, uat → prod, prod → test
branch-specific: .template-answers.yml
* marks the environment of the current branch. odx suggest --env <name>
and the dashboard use each environment’s series.
Databases#
An environment can say where it runs, for tools that read or change its database, such as Fluvo:
[tool.odx.environments.env.test]
url = "https://test.erp.example.com"
database = "customer-test"
connection = "conf/test.conf" # Fluvo connection file, kept out of git
[tool.odx.environments.env.prod]
url = "https://erp.example.com"
database = "customer"
connection = "conf/prod.conf"
odx env list shows them under each environment, and odx --json env list
gives them to scripts:
$ fluvo export --connection-file "$(odx --json env list | jq -r '.environments[] | select(.name == "test") | .connection')" ...
The connection file holds the credentials: keep it out of the repository,
for instance with conf/*.conf in .gitignore. odx itself does not
connect to the databases.
Comparing environments#
$ odx env diff test prod
test (refs/remotes/origin/19-test) and prod (refs/remotes/origin/19-prod)
newer in test:
account_invoice_x 19.0.1.1.0 tree dcbe79a | 19.0.1.0.0 tree f13f539
odoo-addon-sale-y 19.0.1.2.0 sha256:41c9e0a | 19.0.1.1.0 sha256:9b03f4d
changed:
repository pos tree 3d1f9a2 | tree 77ab0c1
only in test:
odoo-addon-z 19.0.1.0.0 sha256:e2f6b11 | -
branch-specific files that differ (never promoted):
.template-answers.yml
4 differ, 38 identical
odx env diff reads pyproject.toml, uv.lock and odx.lock of both
environments’ branches through git, without checking anything out:
<remote>/<branch> (--remote, default origin), else the local branch; an
environment with its own repo is fetched from there. It compares, by
name, the project’s own addons (by the tree of their folder, and their
manifest version), every package uv.lock resolves (by wheel hash or
commit), the aggregated repositories (by the tree they build) and the Odoo
core revision. Each is identical, newer or older (by version), changed (a
different pin, same version), from another source, or only on one side; a
series mismatch is reported first. Fetch first: odx compares what git has.
An environment or one of its branches can be named (odx env diff staging/sprint-4 prod for the staging/* pattern of the odoo-sh preset).
--vendored A B compares two vendor manifests and --installed A B the
wheels installed in two virtual environments, file hash by file hash:
identical entries there are the proof that one environment runs exactly
what the other ran. --json lists every entry with its kind, change and
both pins.
Promoting addons#
$ odx promote sale_x --to prod
promote test (refs/remotes/origin/19-test) into prod (refs/remotes/origin/19-prod), series 19.0
selection: sale_x
sale_x new 19.0.1.0.0 0748fce7724a9e | -
helper identical 19.0.1.0.0 eaa0bb8a8497d7 | 19.0.1.0.0 eaa0bb8a8497d7
x newer 19.0.1.1.0 sha256:41c9e0 | 19.0.1.0.0 sha256:9b03f4
not copied (Odoo's own addons, or of aggregated repositories): base, sale
core drift: odoo bbbbbbbbbbbbbb | aaaaaaaaaaaaaa (copied with the pins; --no-with-odoo leaves it)
2 to copy into prod; plan only, nothing written
$ odx promote --feature invoicing --to prod
$ odx promote --search "invoicing" --to prod # candidates, scored
$ odx promote --search "invoicing" --select account_due --to prod # the plan of one
odx promote plans copying exactly what the source environment runs for
the selected addons and everything they depend on (depends of the
project’s addons, the dependencies uv.lock records between addon
packages) into the target. Both sides are read through git as for odx env diff. --from defaults to the one environment allowed to promote into
--to that is not disposable; the edge must be in promotion, and both
environments must run the same series (odx upgrade prepares a move to
another series).
Every addon of the closure is listed with its pin on both sides. The plan is
refused (exit 1) when the target has a newer version of a package
(--allow-downgrade, or [tool.odx.promote] allow_downgrade), has the
addon from another source (--allow-source-change), or has a newer version
of a project addon (sync the target into the source first). A project addon
whose content differs at the same version, and a pinned merge whose new
head is red on the source, are warnings. The Odoo core revision is part of
the plan unless --no-with-odoo or with_odoo = false.
--search scores the source’s project addons (technical name, manifest
name, summary, category, description) and feature sets (an exact
feature name first) against the words given and their synonyms (shipped per
Odoo domain, extended by [tool.odx.promote] search_synonyms), and only
lists candidates; --select names the ones to plan. --json gives the
candidates or the plan.
The plan also lists the source’s commits on the closure’s project addons that the target does not have (compared by patch id): the code that goes with the pins.
$ odx promote sale_x --to prod --commit --yes # a commit on odx/promote/prod-sale_x
$ odx promote sale_x --to prod --commit --yes --with-code # the missing commits cherry-picked first
$ odx promote sale_x --to prod --open-pr --yes # pushed, and its pull request opened
--commit writes the plan on odx/promote/<to>-<selection> (the feature
name with --feature), branched from the target branch in a temporary
worktree, so the checkout is left alone. For every package of the closure it
copies the requirement strings of pyproject.toml (into the same group),
the [tool.uv.sources] entry and the uv.lock blocks verbatim, and the
odx.lock entries; the Odoo core revision too unless --no-with-odoo. It
then runs uv lock there and refuses when that re-resolves anything else
(transitive drift; --allow-transitive keeps it). --with-code
cherry-picks the listed commits first, authors kept; a commit that does not
apply stops the promotion. [tool.odx.promote] with_code = true makes that
the default, and --no-with-code copies the pins only. Writing needs --yes: odx asks no questions.
Branch-specific files are never promoted: what cherry-picked commits change
in them is put back as the target has it. --include-branch-specific <path>
copies one such file or folder from the source anyway (or removes it when
the source has none); the path must match branch_specific, it is repeated
per path, and the output and the commit message say so loudly.
--open-pr also pushes the branch to the environment’s repository (its
repo, else --remote) and opens a pull request into the environment’s
branch, with every addon and its pin on both sides in the description. The
description and the commit message end with the changelog of the target’s
odx.lock, as odx changelog writes it (Release notes), when the
promotion changes it (a package or the core; addons alone leave it as it
is). The
branch is odx’s: writing it again replaces it. Approvals stay with the
forge’s branch protection.
$ odx promote --from-pr 42 --to prod # the project addons #42 changed on test
$ odx promote --rollback 57 --to prod # revert the promotion #57 merged into prod
--from-pr selects the project addons that a pull request merged into the
source environment’s branch changed. --rollback finds the commits that
brought pull request #57 onto the target, however it was merged (its own
commits after a merge commit; the same change by patch id after a rebase,
or all of it at once after a squash), and plans reverting them, newest
first. With --commit --yes the revert is written on
odx/rollback/<to>-<n>; with --open-pr its pull request gets the odx env diff of the target against the rollback, so reviewers see the
previous pins come back. Database changes are not undone: check the
migration scripts the promotion carried.