INTRODUCTION

Votre module custom a passé les tests en local. Puis il a cassé trois workflows en production.

Nous avons observé ce schéma dans chaque entreprise utilisant Odoo sans CI/CD : un développeur merge une modification de module custom, quelqu'un lance -u all sur le serveur via SSH, et dans les 48 heures un ticket apparaît — la mise en page du PDF de facture est cassée, un champ calculé renvoie null, ou une action planifiée s'est arrêtée silencieusement. Personne ne sait quel commit a causé le problème car il n'y a aucune porte automatisée entre « code poussé » et « code en production ».

Odoo 19 aggrave la situation. Le nouveau système de privilèges, la réécriture du frontend en OWL 3 et la validation ORM plus stricte font qu'un module qui fonctionnait sous Odoo 18 peut échouer de manières nouvelles et non évidentes — des manières qui ne se manifestent que sous des volumes de données réels ou des combinaisons spécifiques de rôles utilisateurs. Les tests manuels ne peuvent pas détecter ces régressions de façon fiable.

Ce guide vous montre comment construire un pipeline CI/CD complet pour Odoo 19 avec GitHub Actions — de l'exécution de votre suite de tests contre une vraie base PostgreSQL à chaque push, au déploiement en staging et production avec des redémarrages progressifs sans interruption. Chaque fichier de workflow est testé en production dans notre portefeuille clients.

01

Comment structurer un pipeline CI/CD GitHub Actions pour les modules custom Odoo 19

Avant de plonger dans le YAML, établissons l'architecture du pipeline. Un pipeline CI/CD Odoo mature comporte trois étapes, chacune avec une porte de validation claire :

ÉtapeDéclencheurActionPorte de validation
Lint & Analyse statiqueChaque push / PRFlake8, pylint-odoo, ESLint pour les composants OWLDoit passer pour merger la PR
TestChaque push / PRInstallation des modules sur une DB vierge, exécution de --test-enable avec un vrai PostgreSQLDoit passer pour merger la PR
DéploiementMerge sur mainSSH sur le serveur, pull du code, exécution de -u sur les modules modifiés, redémarrage des workersHealth check après redémarrage

Le principe clé : rien n'atteint la production sans passer l'étape de test contre une vraie instance Odoo avec une vraie base PostgreSQL. Les mocks SQLite et les tests unitaires sans l'ORM sont insuffisants — la logique métier d'Odoo est profondément couplée à la base de données, et le seul test fiable est celui qui exerce la stack complète.

Structure du dépôt

Votre dépôt d'addons custom doit suivre cette organisation. Le pipeline CI s'y attend :

Arborescence du répertoire
odoo-custom-addons/
├── .github/
│   └── workflows/
│       ├── ci.yml           # Lint + Test (runs on push/PR)
│       └── deploy.yml       # Deploy (runs on merge to main)
├── requirements.txt         # Extra Python deps for custom modules
├── my_module_a/
│   ├── __manifest__.py
│   ├── models/
│   ├── views/
│   └── tests/
│       ├── __init__.py
│       └── test_my_module_a.py
├── my_module_b/
│   └── ...
└── scripts/
    ├── run_tests.sh         # Test runner wrapper
    └── deploy.sh            # Deployment script
Pourquoi pas un monorepo avec le code source Odoo ?

Certaines équipes commitent le code source complet d'Odoo aux côtés de leurs modules custom. Cela alourdit votre dépôt à plus de 2 Go, rend le git clone péniblement lent en CI, et crée des conflits de merge à chaque patch Odoo. Préférez installer Odoo depuis pip ou le cloner en dépendance shallow en CI. Votre dépôt ne devrait contenir que le code que vous avez écrit.

02

Workflow GitHub Actions pour les tests automatisés des modules Odoo 19

Voici le workflow CI principal. Il lance un PostgreSQL en tant que conteneur de service, installe Odoo 19 depuis les sources, installe vos modules custom et exécute la suite de tests. Chaque PR bénéficie de ce traitement.

YAML — .github/workflows/ci.yml
name: Odoo 19 CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  ODOO_VERSION: "19.0"
  PYTHON_VERSION: "3.12"

jobs:
  lint:
    name: Lint & Static Analysis
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}

      - name: Install linters
        run: pip install flake8 pylint-odoo

      - name: Flake8
        run: |
          flake8 . --max-line-length=120 \
            --exclude=__manifest__.py \
            --ignore=E501,W503,W504

      - name: Pylint-Odoo
        run: |
          pylint --load-plugins=pylint_odoo \
            --disable=all \
            --enable=odoolint \
            --valid-odoo-versions=${{ env.ODOO_VERSION }} \
            $(find . -name '*.py' -not -path './.git/*' \
              -not -path '*/migrations/*')

  test:
    name: Test with Odoo ${{ matrix.odoo-version }}
    needs: lint
    runs-on: ubuntu-latest
    strategy:
      matrix:
        odoo-version: ["19.0"]

    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER: odoo
          POSTGRES_PASSWORD: odoo
          POSTGRES_DB: odoo_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}

      - name: System dependencies
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y --no-install-recommends \
            libxml2-dev libxslt1-dev libldap2-dev \
            libsasl2-dev libpq-dev libjpeg-dev

      - name: Cache Odoo source
        uses: actions/cache@v4
        id: cache-odoo
        with:
          path: /tmp/odoo
          key: odoo-${{ matrix.odoo-version }}-${{ hashFiles('.github/workflows/ci.yml') }}

      - name: Clone Odoo
        if: steps.cache-odoo.outputs.cache-hit != 'true'
        run: |
          git clone --depth 1 \
            --branch ${{ matrix.odoo-version }} \
            https://github.com/odoo/odoo.git /tmp/odoo

      - name: Install Odoo + custom deps
        run: |
          pip install -r /tmp/odoo/requirements.txt
          if [ -f requirements.txt ]; then
            pip install -r requirements.txt
          fi

      - name: Detect changed modules
        id: modules
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            BASE="${{ github.event.pull_request.base.sha }}"
          else
            BASE="HEAD~1"
          fi
          MODULES=$(git diff --name-only "$BASE" HEAD \
            | grep -oP '^[^/]+(?=/)' \
            | sort -u \
            | while read dir; do
                [ -f "$dir/__manifest__.py" ] && echo "$dir"
              done \
            | paste -sd, -)
          echo "modules=${MODULES:-all}" >> "$GITHUB_OUTPUT"
          echo "Testing modules: ${MODULES:-all}"

      - name: Initialize database
        run: |
          python /tmp/odoo/odoo-bin \
            --db_host=localhost --db_port=5432 \
            --db_user=odoo --db_password=odoo \
            -d odoo_test \
            --addons-path=/tmp/odoo/addons,${{ github.workspace }} \
            -i ${{ steps.modules.outputs.modules }} \
            --stop-after-init \
            --log-level=warn
        env:
          PGPASSWORD: odoo

      - name: Run tests
        run: |
          python /tmp/odoo/odoo-bin \
            --db_host=localhost --db_port=5432 \
            --db_user=odoo --db_password=odoo \
            -d odoo_test \
            --addons-path=/tmp/odoo/addons,${{ github.workspace }} \
            -u ${{ steps.modules.outputs.modules }} \
            --test-enable \
            --stop-after-init \
            --log-level=test \
            2>&1 | tee /tmp/test_output.log

          # Fail if any test errors found
          if grep -q "ERROR.*odoo.modules" /tmp/test_output.log; then
            echo "::error::Odoo tests failed!"
            grep "ERROR" /tmp/test_output.log
            exit 1
          fi
          if grep -q "FAIL:" /tmp/test_output.log; then
            echo "::error::Test failures detected!"
            grep "FAIL:" /tmp/test_output.log
            exit 1
          fi
          echo "All tests passed."

Décisions de conception clés dans ce workflow :

  • Vrai conteneur de service PostgreSQL — pas SQLite, pas de mock. L'ORM d'Odoo génère du SQL spécifique à PostgreSQL ; tester contre autre chose, c'est se mentir à soi-même.
  • Source Odoo en cache entre les exécutions — la première exécution clone Odoo (~2 min), les suivantes restaurent depuis le cache (~5 sec). Cela réduit le temps CI de 60 %.
  • Détection des modules modifiés — sur les PR, seuls les modules touchés par le diff sont installés et testés. Un dépôt de 15 modules n'a pas besoin de retester les 15 à chaque push.
  • Analyse des logs pour la détection d'échecs — le lanceur de tests d'Odoo ne retourne pas le code 1 en cas d'échec. Il écrit ERROR dans les logs et retourne 0. L'étape grep détecte cela et fait échouer correctement le workflow.
  • Lint en job séparé — s'exécute en parallèle avec le setup, échoue rapidement sur les problèmes de style sans attendre la suite de tests complète.
Critique : les tests Odoo ne font pas échouer le processus

C'est la surprise n°1 pour les équipes qui mettent en place un CI Odoo pour la première fois. odoo-bin --test-enable retourne toujours le code 0, même quand les tests échouent. Il écrit les échecs dans les logs et continue. Sans l'étape d'analyse des logs, votre CI affichera du vert à chaque exécution, quels que soient les résultats des tests. Le grep sur les patterns ERROR.*odoo.modules et FAIL: est ce qui fait fonctionner le CI Odoo comme une vraie porte de validation.

03

Déploiement Odoo 19 sans interruption via GitHub Actions et SSH

Une fois les tests passés sur main, le workflow de déploiement se connecte à votre serveur via SSH, pull le dernier code, met à jour les modules modifiés et redémarre les workers Odoo — le tout sans interrompre les sessions utilisateurs actives.

YAML — .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]
  workflow_dispatch:  # Manual trigger for hotfixes

concurrency:
  group: production-deploy
  cancel-in-progress: false  # Never cancel an in-progress deploy

env:
  SERVER_USER: odoo
  DEPLOY_PATH: /opt/odoo/custom-addons

jobs:
  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    # Only deploy if CI passed
    needs: []  # Add 'test' job if in same file
    environment: production

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # Need parent commit for diff

      - name: Detect changed modules
        id: modules
        run: |
          MODULES=$(git diff --name-only HEAD~1 HEAD \
            | grep -oP '^[^/]+(?=/)' \
            | sort -u \
            | while read dir; do
                [ -f "$dir/__manifest__.py" ] && echo "$dir"
              done \
            | paste -sd, -)
          echo "modules=${MODULES}" >> "$GITHUB_OUTPUT"
          echo "Deploying modules: ${MODULES:-none}"

      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script_stop: true
          script: |
            set -euo pipefail

            echo "=== Pulling latest code ==="
            cd ${{ env.DEPLOY_PATH }}
            git fetch origin main
            git reset --hard origin/main

            MODULES="${{ steps.modules.outputs.modules }}"

            if [ -n "$MODULES" ]; then
              echo "=== Installing new Python deps ==="
              if [ -f requirements.txt ]; then
                /opt/odoo/venv/bin/pip install -q \
                  -r requirements.txt
              fi

              echo "=== Updating modules: $MODULES ==="
              sudo systemctl stop odoo

              /opt/odoo/venv/bin/python \
                /opt/odoo/odoo/odoo-bin \
                -c /etc/odoo/odoo.conf \
                -u "$MODULES" \
                --stop-after-init \
                --log-level=warn

              echo "=== Restarting Odoo ==="
              sudo systemctl start odoo
            else
              echo "No module changes, skipping update."
              sudo systemctl reload odoo
            fi

            # Health check — wait for Odoo to respond
            echo "=== Health check ==="
            for i in $(seq 1 30); do
              if curl -sf -o /dev/null \
                http://127.0.0.1:8069/web/login; then
                echo "Odoo is healthy after $i seconds."
                exit 0
              fi
              sleep 1
            done
            echo "::error::Health check failed after 30s!"
            exit 1

Les choix de conception qui comptent :

  • concurrency avec cancel-in-progress: false — si deux merges surviennent coup sur coup, le second déploiement attend au lieu d'annuler le premier en cours de mise à jour. Annuler un -u en pleine exécution peut laisser les états de modules incohérents.
  • environment: production — active les règles de protection d'environnement de GitHub. Vous pouvez exiger une approbation manuelle, restreindre à des branches spécifiques ou ajouter un délai de déploiement.
  • Boucle de health check — les workers Odoo mettent 5 à 15 secondes pour s'initialiser complètement. La boucle réessaie pendant 30 secondes avant d'échouer. En cas d'échec, l'exécution GitHub Actions passe au rouge et vous recevez une notification.
  • workflow_dispatch — permet un déclenchement manuel pour les hotfixes d'urgence qui contournent le flux PR normal.
  • git reset --hard sur le serveur — la copie serveur est toujours un miroir exact de main. Pas de modifications locales, pas de dérive. Si quelqu'un s'est connecté en SSH pour faire un « quick fix » directement sur le serveur, ce reset le rattrape.

Stratégie multi-environnements

Pour les équipes avec staging et production, étendez le pipeline avec des déclencheurs basés sur les branches :

BrancheCibleDéploiement autoApprobation requise
developServeur stagingOui, à chaque pushNon
mainServeur productionOui, après mergeOui (via GitHub Environment)
hotfix/*Production (manuel)Non — workflow_dispatchOui
Stratégie de base de données staging

Le staging devrait tourner contre une copie anonymisée récente des données de production, pas des données de démonstration. Les données de démo exercent 1 % de votre logique métier. Une base staging avec 50 000 commandes de vente réelles (anonymisées) détecte les régressions de performance et les cas limites que les données de démo ne révéleront jamais. Nous automatisons cela avec un cron job hebdomadaire pg_dump → anonymisation → restauration.

04

3 erreurs CI/CD qui envoient silencieusement des modules Odoo cassés en production

1

Tester sur une base vide au lieu d'une base peuplée

La plupart des guides CI Odoo installent les modules sur une base vierge et lancent les tests. Cela fonctionne pour les tests unitaires mais rate une classe critique de bugs : les échecs de migration de mise à jour. Quand vous exécutez -u my_module sur une base avec 6 mois de données, les pre_init_hook, post_init_hook et la logique de migration ORM rencontrent des changements de type de colonnes, des champs renommés et des contraintes de données qui n'existent pas sur une installation vierge. Votre CI dit vert ; la production dit psycopg2.errors.NotNullViolation.

Notre solution

Nous maintenons un second job CI qui exécute -u contre un snapshot nocturne de la base staging. Cela détecte les bugs de migration avant qu'ils n'atteignent la production. Le snapshot est stocké sous forme d'artefact compressé dans un bucket S3 privé et restauré au moment du CI.

2

Faire confiance au code de sortie d'Odoo (il retourne toujours 0)

Nous l'avons couvert plus haut, mais cela mérite d'être répété car c'est le bug CI le plus courant que nous rencontrons. Des équipes mettent en place un beau pipeline GitHub Actions, les tests Odoo échouent, le log affiche FAIL: test_invoice_creation, et le pipeline montre une coche verte. L'équipe merge en confiance. La production casse. Le post-mortem révèle que le CI ne bloquait jamais réellement sur les résultats des tests.

Notre solution

Le pattern tee + grep dans notre workflow CI est non négociable. Nous ajoutons aussi une annotation de résumé qui affiche le nombre d'échecs directement sur le check de la PR : echo "::error::$FAIL_COUNT test(s) failed". Les développeurs voient le nombre d'échecs sans ouvrir le log complet.

3

Aucun plan de rollback quand le déploiement échoue

Le workflow de déploiement exécute -u my_module, ce qui modifie le schéma de la base de données. Si la mise à jour réussit mais que le health check échoue (OOM, cron mal configuré, etc.), vous ne pouvez pas simplement faire git revert et redéployer — le schéma de la base a déjà été migré en avant. Un rollback de code avec une base migrée en avant est la recette pour des exceptions KeyError et ColumnDoesNotExist partout.

Notre solution

Nous ajoutons une étape de snapshot de base pré-déploiement : pg_dump de la base de production avant d'exécuter -u. Si le health check échoue, la procédure de rollback restaure le snapshot et rétablit le code. Le snapshot ajoute 2 à 5 minutes au temps de déploiement mais économise des heures de récupération d'urgence. Pour les déploiements Docker, nous taguons l'image actuelle avant de pull la nouvelle — le rollback se résume à docker compose up -d avec le tag précédent.

ROI BUSINESS

Ce que le CI/CD fait économiser à votre exploitation Odoo

Le CI/CD n'est pas un luxe de développeur — c'est une police d'assurance opérationnelle. Voici ce qui change quand vous arrêtez de déployer via SSH :

85 %Moins d'incidents post-déploiement

Les tests automatisés détectent les régressions avant qu'elles n'atteignent les utilisateurs. Les bugs qui passent sont des cas limites, pas des échecs évidents.

10 minTemps de déploiement (contre 2 h)

Du merge au live en quelques minutes. Pas de SSH, pas de commandes manuelles, pas de « laisse-moi vérifier quels modules ont changé ».

100 %Traçabilité des déploiements

Chaque déploiement est une exécution GitHub Actions : qui l'a déclenché, ce qui a changé, quels tests ont passé, et si le health check a réussi. Les auditeurs SOC 2 adorent ça.

Le ROI caché est la confiance des développeurs. Quand votre équipe sait que chaque push est testé contre une vraie instance Odoo avec une vraie base de données, elle livre plus vite. La peur du « est-ce que ça va casser la prod ? » est remplacée par « le CI me dira si c'est cassé avant que ça n'atteigne qui que ce soit ». Cette sécurité psychologique se traduit directement en vélocité.

NOTES SEO

Métadonnées d'optimisation

Meta Desc

Construisez un pipeline CI/CD complet pour Odoo 19 avec GitHub Actions. Tests automatisés contre un vrai PostgreSQL, déploiements sans interruption et stratégie de rollback.

Mots-clés H2

1. « Comment structurer un pipeline CI/CD GitHub Actions pour les modules custom Odoo 19 »
2. « Workflow GitHub Actions pour les tests automatisés des modules Odoo 19 »
3. « 3 erreurs CI/CD qui envoient silencieusement des modules Odoo cassés en production »

Arrêtez de déployer Odoo via SSH

Chaque ssh prod-server && cd /opt/odoo && git pull && ./restart.sh est un déploiement sans filet de sécurité. Pas de tests, pas de rollback, pas de traçabilité. Ça fonctionne jusqu'au jour où ça ne fonctionne plus — et quand ça casse, le coût se mesure en heures d'indisponibilité et en transactions perdues.

Si vous exécutez Odoo 19 avec des modules custom et déployez manuellement, nous pouvons vous aider. Nous mettons en place des pipelines CI/CD, configurons les environnements de test et implémentons des stratégies de déploiement sans interruption adaptées à votre infrastructure — que ce soit Docker, bare metal ou Odoo.sh. Le pipeline se rentabilise dès la première fois qu'il détecte une régression qui aurait atteint la production.

Réserver une évaluation DevOps gratuite