CI/CD Integration with GitHub Actions, GitLab CI, and Jenkins: Guided Hands-On Workflow
Build a disposable CI contract locally, then map the same command to GitHub Actions, GitLab CI/CD, and Jenkins while preserving failure status, evidence, version provenance, and a fake secret boundary.
Learning objectives
- Create a small synthetic Robot suite whose pass/fail outcome can be controlled without touching a real system.
- Implement a cross-platform Python runner that writes a version manifest, invokes Robot, and exits with Robot's real return code.
- Prove that a deliberately failing run still generates native Robot evidence and xUnit output.
- Map the same command to current GitHub Actions, GitLab CI/CD, and Jenkins job skeletons.
- Inject a fake secret without printing or persisting the secret value.
Everything in this workflow is repository-local and synthetic. The only external actions shown are optional CI-provider wrappers. The mandatory learning path runs on a local Python environment and does not require a hosted runner, production endpoint, paid plan, container runtime, browser, database, SSH host, or real credential.
1. Create the disposable repository layout
Start in an empty training directory or a disposable branch. Keep source, runner logic, and generated evidence separate.
rf25-ci-lab/
├── requirements-ci.txt
├── suites/
│ └── ci_contract.robot
├── tools/
│ └── ci_run.py
└── artifacts/
└── robot/ # generated, not committed
The suite contains no external integration. The runner owns only the process invocation and evidence directory. CI providers later own checkout/runtime/secret/artifact orchestration.
2. Preflight and pinned dependencies
# requirements-ci.txt
robotframework==7.4.2
python -m venv .venv
# Bash/zsh
source .venv/bin/activate
# Windows PowerShell
# .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install --requirement requirements-ci.txt
python --version
python -m robot --version
For this lab, Python 3.13 is a practical CI example version, but Robot Framework 7.4.2 supports older Python versions too. The key CI property is not “always newest”; it is that the version is explicit and recorded.
3. Create a deterministic synthetic suite
*** Settings ***
Documentation Synthetic CI contract. No network or production state.
*** Variables ***
${EXPECTED_GATE} PASS
*** Test Cases ***
Portable Contract Is Healthy
${payload}= Set Variable build-42
Should Be Equal ${payload} build-42
Log To Console portable-check=ok
Release Gate Matches Expected State
${actual}= Set Variable PASS
Should Be Equal ${actual} ${EXPECTED_GATE}
The first test proves the suite itself can run. The second creates a
controlled failure switch: the wrapper can set
EXPECTED_GATE=FAIL for a negative-path CI exercise.
There is no random input, wall-clock dependency, network call, or
shared mutable state.
4. Make one provider-neutral runner
from __future__ import annotations
import os
import platform
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ARTIFACT_DIR = ROOT / "artifacts" / "robot"
SUITES = ROOT / "suites"
def write_manifest() -> None:
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
secret_present = bool(os.environ.get("RF_FAKE_TOKEN"))
lines = [
f"python={platform.python_version()}",
f"python_executable={sys.executable}",
f"platform={platform.platform()}",
f"cwd={ROOT}",
f"fake_secret_present={str(secret_present).lower()}",
]
(ARTIFACT_DIR / "runtime-manifest.txt").write_text(
"\n".join(lines) + "\n", encoding="utf-8"
)
def main() -> int:
write_manifest()
expected = os.environ.get("RF_EXPECTED_GATE", "PASS")
command = [
sys.executable, "-m", "robot",
"--outputdir", str(ARTIFACT_DIR),
"--output", "output.xml",
"--log", "log.html",
"--report", "report.html",
"--xunit", "xunit.xml",
"--variable", f"EXPECTED_GATE:{expected}",
str(SUITES),
]
print("Running provider-neutral Robot contract")
completed = subprocess.run(command, cwd=ROOT)
(ARTIFACT_DIR / "exit-code.txt").write_text(
f"{completed.returncode}\n", encoding="utf-8"
)
return completed.returncode
if __name__ == "__main__":
raise SystemExit(main())
The script deliberately does not print
RF_FAKE_TOKEN. It records only whether the variable was
present. The Robot command does not receive the token because the
synthetic suite does not need it. This is the correct security
instinct: inject the least data required by the test contract.
The wrapper exits with the exact Robot return code. It does not use
check=False as an excuse to ignore failure;
subprocess.run is used so the wrapper can write
exit-code.txt and then propagate the code.
5. Run PASS and intentional FAIL locally
# Bash/zsh — PASS
export RF_FAKE_TOKEN='fake-training-token'
unset RF_EXPECTED_GATE
python tools/ci_run.py
printf 'exit=%s\n' "$?"
# Intentional FAIL. This should return 1, not 0.
export RF_EXPECTED_GATE='FAIL'
python tools/ci_run.py
rc=$?
printf 'exit=%s\n' "$rc"
find artifacts/robot -maxdepth 1 -type f -print | sort
# Windows PowerShell equivalent:
# $env:RF_FAKE_TOKEN = 'fake-training-token'
# $env:RF_EXPECTED_GATE = 'FAIL'
# python . ools\ci_run.py
# $rc = $LASTEXITCODE
# Write-Host "exit=$rc"
# Get-ChildItem .rtifacts
obot
Expected failing-run evidence: exit=1, plus
output.xml, log.html,
report.html, xunit.xml,
runtime-manifest.txt, and exit-code.txt.
The negative run is successful as a
diagnostic exercise only because the process is correctly
non-zero.
6. GitHub Actions mapping — current major actions
The provider job calls the same Python script. The artifact step has
if: always() so a Robot failure does not skip evidence
upload.
name: robot-ci
on:
push:
pull_request:
permissions:
contents: read
jobs:
robot:
runs-on: ubuntu-latest
env:
RF_FAKE_TOKEN: ${{ secrets.RF_FAKE_TOKEN }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Install pinned dependencies
run: python -m pip install --requirement requirements-ci.txt
- name: Run provider-neutral Robot contract
run: python tools/ci_run.py
- name: Upload Robot evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: robot-evidence-${{ github.run_id }}
path: artifacts/robot/
if-no-files-found: warn
For the lab, create RF_FAKE_TOKEN in the repository's
Actions secret store with a clearly fake value. Never use a
production credential to prove secret wiring. On self-hosted
runners, keep the runner version compatible with the JavaScript
runtime required by the selected action major versions.
7. GitLab CI/CD mapping
robot:
image: python:3.13-slim
stage: test
before_script:
- python -m pip install --requirement requirements-ci.txt
script:
- python tools/ci_run.py
artifacts:
when: always
paths:
- artifacts/robot/
reports:
junit: artifacts/robot/xunit.xml
Store a fake lab variable named RF_FAKE_TOKEN in GitLab
CI/CD variables, or set it only in a local runner simulation.
GitLab's artifacts:when: always keeps ordinary paths on
failed jobs, and artifacts:reports:junit makes the
xUnit file available to the test-report UI. The job still fails
because the script returns Robot's non-zero status.
8. Jenkins mapping
pipeline {
agent any
stages {
stage('Robot') {
environment {
RF_FAKE_TOKEN = 'fake-training-token' // simulation only
}
steps {
sh 'python -m pip install --requirement requirements-ci.txt'
sh 'python tools/ci_run.py'
}
post {
always {
archiveArtifacts artifacts: 'artifacts/robot/**',
allowEmptyArchive: true
junit testResults: 'artifacts/robot/xunit.xml',
allowEmptyResults: true
}
}
}
}
}
On a Windows Jenkins agent, use the provider's Windows shell step
instead of sh. In production, replace the literal fake
token with a Jenkins credential binding appropriate to the dedicated
Jenkins course. The important Robot integration invariant is the
same: non-zero runner status fails the stage;
post { always { ... } } preserves evidence.
9. Evidence matrix: before and after
| Observation | Before run | After PASS | After intentional FAIL |
|---|---|---|---|
| Process status | not started | 0 | 1 |
| runtime-manifest.txt | absent | present | present |
| output.xml/log/report | absent or older run | current run | current failed run |
| xunit.xml | absent or older run | all passing | contains failing test |
| Secret evidence | unknown | presence flag only | presence flag only |
| CI gate | not evaluated | open | closed while artifacts remain available |
10. Challenge: choose the correct layer
Your team asks for smoke tests on pull requests and a larger regression suite nightly. Do not duplicate two CI-specific Robot commands. Decide which repository-owned input should represent the selection, for example, versioned argument files or an execution-profile mechanism from Chapter 15, then let each provider pass only that profile choice to the same runner. Explain which layer owns selection, which owns scheduling, and which owns evidence.
Knowledge check
Why does the wrapper use subprocess.run and then
return completed.returncode?
It needs a chance to persist the exit-code evidence while still propagating Robot's real status to the caller. The wrapper must not convert a failed Robot run into success.
What does if: always() protect in the GitHub
Actions example?
It keeps the artifact-upload step eligible even when the Robot step failed. It does not change the Robot step's failure status.
Why is RF_FAKE_TOKEN not passed into the Robot
suite?
The synthetic suite does not require a credential. Least-privilege design says do not expose secret material to a process or log surface that has no need for it.
GitLab shows the test report but the artifact browser lacks
log.html. What configuration should you
inspect?
Check artifacts:paths. The JUnit report declaration
and browsable artifact paths are related but distinct CI
features.
Summary and bridge
You now have a single cross-platform Robot execution contract and three thin provider mappings. Lesson 3 examines when to deviate from this baseline—caches, containers, parallelism, xUnit surfaces, environment promotion, and provider-specific scripts—and what each deviation costs.
Current primary references
- Robot Framework 7.4.2 User Guide — execution, return codes, output files, xUnit, selection and result semantics.
- Robot Framework releases — current stable/prerelease status.
- Pabot 5.2.2 and Pabot documentation — optional parallel execution.
- actions/checkout, actions/setup-python, and actions/upload-artifact — current GitHub Actions examples.
- GitLab CI/CD YAML reference and unit test reports.
- Jenkins recording tests and artifacts, Pipeline syntax, and the JUnit step.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.