Every production Odoo deployment ends up with the same problem: addons from multiple sources. The core Odoo modules live in the official Docker image. A handful of OCA modules are cloned from GitHub. One module is a fork with a patch your team has not yet upstreamed. Another is a private repo.
Six months later, nobody is sure which OCA commit is running in production. The fork has diverged from upstream and nobody tracked the merge base. CI uses different OCA versions than staging uses.
git-aggregator solves this by declaring all addon sources in a single YAML file, fetching everything reproducibly, and producing a merged addons directory.
Installing git-aggregator#
pip install git-aggregatorThe tool is maintained by the OCA and is the standard way OCA project maintainers manage their own multi-repo addon stacks.
The repos.yml file#
# repos.yml
./addons:
remotes:
oca: https://github.com/OCA
origin: git@github.com:your-org
merges:
# OCA server-tools: pin to a specific commit for reproducibility
- oca/server-tools 18.0 a1b2c3d4
# OCA account: pin to a tag
- oca/account-financial-tools 18.0.1.0.0
# Your private addon repo
- origin/my-custom-addons main
# OCA module + your patch on top
- oca/sale-workflow 18.0
- origin/sale-workflow-patches main
target: origin merged-addons
# Only copy specific modules from the merged tree
fetch:
# From oca/server-tools
- base_setup_partner
- base_technical_user
# From oca/account-financial-tools
- account_cash_basis_base_accountThe merges section declares remote + branch/ref pairs. git-aggregator fetches each one and merges them in order using git merge --no-edit. Conflicts must be resolved in your patch branches before adding them.
The fetch section limits which module directories are copied to the final addons path. Without it, every module in every cloned repo ends up in your addons directory - including dozens you do not need and that slow down Odoo's addon scan.
Running the aggregator#
# Aggregate all sources and produce the ./addons directory
gitaggregate -c repos.yml
# With verbose output to see what is being fetched
gitaggregate -c repos.yml -v
# Only aggregate a specific target
gitaggregate -c repos.yml -p ./addonsThe result is a single ./addons directory containing exactly the modules listed in fetch, from the exact commits specified.
Pinning to commits for reproducibility#
The 18.0 branch references are not pinned - they move as OCA merges PRs. For production stability, pin to a specific commit hash:
merges:
- oca/server-tools 18.0 a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2Run this to find the current HEAD of an OCA branch and pin it:
git ls-remote https://github.com/OCA/server-tools.git 18.0 | cut -f1Update the hash whenever you deliberately want to pull in new OCA changes. This gives you explicit control over when OCA updates land in your addons directory.
CI integration#
Add the aggregation step before your Odoo tests run:
# .github/workflows/ci.yml
- name: Aggregate OCA modules
run: |
pip install git-aggregator
gitaggregate -c repos.yml
- name: Run Odoo tests
run: |
# ./addons now contains all declared modules
docker compose run --rm odoo odoo --init all --database test --stop-after-init --test-enableCommit repos.yml to your repo. Do not commit the ./addons directory itself (add it to .gitignore). The YAML file is the source of truth; the aggregated directory is a build artifact.
Handling merge conflicts#
When two OCA modules both patch the same base file (common with accounting modules), git-aggregator will fail on the merge. The correct fix is to create an intermediate branch in your private repo that applies the patches in the correct order, then reference that branch:
merges:
- oca/account-financial-tools 18.0
- oca/account-payment 18.0
- origin/account-conflict-resolution main # your branch resolving the conflictDo not resolve conflicts by editing files in the aggregated ./addons directory - those changes will be lost on the next aggregation run.
Lockfile for exact reproducibility#
git-aggregator does not produce a lockfile by default. For maximum reproducibility - identical bytewise addons on every CI run and every developer machine - record the resolved hashes after aggregation:
gitaggregate -c repos.yml
# After aggregation, export the resolved state
cd .git/aggregator-work
for repo in */; do
echo "$repo: $(git -C $repo rev-parse HEAD)"
done > ../../repos.lockCommit repos.lock to your repo. During CI, you can verify that the aggregation matches the lockfile or fail the build if it does not.
When not to use git-aggregator#
git-aggregator is the right tool when you have more than three OCA modules and need to track their versions. For a single OCA module or two, a simpler approach works fine: add the module directory to your own repo as a git submodule pinned to a specific commit. Submodules are more widely understood and have better IDE integration. The tradeoff is that managing multiple submodules for large OCA stacks becomes unwieldy - that is the point where git-aggregator pays off.

