Building Reusable GitLab CI Templates for OpenERP/Odoo Testing

Building Reusable GitLab CI Templates for OpenERP/Odoo Testing

Table of Contents

  1. Introduction
  2. Problem Statement
  3. Architecture Overview
  4. Step-by-Step Implementation
  5. Template Configuration
  6. Usage Examples
  7. Troubleshooting
  8. Best Practices
  9. Conclusion

Introduction

In this guide, we’ll explore how to create a reusable GitLab CI/CD pipeline template for testing OpenERP/Odoo modules. This approach allows teams to standardize their testing infrastructure while maintaining flexibility for project-specific requirements.

What You’ll Learn

Prerequisites


Problem Statement

The Challenge

When working with multiple OpenERP/Odoo projects, teams often face:

  1. Code Duplication: Each project maintains its own CI/CD configuration
  2. Inconsistent Testing: Different test setups across projects lead to reliability issues
  3. Maintenance Overhead: Updates must be replicated across all projects
  4. Access Management: Complex credential handling for private container registries
  5. Environment Setup: Repetitive database and application container configuration

The Solution

Create a centralized CI/CD template that:


Architecture Overview

Component Diagram

┌─────────────────────────────────────────────────────────────┐
│                    GitLab Instance                           │
│                                                              │
│  ┌────────────────────────────────────────────────────┐    │
│  │  kazacube-ci-tools Repository                       │    │
│  │                                                      │    │
│  │  ├── .gitlab-ci.yml (main template)                │    │
│  │  ├── docker/                                        │    │
│  │  │   ├── openprodtest-db/                          │    │
│  │  │   └── openprodtest-openprod/                    │    │
│  │  └── Container Registry                             │    │
│  │      ├── openprodtest-db:latest                     │    │
│  │      └── openprodtest-openprod:latest               │    │
│  └──────────────────────────────────────────────────────┘    │
│                         ▲                                    │
│                         │ include                            │
│  ┌──────────────────────┴─────────────────────────────┐    │
│  │  Project Repository (e.g., OPENPROD-EXTRA)         │    │
│  │                                                      │    │
│  │  ├── .gitlab-ci.yml (uses template)                │    │
│  │  ├── module1/                                       │    │
│  │  ├── module2/                                       │    │
│  │  └── moduleN/                                       │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  ┌────────────────────────────────────────────────────┐    │
│  │  GitLab Runner                                      │    │
│  │                                                      │    │
│  │  ├── Docker Executor                                │    │
│  │  └── Volume Mounts: /builds:/builds                │    │
│  └──────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

Workflow

  1. Project Trigger: Developer pushes code or manually triggers pipeline
  2. Template Loading: GitLab loads the shared template from kazacube-ci-tools
  3. Authentication: Deploy Token or CI credentials authenticate with registry
  4. Container Orchestration:
    • PostgreSQL container starts with restored database
    • OpenProd container mounts custom addons
  5. Test Execution: OpenERP runs tests with JUnit XML output
  6. Result Collection: Test reports are extracted and published

Step-by-Step Implementation

Phase 1: Prepare Docker Images

1.1 Database Image

Create a PostgreSQL image that automatically restores your OpenERP database:

# docker/openprodtest-db/Dockerfile
FROM postgres:12

ENV POSTGRES_USER=openprod
ENV POSTGRES_PASSWORD=openprod
ENV POSTGRES_DB=postgres

# Copy database dump
COPY dump.sql /docker-entrypoint-initdb.d/

# Initialization script
COPY init-db.sh /docker-entrypoint-initdb.d/

RUN chmod +x /docker-entrypoint-initdb.d/init-db.sh

init-db.sh:

#!/bin/bash
set -e

# Wait for PostgreSQL to be ready
until pg_isready -U openprod; do
  echo "Waiting for PostgreSQL..."
  sleep 2
done

# Restore database if not exists
if ! psql -U openprod -lqt | cut -d \| -f 1 | grep -qw $DB_NAME; then
  echo "Creating database $DB_NAME..."
  createdb -U openprod $DB_NAME
  echo "Restoring database from dump..."
  psql -U openprod -d $DB_NAME < /docker-entrypoint-initdb.d/dump.sql
  echo "Database restored successfully!"
fi

1.2 OpenProd Application Image

# docker/openprodtest-openprod/Dockerfile
FROM ubuntu:18.04

# Install Python 2.7 and dependencies
RUN apt-get update && apt-get install -y \
    python2.7 \
    python-pip \
    postgresql-client \
    git \
    && rm -rf /var/lib/apt/lists/*

# Create virtualenv
RUN pip install virtualenv
RUN virtualenv -p python2.7 /opt/openprod/server/venvs/18_04

# Copy OpenProd source
COPY openprod/ /opt/openprod/

# Install Python dependencies
RUN /opt/openprod/server/venvs/18_04/bin/pip install -r /opt/openprod/requirements.txt

WORKDIR /opt/openprod

# Set entrypoint
CMD ["/bin/bash"]

Build and push images:

# Build database image
cd docker/openprodtest-db
docker build -t gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools/openprodtest-db:latest .
docker push gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools/openprodtest-db:latest

# Build application image
cd ../openprodtest-openprod
docker build -t gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools/openprodtest-openprod:latest .
docker push gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools/openprodtest-openprod:latest

Phase 2: Create the CI Template

2.1 Template Structure

Create .gitlab-ci.yml in your kazacube-ci-tools repository:

# .gitlab-ci.yml
stages:
  - build
  - test

variables:
  # Docker configuration
  DOCKER_DRIVER: overlay2
  REGISTRY: gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools
  
  # Image references
  IMAGE_DB: $REGISTRY/openprodtest-db
  IMAGE_APP: $REGISTRY/openprodtest-openprod
  
  # Database configuration
  DB_NAME: openprod_restored
  DB_INIT_SLEEP: "60"
  
  # OpenERP paths
  PYTHON_PATH: /opt/openprod/server/venvs/18_04/bin/python2.7
  OPENERP_SERVER_PATH: /opt/openprod/server/openerp-server
  ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons
  
  # Optional custom addons
  CUSTOM_ADDONS_MOUNT: ""
  CUSTOM_ADDONS_PATH: ""
  
  # OpenERP options
  MODULES_TO_INSTALL: ""
  MODULES_TO_UPDATE: ""
  OPENERP_EXTRA_OPTIONS: ""

.default_docker:
  tags:
    - docker
    - local
  image: docker:26
  before_script:
    - docker info
    # Smart authentication: use custom credentials or fallback to CI defaults
    - |
      if [ -n "$DOCKER_REGISTRY_USER" ] && [ -n "$DOCKER_REGISTRY_TOKEN" ]; then
        echo "Using custom Docker registry credentials"
        echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin $REGISTRY
      else
        echo "Using default CI credentials"
        echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $REGISTRY
      fi

test-db-and-unit:
  stage: test
  extends: .default_docker
  script:
    # Pre-cleanup
    - echo "Pre-cleanup: removing any existing containers and network..."
    - docker rm -f db-test openprod3-test 2>/dev/null || true
    - docker network rm openprod3-net 2>/dev/null || true
    
    # Pull images
    - echo "Pulling images..."
    - docker pull $IMAGE_APP:latest
    - docker pull $IMAGE_DB:latest
    
    # Create network
    - echo "Creating network..."
    - docker network create openprod3-net
    
    # Start PostgreSQL
    - echo "Starting PostgreSQL..."
    - >
      docker run -d --name db-test
      --network openprod3-net
      -e POSTGRES_USER=openprod
      -e POSTGRES_PASSWORD=openprod
      -e POSTGRES_DB=postgres
      -e DB_NAME=$DB_NAME
      --tmpfs /var/lib/postgresql/data
      $IMAGE_DB:latest
    
    # Wait for database
    - echo "Waiting for database to initialize ($DB_INIT_SLEEP seconds)..."
    - sleep $DB_INIT_SLEEP
    - docker exec db-test pg_isready -U openprod || sleep 10
    - docker exec db-test psql -U openprod -d postgres -c "\l"
    
    # Prepare test results directory
    - mkdir -p test-results
    
    # Build OpenERP command with dynamic options
    - |
      OPENERP_CMD="$PYTHON_PATH $OPENERP_SERVER_PATH \
        -d $DB_NAME \
        --db_host=db-test \
        --db_port=5432 \
        --db_user=openprod \
        --db_password=openprod \
        --addons-path=$ADDONS_PATH"
      
      # Add modules to install
      if [ -n "$MODULES_TO_INSTALL" ]; then
        echo "Installing modules: $MODULES_TO_INSTALL"
        OPENERP_CMD="$OPENERP_CMD -i $MODULES_TO_INSTALL"
      fi
      
      # Add modules to update
      if [ -n "$MODULES_TO_UPDATE" ]; then
        echo "Updating modules: $MODULES_TO_UPDATE"
        OPENERP_CMD="$OPENERP_CMD -u $MODULES_TO_UPDATE"
      fi
      
      # Add extra options
      if [ -n "$OPENERP_EXTRA_OPTIONS" ]; then
        echo "Extra options: $OPENERP_EXTRA_OPTIONS"
        OPENERP_CMD="$OPENERP_CMD $OPENERP_EXTRA_OPTIONS"
      fi
      
      # Add test options (always present)
      OPENERP_CMD="$OPENERP_CMD \
        --test-enable \
        --stop-after-init \
        --log-level=test \
        --test-junitxml \
        --test-report-directory=/tmp/test-reports"
    
    # Build Docker run command
    - |
      DOCKER_RUN_CMD="docker run --name openprod3-test \
        --network openprod3-net \
        -e DB_HOST=db-test \
        -e DB_PORT=5432 \
        -e DB_USER=openprod \
        -e DB_PASSWORD=openprod \
        -e DB_NAME=$DB_NAME"
      
      # Add volume mount if custom addons defined
      if [ -n "$CUSTOM_ADDONS_MOUNT" ] && [ -n "$CUSTOM_ADDONS_PATH" ]; then
        echo "Mounting custom addons from $CUSTOM_ADDONS_MOUNT to $CUSTOM_ADDONS_PATH"
        DOCKER_RUN_CMD="$DOCKER_RUN_CMD -v $CUSTOM_ADDONS_MOUNT:$CUSTOM_ADDONS_PATH:rw"
      fi
      
      # Finalize command
      DOCKER_RUN_CMD="$DOCKER_RUN_CMD $IMAGE_APP:latest $OPENERP_CMD"
      
      echo "Executing: $DOCKER_RUN_CMD"
      eval $DOCKER_RUN_CMD || true
    
    # Collect results
    - echo "Collecting test logs..."
    - docker logs openprod3-test 2>&1 | tee tests_output.log
    
    - echo "Copying XML files from container..."
    - docker cp openprod3-test:/tmp/test-reports/. test-results/ || echo "Failed to copy test results"
    
    - echo "Checking generated XML files..."
    - ls -lah test-results/
    - find test-results/ -name "*.xml" -ls || echo "No XML files found"
    
    - echo "==== Test Summary ===="
    - grep -E "Ran.*tests" tests_output.log | tail -5 || echo "No test summary found"
  
  after_script:
    - echo "Cleaning up..."
    - docker rm -f db-test openprod3-test 2>/dev/null || true
    - docker network rm openprod3-net 2>/dev/null || true
  
  artifacts:
    paths:
      - tests_output.log
      - test-results/
    reports:
      junit: test-results/*.xml
    when: always
    expire_in: 1 week
  
  when: manual
  timeout: 30m

Phase 3: Configure GitLab Runner

3.1 Runner Configuration

Edit /etc/gitlab-runner/config.toml:

concurrent = 4

[[runners]]
  name = "docker-runner"
  url = "https://gitlab.example.com/"
  token = "YOUR_RUNNER_TOKEN"
  executor = "docker"
  
  [runners.docker]
    tls_verify = false
    image = "docker:26"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    
    # CRITICAL: Volume mounts for Docker-in-Docker
    volumes = [
      "/var/run/docker.sock:/var/run/docker.sock",  # Docker socket
      "/cache",                                       # GitLab cache
      "/builds:/builds"                               # Source code access
    ]
    
    shm_size = 0
  
  [runners.cache]
    MaxUploadedArchiveSize = 0

Why /builds:/builds is critical:

Without this mount, the Docker daemon cannot access files in /builds/... when you use volume mounts like -v $CI_PROJECT_DIR:/workspace.

Restart the runner:

sudo gitlab-runner restart
sudo gitlab-runner verify

Phase 4: Set Up Access Control

4.1 Create Deploy Token

In your kazacube-ci-tools repository:

  1. Go to Settings → Repository → Deploy tokens
  2. Create a new token:
    • Name: openprod-registry-access
    • Scopes: ✅ read_registry
    • Expires at: (optional)
  3. Copy the generated username and token (shown only once!)

Example output:

Username: gitlab+deploy-token-12345
Token: gldt-aBcDeFgHiJkLmNoPqRsTuVwXyZ

4.2 Configure Variables in Target Project

In your project that uses the template (e.g., OPENPROD-EXTRA):

  1. Go to Settings → CI/CD → Variables

  2. Add variable DOCKER_REGISTRY_USER:

    • Value: gitlab+deploy-token-12345
    • Type: Variable
    • Flags:
      • ☐ Protect variable
      • ☐ Mask variable ← IMPORTANT: UNCHECKED
      • ☑ Expand variable reference
  3. Add variable DOCKER_REGISTRY_TOKEN:

    • Value: gldt-aBcDeFgHiJkLmNoPqRsTuVwXyZ
    • Type: Variable
    • Flags:
      • ☐ Protect variable
      • ☑ Mask variable ← Checked for security
      • ☑ Expand variable reference

Why uncheck “Mask” for username?

GitLab replaces masked values with [MASKED] in command output. If the username is masked, Docker receives an empty string, causing authentication to fail.


Template Configuration

Available Variables

VariableDescriptionDefaultExample
REGISTRYContainer registry URLgl.kazacube.fr:5050/...Your registry URL
IMAGE_DBPostgreSQL image$REGISTRY/openprodtest-dbCustom DB image
IMAGE_APPOpenProd image$REGISTRY/openprodtest-openprodCustom app image
DB_NAMEDatabase nameopenprod_restoredmy_test_db
DB_INIT_SLEEPDB init wait time (seconds)6090
PYTHON_PATHPython interpreter path/opt/openprod/.../python2.7Custom Python
OPENERP_SERVER_PATHOpenERP server script/opt/openprod/.../openerp-serverCustom path
ADDONS_PATHBase addons pathpath1,path2,path3Comma-separated
CUSTOM_ADDONS_MOUNTHost path to mount""$CI_PROJECT_DIR
CUSTOM_ADDONS_PATHContainer mount point""/workspace/custom
MODULES_TO_INSTALLModules to install""module1,module2
MODULES_TO_UPDATEModules to update""module1,module2
OPENERP_EXTRA_OPTIONSAdditional OpenERP flags""--update-at-init

Usage Examples

Example 1: Basic Usage (Default Tests)

In your project repository, create .gitlab-ci.yml:

# .gitlab-ci.yml
include:
  - project: 'kazacube/kazacube-interne/kazacube-ci-tools'
    file: '.gitlab-ci.yml'
    ref: main

# The job 'test-db-and-unit' is now available
# It will run with default configuration

Example 2: Testing Custom Modules

include:
  - project: 'kazacube/kazacube-interne/kazacube-ci-tools'
    file: '.gitlab-ci.yml'
    ref: main

test-db-and-unit:
  variables:
    # Mount project directory as custom addons
    CUSTOM_ADDONS_MOUNT: $CI_PROJECT_DIR
    CUSTOM_ADDONS_PATH: /workspace/custom-addons
    # Add to addons path
    ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons
  before_script:
    - docker info
    - echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin gl.kazacube.fr:5050

Example 3: Installing Specific Modules

include:
  - project: 'kazacube/kazacube-interne/kazacube-ci-tools'
    file: '.gitlab-ci.yml'
    ref: main

test-db-and-unit:
  variables:
    CUSTOM_ADDONS_MOUNT: $CI_PROJECT_DIR
    CUSTOM_ADDONS_PATH: /workspace/custom-addons
    ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons
    # Install specific module
    MODULES_TO_INSTALL: my_custom_module
    OPENERP_EXTRA_OPTIONS: "--update-at-init"
  before_script:
    - docker info
    - echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin gl.kazacube.fr:5050

Example 4: Multiple Test Jobs

include:
  - project: 'kazacube/kazacube-interne/kazacube-ci-tools'
    file: '.gitlab-ci.yml'
    ref: main

# Override with custom credentials
.openprod_base:
  before_script:
    - docker info
    - echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin gl.kazacube.fr:5050

# Test all modules (default)
test-all-modules:
  extends: 
    - test-db-and-unit
    - .openprod_base
  variables:
    CUSTOM_ADDONS_MOUNT: $CI_PROJECT_DIR
    CUSTOM_ADDONS_PATH: /workspace/custom-addons
    ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons

# Test specific module
test-module-a:
  extends: 
    - test-db-and-unit
    - .openprod_base
  variables:
    CUSTOM_ADDONS_MOUNT: $CI_PROJECT_DIR
    CUSTOM_ADDONS_PATH: /workspace/custom-addons
    ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons
    MODULES_TO_INSTALL: module_a
    OPENERP_EXTRA_OPTIONS: "--update-at-init"
  when: manual

# Test with different database
test-with-custom-db:
  extends: 
    - test-db-and-unit
    - .openprod_base
  variables:
    DB_NAME: custom_test_db
    DB_INIT_SLEEP: "90"

Example 5: Advanced Configuration

include:
  - project: 'kazacube/kazacube-interne/kazacube-ci-tools'
    file: '.gitlab-ci.yml'
    ref: v1.0.0  # Use specific version tag

stages:
  - test
  - deploy

test-db-and-unit:
  stage: test
  variables:
    CUSTOM_ADDONS_MOUNT: $CI_PROJECT_DIR
    CUSTOM_ADDONS_PATH: /workspace/custom-addons
    ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons
    MODULES_TO_INSTALL: module1,module2,module3
    MODULES_TO_UPDATE: base,stock
    OPENERP_EXTRA_OPTIONS: "--update-at-init --without-demo=all --load-language=fr_FR"
    DB_INIT_SLEEP: "120"
  before_script:
    - docker info
    - echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin gl.kazacube.fr:5050
  only:
    - merge_requests
    - main

deploy-production:
  stage: deploy
  script:
    - echo "Deploying to production..."
  only:
    - tags
  when: manual

Troubleshooting

Issue 1: “Must provide —username with —password-stdin”

Symptom:

$ echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin
Must provide --username with --password-stdin

Cause: The DOCKER_REGISTRY_USER variable is empty or masked.

Solution:

  1. Check that the variable is defined in Settings → CI/CD → Variables
  2. Uncheck “Mask variable” for DOCKER_REGISTRY_USER
  3. Keep “Mask variable” checked only for DOCKER_REGISTRY_TOKEN

Issue 2: “pull access denied”

Symptom:

Error response from daemon: pull access denied for registry/image, 
repository does not exist or may require 'docker login': denied

Cause: Insufficient permissions to access the container registry.

Solutions:

A. Verify Deploy Token:

B. Test authentication manually:

echo "YOUR_TOKEN" | docker login -u "YOUR_USERNAME" --password-stdin registry.example.com
docker pull registry.example.com/image:latest

C. Check registry visibility:


Issue 3: “The addons-path ‘/path’ does not seem to be a valid Addons Directory”

Symptom:

openerp-server: error: option --addons-path: 
The addons-path '/workspace/custom-addons' does not seem to a be a valid Addons Directory!

Causes:

  1. Volume mount not working (GitLab Runner misconfiguration)
  2. Directory doesn’t contain valid OpenERP modules
  3. Permission issues

Solutions:

A. Verify GitLab Runner volumes:

Check /etc/gitlab-runner/config.toml:

volumes = [
  "/var/run/docker.sock:/var/run/docker.sock",
  "/cache",
  "/builds:/builds"  # ← This is CRITICAL
]

Restart runner after changes:

sudo gitlab-runner restart

B. Debug volume mount:

Add to your .gitlab-ci.yml:

script:
  # Before running tests
  - echo "Checking project structure..."
  - ls -la $CI_PROJECT_DIR
  - find $CI_PROJECT_DIR -name "__openerp__.py" -o -name "__manifest__.py"
  
  # After container starts
  - docker exec openprod3-test ls -la /workspace/custom-addons
  - docker exec openprod3-test find /workspace/custom-addons -name "*.py" | head -10

C. Verify module structure:

Ensure your modules have this structure:

project/
├── module1/
│   ├── __init__.py
│   ├── __openerp__.py  (or __manifest__.py)
│   └── ...
├── module2/
│   ├── __init__.py
│   ├── __openerp__.py
│   └── ...

D. Try read-write mount:

Change :ro to :rw in volume mount:

-v $CI_PROJECT_DIR:/workspace/custom-addons:rw

Issue 4: No JUnit XML files generated

Symptom:

WARNING: test-results/*.xml: no matching files
ERROR: No files to upload

Causes:

  1. OpenERP version doesn’t support --test-junitxml
  2. Tests failed before XML generation
  3. Wrong output directory

Solutions:

A. Check OpenERP version:

OpenERP 9 and earlier may not support JUnit XML natively.

B. Verify test execution:

script:
  # After docker run
  - docker logs openprod3-test 2>&1 | tee tests_output.log
  - grep -i "xml" tests_output.log || echo "No XML mentioned in logs"

C. Manual XML generation for older versions:

Create a Python script to parse logs and generate JUnit XML:

# convert_to_junit.py
import re
import sys
from xml.etree.ElementTree import Element, SubElement, tostring
from xml.dom import minidom

def parse_test_log(log_file):
    with open(log_file, 'r') as f:
        content = f.read()
    
    # Parse test results
    test_pattern = r'(\d+) test\(s\).*?(\d+) failure\(s\)'
    matches = re.search(test_pattern, content)
    
    if not matches:
        return None
    
    total_tests = int(matches.group(1))
    failures = int(matches.group(2))
    
    # Create JUnit XML
    testsuites = Element('testsuites', tests=str(total_tests), failures=str(failures))
    testsuite = SubElement(testsuites, 'testsuite', name='OpenProd', tests=str(total_tests), failures=str(failures))
    
    # Add placeholder test case
    testcase = SubElement(testsuite, 'testcase', name='all_tests', classname='openprod')
    if failures > 0:
        failure = SubElement(testcase, 'failure', message=f'{failures} test(s) failed')
    
    # Pretty print
    xml_str = minidom.parseString(tostring(testsuites)).toprettyxml(indent="  ")
    return xml_str

if __name__ == '__main__':
    xml = parse_test_log('tests_output.log')
    if xml:
        with open('test-results/TEST-openprod.xml', 'w') as f:
            f.write(xml)
        print("JUnit XML generated successfully")
    else:
        print("No test results found in logs")

Add to pipeline:

script:
  # ... after collecting logs
  - python3 convert_to_junit.py
  - ls -la test-results/

Issue 5: Container exits immediately

Symptom:

Running as user 'root' is a security risk.
Usage: openerp-server [options]
(Container exits)

Cause: OpenERP command has syntax errors or missing parameters.

Solution:

Add debug output:

script:
  # Before eval
  - echo "Full command:"
  - echo "$DOCKER_RUN_CMD"
  - echo "---"
  
  # Run with verbose error handling
  - eval $DOCKER_RUN_CMD || echo "Exit code: $?"
  
  # Check if container started
  - docker ps -a | grep openprod3-test

Issue 6: Database connection refused

Symptom:

psycopg2.OperationalError: could not connect to server: Connection refused
Is the server running on host "db-test" (172.23.0.2) and accepting
TCP/IP connections on port 5432?

Causes:

  1. Database not ready yet
  2. Wrong network configuration
  3. Database failed to start

Solutions:

A. Increase wait time:

variables:
  DB_INIT_SLEEP: "90"  # Increase from 60

B. Add robust wait loop:

script:
  # Replace simple sleep with retry loop
  - |
    echo "Waiting for database..."
    for i in {1..60}; do
      if docker exec db-test pg_isready -U openprod -d $DB_NAME > /dev/null 2>&1; then
        echo "Database is ready!"
        break
      fi
      echo "Still waiting... ($i/60)"
      sleep 2
    done

C. Check database logs:

script:
  # After database start
  - echo "Database logs:"
  - docker logs db-test

Best Practices

1. Version Control for Templates

Use Git tags for template versions:

# In kazacube-ci-tools
git tag v1.0.0 -m "Stable release - initial template"
git push origin v1.0.0

In projects, reference specific versions:

include:
  - project: 'kazacube/kazacube-interne/kazacube-ci-tools'
    file: '.gitlab-ci.yml'
    ref: v1.0.0  # Pinned version

Benefits:


2. Separate Concerns

Template responsibilities:

Project responsibilities:


3. Use Semantic Versioning

v1.0.0 - Initial stable release
v1.1.0 - Added custom addons support
v1.2.0 - Added module installation options
v2.0.0 - Breaking change: new Docker images

4. Document Variables

Create a README.md in your template repository:

# OpenProd CI Template

## Available Variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `DOCKER_REGISTRY_USER` | ✅ Yes* | - | Deploy token username |
| `DOCKER_REGISTRY_TOKEN` | ✅ Yes* | - | Deploy token password |
| `CUSTOM_ADDONS_MOUNT` | ❌ No | "" | Host path for custom addons |
| `MODULES_TO_INSTALL` | ❌ No | "" | Comma-separated module names |

*Required only in external projects

## Usage

See [USAGE.md](USAGE.md) for detailed examples.

5. Implement Caching

Speed up pipelines with Docker layer caching:

.default_docker:
  before_script:
    - docker info
    # Enable BuildKit for better caching
    - export DOCKER_BUILDKIT=1
    - |
      if [ -n "$DOCKER_REGISTRY_USER" ]; then
        echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin $REGISTRY
      else
        echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $REGISTRY
      fi
  cache:
    key: docker-cache
    paths:
      - .docker-cache/

6. Set Up Notifications

Add Slack/email notifications for test failures:

test-db-and-unit:
  # ... existing configuration
  after_script:
    - |
      if [ "$CI_JOB_STATUS" == "failed" ]; then
        curl -X POST -H 'Content-type: application/json' \
          --data "{\"text\":\"Test failed in $CI_PROJECT_NAME: $CI_PIPELINE_URL\"}" \
          $SLACK_WEBHOOK_URL
      fi
    - docker rm -f db-test openprod3-test 2>/dev/null || true
    - docker network rm openprod3-net 2>/dev/null || true

7. Implement Resource Limits

Prevent runaway containers:

script:
  - |
    docker run --name openprod3-test \
      --memory="2g" \
      --memory-swap="2g" \
      --cpus="2.0" \
      --network openprod3-net \
      # ... rest of configuration

8. Create Health Checks

Verify containers are healthy before running tests:

script:
  - |
    docker run -d --name db-test \
      --health-cmd "pg_isready -U openprod -d $DB_NAME" \
      --health-interval=5s \
      --health-timeout=3s \
      --health-retries=10 \
      --network openprod3-net \
      # ... rest of configuration
  
  # Wait for healthy status
  - |
    until [ "$(docker inspect --format='{{.State.Health.Status}}' db-test)" == "healthy" ]; do
      echo "Waiting for database to be healthy..."
      sleep 2
    done

9. Implement Retry Logic

Handle transient failures:

test-db-and-unit:
  retry:
    max: 2
    when:
      - runner_system_failure
      - stuck_or_timeout_failure

10. Use Matrix Testing

Test multiple configurations:

test-db-and-unit:
  parallel:
    matrix:
      - MODULE: [module1, module2, module3]
  variables:
    MODULES_TO_INSTALL: $MODULE

Conclusion

What We’ve Built

We’ve created a robust, reusable CI/CD pipeline template that:

Eliminates duplication across multiple projects
Standardizes testing with consistent environments
Simplifies maintenance through centralized configuration
Provides flexibility via configurable variables
Ensures security with proper credential management
Generates reports in standard JUnit XML format

Key Takeaways

  1. Docker-in-Docker requires careful runner configuration, especially the /builds:/builds volume mount
  2. Deploy Tokens provide secure, scoped access to container registries
  3. Variable masking should be used selectively - usernames can remain visible, but tokens must be masked
  4. Template versioning prevents breaking changes and enables gradual rollouts
  5. Comprehensive error handling makes debugging much easier

Next Steps

Immediate improvements:

Long-term enhancements:

Resources

Official Documentation:

OpenERP/Odoo:

Final Thoughts

Building reusable CI/CD infrastructure requires initial investment but pays dividends through:

The template approach scales excellently as your organization grows, and the techniques demonstrated here apply beyond OpenERP/Odoo to any containerized application testing.


Appendix

A. Complete Example Repository Structure

kazacube-ci-tools/
├── .gitlab-ci.yml                    # Main template
├── README.md                         # Documentation
├── USAGE.md                          # Usage examples
├── CHANGELOG.md                      # Version history
├── docker/
│   ├── openprodtest-db/
│   │   ├── Dockerfile
│   │   ├── init-db.sh
│   │   └── dump.sql
│   └── openprodtest-openprod/
│       ├── Dockerfile
│       └── requirements.txt
└── scripts/
    ├── convert_to_junit.py           # Fallback for old OpenERP
    └── test-local.sh                 # Local testing script

B. Local Testing Script

Save as scripts/test-local.sh:

#!/bin/bash
# Local testing script for development

set -e

# Configuration
REGISTRY="gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools"
DB_NAME="openprod_restored"
IMAGE_DB="$REGISTRY/openprodtest-db"
IMAGE_APP="$REGISTRY/openprodtest-openprod"

# Load credentials
if [ -f .env.local ]; then
    source .env.local
else
    echo "Create .env.local with:"
    echo "export DOCKER_REGISTRY_USER='gitlab+deploy-token-xxxxx'"
    echo "export DOCKER_REGISTRY_TOKEN='gldt-xxxxx'"
    exit 1
fi

# Test authentication
echo "Testing Docker login..."
echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin gl.kazacube.fr:5050

if [ $? -eq 0 ]; then
    echo "✓ Authentication successful"
else
    echo "✗ Authentication failed"
    exit 1
fi

# Pull images
echo "Pulling images..."
docker pull $IMAGE_APP:latest
docker pull $IMAGE_DB:latest

# Cleanup
docker rm -f db-test openprod3-test 2>/dev/null || true
docker network rm openprod3-net 2>/dev/null || true

# Create network
docker network create openprod3-net

# Start database
docker run -d --name db-test \
  --network openprod3-net \
  -e POSTGRES_USER=openprod \
  -e POSTGRES_PASSWORD=openprod \
  -e POSTGRES_DB=postgres \
  -e DB_NAME=$DB_NAME \
  --tmpfs /var/lib/postgresql/data \
  $IMAGE_DB:latest

# Wait for database
echo "Waiting for database..."
sleep 60

# Run tests
mkdir -p test-results

docker run --name openprod3-test \
  --network openprod3-net \
  -e DB_HOST=db-test \
  -e DB_PORT=5432 \
  -e DB_USER=openprod \
  -e DB_PASSWORD=openprod \
  -e DB_NAME=$DB_NAME \
  -v $(pwd):/workspace/custom-addons:rw \
  $IMAGE_APP:latest \
  /opt/openprod/server/venvs/18_04/bin/python2.7 \
  /opt/openprod/server/openerp-server \
  -d $DB_NAME \
  --db_host=db-test \
  --db_port=5432 \
  --db_user=openprod \
  --db_password=openprod \
  --addons-path=/opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons \
  --test-enable \
  --stop-after-init \
  --log-level=test \
  --test-junitxml \
  --test-report-directory=/tmp/test-reports || true

# Collect results
docker logs openprod3-test 2>&1 | tee tests_output.log
docker cp openprod3-test:/tmp/test-reports/. test-results/ || true

# Display results
echo "=== Test Results ==="
ls -lh test-results/
grep -E "Ran.*tests" tests_output.log | tail -5 || echo "No summary found"

# Cleanup
docker rm -f db-test openprod3-test
docker network rm openprod3-net

echo "Done!"

C. Changelog Template

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.2.0] - 2024-01-15

### Added
- Support for custom module installation via `MODULES_TO_INSTALL`
- Support for module updates via `MODULES_TO_UPDATE`
- Extra OpenERP options via `OPENERP_EXTRA_OPTIONS`

### Changed
- Improved error messages in authentication
- Increased default DB_INIT_SLEEP to 60 seconds

### Fixed
- Volume mount permissions issue with custom addons

## [1.1.0] - 2024-01-01

### Added
- Custom addons mounting support
- Flexible authentication (Deploy Token or CI credentials)

## [1.0.0] - 2023-12-15

### Added
- Initial stable release
- Basic OpenERP testing with PostgreSQL
- JUnit XML report generation
- Docker-based execution

Author: [Your Name]
Date: November 2024
License: MIT
Repository: github.com/yourorg/openprod-ci-template


This guide is part of a series on building robust CI/CD pipelines for legacy ERP systems. Stay tuned for more articles on advanced deployment strategies and test automation!

aria

© 2025 Aria

Instagram 𝕏 GitHub