Capstone: Build a Governed End-to-End Robot Framework Automation Platform: Guided Hands-On Workflow
Build the governed platform against a disposable local service, then produce the same evidence from a documented serial command and an optional alternate execution path.
Learning objectives
- Create the capstone project with explicit Robot/resource/Python/system-under-automation boundaries.
- Inject a fake Secret from the environment and consume it inside a typed Python library without logging the value.
- Exercise two automation boundaries: a local HTTP API contract and a harmless child-process contract.
- Run structural, serial, optional Pabot, Rebot, and Robocop gates while preserving their exit status and artifacts separately.
- Record a dependency/runtime manifest, evidence inventory, and operating runbook suitable for later incident reproduction.
Current compatibility baseline — verified 2026-09-01. The mandatory capstone path uses Python 3.12, Robot Framework 7.4.2, RequestsLibrary 0.9.7, Requests 2.34.2, and Robocop 8.5.0. Pabot 5.2.2 is optional for the alternate execution slice. Robot Framework 7.5b1 and RequestsLibrary 1.0a14 are pre-releases and are not required. Browser automation is an optional architecture branch: Browser 20.4.0 and SeleniumLibrary 6.9.0 are discussed in Lesson 3, not installed by the mandatory lab. Record the versions actually used before adopting the platform outside this dated course lab.
1. Create the disposable workspace
Use a fresh directory outside production repositories. The fixture binds only to loopback by default; the container path uses a private service network with no published host port. Every credential in the lesson is synthetic.
rf-governed-capstone/
├─ requirements.txt
├─ tests/
│ ├─ api.robot
│ ├─ process.robot
│ └─ parallel/
│ ├─ shard_a.robot
│ └─ shard_b.robot
├─ resources/
│ └─ platform.resource
├─ libraries/
│ └─ CapstoneGuard.py
├─ fixtures/
│ └─ fixture_service.py
├─ containers/
│ ├─ Dockerfile
│ └─ compose.yaml
├─ evidence/
│ ├─ versions/
│ ├─ incidents/
│ └─ budgets/
└─ results/ # generated; not source truth
robotframework==7.4.2
robotframework-requests==0.9.7
requests==2.34.2
robotframework-pabot==5.2.2
robotframework-robocop==8.5.0
Create and activate a Python 3.12 virtual environment using the shell appropriate to your operating system, then install the pinned requirements. Record the environment immediately after installation.
python -m venv .venv
# Bash/zsh:
source .venv/bin/activate
# PowerShell alternative:
# .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -c "from pathlib import Path; [Path(p).mkdir(parents=True, exist_ok=True) for p in ('evidence/versions','results')]"
python --version > evidence/versions/python.txt
python -m robot --version > evidence/versions/robot.txt
python -m pip freeze > evidence/versions/pip-freeze.txt
2. Build the synthetic system under automation
The fixture has two endpoints: /health is public within
the lab, and /protected requires the fake token. It
deliberately never logs request headers. That makes the trust
boundary observable: the service owns HTTP state; Robot owns
orchestration and assertions.
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
TOKEN = os.environ["RF_CAPSTONE_TOKEN"]
HOST = os.environ.get("RF_CAPSTONE_BIND", "127.0.0.1")
PORT = int(os.environ.get("RF_CAPSTONE_PORT", "8765"))
class Handler(BaseHTTPRequestHandler):
def _json(self, status: int, payload: dict):
body = json.dumps(payload).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/health":
self._json(200, {"status": "ok", "service": "capstone-fixture"})
return
if self.path == "/protected":
supplied = self.headers.get("X-Lab-Token", "")
if supplied != TOKEN:
self._json(401, {"status": "denied"})
return
self._json(200, {"status": "authorized"})
return
self._json(404, {"status": "not-found"})
def log_message(self, fmt, *args):
# Deliberately do not log request headers or the token.
print(f"fixture path={self.path} status-log={fmt % args}", flush=True)
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
Set the fake token in the terminal that will launch both the fixture and Robot. Do not place real secrets in source, examples, screenshots, or command-line literals.
# Bash/zsh
export RF_CAPSTONE_TOKEN='capstone-FAKE_DO_NOT_USE'
python fixtures/fixture_service.py
# PowerShell equivalent
# $env:RF_CAPSTONE_TOKEN = 'capstone-FAKE_DO_NOT_USE'
# python fixtures/fixture_service.py
Keep the fixture running in Terminal A. In Terminal B, use the same fake environment variable and verify the endpoint before Robot mutates or tests anything:
python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8765/health', timeout=2).read().decode('utf-8'))"
3. Add the narrow custom Python boundary
The Python library owns exactly two responsibilities that are
awkward or unsafe to express as generic Robot keywords: target
authorization and passing a typed Secret into an authenticated HTTP
request. It is explicitly TEST scoped and stateless so
each test gets an independent library instance.
import requests
from robot.api.deco import keyword, library
from robot.api.types import Secret
@library(scope="TEST", auto_keywords=False)
class CapstoneGuard:
@keyword
def protected_ping(self, base_url: str, token: Secret) -> int:
response = requests.get(
f"{base_url}/protected",
headers={"X-Lab-Token": token.value},
timeout=2.0,
)
return response.status_code
@keyword
def assert_local_lab_target(self, base_url: str) -> None:
allowed = (
base_url.startswith("http://127.0.0.1:")
or base_url.startswith("http://fixture:")
)
if not allowed:
raise AssertionError(f"Refusing non-lab target: {base_url}")
The Secret type hint rejects normal strings for that
argument. The library unwraps token.value only at the
transport boundary and returns only the HTTP status code. It does
not return headers, the token, or a request object that could later
be logged accidentally.
4. Compose the Robot-level resource layer
*** Settings ***
Library RequestsLibrary
Library ../libraries/CapstoneGuard.py
Library Process
Library OperatingSystem
*** Variables ***
${BASE_URL} %{RF_CAPSTONE_BASE_URL=http://127.0.0.1:8765}
${API_TOKEN: Secret} %{RF_CAPSTONE_TOKEN}
${PYTHON} %{RF_CAPSTONE_PYTHON=python}
*** Keywords ***
Preflight Local Target
Assert Local Lab Target ${BASE_URL}
Health Should Be Ready
${response}= GET ${BASE_URL}/health expected_status=200
Should Be Equal ${response.json()}[status] ok
Protected Endpoint Should Accept Secret
${status}= Protected Ping ${BASE_URL} ${API_TOKEN}
Should Be Equal As Integers ${status} 200
Run Harmless Process Boundary
${result}= Run Process ${PYTHON} -c print("process-boundary-ok")
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} process-boundary-ok
Preflight Local Target is a guard, not documentation.
Any suite that uses this resource refuses an arbitrary public or
production URL before domain activity begins. The environment
variable default for ${BASE_URL} is loopback; the
container path later supplies http://fixture:8765.
5. Write acceptance intent above the implementation details
*** Settings ***
Resource ../resources/platform.resource
Suite Setup Preflight Local Target
*** Test Cases ***
Public Health Contract
Health Should Be Ready
Protected Contract Uses Secret Boundary
Protected Endpoint Should Accept Secret
*** Settings ***
Resource ../resources/platform.resource
Suite Setup Preflight Local Target
*** Test Cases ***
Local Child Process Contract
Run Harmless Process Boundary
The API suite has one public contract and one protected contract. The process suite verifies a harmless child-process boundary with explicit return code and stdout evidence. Neither suite knows how the token is unwrapped or how RequestsLibrary constructs the HTTP transport; those lower-level details remain in the resource/library boundary.
6. Run the gate in layers
Run cheap deterministic checks before external execution. Then run serially and preserve raw output separately from Rebot presentation.
# 1) Structural validation
python -m robot --dryrun --outputdir results/dryrun tests
# 2) Style/static gate
robocop check tests resources
robocop format --check tests resources
# 3) Serial execution: keep machine-readable evidence
python -m robot --outputdir results/serial --log NONE --report NONE tests
# 4) Post-process the preserved raw result into presentation artifacts
python -m robot.rebot --outputdir results/published --name "Governed Capstone" results/serial/output.xml
Capture the exit status of the Robot execution in CI rather than
replacing it with Rebot’s exit status. Rebot creates a view of
results; it does not redefine whether the original run failed. If a
CI wrapper is needed, make it preserve the Robot code, run
post-processing in a finally-style path, upload
artifacts regardless of pass/fail, and then exit with the original
Robot status.
7. Optional alternate mode: controlled Pabot slice
Parallelism is optional because the platform is already valid serially. Add it only to an isolated subset whose mutable resources do not collide. These two small suites deliberately share only read-only fixture state; neither writes a common file, database record, account, download directory, or port.
*** Settings ***
Resource ../../resources/platform.resource
Suite Setup Preflight Local Target
*** Test Cases ***
Shard A Health Contract
Health Should Be Ready
*** Settings ***
Resource ../../resources/platform.resource
Suite Setup Preflight Local Target
*** Test Cases ***
Shard B Process Contract
Run Harmless Process Boundary
Save them as tests/parallel/shard_a.robot and
tests/parallel/shard_b.robot, then start with two
processes and preserve the entire Pabot result directory, including
manager/worker evidence, instead of judging success from the merged
report alone.
pabot --processes 2 --outputdir results/pabot tests/parallel
Do not multiply Pabot processes by CI job concurrency without a capacity model. The external fixture, CPU, memory, file system, browser/session pool, and downstream APIs all impose limits independent of Robot’s own parser/executor.
8. Reproducible container path without hiding networking
FROM python:3.12-slim
WORKDIR /workspace
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "-m", "robot", "--help"]
services:
fixture:
build:
context: ..
dockerfile: containers/Dockerfile
command: ["python", "fixtures/fixture_service.py"]
environment:
RF_CAPSTONE_BIND: "0.0.0.0"
RF_CAPSTONE_PORT: "8765"
RF_CAPSTONE_TOKEN: "capstone-FAKE_DO_NOT_USE"
expose: ["8765"]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8765/health', timeout=1).read()"]
interval: 1s
timeout: 2s
retries: 10
start_period: 1s
robot:
build:
context: ..
dockerfile: containers/Dockerfile
depends_on:
fixture:
condition: service_healthy
environment:
RF_CAPSTONE_BASE_URL: "http://fixture:8765"
RF_CAPSTONE_TOKEN: "capstone-FAKE_DO_NOT_USE"
volumes:
- ../results:/workspace/results
command: ["python", "-m", "robot", "--outputdir", "results/container", "tests"]
Inside the Robot container, localhost would refer to
that container, not the fixture service. The alternate base URL is
therefore http://fixture:8765, the private Compose
service name. The fixture port is exposed to the private network but
not published to the host/public network. A Compose health check
gates the Robot service on actual fixture readiness instead of a
fixed sleep. In a real platform, replace the fake inline token with
the orchestrator’s secret mechanism; the literal fake value is
acceptable only because this is a disposable course lab.
9. Assemble the evidence packet
| Evidence | Why it exists | Acceptance signal |
|---|---|---|
| Architecture/project tree | Shows ownership and import/runtime boundaries | Paths match the documented model; no hidden production target. |
Version manifest + pip freeze |
Reproduces dependency state | Python/Robot/direct tools recorded with timestamp. |
results/serial/output.xml |
First machine-readable execution evidence | Preserved before post-processing. |
| Rebot log/report | Human diagnosis/review | Generated from the preserved output, not substituted for it. |
| Robocop results | Source quality gate | No unexplained lint/format failures or blanket suppression. |
| Pabot/container artifacts when used | Proves alternate-mode behavior | Same test meaning/status with isolated state and correct network target. |
| Secret audit | Checks evidence/privacy boundary | Fake token string absent from retained Robot logs/reports/debug files. |
| Runbook | Makes operation repeatable | Setup, preflight, commands, expected states, cleanup, owner/escalation documented. |
10. Challenge: choose the layer, do not copy the command
The protected test returns 401, while /health passes
and the target guard passes. Choose the first layer to inspect and
justify it. A strong answer starts with environment/Secret
provenance and the custom library transport boundary, not Pabot,
Robocop, or a retry. Confirm the fake environment variable exists in
both the fixture and Robot processes, preserve the 401 evidence, and
only then change configuration.
Knowledge check
Why does the capstone use a custom library for the protected
endpoint instead of logging or interpolating
${API_TOKEN.value} in Robot data?
The typed Python boundary can accept a Secret, unwrap it only at
the transport call, and avoid exposing the real value in Robot
data/logs. Accessing .value in Robot data would
make accidental disclosure much easier.
What does python -m robot --dryrun prove in this
workflow?
It validates much of the source/import/keyword structure without executing normal library keywords, but it does not prove runtime variable values or external API/process behavior are correct.
Why generate Rebot artifacts from a separate raw output directory?
It preserves the original machine-readable evidence and makes post-processing an explicit later step, which is safer for incident diagnosis and auditability.
Why is http://fixture:8765 correct in the Compose
Robot service while http://127.0.0.1:8765 is
correct locally?
Container localhost belongs to the container itself. The private Compose service is reached through service DNS, while the local non-container run reaches the loopback fixture process on the host.
Summary and bridge
You now have a multi-layer Robot platform with an explicit API boundary, process boundary, Secret-aware Python library, target guard, serial gate, optional Pabot mode, Rebot evidence, Robocop gate, container path, and evidence inventory. Lesson 3 evaluates the design choices behind those decisions rather than treating this project shape as universally correct.
Further reading
- Robot Framework User Guide and Process library.
- RequestsLibrary keyword documentation.
-
Robocop
—
checkand formatter--checkbehavior. - Pabot documentation.
- Docker Compose documentation — optional container path only; deeper Docker operation belongs in the dedicated course.
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.