Skip to content

of release — release promotion

of release promotes a pinned release to an environment. A release is a lockfile: a YAML file that pins every repository of an environment to an exact Git ref (a tag, ideally, or a commit SHA). Promoting test → production is "apply the same lockfile to production", not "merge a branch".

The flow mirrors what Odoo.sh does: only code travels between environments, the database never does; a module is updated (-u) only when its manifest version changed; a backup is taken before touching production; and every promotion is recorded.

Lockfile

version: 1
project: nikro
environment: test
created_at: '2026-10-03T18:21:07'
repositories:
- url: git@github.com:trixocom/payment-siro-roela-ce.git
  ref: v19.0.2.2.6        # tag or commit SHA — what gets checked out
  commit: 18657fd0c2e0...  # informative; verified after checkout
  branch: main             # informative
image: nikro_test:19.0 build=3f1c9a0b2d44

Keep one lockfile per environment in the customer's repository (envs/stg.lock.yml, envs/test.lock.yml, envs/main.lock.yml). A promotion is then a pull request that copies test.lock.yml to main.lock.yml: branch protection on that repository is the approval gate, and the PR is the record of who asked for what.

Repositories are matched by org/repo, ignoring the host, the SSH/HTTPS spelling and per-repository SSH host aliases: git@github.com-apibank:org/repo.git, https://github.com/org/repo and git@forgejo.example:org/repo.git are the same repository (an environment may clone from a mirror; the commits are the same).

Commands

of release lock <env> [-o FILE] [--no-tags]

Snapshot the refs currently checked out in <env>/addons/*. When a tag points at HEAD it is used as ref (semantic-looking tags first), otherwise the commit SHA. --no-tags always pins SHAs. Repositories that are not cloned are listed as a warning and must be fixed before the file is usable.

of release plan <env> <db> <lockfile>

Dry run: which repositories would change (current HEAD → target ref) and which installed modules are already out of sync (manifest version ≠ ir_module_module.latest_version). The final list of modules to update is only known after the checkout, so apply recomputes it.

of release apply <env> <db> <lockfile>

  1. Validate — every repository of the environment must be pinned by the lockfile (--allow-partial lets unpinned repositories keep their branch); the lockfile's project must match.
  2. Plan — repositories and modules, then ask for confirmation (--yes). If no repository changes and no installed module changed version, apply writes a noop ledger entry and stops: nothing is backed up or restarted.
  3. Backup — generate_backup_odoo() (DB dump + filestore, streamed) into <env>/backups/; --no-backup skips it (not for production). A failed backup aborts before any change.
  4. Checkout — clone_or_update_repos() with rev set from the lockfile, only for the repositories whose ref changes (unchanged ones are not fetched). If the lockfile carries commit, the resolved HEAD must start with it (a tag that moved aborts). On failure every repository is put back at its previous commit. .project.lock and the addons paths are refreshed.
  5. Update — -u of the installed modules whose manifest version differs from the database (series prefix is normalised: 1.2.3 == 19.0.1.2.3), with the same Odoo stop/update/start isolation and state verification as of env update. --update-all runs -u all; --no-update skips this step. No bump, no update.
  6. Restart + smoke test — Odoo restarted; /web/login must answer 200 within 2 minutes; no module may be left in to upgrade/to install/to remove; active crons, active mail servers and database.is_neutralized are compared before/after and a difference is reported as a warning (a module update that re-activated a cron or a mail server is exactly what you want to know before opening the environment).
  7. Offload — when project.backup_remote (or OF_BACKUP_REMOTE) names an rclone remote, the pre-deploy backup is moved to <remote>/<project>/<env>/<file> after the checks passed (--keep-backup-local to skip). A failed release keeps the backup on disk: it is the rollback.
  8. Ledger — one JSON line appended to <env>/releases.log:
{"ts": "...", "action": "release.apply", "actor": "hector", "project": "nikro",
 "environment": "main", "db": "nikro", "lockfile": "/srv/cliente_nikro/envs/main.lock.yml",
 "lockfile_sha256": "...", "repos_changed": [{"repo": "payment-siro-roela-ce", "from": "0eb9c49…", "to": "v19.0.2.2.6"}],
 "modules": ["payment_siro_roela"], "backup": "nikro_2026-10-03_18-30-01.zip",
 "checks": {"http_login_200": true, "no_pending_modules": true, "crons_unchanged": true, ...},
 "result": "success", "duration_s": 94.2}

The actor is RELEASE_ACTOR if set (CI runners should set it to the person who triggered the workflow), else SUDO_USER / USER.

Exit code 1 and "result": "error" when the update failed, the login page does not answer or modules are left pending. The command then prints the rollback: of env restore <env> <env>/backups/<backup> --db <db> followed by applying the previous lockfile. It is never run automatically — restoring a production database is a decision, not a reflex.

--json prints the ledger record instead of the human output.

of release log <env> [-n N]

Show the last N ledger entries of an environment.

Pinned environments and of env update -r

After apply, every repository is checked out at the pinned ref (detached HEAD when the ref is a commit or a tag). of env update -r still follows project.yml: it puts each repository back on its branch and pulls the tip, i.e. it takes the environment out of the release and back to "developer mode". Use it on stg; on test/main promote with of release apply only.

Rollback

  • Code only (nothing was updated, or you want the previous refs back): of release apply <env> <db> <previous lockfile>.
  • A module update that went wrong: restore the backup printed by apply, then apply the previous lockfile. A module -u is not reversible by checking out old code.

Typical CI job

- name: Promote
  run: |
    export RELEASE_ACTOR="${{ github.actor }}"
    ssh deploy@docker4 "cd /home/hector/ofprojects/nikro && \
      of release apply main nikro /srv/cliente_nikro/envs/main.lock.yml --yes --json"