Release Management
This page describes the release strategy for addon repositories managed with Odood, covering version conventions, the standard release flow, and the hotfix flow.
Scope: This page applies to addon repositories — git repositories containing one or more Odoo addons, with one stable branch per supported Odoo series. It is not about upgrading Odoo itself (see Upgrading Odoo).
Why repository versions?
When multiple addon repositories depend on each other, there is a natural consistency risk: repo A merges a breaking change, and repo B — which depends on A — has not yet been updated. If assembly sources point to branches, any deploy during that window picks up an inconsistent combination.
Repository versions solve this by giving each repo a pinnable snapshot. An assembly spec can reference a specific release of repo A alongside a specific release of repo B — a combination that was tested together. The typical promotion path then becomes:
- Cut new releases of the relevant repos.
- Update the assembly spec to pin the new versions.
- Sync the assembly and deploy to a staging environment.
- Validate. Promote to production.
Enabling this pinning is the primary reason repository versions exist — a release is first and foremost an immutable, tested snapshot you can reference. On top of that, the version number itself carries meaning: it follows semver-like semantics derived from the aggregate addon changes (see Concepts), so the segment that changed signals whether an upgrade may carry breaking changes.
Code freeze, scoped to a tag
The traditional model — where the stable branch (e.g. 18.0) is always deployable
and every merged PR is production-ready — works well for mature, slow-moving
repositories. For repositories under active development it creates a subtle risk:
the version deployed to a test environment and the version promoted to production
a day or two later may differ, because new commits landed on the branch in between.
The usual remedy is a code freeze — a window during which no new merges are allowed while testing and promotion happen. That stabilises what you test, but at the cost of blocking everyone’s work for the duration.
Repository releases don’t eliminate the freeze — they make it light, and they end it with a tag. The purpose of a freeze is to have one immutable, tested thing to promote, and a tag is exactly that: an immutable snapshot of a single commit. In practice the cycle becomes:
- Active development — feature branches merge into stable as usual.
- Light freeze — in the run-up to a release, stop merging new features into stable. Bug fixes and translation updates are still welcome — that is the only restriction, so it is far less disruptive than a full merge freeze.
- Release — when stable is in good shape, cut a tag. You test and promote that exact tag, so it no longer matters what lands on the branch afterwards.
- Next iteration — held-back feature branches merge and active development resumes.
The freeze still exists, but it shrinks to “no new features for a short while,” scoped to the snapshot the tag captures rather than to the branch as a whole.
Translation batching
With branch-based pinning, translation updates must be kept in sync with ongoing development and merged as they are ready. With release pinning, translations can be batched and merged as a single MR in the days before a planned release, keeping them entirely out of the development cycle until they are needed.
Concepts
Repository version
A repository version is a git tag on the stable branch. The git tag is the sole
source of truth. Tags follow the format A.B.X.Y.Z:
| Segment | Meaning |
|---|---|
A.B | Odoo series (e.g. 18.0) |
X | Major — a breaking change: any addon had a major version bump, or an addon was removed, since the last release |
Y | Minor — any addon had a minor or patch bump, or an addon was added, since the last release |
Z | Patch — only ever set by a deliberate patch release (release --patch or hotfix release), never by auto-detection |
Repository versions follow semver-like semantics over the aggregate of addon changes, so that — when addon authors follow the same convention — a glance at the version segment that changed tells you whether a release carries potentially breaking changes.
Auto-detected releases always end in .0 (e.g. 18.0.1.0.0, 18.0.2.1.0).
A Z > 0 tag is a patch release, produced only by a deliberate patch: either
release --patch (a mainline patch on the latest serie release) or the
hotfix flow (hotfix release, on an older chain).
The bump level is computed automatically by odood repo release from the aggregate
of addon version changes since the last release tag. You can override it with
--major, --minor, or --patch.
Why does a patch-only addon change bump repository
Y, notZ? Because auto-detection never produces aZbump — a routine release, even one that only contains addon patch bumps, always ends in.0. AZ > 0release requires a deliberate patch —release --patch(mainline) or the hotfix flow (hotfix release, on an older chain).
Addon versions
Addon versions (the A.B.X.Y.Z string in each addon’s __manifest__.py) are a
separate concern, managed by each addon’s maintainer independently of the
repository release cycle. The rule is: every change to an addon must increase its
version before the branch is merged to stable. This is enforced by CI on every pull
request — by the time anything lands on the stable branch, addon versions are already
correct.
odood repo release re-verifies this internally as a safety gate, but in normal
practice the gate is always clear by release time.
Addon version bump semantics (when to bump X vs Y vs Z within an addon) are
a separate topic and are not covered here.
Standard Release Flow
Development follows a standard feature-branch model (see Development Workflow for branching strategy, local testing, and CI/CD setup):
- Work happens on branches named after the target series (e.g.
18.0-my-feature). - Each branch is reviewed and merged into the stable series branch (e.g.
18.0). - Releases are decoupled from individual merges — a release is a deliberate decision made when the maintainer considers the stable branch ready to snapshot. Multiple feature PRs, fixes, and translation updates may all land on stable before a single release is cut.
Making a release
When the stable branch is in a state worth pinning:
# Standard release — auto-detects bump level, generates changelog, tags, pushes:
odood repo release --changelog --push
odood repo release will:
- Fetch remote tags to find the latest release (union of local and remote, to handle tags pushed by others not yet fetched locally).
- Verify that every addon changed since the last release tag has a bumped version (safety gate — normally already satisfied by CI).
- Compute the next repository version from the aggregate addon changes
(the most significant change wins):
- Any addon had a major bump, or an addon was removed →
Xincremented,YandZreset to0. - Any addon had a minor or patch bump, or an addon was added →
Yincremented,Zreset to0.
- Any addon had a major bump, or an addon was removed →
- Optionally generate
CHANGELOG.md/CHANGELOG.latest.mdand commit them. - Create a git tag with the new version.
- Push the branch and tag if
--pushis given.
--pushbranch guard: When--pushis supplied for a standard release, the command requires the current branch to match the stable series branch (e.g.18.0). This prevents accidentally tagging from a feature branch. The guard does not apply to--patch, which is a conscious, explicit choice.
Override the auto-detected bump level when needed:
odood repo release --major # force X bump
odood repo release --minor # force Y bump
odood repo release --patch # force Z bump (mainline patch release)
--patchis the mainline patch: a deliberateZbump on top of the latest serie release (e.g. a small fix you want to ship as18.0.2.1.1without a minor bump). It must be requested explicitly — auto-detection never produces aZbump — and, being explicit, carries no branch restriction.To patch an older release while the stable branch has already moved on, use the hotfix flow (
odood repo hotfix) instead — it builds on the right patch chain rather than the latest serie tag.
When nothing changed since the last tag, release exits successfully without
creating a tag. In automation, add --fail-nothing-to-release to exit with
code 1 instead — handy for gating downstream steps that should only run when a
release was actually cut. (hotfix release accepts the same flag.)
First release
For a repository that has never been tagged:
odood repo release --initial
This creates tag <serie>.1.0.0 without inspecting addon changes. The first
non---initial release bootstraps from <serie>.1.0.0 and then applies the
bump, so it lands above <serie>.1.0.0 (e.g. <serie>.1.1.0 for a minor
bump) — never <serie>.1.0.0 itself.
Hotfix Flow
A hotfix is a targeted fix applied to an already-released version, independent of ongoing development on the stable branch.
Hotfix vs. mainline patch. Both bump
Z, but they differ in their base. A mainlinerelease --patchbuilds on the latest serie release — use it for a quick fix on top of the current line. The hotfix flow builds on a specific older patch chain via a dedicated branch — use it when stable has already moved past the release you need to fix.
When to use: A critical bug is found in the deployed version 18.0.2.1.0, but
the stable branch already has several changes that are not ready to ship. You need
to release a fix as 18.0.2.1.1 without including the unreleased work.
Conceptual workflow
stable branch (18.0):
... ── [2.1.0] ──── [ongoing work] ─── [ongoing work] ──→ (not ready)
│
└── hotfix/18.0.2.1.x
│
[fix commit]
│
[2.1.1 tag] ←── hotfix release here
│
cherry-pick back to stable branch
The entire hotfix lifecycle lives under the odood repo hotfix command group
(start → check → release), keeping it separate from mainline releases.
Step-by-step
-
Create a hotfix branch with
odood repo hotfix start:odood repo hotfix start --from=18.0.2.1.0This validates the primary tag, finds the latest existing tag in the
18.0.2.1.*chain (so subsequent patches like18.0.2.1.2are handled correctly), createshotfix/18.0.2.1.xfrom it, and prints next steps.The
--fromargument always takes the primary release (Z == 0), never an intermediate hotfix.startis non-destructive when the branch already exists: it switches to the existing local branch (preserving any work-in-progress), or — if the branch exists only onorigin— checks out a tracking branch from it rather than creating a divergent local one. A fresh branch is created only when none exists locally or remotely. -
Apply the fix. Commit normally; bump affected addon versions.
Optionally preview the check before releasing — run from the hotfix branch:
odood repo hotfix check --ignore-translationsThis compares against the latest tag in the branch’s patch chain — exactly what
hotfix releasewill verify. -
Release the patch — from the hotfix branch:
odood repo hotfix release --changelog --pushhotfix releasederives the chain from the currenthotfix/A.B.X.Y.xbranch, takes the chain’s latest tag as the base, and bumpsZ— producing e.g.18.0.2.1.1. Because the base is the chain (not the latest serie tag), the patch builds on the old release even when stable has moved on. It refuses to run when not on a hotfix branch.
In CI: both
hotfix checkandhotfix releasederive the chain from the branch name, so they must run with thehotfix/A.B.X.Y.xbranch checked out — they cannot work from a detachedHEAD(e.g. a tag-triggered pipeline). Use a branch-triggered job for the hotfix branch.
-
Cherry-pick the fix back to the stable branch:
git checkout 18.0 git cherry-pick <fix-commit-hash>Version conflicts during cherry-pick are expected and mechanical: always keep the stable branch’s (higher) addon version.
-
Do not merge the hotfix branch into stable. Cherry-pick individual fix commits only. The hotfix branch diverged from an old release tag; a merge would bring in stale version history.
Versioning Invariants
| Property | Rule |
|---|---|
| Auto-detected release | Always ends in .0 (e.g. 18.0.2.1.0) |
| Patch release | Only Z changes; X.Y stays equal to the release it builds on (e.g. 18.0.2.1.1) |
| Patch chain | All patches sharing the same A.B.X.Y prefix build on the same X.Y release |
These invariants make it trivial to:
- Tell a patch release from a primary release:
Z > 0means patch (a hotfix, or a deliberate patch on stable);Z == 0means primary. - Find the latest release for a series (including patches): take the max over all
A.B.*tags. - Find all patches for a given primary release: filter tags by
A.B.X.Y.*and sort.
Caveats
A few sharp edges worth knowing:
-
The patch chain is a tag namespace, not a single branch.
release --patchpatches the latest serie release;hotfix releasepatches the chain its branch is based on (getLatestPatchpicks the chain’s highest tag, so it always counts up). When both target the sameA.B.X.Ychain, theirZnumbers interleave across divergent commits — a mainline patch may take18.0.2.1.1, then a hotfix on the same primary becomes18.0.2.1.2on a different commit. Tags never collide, but read the chain as “all patches built on theX.Yrelease”, not as one linear history. -
A mainline release bases on the highest serie tag.
releaseandrelease --patchcompare against — and bump from — the numerically latest serie tag. If you hotfix the latest release (18.0.2.1.0→18.0.2.1.1) and then run a normal release on stable before cherry-picking the fix back, that hotfix tag becomes the base, and the version check may report “current version must be greater than origin” for addons the hotfix bumped. Cherry-picking the fix back to stable first (step 4 of the hotfix flow) resolves this — treat the error as a reminder to back-port.
Pre-release version check
Before releasing, you can preview exactly what odood repo release will verify:
odood repo check-versions --since-last-release --ignore-translations
check-versions chooses its comparison baseline from the flags:
| Command | Compares against | Use for |
|---|---|---|
check-versions | stable branch tip (origin/<serie>) | CI on a PR |
check-versions --since-last-release | the latest release tag for the serie (any chain, including patches) | previewing a standard release |
hotfix check | the latest tag in the current hotfix branch’s patch chain | previewing hotfix release |
To preview a hotfix instead, run odood repo hotfix check from the hotfix
branch — it mirrors what hotfix release will verify (see the
Hotfix flow).
CI setup for addon repositories
Add a version check job to your PR pipeline to catch unbumped addon versions before they reach the stable branch.
GitHub Actions
name: Check addon versions
on:
pull_request:
branches:
- '18.0'
jobs:
check-versions:
runs-on: ubuntu-latest
container:
image: ghcr.io/katyukha/odood/odoo/18.0:latest
steps:
- uses: actions/checkout@v6
- name: Add safe directory
run: git config --global --add safe.directory "$(pwd)"
- name: Check addon versions
run: odood repo check-versions --ignore-translations
GitLab CI
check_addon_versions:
image: ghcr.io/katyukha/odood/odoo/18.0:latest
before_script:
- git config --global --add safe.directory "$(pwd)"
script:
- odood repo check-versions --ignore-translations
only:
- merge_requests
Both examples use the Odood Docker image (which includes a pre-configured Odoo
installation and the odood binary). The check compares the PR branch against
origin/<serie> — the stable branch — and fails if any changed addon has not had
its version bumped.