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¶
Register a Git source — Settings § 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.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.
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:
Shallow-clones the source repo (
--depth 1, the configured branch) into a temp directory. Authentication usessource.github_tokenvia a short-livedGIT_ASKPASSscript — the token is never written to disk outside that temp script, which is deleted afterward regardless of success or failure.Resolves the module’s directory using the source’s configured
addons_path(if any) plus the module’s ownaddon_path, and rejects any path that escapes the cloned repo or the configured addons directory — no../, no absolute paths that walk outside the clone.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.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.
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 -rfold copy →mvstaged copy into place →chownto 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.
Deletes the temp clone (and the askpass script, if one was created) in a
finallyblock — cleanup runs even if any step above raised.
Requirements your module must meet¶
A valid
__manifest__.pyat 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 |
“Selected addon was not found in the repository” |
|
“does not contain |
Pointed at a directory that isn’t actually an Odoo module (e.g. a parent folder) |
“Selected addon path escapes…” |
|
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.