Annual Odoo port
Audience
PLANA staff. The October-cadence checklist for bringing the platform onto the next upstream Odoo major.
Upstream Odoo releases a new major every October. PLANA Pulse supports this cadence with a structured, ~12-week effort that turns a new major release into a production-ready PLANA tier on plana.planapulse.app plus the customer tenants that opt in.
This runbook is the checklist for each port. It is updated during each cycle.
When this runs
- Year-round trigger: a CronJob opens the porting epic on October 1.
- First-week activity: branch bootstrap (W0).
- Production readiness target: by end of Q4.
Effort
| Phase | Effort | Risk |
|---|---|---|
| Framework adjustments (only first port) | ~9.5 eng-days | Low |
| PLANA module port | ~25 eng-days | Medium — plana_bus_redis is the riskiest per cycle |
| Pilot tenant migration | ~5 eng-days | Medium |
| Bulk customer migration | ~5 eng-days | Low (per-tenant), Medium overall |
| Recurring annual total | ~30–40 eng-days |
After the first cycle (2026), subsequent ports drop the framework cost, landing at ~25–30 eng-days/year.
Phase 0 — bootstrap (W0)
| Task | Owner | Command / file |
|---|---|---|
Create N+1 branch on odoo-modules from N | Lead | git checkout -b 20.0 origin/19.0 && git push -u origin 20.0 |
Bump ARG ODOO_VERSION in Dockerfile on N+1 branch | Lead | sed -i 's/ARG ODOO_VERSION=19.0/ARG ODOO_VERSION=20.0/' Dockerfile |
Bump SUPPORTED_VERSIONS in .forgejo/workflows/oca-sync.yml (on the default branch) | Lead | SUPPORTED_VERSIONS="18.0 19.0 20.0" |
| Trigger first base image build via Forgejo Actions on the new branch | Lead | Push commit → pipeline.yml fires |
Create new namespace plana-odoo-N+1 with quota, secrets, NetworkPolicies, redis-allow-consumers entry | Platform | Add to infra/k8s/plana-odoo-N+1/ + PR |
Phase 1 — port PLANA modules (W1–W3)
Order matters — dependencies first.
| Order | Module | Risk | Common breaks |
|---|---|---|---|
| 1 | plana_logging_json | Low | API surface stable |
| 2 | plana_session_redis | Low | Session API stable |
| 3 | plana_proxy_fix | Low | Proxy header API stable |
| 4 | plana_k8s_healthz | Low–medium | from odoo import registry removal in v19 |
| 5 | plana_bus_redis | High | Bus mechanism is rewritten between majors — pair-port |
| 6 | plana_session_timeout_redis | Low | Depends on session_redis |
| 7 | auth_session_timeout patches | Low | Behaviour stable |
| 8 | pulse_account_api | Medium | Controller API + auth API can change |
| 9 | plana_saas | Medium | Field renames on project.project, etc. |
| 10 | plana_cron_db_filter | Low | Stable |
| 11 | plana_auth | Medium | _login signature changes between v17 and v18 |
| 12 | plana_user_roles | Medium | _login signature same as plana_auth |
For each module: port → run flake8 + Odoo's --test-tags if available → commit on the N+1 branch → cherry-pick back to earlier branches if the fix is cross-version safe.
Phase 2 — OCA fork patches (W4–W5)
PLANA does NOT back-port OCA modules. We follow upstream OCA branches and pick them up as they land. For modules where we have a private fork (in OCA-fork/<repo> with branches 17.0-planapulse, 18.0-planapulse, 19.0-planapulse, etc.):
- Bootstrap
N+1.0-planapulsefrom upstreamN+1.0 - Apply our PLANA-specific patches on top
- File upstream PRs where the patches are generally useful
OCA modules that have not yet shipped on N+1 go on a watchlist. PLANA tenants that depend on those modules stay on the previous major until upstream catches up.
Phase 3 — Staging cluster (W6)
| Task | Output |
|---|---|
TemplateSnapshot{templateCode=basic, odooVersion=N+1} applied | basic-template-N+1.planapulse.app |
TemplateSnapshot{templateCode=pro, odooVersion=N+1} applied | pro-template-N+1.planapulse.app |
Synthetic PLANAClient{name: e2etest-N+1, odooVersion: "N+1"} applied | e2etest-N+1.planapulse.app |
Verify /web/health and /web/login | 200 on both |
Phase 4 — Pilot tenants (W7–W8)
Pick 2–3 friendly tenants whose installed module set is known to be fully ported. Schedule a maintenance window. For each:
apiVersion: planapulse.com/v1alpha1
kind: TenantUpgrade
metadata: { name: pilot-<slug>-to-<N+1> }
spec:
slug: <slug>
fromVersion: "N"
toVersion: "N+1"
strategy: snapshot-then-upgradeSoak each pilot for 5–7 days before declaring success.
Phase 5 — GA to new tenants (W9–W10)
| Task | Where |
|---|---|
Bump XRD default: PLANAClient.spec.odooVersion = "N+1" | infra/crossplane/xrd-planaclient.yaml |
Bump XRD default: TemplateSnapshot.spec.odooVersion = "N+1" | infra/crossplane/xrd-templatesnapshot.yaml |
Bump XRD default: TenantUpgrade.spec.fromVersion = "N+1", toVersion = "N+1" | infra/crossplane/xrd-tenantupgrade.yaml |
plana_saas.tier.basic.supported_odoo_versions += "N+1" | ERP data |
plana_saas.tier.basic.default_odoo_version = "N+1" | ERP data |
Update marketing copy on planapulse.ai/pricing | pulse-website repo |
Phase 6 — Bulk migration (W11–W12)
Open the migration window. Per-tenant:
- Audit installed modules (
SELECT name FROM ir_module_module WHERE state='installed') - Confirm every module is available on
N+1 - If yes, schedule
TenantUpgrade{strategy: snapshot-then-upgrade}in an off-peak window - If no, defer — note which module is blocking
Throughout: monitor the soak of recently-migrated tenants. Roll back individual tenants if a problem appears (insurance backups retained 7 days).
Phase 7 — Sunset the N-1 major
Once N-1 has no remaining production tenants:
| Task | Where |
|---|---|
Mark N-1 branch protected and read-only | Forgejo settings |
Remove N-1 from SUPPORTED_VERSIONS in oca-sync.yml | Default branch |
Stop building base-N-1 images in pipeline.yml | Per-version branch |
Scale plana-odoo-N-1 Deployment to 0 | Wait 30 days, then archive namespace |
Remove the N-1 row from this table on docs.planapulse.com | /plana-business-cloud/version-differences |
Drop the N-1 namespace's filestore PVC and image-pull secret after the 30-day soak.
What goes wrong each year
A short list of bugs that surface during every port and how to handle them.
Field renames
Odoo renames fields between majors. The Python compiler does not catch these; tests do, sometimes. Grep the codebase for the old field name in all PLANA modules.
Import path moves
Where you previously did from odoo import registry, now it's from odoo.modules.registry import Registry. Use try/except aliases so the same module file compiles on multiple majors:
try:
from odoo.modules.registry import Registry
except ImportError:
from odoo import registry as Registry # pre-19 fallbackView tag renames
<tree> → <list> in v18+. A simple sed over all view XML works:
git grep -lE '<(tree|/tree)' -- '*.xml' | xargs sed -i 's/<tree/<list/g; s|</tree>|</list>|g'Same again with the <tree string=...> form. Confirm by opening a sample view in Studio.
UID change in the base image
v17 used UID 101; v18 and v19 use UID 100. If you tar-pipe a filestore across a major boundary with --same-owner, the target worker can't write because the UID owner does not match. Always pass --no-same-owner --no-same-permissions to the receiving tar, or chown after with a root-uid Job.
PEP 668 on Debian 12 base
Odoo 18+'s base is Debian 12. Pip refuses to install system-wide without --break-system-packages. Update any pip install in our Dockerfile or ad-hoc Job manifests.
Enterprise dependencies
A module that was Community in v17 might be moved to Enterprise in a later release. Drop it from the tier seed data; replace with an OCA equivalent if one exists.
Where to read more
- Architecture → Multi-version Odoo
- Operations → Upgrading a tenant — the per-tenant procedure that this annual cycle ultimately enables
- Operations → Provisioning a tenant
- Source:
infra/docs/runbooks/annual-odoo-port.md— the operator-side living checklist