Robot Framework in CI/CD: From Local Tests to Pipeline Automation

Robot Framework in CI/CD: From Local Tests to Pipeline Automation

A test suite that only runs on developer laptops isn't a test suite — it's a suggestion. The value of Robot Framework tests materializes when they run automatically on every pull request, block deployments when they fail, and produce reports that everyone on the team can read. Getting there requires thoughtful CI/CD integration: the right GitHub Actions configuration, parallel execution to keep pipelines fast, smart test tagging for selective runs, and Docker containerization for reproducible environments.

This guide walks through each of these pieces with production-ready configurations you can adapt directly.

Project Structure for CI

A CI-friendly Robot Framework project has a clear, predictable layout that pipeline tools can navigate without custom configuration:

project-root/
  tests/
    smoke/
      smoke_tests.robot
    regression/
      auth/
        login_tests.robot
        session_tests.robot
      orders/
        checkout_tests.robot
        order_history_tests.robot
    api/
      orders_api_tests.robot
      users_api_tests.robot
  resources/
    common/
      navigation.resource
      assertions.resource
    pages/
      login_page.resource
      checkout_page.resource
    api/
      api_setup.resource
  libraries/
    CustomHelpers.py
    DatabaseHelper.py
  schemas/
    order_response.json
  requirements.txt
  robot.yaml          # pabot configuration
  Dockerfile.test
  .github/
    workflows/
      test.yaml

Keep a requirements.txt pinned to exact versions. Floating dependencies cause pipelines to break unpredictably on library upgrades:

robotframework==7.1.1
robotframework-seleniumlibrary==6.3.0
robotframework-restinstance==0.9.6
pabot==2.20.0
selenium==4.19.0
webdriver-manager==4.0.1

GitHub Actions: The Foundation Workflow

Start with a workflow that runs all tests on pull requests and pushes to main:

# .github/workflows/test.yaml
name: Robot Framework Tests

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

env:
  PYTHON_VERSION: '3.11'

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    timeout-minutes: 30

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set Up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          cache: pip

      - name: Install Dependencies
        run: pip install -r requirements.txt

      - name: Install Chrome
        uses: browser-actions/setup-chrome@v1

      - name: Install ChromeDriver
        uses: nanasess/setup-chromedriver@v2

      - name: Run Smoke Tests
        run: |
          robot \
            --include smoke \
            --variable HEADLESS:True \
            --variable BASE_URL:${{ vars.STAGING_URL }} \
            --outputdir results/smoke \
            --output smoke_output.xml \
            --log smoke_log.html \
            --report smoke_report.html \
            tests/

      - name: Run Full Test Suite
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: |
          python -m pabot \
            --processes 4 \
            --variable HEADLESS:True \
            --variable BASE_URL:${{ vars.STAGING_URL }} \
            --variable DB_HOST:localhost \
            --variable DB_PORT:5432 \
            --outputdir results/full \
            tests/

      - name: Upload Test Results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: robot-results-${{ github.run_number }}
          path: results/
          retention-days: 14

      - name: Publish Test Report
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: Robot Framework Results
          path: results/**/*.xml
          reporter: java-junit

The if: always() on artifact upload is non-negotiable — test results are most valuable when tests fail, and you don't want CI to hide the evidence.

The workflow splits into smoke tests (run on every PR, fast) and full regression (run only on main, can be slower). This keeps PR feedback loops short while ensuring comprehensive coverage before deployment.

Parallel Execution with Pabot

Pabot (Parallel Robot Framework Executor) is the standard solution for parallel test execution in Robot Framework. It distributes test suites across multiple processes and merges the results into a single combined report.

Install and configure:

pip install pabot

Basic parallel run:

python -m pabot --processes 4 --outputdir results tests/

Pabot Configuration File

For reproducible parallel execution, define split strategies in a robot.yaml file:

# robot.yaml
default_execution_context:
  processes: 4

suites:
  tests/regression/auth:
    processes: 2  # Auth tests must not run in parallel (session conflicts)
  tests/regression/orders:
    processes: 4
  tests/api:
    processes: 8  # API tests are stateless, max parallelism

variables:
  HEADLESS: "True"
  BROWSER: chrome

Managing State in Parallel Tests

The biggest challenge with parallel execution is test isolation — tests running concurrently must not share mutable state. Common pitfalls and solutions:

Database isolation: Each parallel process should use isolated data. Use unique prefixes or UUIDs in test data:

*** Keywords ***
Create Unique Test Customer
  ${uuid}=    Generate Random String    8    [LETTERS][NUMBERS]
  ${email}=    Set Variable    test-${uuid}@example.com
  POST    /api/customers    {"email": "${email}", "name": "Test User ${uuid}"}
  Integer    response status    201
  ${customer_id}=    Output    response body id
  [Return]    ${customer_id}    ${email}

Browser isolation: Each Pabot process has its own browser instance — no shared state by default. This is already correct.

File isolation: Test artifacts (screenshots, downloads) must use unique paths:

*** Keywords ***
Capture Named Screenshot
  ${timestamp}=    Get Current Date    result_format=%Y%m%d_%H%M%S_%f
  ${test_name}=    Get Variable Value    ${TEST NAME}
  Capture Page Screenshot    ${OUTPUT DIR}/${test_name}_${timestamp}.png

Test Tagging for Selective Runs

Robot Framework's tagging system is how you control which tests run in which pipeline stage. Think of tags as a filter language for your test suite.

Define tags at the test level:

*** Test Cases ***
User Can Log In
  [Tags]    smoke    auth    critical
  ...

Add To Cart Works For Guest User
  [Tags]    smoke    cart    regression
  ...

Order History Paginates Correctly
  [Tags]    regression    orders    non-critical
  ...

Payment Gateway Timeout Is Handled
  [Tags]    regression    payments    edge-case    slow
  ...

Run specific tag combinations:

# Smoke tests only (fast CI check)
robot --include smoke tests/

# All tests except slow ones
robot --exclude slow tests/

# Auth AND critical (boolean AND)
robot --include authANDcritical tests/

# Smoke OR critical (boolean OR)
robot --include smoke --include critical tests/

# Everything except non-critical and slow
robot --exclude non-critical --exclude slow tests/

Tagging Strategy for Pipeline Stages

A practical tagging taxonomy:

Tag Meaning When to Run
smoke Core happy paths, <5 min total Every PR, every deployment
regression Full coverage Before production deployment
critical Business-critical flows Included in smoke + regression
slow Takes >30s per test Nightly only
flaky Known intermittent failures Separate reporting
api API tests only, no browser Fast parallel CI job
ui Browser tests only Separate job with browser setup
destructive Mutates prod-like data Staging only, never prod

Multi-stage pipeline using tags:

jobs:
  smoke:
    name: Smoke Tests (PR Gate)
    runs-on: ubuntu-latest
    steps:
      - run: robot --include smoke --outputdir results/smoke tests/

  api-tests:
    name: API Tests
    runs-on: ubuntu-latest
    needs: smoke
    steps:
      - run: |
          python -m pabot --processes 8 --include api \
            --outputdir results/api tests/

  regression:
    name: Full Regression
    runs-on: ubuntu-latest
    needs: [smoke, api-tests]
    if: github.ref == 'refs/heads/main'
    steps:
      - run: |
          python -m pabot --processes 4 --exclude slow \
            --outputdir results/regression tests/

  nightly:
    name: Nightly Full Suite
    runs-on: ubuntu-latest
    schedule:
      - cron: '0 2 * * *'
    steps:
      - run: python -m pabot --processes 4 --outputdir results/nightly tests/

Docker Containerization

Docker eliminates "works on my machine" for test environments. A consistent container means the same Chrome version, Python version, and library versions everywhere.

# Dockerfile.test
FROM python:3.11-slim

# Install Chrome and its dependencies
RUN apt-get update && apt-get install -y \
  chromium \
  chromium-driver \
  fonts-liberation \
  libappindicator3-1 \
  libasound2 \
  libatk-bridge2.0-0 \
  libatk1.0-0 \
  libcups2 \
  libdbus-1-3 \
  libgdk-pixbuf2.0-0 \
  libnspr4 \
  libnss3 \
  libx11-xcb1 \
  libxcomposite1 \
  libxdamage1 \
  libxrandr2 \
  xdg-utils \
  --no-install-recommends \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /tests

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Use system-installed chromium, not webdriver-manager
ENV CHROME_BIN=/usr/bin/chromium
ENV CHROMEDRIVER_PATH=/usr/bin/chromedriver

# Default command: run all tests headless
CMD ["python", "-m", "pabot", \
  "--processes", "4", \
  "--variable", "HEADLESS:True", \
  "--outputdir", "/results", \
  "tests/"]

Build and run locally:

# Build test image
docker build -f Dockerfile.test -t my-robot-tests:latest .

# Run tests, mount results directory
docker run --rm \
  -v $(pwd)/results:/results \
  -e BASE_URL=https://staging.example.com \
  -e API_KEY=${API_KEY} \
  my-robot-tests:latest

# Run smoke tests only
docker run --rm \
  -v $(pwd)/results:/results \
  my-robot-tests:latest \
  python -m robot --include smoke --outputdir /results tests/

In GitHub Actions with Docker:

- name: Build Test Image
  run: docker build -f Dockerfile.test -t robot-tests:${{ github.sha }} .

- name: Run Tests in Container
  run: |
    docker run --rm \
      -v ${{ github.workspace }}/results:/results \
      -e BASE_URL=${{ vars.STAGING_URL }} \
      -e API_KEY=${{ secrets.API_KEY }} \
      robot-tests:${{ github.sha }}

Reports and Artifacts

Robot Framework generates three output files by default:

  • output.xml — machine-readable, used for Pabot result merging and custom reporting tools
  • log.html — detailed test log with expandable keyword trees, timing, and screenshots
  • report.html — high-level summary suitable for sharing with stakeholders

Merge parallel Pabot results into a single report:

python -m pabot --processes 4 --outputdir results tests/
rebot --outputdir results --output merged.xml results/pabot_results/*/output.xml

For CI visibility, parse the XML and fail the build with a useful message:

# scripts/check_results.py
import xml.etree.ElementTree as ET
import sys

tree = ET.parse('results/output.xml')
root = tree.getroot()
stats = root.find('.//statistics/total/stat[@name="All Tests"]')

passed = int(stats.get('pass', 0))
failed = int(stats.get('fail', 0))
total = passed + failed

print(f"Results: {passed}/{total} passed ({failed} failed)")

if failed > 0:
    print(f"FAILED: {failed} test(s) failed")
    sys.exit(1)

Deployment Gating

The final piece: use test results to gate deployments. Only promote to production after tests pass:

jobs:
  test:
    name: Run Tests
    # ... test job configuration

  deploy-staging:
    name: Deploy to Staging
    needs: test
    if: success()
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: ./scripts/deploy.sh staging

  smoke-staging:
    name: Smoke Test Staging
    needs: deploy-staging
    runs-on: ubuntu-latest
    steps:
      - run: |
          robot --include smoke \
            --variable BASE_URL:https://staging.example.com \
            --outputdir results/staging-smoke \
            tests/

  deploy-production:
    name: Deploy to Production
    needs: smoke-staging
    if: success() && github.ref == 'refs/heads/main'
    environment: production  # Requires manual approval in GitHub
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: ./scripts/deploy.sh production

HelpMeTest integrates with this pipeline pattern directly — its continuous monitoring tests run against production on a schedule and alert when real user flows break, complementing the pre-deployment gate with post-deployment verification. Together, CI gates and continuous monitoring cover the full deployment lifecycle.

Conclusion

Moving Robot Framework tests from developer laptops to a reliable CI/CD pipeline requires attention to four areas: a predictable project structure, smart tagging for selective execution, Pabot for parallel speed, and Docker for environmental consistency. Get these right and your test suite stops being an obstacle to deployment velocity and starts being the mechanism that makes confident deployments possible.

The configurations in this guide are production-tested starting points, not theoretical templates. Adapt the tag taxonomy to your release cadence, tune the parallelism to your suite's isolation characteristics, and pin your dependencies ruthlessly. The payoff is a pipeline where every pull request gets fast, reliable feedback and every production deployment is backed by evidence.

Read more

Start now free