Custom modules

How a custom Odoo module actually gets from your Git repository onto a customer’s running instance. This ties together three pieces documented elsewhere — addon sources, the Modules page, and saas.provisioner’s deployment logic — into one end-to-end guide.

The three-step lifecycle

  1. Register a Git sourceSettings § Addon Sources (POST /api/addon-sources, see Modules § Addon sources) points the platform at a repository, branch, and (optionally) a subdirectory within it where addons live.

  2. Deploy a specific module — from the Modules page, pick one instance and one module from that source to deploy. This is the step covered in detail below.

  3. Install it — deploying only gets the code onto the instance; the customer still explicitly installs it from Odoo’s own Apps screen afterward. Git ADD ≠ Odoo INSTALL — see Architecture § Custom addons.

What “deploy” actually does

saas.provisioner.deploy_custom_module_to_instance() handles step 2:

  1. Shallow-clones the source repo (--depth 1, the configured branch) into a temp directory. Authentication uses source.github_token via a short-lived GIT_ASKPASS script — the token is never written to disk outside that temp script, which is deleted afterward regardless of success or failure.

  2. Resolves the module’s directory using the source’s configured addons_path (if any) plus the module’s own addon_path, and rejects any path that escapes the cloned repo or the configured addons directory — no ../, no absolute paths that walk outside the clone.

  3. Confirms the resolved directory contains a __manifest__.py, and that the directory name matches the module’s registered technical name exactly — a mismatch here fails the deploy rather than silently installing something else.

  4. Packages only that one module’s directory into an in-memory tar archive — the rest of the cloned repo (and any other modules in it) never leaves the temporary clone.

  5. Transfers the archive into the tenant’s own persistent addon storage and restarts only that tenant:

    • Docker — via put_archive() into a short-lived helper container that has the tenant’s addons directory bind-mounted, then a promote step (rm -rf old copy → mv staged copy into place → chown to the Odoo UID/GID).

    • Kubernetes — base64-encoded over the pod-exec stdin stream (there’s no separate “upload” API in the pod-exec model), then extracted and promoted the same way inside the pod.

  6. Deletes the temp clone (and the askpass script, if one was created) in a finally block — cleanup runs even if any step above raised.

Requirements your module must meet

  • A valid __manifest__.py at the root of the module’s directory.

  • Technical name must match the folder name exactly, and must match ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ — this is validated both when the module is registered and again at deploy time.

  • The module’s directory must actually be reachable from the source’s configured branch and addons_path — a module registered against the wrong path fails deployment with a clear “addon was not found in the repository” error rather than deploying the wrong thing.

Troubleshooting a failed deployment

Symptom

Likely cause

“GitHub repository clone failed”

Bad branch name, revoked/expired github_token, or the repo URL is wrong

“Selected addon was not found in the repository”

addon_path doesn’t match where the module actually lives relative to addons_path

“does not contain __manifest__.py

Pointed at a directory that isn’t actually an Odoo module (e.g. a parent folder)

“Selected addon path escapes…”

addon_path contains ../ or otherwise resolves outside the repo/addons root — this is a deliberate security check, not a bug to work around

Deploy succeeds but module never appears in Apps

Expected — deploying doesn’t install. Update Apps List, then install manually. See step 3 above.

For platform-side operational issues during deployment (transfer failures, restart problems), see Administrator troubleshooting.