Development Workflow
Overview
This page describes a recommended workflow for developing Odoo addons in a repository managed alongside Odood. It covers local testing (including coverage and warnings), translation management, forward-porting across Odoo series, and CI/CD configuration for both GitHub Actions and GitLab CI.
Assumption: This workflow is designed for multi-addon repositories — a single git repository containing several related Odoo addons, with one stable branch per supported Odoo series (
17.0,18.0,19.0, …). This is the standard layout in the Odoo ecosystem, used by OCA and most independent addon vendors.
Branching Strategy
The recommended branch naming convention mirrors the Odoo series:
- Stable branch —
{serie}(e.g.18.0): production-ready code. - Development branches —
{serie}-{feature}(e.g.18.0-my-feature): feature or fix branches. CI pipelines are typically triggered on these branches.
Local Development
Running several series at once? When you develop and forward-port across Odoo versions you typically keep one instance per series running side by side. See Working with Multiple Instances for how Odood isolates them and how to avoid port and database conflicts.
Running Tests
Run tests for a single module on a temporary database:
odood test -t <module>
Run tests for all installable addons in the current directory:
odood test -t --dir .
Odood creates a temporary database, runs the tests, prints a summary with highlighted errors and warnings, then drops the database.
Coverage
Odood uses Python’s coverage tool (installed in the project virtualenv) to measure test coverage.
# Print a terminal coverage summary after tests
odood test -t --dir . --coverage-report
# Fail if total coverage falls below a threshold (useful in CI)
odood test -t --dir . --coverage-report --coverage-fail-under 90
# Generate an HTML report in htmlcov/ (open htmlcov/index.html in a browser)
odood test -t --dir . --coverage-html
--coverage-report and --coverage-html can be combined in the same command.
Warnings report
Odood collects all Odoo log warnings during the test run. To print a deduplicated block of all collected warnings at the end (after the pass/fail summary):
odood test -t --dir . --warning-report
This is useful for spotting deprecation notices or misconfigured modules without having to scan the full test log.
Migration Tests
Migration tests verify that your addons upgrade correctly from the stable branch to the current development branch. Odood automates the full cycle: checks out the stable branch, installs modules, optionally populates data, checks out the development branch, and runs the upgrade tests.
Run migration tests for all addons in the current directory:
odood test -t --migration --dir .
Note: Migration tests are expected to be a soft failure in CI. They may fail if third-party dependencies introduce changes that are incompatible with the older (stable) version of this repository — a situation outside the developer’s control.
Running scripts around the test cycle
Sometimes a migration test needs to set up state that can only be created on the old version — for example seeding records with the old data structure so the migration has something realistic to upgrade — and then inspect the result on the new version. Two hooks let you plug scripts into the run:
| Flag | When it runs | Repo state |
|---|---|---|
--script-after-install | after addons are installed, before tests | start ref (old version) in migration mode |
--script-after-migration | after addons are updated and migrations applied, before tests | current branch (new version); migration mode only |
A typical migration test seeds data on the old version, then runs assertions (or just lets the upgrade tests run) on the new version:
odood test -t --migration --dir . \
--script-after-install seed_legacy_data.py \
--script-after-migration assert_migrated.py
Both flags are repeatable (scripts run in the order given) and work outside
migration runs too — --script-after-install is handy for seeding fixtures
before a normal test run. A script that fails (non-zero exit, or a SQL error)
fails the whole run.
Scripts may be .py (run with a full Odoo ORM environment) or .sql, and are
passed as a path or a bare name resolved against <repo>/.odood-scripts/,
<project>/scripts/, or the current directory. For how to write them, pass
parameters via environment variables, and a worked example, see
Custom Scripts.
Gotcha: because
<repo>/.odood-scripts/is version-locked to the checked-out ref, a pre-migration script placed there must already exist on the stable branch to run as--script-after-install— a script added only on your development branch will not be present at the old ref. Put such scripts in<project>/scripts/(unaffected by the repository checkout) instead.
Translation Management
See the dedicated Translation Management page for the full workflow, flag reference, and guidance on using AI assistants for translations.
Module Versioning
Odoo addon versions follow the A.B.X.Y.Z scheme, where:
A.B— Odoo series (e.g.18.0). Set once when the addon or branch is created; never changed manually.X— addon major version. Increment for significant data-structure changes that may break backward compatibility.Y— addon minor version. Increment for noticeable data-structure changes, or whenever a database migration script is required.Z— addon patch version. Increment for low-risk changes (bug fixes, UI tweaks, new fields that don’t require migration).
Rules of thumb:
- If you add a migration script → bump at least
Y. - If the migration may break backward compatibility → bump
X. - Everything else → bump
Z.
Rather than editing __manifest__.py files by hand, you can let Odood bump versions automatically:
odood repo bump-versions
This inspects the git diff, identifies which modules have changed, and increments their patch version (Z).
Run it inside the repository directory before committing. For minor or major bumps, adjust the version manually afterwards.
Odood also enforces that every changed module has its version bumped via odood repo check-versions.
Version Checks and Pre-commit
Before pushing, verify that all modified modules have their versions bumped:
odood repo check-versions --ignore-translations
The --ignore-translations flag prevents translation-only commits from triggering a version bump requirement.
To run pre-commit hooks (linters, formatters, etc.) locally:
# First-time setup — installs pre-commit and all hooks into the virtualenv:
odood pre-commit set-up
# Run hooks manually against all staged files:
odood pre-commit run
Per-addon Changelog
To track user-facing changes at the module level, each addon can carry a
changelog/ directory holding one markdown file per notable version:
my_addon/
├── __manifest__.py
└── changelog/
├── changelog.1.2.0.md
└── changelog.1.3.0.md
File naming
Each entry is named changelog.X.Y.Z.md, where X.Y.Z is the addon’s module
version with the Odoo serie stripped (X — major, Y — minor, Z —
patch). The serie is ignored so the same entry stays valid when an addon is
forward-ported between series.
For example, an addon at version 18.0.1.3.0 records its notable changes in
changelog/changelog.1.3.0.md.
Only versions with notable changes need a file — there is no requirement to add one for every bump.
File content
The content is free-form markdown describing what changed from an end-user perspective (new features, behaviour changes, breaking changes), not implementation details:
###### New features
- Added a "Reorder lines" button to the sale order form.
###### Breaking changes
- The `state` field no longer accepts the legacy `draft2` value.
Note: only
h6headers (######) — or no headers at all — may be used inside a changelog entry. Larger headers (#…#####) are reserved for the structure of the generated, aggregated changelog, so using them here would break that layout.
What deserves an entry
Reserve changelog entries for changes that matter to the people using the module — new features, UI or behaviour changes, breaking changes, notable bug fixes. Purely technical work (refactors, code-style or typo fixes, test-only changes, small internal fixes) usually does not need its own entry.
Where exactly to draw that line, and how strictly to enforce it, is a project decision — see Enforcing changelog entries below.
Where changelogs are used
Odood reads these entries in three places:
| Command | What it does with changelogs |
|---|---|
odood repo ensure-changelog | Verifies that addons changed since the release branch (or last tag) carry a changelog entry covering their version bump. Useful as a CI / pre-merge gate. |
odood repo release --changelog | Generates and commits repo-level CHANGELOG.md and CHANGELOG.latest.md while cutting a release (see Release Management). |
odood assembly sync --changelog | Aggregates per-addon entries across all updated modules into a single assembly-level CHANGELOG.md (and CHANGELOG.latest.md) — see the assembly Notable changes section. |
Enforcing changelog entries
odood repo ensure-changelog checks that changed addons carry a changelog entry
for their version bump, so you can wire it into CI or a pre-merge hook. An addon
is considered to need an entry when its version was bumped relative to the
comparison ref — and the entry’s version must be newer than the addon’s
version at that ref.
How strict to be is a per-project policy, selected with --require:
--require all(default) — every changed addon must have an entry. Best when you want a complete, auditable history of user-facing changes.--require any— at least one changed addon in the set must have an entry. Lighter-weight: it ensures a pull request documents something user-facing without forcing an entry onto every incidental change.
Some teams skip the automated check entirely and rely on review discipline instead. Pick whatever matches your release process.
# Strict: every changed addon needs an entry (compared against origin/<serie>):
odood repo ensure-changelog .
# Relaxed: at least one entry across the change set, compared against the
# latest release tag:
odood repo ensure-changelog . --require any --since-last-release
Translation-only changes (.po/.pot) are counted as changes; add
--ignore-translations to skip them. The compared refs can be overridden with
--start-ref / --end-ref.
Releasing a Repository
Once your changes are committed and versions are bumped, cut a release with
odood repo release. The full release strategy — version conventions, the
standard release flow, the hotfix flow, and CI setup — is documented on a
dedicated page: Release Management.
In short:
# Auto-detect the bump level from changed addons, verify versions,
# generate the changelog, tag, and push:
odood repo release --changelog --push
# First release of a never-tagged repository:
odood repo release --initial
To patch an already-released version while the stable branch has moved on, use
the hotfix flow (odood repo hotfix) — see
Release Management → Hotfix flow.
Forward-porting
When you maintain addons across multiple Odoo series (e.g. 17.0 and 18.0), development typically happens on the oldest supported series first, and the resulting changes are then forward-ported to newer series.
For example, you develop a fix on 17.0, then need to carry it into 18.0 and 19.0.
The naive approach — manually cherry-picking or re-applying changes — is tedious because:
- Module versions embed the series prefix and must be updated (e.g.
17.0.1.2.3→18.0.1.2.3). - Migration script directories also embed the series (e.g.
migrations/17.0.1.2.3/→migrations/18.0.1.2.3/). - Translation files in the target branch should be kept as-is — conflicts in
.po/.potfiles are meaningless and always resolved in favour of the target branch.
odood repo do-forward-port automates all of this, leaving only genuine business-logic conflicts for you to resolve manually.
Workflow
-
Switch to your Odoo environment for the target series (e.g. your
18.0project). -
Change into the repository directory.
-
Create (or check out) a forward-port branch:
git checkout -b 18.0-forward-port-<feature> -
Run the forward-port command, naming the source series:
odood repo do-forward-port -s 17.0The command will automatically:
- Fetch
origin/17.0and open a merge (--no-ff --no-commit) into the current branch, staging all changes for review. - Reset
.po/.potfiles to the target-branch version — translation conflicts are always discarded. - Fix version number conflicts in each addon’s
__manifest__.py, rewriting the series prefix. - Rename migration script directories from the source series to the target series (e.g.
migrations/17.0.1.2.3/→migrations/18.0.1.2.3/).
- Fetch
-
Resolve any remaining merge conflicts (business logic, structural changes, etc.).
-
Run tests to verify everything works in the target series:
odood test -t --dir . -
Commit and push:
git push origin 18.0-forward-port-<feature> -
Open a pull/merge request into the
18.0stable branch and wait for CI to pass. -
Repeat steps 1–8 for each remaining target series (
19.0, etc.).
Note:
do-forward-portis currently marked experimental. In straightforward cases (no structural conflicts) it produces a ready-to-commit merge with zero manual intervention.
CI/CD Configuration
Key concept: --config-from-env and ODOOD_OPT_*
The prebuilt Docker images already have Odoo installed and ready. Two variants are published per serie:
ghcr.io/katyukha/odood/odoo/{serie}:latest— the production image (includes a containerHEALTHCHECK).ghcr.io/katyukha/odood/odoo-ci/{serie}:latest— the CI image, recommended for test/lint jobs (see the CI image below).
ODOOD_OPT_* environment variables allow you to override individual Odoo configuration options (i.e. values in odoo.conf) at runtime — without modifying any file on disk.
Combined with the --config-from-env flag, this is the standard way to point CI containers at the PostgreSQL sidecar.
Note: The
--config-from-envflag andODOOD_OPT_*support are only compiled in when Odood is built with the-d-version OdoodInDockerflag, and thus are available only in the official prebuilt Docker images. The Debian package and source builds do not include this flag.
For example, set these environment variables in your CI job:
ODOOD_OPT_DB_HOST=postgres
ODOOD_OPT_DB_USER=odoo
ODOOD_OPT_DB_PASSWORD=odoo
Then invoke Odood as:
odood --config-from-env addons link .
odood --config-from-env test -t --dir .
Common ODOOD_OPT_* variables
Each variable maps directly to the corresponding key in Odoo’s [options] section of odoo.conf.
The prefix ODOOD_OPT_ is stripped and the remainder is lowercased before being applied.
| Environment variable | odoo.conf key | Description |
|---|---|---|
ODOOD_OPT_DB_HOST | db_host | PostgreSQL host |
ODOOD_OPT_DB_PORT | db_port | PostgreSQL port (default: 5432) |
ODOOD_OPT_DB_USER | db_user | PostgreSQL user |
ODOOD_OPT_DB_PASSWORD | db_password | PostgreSQL password |
ODOOD_OPT_ADMIN_PASSWD | admin_passwd | Odoo master password (database manager) |
ODOOD_OPT_WORKERS | workers | Number of worker processes (0 = single-process mode) |
ODOOD_OPT_PROXY_MODE | proxy_mode | Set True when running behind a reverse proxy |
ODOOD_OPT_DBFILTER | dbfilter | Regex to restrict which databases are served |
ODOOD_OPT_LIMIT_MEMORY_HARD | limit_memory_hard | Hard memory limit per worker (bytes) |
ODOOD_OPT_LIMIT_MEMORY_SOFT | limit_memory_soft | Soft memory limit per worker (bytes) |
ODOOD_OPT_LIMIT_TIME_CPU | limit_time_cpu | CPU time limit per request (seconds) |
ODOOD_OPT_LIMIT_TIME_REAL | limit_time_real | Real time limit per request (seconds) |
ODOOD_OPT_LOG_LEVEL | log_level | Log level (info, debug, warning, error) |
Any other valid odoo.conf option can be set the same way — the list above covers the most commonly needed ones in containerised deployments.
For deployment context (not CI), see Docker Compose deployment.
Key concept: the CI image
The odoo-ci/{serie} image is a drop-in CI variant of the production image (same
--config-from-env / ODOOD_OPT_* mechanism), used by the examples below. It differs in two ways:
- No
HEALTHCHECK— the container is a disposable test runner, not a server. - Dev/test tooling pre-installed (
odood venv install-dev-tools):pre-commit,eslint,flake8,pylint-odoo,coverage, etc. — so lint and coverage jobs don’t reinstall it each run.
Tip:
pre-commitis baked in, but its hook environments are still built from your repo’s.pre-commit-config.yamlon first run — cache~/.cache/pre-commitbetween runs.
GitHub Actions
The following workflow runs on development branches (18.0-*).
It has three jobs: lint (runs first), then tests and migration-tests in parallel.
name: Tests
on:
push:
branches:
- '18.0-*'
jobs:
lint:
name: Lint & version checks
runs-on: ubuntu-latest
container:
image: ghcr.io/katyukha/odood/odoo-ci/18.0:latest
steps:
- uses: actions/checkout@v4
- name: Add repo as git safe directory
run: git config --global --add safe.directory "$(pwd)"
- name: Link addons
run: odood --config-from-env addons link .
- name: Add dependencies
run: odood --config-from-env addons add --single-branch --odoo-requirements ./odoo_requirements.txt
- name: Check versions
run: odood --config-from-env repo check-versions --ignore-translations
- name: Install pre-commit
run: odood --config-from-env pre-commit set-up
- name: Run pre-commit
run: odood --config-from-env pre-commit run
tests:
name: Tests
runs-on: ubuntu-latest
needs: lint
container:
image: ghcr.io/katyukha/odood/odoo-ci/18.0:latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
POSTGRES_DB: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
ODOOD_OPT_DB_HOST: postgres
ODOOD_OPT_DB_USER: odoo
ODOOD_OPT_DB_PASSWORD: odoo
steps:
- uses: actions/checkout@v4
- name: Add repo as git safe directory
run: git config --global --add safe.directory "$(pwd)"
- name: Link addons
run: odood --config-from-env addons link .
- name: Add dependencies
run: odood --config-from-env addons add --single-branch --odoo-requirements ./odoo_requirements.txt
- name: Run tests
run: odood --config-from-env test -t --dir .
migration-tests:
name: Migration Tests
runs-on: ubuntu-latest
needs: lint
# Migration tests may fail if dependencies introduce incompatible changes
# with an older version of this repo — treated as a soft failure.
continue-on-error: true
container:
image: ghcr.io/katyukha/odood/odoo-ci/18.0:latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
POSTGRES_DB: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
ODOOD_OPT_DB_HOST: postgres
ODOOD_OPT_DB_USER: odoo
ODOOD_OPT_DB_PASSWORD: odoo
steps:
- uses: actions/checkout@v4
- name: Add repo as git safe directory
run: git config --global --add safe.directory "$(pwd)"
- name: Link addons
run: odood --config-from-env addons link .
- name: Add dependencies
run: odood --config-from-env addons add --single-branch --odoo-requirements ./odoo_requirements.txt
- name: Run migration tests
run: odood --config-from-env test -t --migration --dir .
Tip: If your addons have no third-party dependencies, you can omit the “Add dependencies” step and the
odoo_requirements.txtfile.
GitLab CI
The following pipeline mirrors the GitHub Actions structure using GitLab CI’s extends keyword to share the common setup.
image: ghcr.io/katyukha/odood/odoo-ci/18.0:latest
stages:
- lint
- test
# Shared setup: link addons and fetch dependencies.
# Requires odoo_requirements.txt at repo root; remove the second line if not needed.
.setup:
before_script:
- git config --global --add safe.directory "$(pwd)"
- odood --config-from-env addons link .
- odood --config-from-env addons add --single-branch --odoo-requirements ./odoo_requirements.txt
# Shared PostgreSQL sidecar and matching ODOOD_OPT_* variables.
.with-postgres:
services:
- name: postgres:15
alias: postgres
variables:
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
POSTGRES_DB: postgres
ODOOD_OPT_DB_HOST: postgres
ODOOD_OPT_DB_USER: odoo
ODOOD_OPT_DB_PASSWORD: odoo
lint:
extends: .setup
stage: lint
script:
- odood --config-from-env repo check-versions --ignore-translations
- odood --config-from-env pre-commit set-up
- odood --config-from-env pre-commit run
tests:
extends:
- .setup
- .with-postgres
stage: test
script:
- odood --config-from-env test -t --dir .
migration-tests:
extends:
- .setup
- .with-postgres
stage: test
only:
- /^18\.0-.*$/
script:
# Migration tests may fail if dependencies introduce incompatible changes
# with an older version of this repo — treated as a soft failure.
- odood --config-from-env test -t --migration --dir .
allow_failure: true
Note: Adjust the Odoo series (
18.0) in the image tag and theonlyregex to match your project’s series.