Chapter 30Lesson 02270–360 min

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.

Local fixtureRequestsLibrarySecretCustom libraryRebot

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?

What does python -m robot --dryrun prove in this workflow?

Why generate Rebot artifacts from a separate raw output directory?

Why is http://fixture:8765 correct in the Compose Robot service while http://127.0.0.1:8765 is correct locally?

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.

Next lesson

Capstone: Build a Governed End-to-End Robot Framework Automation Platform: Configuration, Design Patterns, and Trade-Offs

Continue with Capstone: Build a Governed End-to-End Robot Framework Automation Platform: Configuration, Design Patterns, and Trade-Offs. It builds directly on the state, evidence, and operating assumptions established here, so carry those constraints forward rather than treating the next page as an isolated topic.

Further reading

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.