Developer guide
This guide explains how the repository's code fits together and where to make changes. The documentation portal links operational instructions; source comments and docstrings explain local contracts and implementation choices.
Start with the owning component
| Area | Entry points and responsibility | Focused evidence |
|---|---|---|
| Shared settings and catalogs | config/, data/repos/; target resolution and repository selection |
Backup registry, restore configuration, and updater catalog tests |
| Backup production | scripts/backup/backup_odoo18_dbs.sh, sourced backup_config.bash |
test_backup_registry.sh, test_backup_locking.sh |
| Backup transport | scripts/laptop-backup/; latest links, synchronization, service following, deployment, restore delegation |
test_backup_locking.sh, test_backup_follow.sh, test_backup_registry.sh |
| Restore | scripts/restore/restore_database.sh and its five lib/ libraries |
Library tests, test_restore_cli_contract.sh, test_restore_safety.sh |
| Legacy restore | Database-specific commands in _pmiregistry/scripts/restore/ |
_pmiregistry/tests/test_restore_local_sources.sh, _pmiregistry/tests/test_restore_cli.sh |
| Database copy/delete | scripts/duplicate/duplicate_database.sh, scripts/dropdb/deletedb |
test_duplicate_database.sh, test_deletedb.sh |
| Database creation | scripts/create/create_database.py |
test_create_database.py, focused CLI/completion tests |
| Administrator recovery | scripts/admin/reset_admin_password.py; shared password/runtime helpers in scripts/create/create_database.py |
test_reset_admin_password.py, shared helper and CLI tests |
| Migration | scripts/migrate/, openupgrade_overlays/ |
Approved disposable upgrade validation; no general mocked migration suite here |
| Requirement collection | scripts/fetch/fetch_requirements.py |
test_fetch_requirements.py |
| Repository updates | scripts/update/pmirepos_update.py and version-specific commands |
test_enhanced_update18_catalog.py covers the Odoo 18 catalog path |
| Translation export | scripts/translate/update_pot_addon.py, update_pot_repo.py |
Preview/command inspection; no dedicated export test suite here |
| Addon test/lint tooling | scripts/test/, reusable testkit/ assets |
Scheduler, namespace, Ruff policy, and testkit layout tests |
| Host integration | scripts/integration/, scripts/completion/ |
Integration layout and completion tests |
| Python compatibility | tools/python_compatibility.py delegates to sibling _pmiguidelines |
test_python_compatibility.py |
| Deployment templates | templates/, testkit/workflows/, testkit/pre-commit/ |
Inspect intended consumers and rendered configuration before deployment |
Unless explicitly located in _pmiregistry, tests above live in
../tests/. They cover specific
contracts, not every path of every entry point. Do not infer coverage of older
updaters, migrations, or translation operations from an unrelated passing test.
For database creation and administrator recovery, start with the shared implementation guide. It maps helper ownership, configuration precedence, registry/transaction boundaries, credential publication, and the tests needed when changing either command.
Configuration and generated-source boundaries
The active /srv/pmiverse/_pmiregistry/config/pmiverse.conf is sourced Bash
(PMIVERSE_CONF overrides its location). _pmiconfig/config/pmiverse.conf
is an empty generic template, whereas config/pmisetup.conf is INI
read by Python's ConfigParser. They are different interfaces. The database
registry connects aliases to database names, suite versions, relative source
directories, target directories, and optional backup-parent directories. The
instance registry adds the producing host, output roots, connection defaults,
and database membership. See configuration and the
backup registry specification.
Host-separated restore mirrors use data/backups/<host>/<database>. Producers
on the originating hosts still write data/backups/<database>. Latest-only
synchronization flattens selected archives into one directory and rejects
duplicate basenames across hosts. Keep these three layouts distinct when
editing registries or test expectations.
data/repos/<version>/*.csv supplies repository names, URLs, and destination
folder names to the updater selected for that version. A folder named OCB
can select special root placement instead of a child repository directory.
Group-to-directory mappings differ between the generic and retained versioned
updaters; inspect get_clone_system_dir in the actual consumer before changing
a catalog. Versioned requirement files are maintained deployment inputs.
The requirement collector instead generates scripts/fetch/requirements-*.txt
by default; modify its inputs or implementation, not those generated results.
testkit/ owns reusable templates and shared policy. Downstream scaffold files
and copied workflows are consumers, not alternative canonical sources.
templates/ contains deployment inputs: edits do not install units, reload
systemd, update an instance, or run an upgrade. Service units separate ordinary
startup, full upgrades, selected-module maintenance, backup production, and
rsync. Timers schedule the unit named by their configuration; they do not
automatically schedule every instantiated @.service.
Bash interfaces and process ownership
Operational entry points generally use strict shell modes. Sourced restore libraries define functions without running operations or replacing the caller's traps. The completion library is also sourced, but must tolerate failures without enabling strict modes or terminating an interactive shell.
Configuration libraries publish named globals; functions using local -n
write results into caller-owned variables. Completion fixtures similarly rely
on Bash dynamic scope to fill the caller's cur, prev, words, and cword.
An assignment that appears unused within its file can therefore be part of a
cross-file interface. Renaming it or exporting it merely to silence an
inspection changes that interface.
Keep executable argument vectors in arrays. In the legacy Odoo restore
commands, the interpreter and odoo-bin path are separate elements; quoted
array expansion preserves spaces and glob characters. Quoted heredoc
delimiters keep SQL/Python text literal; unquoted delimiters intentionally
interpolate shell values. Comments inside heredocs become part of generated
programs or output, so they require the same care as other template edits.
Traps own process cleanup. Capture the original exit status before executing
cleanup commands, preserve ownership of temporary files, and avoid deleting
artifacts whose creation was not confirmed. A service follower owns its
journalctl child, not the systemd synchronization job. Detaching the terminal
must leave that job running.
Backup and restore state transitions
The Odoo 18 producer resolves targets before writing, locks the canonical backup root, creates a temporary dump/ZIP, validates the ZIP, then publishes with a same-filesystem hard link that refuses an existing destination. Retention runs after publication. Each database succeeds or fails independently; the final status aggregates failures and completed archives remain available.
Latest links are published with individual renames. Synchronization first stages every configured host and checks basename uniqueness, then replaces destination files individually and prunes stale files last. A rename is atomic for one file; the complete set is not an atomic transaction and can contain mixed generations after interrupted publication.
The generic restore separates configuration, archive validation, PostgreSQL primitives, neutralization, and publication. Its entry point owns resource locks, phase changes, traps, and temporary names. Publication observes actual database/path state before rollback: an interrupted command may have taken effect before its success flag was recorded. Unknown or ambiguous state means preserve artifacts and report recovery information, rather than guessing. See the detailed restore architecture before changing these transitions.
Legacy per-target restore commands share configuration resolution but retain
their own destructive restore bodies. They do not acquire all the generic
runner's guarantees. The Odoo 12 backup producer in _pmiregistry likewise writes final ZIP
names directly and remains a separately documented legacy implementation.
Database duplication retains incomplete destinations after failure and only cleans its private dump directory. Assigning a new UUID does not neutralize a copy. Database deletion performs the database drop first, then rechecks the filestore's device/inode before removal; it cannot roll the database back if filestore removal fails.
Python tools and migration code
Requirement collection prunes excluded directories, expands local includes without leaving the source root, and merges normalized names with identical environment markers. Group order is OCB, OCA, then PMIVerse: earlier ownership excludes that package from later groups. This is an inventory of declarations, not a dependency solver. Parsing completes before publication, but replacing the output files is not atomic as a group.
The generic updater returns per-repository worker results and uses fast-forward-only pulls. Enhanced version-specific updaters use ordinary pulls and initialize configuration/logging during import. Older sequential updaters have neither the worker pool nor the retry layer. Importing those modules is not an inert way to obtain CLI help; tests redirect configuration, logging, and subprocesses before import.
Ruff synchronization compares parsed policy but edits TOML source text to preserve unrelated sections. It refuses staged/unstaged edits in an existing target policy, reparses proposed text before writing, and reports competing nested configurations without rewriting them. Comment or marker changes in the canonical source can affect later synchronization output.
Translation exporters create deterministic l10n_... databases and write POT
files in addon repositories. The single-addon and repository commands have
different cleanup and publication behavior; see the
translation implementation guide.
The migration runner clones through successive Odoo versions using each version's environment, configuration, and OpenUpgrade path. Derived databases and backups are recovery artifacts, not an all-or-nothing transaction. The SQL repair backfills missing sale-attachment visibility when that column exists. The website-sale overlay converts translated ribbon HTML and copies legacy attachment metadata into product documents. Its inserts rely on migration sequencing; do not invoke the migration hook as an independent replayable maintenance command.
Test and lint infrastructure
The single-addon runner prepares the database before pytest. The namespace
plugin extends odoo.addons.__path__ instead of placing addon roots on
top-level sys.path, where an addon could shadow a standard-library module.
The base environment fixture supplies a cursor per test and rolls it back;
tests must not perform addon installation themselves.
The repository runner discovers candidates, applies include/exclude globs, dispatches per-addon workers, then derives summaries and badges from their artifacts. The rolling scheduler tracks and reaps each PID once, including children that finish before a wait starts. Stop-on-first-failure handling is in the serial branch; parallel work is drained before reporting.
Scaffolding preserves existing test files unless forced. Its preview suppresses file copies but still creates logging/test directories. Initialization can also edit README/ignore files and install pre-commit hooks. Badge publishing compares run-summary addon membership with the destination repository before pruning; the absence of a particular SVG alone does not establish a partial run.
Repository shell tests use temporary trees and command mocks. Some extract a
function or scheduling block rather than source an operational entry point.
Keep extraction markers stable, keep mocks first in PATH, and preserve
failure-injection arguments passed through wrapper functions. The safety
test's run_only_neutralize receives environment overrides indirectly through
expect_failure; its argument forwarding is required even when a static
analyzer cannot find the call.
Maintaining documentation and inspections
Document input/output contracts, side effects, ordering, ownership, and reasons for non-obvious code. Avoid comments that merely restate assignments or test names. Keep detailed user instructions in the component README/runbook and link them from the portal rather than copying command lists into every file.
ShellCheck source hints describe checked-in dependencies; /dev/null hints
are reserved for runtime-generated or environment-owned source files.
Narrow annotations preserve intentional globs, literal injection data, nameref
outputs, and indirect trap calls. Explain such annotations where they occur.
The test inspection notes describe the
Python monkeypatch annotations as well.
For a documentation-only change, syntax/inspection checks and comparison with the pre-edit executable source are useful first checks. Run focused fixture tests when comments affect extraction markers or generated templates. Running the actual backup, migration, restore, update, or deployment command is not a substitute for those checks.