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.yamlKeep 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.1GitHub 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-junitThe 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 pabotBasic 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: chromeManaging 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}.pngTest 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 toolslog.html— detailed test log with expandable keyword trees, timing, and screenshotsreport.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.xmlFor 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 productionHelpMeTest 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.