本文目前仅提供英文版本。翻译即将推出。
Odoo CI/CD with GitHub Actions: Runbot-Style Testing and Deployment
Most Odoo deployments still ship via "SSH in, git pull, restart Odoo." That works until it doesn't — you push a syntax error, the service crashes, and you're explaining to leadership why customers can't log in. A real CI/CD pipeline catches errors before they reach production and adds the safety nets (staging tests, automatic backups, health checks) that turn deploys into routine non-events.
This article walks through a production-grade Odoo CI/CD setup using GitHub Actions, modeled on Odoo's own runbot but tuned for SMB-to-mid-market deployments. ECOSIRE runs this pattern for our own ECOSIRE.COM platform and across 15+ client deployments.
Key Takeaways
- Pipeline stages: lint → unit tests → integration tests → staging deploy → smoke test → production deploy
- Runbot-style: spin up a fresh Odoo + PG container per build; run all tests against it
- Multi-version matrix: test against Odoo 17, 18, 19 if your modules support multiple versions
- Pre-deploy health check refuses to deploy if last CI run failed
- Staging cutover before production; auto-rollback on failed smoke test
- Backup database before every production deploy; verify after
- Maintenance mode flag flips automatically; never leave it on
Pipeline stages
A complete pipeline has these stages, in order:
1. Lint & format check (~30 sec)
2. Unit tests (~2 min)
3. Integration tests (~5 min, runbot-style)
4. Build artifacts (~1 min)
5. Deploy to staging (~3 min)
6. Smoke test staging (~2 min)
7. Deploy to production (~5 min, with maintenance window)
8. Health check prod (~1 min)
9. Notify success/fail (instant)
Total: ~20 minutes for a clean pipeline. Failures short-circuit early.
Stage 1: Lint and format check
Catch obvious errors before spinning up Postgres:
name: Odoo CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install lint tools
run: |
pip install pylint pylint-odoo flake8 black isort
- name: Run pylint-odoo
run: |
pylint --load-plugins=pylint_odoo \
-d all -e odoolint addons/
- name: Run flake8
run: flake8 addons/ --max-line-length=120
- name: Check black formatting
run: black --check addons/
- name: Check import sorting
run: isort --check-only addons/
pylint-odoo catches Odoo-specific anti-patterns: missing manifests, wrong _name, unsafe sql.raw(), etc.
Stage 2: Unit tests
For pure utility functions, run plain pytest:
unit_tests:
runs-on: ubuntu-22.04
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install
run: pip install -r requirements-test.txt
- name: Run pytest
run: pytest addons/*/tests/unit/ -v
Stage 3: Integration tests (runbot-style)
The expensive but high-value stage. Spin up a fresh Odoo + PG, install your modules, run TransactionCase tests:
integration_tests:
runs-on: ubuntu-22.04
needs: unit_tests
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
POSTGRES_DB: odoo_test
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
strategy:
matrix:
odoo_version: ["17.0", "18.0", "19.0"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Odoo ${{ matrix.odoo_version }}
run: |
git clone --depth=1 -b ${{ matrix.odoo_version }} \
https://github.com/odoo/odoo /opt/odoo
pip install -r /opt/odoo/requirements.txt
- name: Install our modules
run: |
ln -s ${GITHUB_WORKSPACE}/addons/* /opt/odoo/addons/
- name: Run Odoo tests
run: |
/opt/odoo/odoo-bin \
-d odoo_test \
--db_host=localhost \
--db_user=odoo \
--db_password=odoo \
-i my_module \
--test-enable \
--test-tags=my_module,post_install \
--stop-after-init \
--log-level=warn
The matrix lets you validate against multiple Odoo versions in parallel. Most modules pin to one version; if yours supports multiple, the matrix is gold.
Stage 4: Build artifacts
If your deployment needs pre-built artifacts (compiled assets, packaged module zip):
build:
runs-on: ubuntu-22.04
needs: integration_tests
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Build module zip
run: |
for module in addons/*/; do
module_name=$(basename "$module")
zip -r "${module_name}.zip" "$module"
done
- uses: actions/upload-artifact@v4
with:
name: modules
path: "*.zip"
Stage 5: Deploy to staging
deploy_staging:
runs-on: ubuntu-22.04
needs: build
if: github.ref == 'refs/heads/main'
environment: staging
steps:
- name: SSH deploy
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
script: |
cd /opt/odoo/custom-addons
git pull origin main
sudo systemctl restart odoo
# Update modules
sudo -u odoo /opt/odoo/odoo-bin -d staging \
-u $(ls -d */ | tr -d '/' | tr '\n' ',') \
--stop-after-init
sudo systemctl restart odoo
Stage 6: Smoke test staging
After staging deploy, hit a couple of URLs to confirm Odoo is up:
smoke_test_staging:
runs-on: ubuntu-22.04
needs: deploy_staging
steps:
- name: Check Odoo login page
run: |
curl -fsSL https://staging.example.com/web/login | grep -q "Odoo"
- name: Check API endpoint
run: |
curl -fsSL https://staging.example.com/web/database/list | grep -q "result"
- name: Check custom endpoint
run: |
curl -fsSL https://staging.example.com/my_module/api/health | grep -q "ok"
If smoke fails, deploy stops. Production never gets touched.
Stage 7: Deploy to production
The critical stage. Includes maintenance mode, backup, deploy, verification:
deploy_production:
runs-on: ubuntu-22.04
needs: smoke_test_staging
environment: production
if: github.ref == 'refs/heads/main'
steps:
- name: SSH production deploy
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
set -e
# 1. Enable maintenance mode (Nginx serves branded page)
sudo touch /opt/odoo/maintenance.flag
sudo nginx -s reload
# 2. Backup database
BACKUP_FILE=/var/backups/prod-$(date +%Y%m%d-%H%M%S).dump
sudo -u odoo pg_dump -Fc -d production > $BACKUP_FILE
# 3. Pull and update
cd /opt/odoo/custom-addons
git fetch origin
git checkout ${{ github.sha }}
# 4. Update Odoo
sudo systemctl stop odoo
sudo -u odoo /opt/odoo/odoo-bin -d production \
-u $(ls -d */ | tr -d '/' | tr '\n' ',') \
--stop-after-init
sudo systemctl start odoo
# 5. Wait for startup
for i in {1..60}; do
if curl -fsS http://localhost:8069/web/login > /dev/null; then
break
fi
sleep 1
done
# 6. Disable maintenance
sudo rm /opt/odoo/maintenance.flag
sudo nginx -s reload
# 7. Verify
curl -fsSL https://example.com/web/login | grep -q "Odoo" || \
(sudo touch /opt/odoo/maintenance.flag && \
sudo nginx -s reload && \
echo "DEPLOY FAILED, MAINTENANCE RE-ENABLED" && \
exit 1)
Critical safety: always re-enable maintenance flag if deploy fails mid-way. The script-deploy from Windows over SSH is notorious for aborting silently and leaving the flag stuck on (we hit this in production — see our deployment notes).
Stage 8: Health check production
Final validation:
health_check:
runs-on: ubuntu-22.04
needs: deploy_production
steps:
- name: HTTP health
run: |
curl -fsSL https://example.com/web/login
curl -fsSL https://example.com/api/health
- name: Database health (via web/database/list)
run: |
curl -fsSL https://example.com/web/database/list
Stage 9: Notify
notify:
runs-on: ubuntu-22.04
needs: health_check
if: always()
steps:
- name: Slack notification
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_DEPLOY_WEBHOOK }}
SLACK_TITLE: "Odoo Deploy ${{ needs.deploy_production.result }}"
SLACK_MESSAGE: "Commit: ${{ github.sha }}"
Pipeline performance comparison
A complete pipeline timing comparison:
| Stage | Conservative (sequential) | Optimized (parallel matrix) |
|---|---|---|
| Lint | 30s | 30s |
| Unit tests | 2m | 2m |
| Integration tests | 15m (3 versions sequential) | 6m (3 versions parallel) |
| Build | 1m | 1m |
| Staging deploy | 3m | 3m |
| Smoke staging | 2m | 2m |
| Prod deploy | 5m | 5m |
| Prod health | 1m | 1m |
| Total | ~30m | ~20m |
For high-frequency deployments (multiple per day), the parallel matrix matters. For weekly deployments, sequential is fine and simpler.
Pre-deploy gates
Before any deploy, gate on:
- All required tests passed
- Latest CI run on the branch concluded with success
- No pending merge conflicts
- Environment-specific approval (production typically requires manual approval after staging)
GitHub Actions environments support manual approval gates — use them for production.
Rollback strategy
If the deploy fails post-cutover:
# 1. Re-enable maintenance
sudo touch /opt/odoo/maintenance.flag && sudo nginx -s reload
# 2. Restore from backup
sudo systemctl stop odoo
sudo -u postgres dropdb production
sudo -u postgres createdb -O odoo production
sudo -u odoo pg_restore -d production $LATEST_BACKUP
# 3. Revert code
cd /opt/odoo/custom-addons
git checkout $PREV_COMMIT_SHA
# 4. Restart
sudo systemctl start odoo
# 5. Disable maintenance
sudo rm /opt/odoo/maintenance.flag && sudo nginx -s reload
Practice this. If you've never tested rollback, you don't have rollback.
Frequently Asked Questions
Should I run integration tests on every push or only on PR?
Run lint + unit on every push (cheap). Run integration on PRs to main (expensive). For trunk-based workflows, run integration on every push to main and on PRs targeting main. Don't run integration on every feature-branch push — too expensive.
What about runbot-style ephemeral environments?
Some teams spin up a fresh Odoo + DB per PR for QA. Heavy but useful for high-traffic projects. GitHub Actions can do this via dedicated runners or by hitting an external orchestrator (Kubernetes, Docker Swarm). For SMBs, a single staging environment with PR-based promote-to-staging is more cost-effective.
How do I handle database migrations in CI?
Two options: (1) Migrations run automatically when you -u my_module (Odoo's openupgrade hooks fire). (2) Custom Python scripts run pre-Odoo-restart. For destructive migrations (rename column, drop table), back up first and test on a staging copy of prod.
Can I deploy without downtime?
Mostly yes. Database schema changes are the bottleneck — for additive changes (new column with default), Odoo 19 can apply them without locking. For destructive changes (rename, drop), you need maintenance mode for the schema-update window. Plan additive-then-cleanup migrations to minimize downtime.
What's the best practice for secrets in GitHub Actions?
GitHub Secrets for: SSH keys, database passwords, API keys, webhook URLs. Environments for production approvals. Never echo secrets to logs (Actions auto-redacts but it's still risky). Rotate quarterly.
CI/CD turns Odoo deploys from anxiety-inducing into routine. ECOSIRE's Odoo support and maintenance service sets up CI/CD pipelines and handles the runbook for production deployments. See our Odoo customization service for module development with full CI integration, or browse our Odoo modules catalog for production-grade modules built on this exact pipeline.
作者
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
相关文章
如何将自定义按钮添加到 Odoo 表单视图 (2026)
将自定义操作按钮添加到 Odoo 19 表单视图:Python 操作方法、视图继承、条件可见性、确认对话框。经过生产测试。
如何在没有 Studio 的情况下在 Odoo 中添加自定义字段 (2026)
通过 Odoo 19 中的自定义模块添加自定义字段:模型继承、视图扩展、计算字段、存储/非存储决策。代码优先,版本控制。
如何使用外部布局在 Odoo 中添加自定义报告
使用 web.external_layout 在 Odoo 19 中构建品牌 PDF 报告:QWeb 模板、paperformat、操作绑定。带有印刷徽标+页脚覆盖。