018 — Release and Build Process¶
Purpose: a step-by-step runbook for actually executing a release — commands, files to edit, and the order to do it in. 014 documents the pipelines' architecture (what deploys where, and why); 017 documents the Odoo.sh branch-promotion policy. This document is the missing third piece: given a finished change sitting in a feature branch, what does a developer actually type to ship it, in what order, and what mistakes have already cost time.
Audience: any developer cutting a release — written so a new team member can do one without asking a teammate to walk them through it.
Two Independent Release Tracks¶
The platform has two release tracks that version, build and deploy independently. They are tied together only by cross-references inside the frontend's own release notes.
Frontend (next_ecommerce) |
Backend (addons_lp_ecommerce) |
|
|---|---|---|
| Version scheme | NEXT_PUBLIC_APP_VERSION, semver (patch/minor only) |
Odoo manifest version and a separate BUILD_NUMBER |
| Release notes | _docs/releases/<version>.json (one file per app version) |
_docs/releases/<module_version>_build<N>.md |
| Built by | opennextjs-cloudflare build (prod) or Docker (docker/<env>/) (non-prod) |
Odoo.sh (ERP Team merges the submodule pointer) |
| Deployed via | Push to GitHub main (prod, automatic) or build_upload_amd64.sh --deploy (non-prod, manual) |
Azure DevOps PR → GitHub sync → ERP Team submodule update |
| Verified via | /release-notes page in the portal |
GET /ecommerce/api/build-info |
Because the two halves version independently, the frontend's release-note file is the record
that ties a release together — it carries a headCommitBackend field pointing at the backend
commit shipping alongside it. See 014 — Coordinating the Pipelines
for the ordering rule this implies: when a change spans both halves, ship the backend first
— the frontend tolerates an unfamiliar backend field far better than the reverse, and Cloudflare
deploys in minutes while Odoo.sh production is the slowest hop in the platform.
Frontend Release Process (next_ecommerce)¶
Step 1 — Sync before starting¶
cd next_ecommerce
git fetch origin && git status -sb
git pull --ff-only origin main # only if behind and the tree is clean
Local main can sit many commits behind origin/main between sessions. Branching for a
release off a stale main produces a diff that silently misses real commits — do this first,
every time.
Also check for a leftover unmerged release branch from a previous session:
If one exists and main has since moved past it, treat it as superseded — branch fresh with a
different name and mention the stale branch rather than silently reusing or force-deleting it.
Step 2 — Decide the version bump¶
Semver decided from what's actually in the diff, not by habit:
| Bump | When |
|---|---|
Patch (1.2.0 → 1.2.1) |
Bug-fix-only release, nothing new to use |
Minor (1.2.1 → 1.3.0) |
Adds something a user can see — a new page/route, a new button or action, a new column/report, new behaviour on an existing screen. If a highlights entry reads "You can now …", it's a minor bump |
| Major | Not used on this project — don't bump it without asking |
State the reasoning in the release file's note field so the choice is auditable later.
Step 3 — Write the release-note file¶
Location: _docs/releases/<version>.json (template: _docs/releases/_TEMPLATE.json).
Schema: { version, date, headCommit, headCommitBackend, environments, pr, changes, highlights,
verify, note }. Two fields serve different audiences — don't conflate them:
| Field | Audience | Rules |
|---|---|---|
changes |
Internal, technical | git log-derived, one bullet per commit/PR, commit-message style. Never shown publicly. |
highlights |
Business / non-technical | The only field the public /release-notes page reads. Describe what changed for the user, not how — no fix(scope): … prefixes, no file/function names. Mandatory for anything user-facing; purely internal changes (refactors, tooling, deploy scripts) can omit highlights entirely. |
Each highlights entry is { "portal": "general" | "client" | "amp" | "backoffice", "text": "…" }
— not a plain string. Pick portal by checking which route/component the change actually lives
in (git diff-tree --no-commit-id --name-only -r <sha>), not by guessing from the commit
message:
client— touches Customer Portal routes/components (served from/).amp— touches Account Management Portal routes/components (served from/admin/*).general— the diff genuinely spans both portals, or is portal-agnostic infra. A shared low-level component (e.g.common/ui/**) being capable of use in both portals does not by itself make a fixgeneralif every other touched file sits under one portal's tree.backoffice— the change actually lives inaddons_lp_ecommerce, not innext_ecommerceat all (see Step 3b below).
A missing/invalid portal falls back to general rather than breaking the build — don't rely
on that, tag it correctly.
Step 3b — Fold in backend changes¶
headCommitBackend (sibling of headCommit) records the addons_lp_ecommerce commit as of
this release, so one file tells the full "what shipped" story:
- Read the previous release file's
headCommitBackend(jq -r .headCommitBackend <file>.json). Ifnull(tracking not started yet), just record the current backend HEAD as the new baseline and skip step 2. - Otherwise,
cd addons_lp_ecommerce && git log --oneline <that-sha>..HEADto see what landed on the backend since the last release note. Summarize intochanges(prefixed[backend]) and, for anything with real operational effect, add ahighlightsentry tagged"portal": "backoffice". - Set
headCommitBackendto the backend repo's current HEAD (git rev-parse HEAD).
This is informational tracking only — it does not imply the backend was deployed together with this frontend release; the two deploy on independent schedules.
Populate headCommit itself the same way, but for the frontend repo:
git log --oneline <previous release's headCommit>..HEAD. Caveat: headCommit records the
previous release's branch-off point, not that release's own tip, so the diff range also
includes the previous release's own bump/release-note commit — cross-check against the previous
file's own changes array and exclude anything already documented there.
Step 4 — Bump NEXT_PUBLIC_APP_VERSION¶
Each environment duplicates the same values in two places — both must be edited together or the built image and the running compose file will disagree:
- Non-production:
docker/<env>/.env.<env>anddocker/<env>/docker-compose.yml(underservices.next-portal.environment) - Production:
.env.productionat the repo root
Edit only the environment(s) actually shipping this release.
Step 5a — Build & deploy: non-production (Docker)¶
# Single environment
bash docker/<env>/build_upload_amd64.sh --deploy
# All three staging environments at once (pre-prod, staging-b2b, training-b2b)
bash docker/build_deploy_all_staging.sh
<env> is one of training, pre-prod, staging-b2b, training-b2b — training is not
included in the combined script and is deployed on its own. Under the hood, per environment,
this builds a linux/amd64 image tagged both <env> and <env>-<version>, saves both tags to
one tar under docker/build_releases/<env>/, scps it to the server, then (with --deploy)
loads, retags and pushes both tags to the server's local registry.
The script never restarts the running container. In Portainer: Stacks → the stack for that environment → Editor → update the stack with Re-pull image and redeploy enabled. This step cannot be done from the CLI — it requires the Portainer UI.
Step 5b — Build & deploy: production (Cloudflare)¶
Production has no manual build step — pushing to the GitHub remote's main is the deploy
action, full stop, with no review gate:
flowchart LR
DEV["git push github main"] --> CFB["Cloudflare Workers Build"]
CFB --> P1["opennextjs-cloudflare build"]
P1 --> P2["upload worker + static assets"]
P2 --> LIVE["b2b.samtia.com"]
Before pushing:
- The change has already been validated on staging-b2b and pre-prod.
- Any backend change it depends on has already reached Odoo.sh production — see the ordering
risk in 014.
- Rehearse locally first — the Worker build differs from the container build:
npm run preview:cloudflare.
Gotcha —
public/release-history.jsongeneration must not depend on which command invoked the build. This file is gitignored and normally regenerated byscripts/generate-release- history.mjsvia npm'spredev/prebuild/prebuild:dockerhooks. Cloudflare Workers Builds runsnpx opennextjs-cloudflare builddirectly rather than throughnpm run build, so those npm hooks never fire there — a build could succeed while/release-notessilently renders empty in production. The generator is invoked as an import side-effect ofnext.config.tsinstead, so it runs on every path that loads Next's config regardless of which command or npm hook triggered the build. If/release-notesever goes empty again after a build that otherwise succeeded, this is the first thing to check — verify withcurl https://b2b.samtia.com/release-history.json(the raw static asset, served independently of the page) against what the page itself renders; if the asset has data but the page doesn't, suspect a server component reading the file vianode:fsat render time — Cloudflare Workers has no real filesystem for that, even though the same file serves fine as a plain static asset. Fixed 2026-09-20 by switching the page to a build-timeimportof the JSON instead of a runtimefs.readFileSync.
Step 6 — Sync the GitHub mirror¶
Azure DevOps (origin) is the source of truth; GitHub (github) is a fast-forward-only mirror
that Cloudflare actually watches. Mandatory every time a frontend release lands on main:
This only fast-forwards the main ref on GitHub — it never touches the local working tree, so
it's safe to run with uncommitted local changes. It aborts (no force push) if github/main has
commits origin/main doesn't, which needs manual resolution first. Run this after merging to
main, not after every feature-branch push.
Step 7 — Verify¶
/release-noteson the deployed environment shows the new entry.- Non-production: confirm the running container actually picked up the new image (Portainer shows the new tag; the page footer/version indicator matches).
- Production:
b2b.samtia.comserves the new version once the Cloudflare build completes.
Backend Release Process (addons_lp_ecommerce)¶
Step 1 — Sync before starting¶
Same pattern as the frontend — stale local main produces a release note whose diff misses
real commits:
cd addons_lp_ecommerce
git fetch origin && git status -sb
git pull --ff-only origin main # only if behind and the tree is clean
git branch -a | grep -i bump # check for a leftover unmerged bump branch first
If a stale bump branch exists and main has moved past it, don't build on top of it or reuse
its name — branch fresh (e.g. release/<version>-build<N>) and flag the stale one.
Step 2 — Bump BUILD_NUMBER (mandatory, every deployable change)¶
ecommerce/controllers/build_info_api.py holds a BUILD_NUMBER constant exposed at the public
endpoint GET /ecommerce/api/build-info (returns build_number + module_version read live
from disk — no module upgrade needed to check it). This is the only way to verify from
outside whether the ERP Team's submodule update actually picked up the latest code, so every
commit that must be verifiable on a server increments it.
Step 3 — Check before bumping the module version¶
Compare grep version ecommerce/__manifest__.py against the latest release file's Module
version. A feature/migration PR may have already raised the manifest version since the last
release file — if the manifest is already ahead, keep it as-is and only move BUILD_NUMBER;
bumping the manifest version again would skip a version for no reason.
Step 4 — Write the release-note file¶
Location: _docs/releases/<module_version>_build<BUILD_NUMBER>.md (e.g.
19.0.1.0.2_build5.md, template: _docs/releases/_TEMPLATE.md).
- Find the latest existing release file and read its
Head commit. git log --oneline <that-sha>..HEADto see what's shipping. Same caveat as the frontend:Head commitis the previous release's branch-off point, not its tip, so this range also includes the previous release's own bump commit — cross-check and exclude anything already documented there.- Write
## Changesin plain business language (what changed for the user/operator, not commit-message jargon), with the PR number in parentheses for traceability — the same business-tone rule the frontend'shighlightsfield applies. - Record: date, module version, build number, the branch-off commit SHA as
Head commit, and the PR URL once known.
Commit the release file in the same commit/PR as the BUILD_NUMBER bump — the Azure DevOps
PR review is the approval gate for the release.
Step 5 — PR, merge, and mirror to GitHub¶
Direct pushes to main are rejected — a PR is mandatory:
https://dev.azure.com/TechnicalUnitedSA/ERP-Odoo/_git/addons_lp_ecommerce/pullrequestcreate?sourceRef=<branch>&targetRef=main
After the PR merges, sync the GitHub mirror — Odoo.sh consumes GitHub, not Azure DevOps:
Step 6 — Wait for the ERP Team to promote it¶
The B2B team's own responsibility ends at the GitHub mirror. From here, promotion through
staging-b2b → pre-prod → training → production is performed exclusively by the ERP Team — see
017 — Release Workflow Step Sequence
for that handoff in full. The B2B team never pushes to an Odoo.sh branch directly.
Step 7 — Verify¶
- Returns the expected
build_number→ the code is live on that build. - Returns an older number → the ERP Team's submodule still points at an older SHA, or the custom domain is serving a stale Odoo.sh build (each push creates a new numbered build; the custom domain must be pointed at the branch, not a specific build number).
404→ the build-info endpoint itself predates this deploy (code older than build 1).
Common Pitfalls¶
- Branching off a stale
main. Both repos drift between sessions — alwaysgit fetchand compare before cutting a release branch, in both repos if the release note folds in backend commits. - Reusing or silently discarding a leftover bump branch. A previous session may have pushed
a version-bump branch without ever opening a PR for it. If
mainhas since moved past it, it's superseded — branch fresh with a distinct name and tell the person doing the release, rather than deleting or force-pushing over someone else's unfinished work. - Double-bumping the manifest version. Check whether a feature/migration PR already raised it before bumping again (Step 3, backend).
- Trusting
headCommit/Head commitas "this release's own tip." It's the previous release's branch-off point — the diff range will include that release's own bump commit too. Cross-check against the previous file'schangesbefore writing new bullets. - A build succeeding is not the same as the release being visible. See the
release-history.jsongotcha under Step 5b — a green build can still ship a page with no visible content if a step that should run unconditionally (regardless of which command triggered the build) was actually wired to only one entrypoint. - Pushing a portal change to GitHub
mainahead of its backend dependency. The Cloudflare pipeline has no gate that catches this — see 014 — The ordering risk introduced by CI/CD.
Quick Reference¶
# Frontend — sync, then non-production release
cd next_ecommerce && git fetch origin && git pull --ff-only origin main
# ...bump NEXT_PUBLIC_APP_VERSION in docker/<env>/.env.<env> + docker-compose.yml
# ...write _docs/releases/<version>.json
bash docker/build_deploy_all_staging.sh # or build_upload_amd64.sh --deploy for one env
# ...redeploy the stack in Portainer (manual)
# Frontend — production
# ...bump NEXT_PUBLIC_APP_VERSION in .env.production, write the release note
npm run preview:cloudflare # rehearse locally first
git push github main # this is the deploy action
bash scripts/sync_github_from_azure.sh # keep the mirror in sync going forward
# Backend
cd addons_lp_ecommerce && git fetch origin && git pull --ff-only origin main
# ...bump BUILD_NUMBER (and manifest version, only if not already raised)
# ...write _docs/releases/<module_version>_build<N>.md
# open PR -> merge -> then:
bash _docs/scripts/sync_github_from_azure.sh
curl -s https://staging-b2b-odoo.adv-photonx.com/ecommerce/api/build-info # verify
Related Documents¶
- 014 — Deployment Architecture — pipeline topology, runtime diagrams, environment tables.
- 017 — Odoo.sh Environments Guide — the branch-promotion policy and guardrails the ERP Team follows after Step 6 above.
- 016 — Architecture Decisions and Customization Points — why the platform is built this way.