Portfolio

Case study

Rendered from the project README. The demo uses fictional data.

ShipLane

Release-pipeline visualizer for a fictional Globex service, checkout-api. The page you can click is a running simulator: seeded history, a live GitHub Actions–style graph, job logs, blue/green environment state, and DORA metrics computed from that history. The workflows and the blue/green scripts in this directory are real files you can read and lint. They are not executed by this demo process.

Overview

ShipLane shows how a release is designed when the target is one VPS: build, a Python test matrix, an SBOM and a vulnerability scan, a staging deploy, a manual production approval, a blue/green switch, a health check, and an automatic rollback if that check fails. Visitors watch a simulated run advance job by job. The same repository holds the workflow YAML and the bash that would perform the switch.

Problem → Solution → Result

Problem

Solution

Result

These are capabilities of this demo, not claims about a client engagement.

Features

Architecture

Live demo (what the public page is):

Browser
         |
         |  relative URLs (api/..., static/..., ../)
         v
      edge nginx, prefix stripped
         |
         v
      systemd: python -m app  127.0.0.1:18805
         |
         +--> SQLite   DATA_DIR/shiplane.db   (runs, jobs, environments)
         +--> in-process simulator (tick, retain 300)
         +--> reads .github/workflows/{ci,release}.yml   (whitelist)

Release path the workflow describes (not executed by the visualizer):

tag v* or workflow_dispatch
         |
         v
      test matrix --> build/push GHCR (buildx, multi-arch)
         |                 |
         |                 +--> SBOM (anchore) + Trivy (fail HIGH/CRITICAL) + cosign keyless
         v
      deploy-staging (blue/green, separate state dir)
         |
         v
      environment: production   (manual approval)
         |
         v
      SSH deploy/bluegreen.sh --> nginx upstream swap --> health check
         |
         +--> ok: previous color stays up, idle
         +--> verify failed: deploy/rollback.sh restores the previous color
flowchart LR
        browser[Browser] --> nginx[edge nginx]
        nginx --> app[FastAPI visualizer]
        app --> db[(SQLite)]
        app --> yaml[workflow files]
        release[release.yml] --> ghcr[GHCR image]
        release --> vps[VPS blue/green]
        vps --> colorBlue[blue stack]
        vps --> colorGreen[green stack]

Notes:

Tech stack

Run locally

venv

python3 -m venv .venv
      .venv/bin/pip install -r requirements-dev.txt
      .venv/bin/python -m app

Or ./run.sh, which creates .venv if needed and listens on 127.0.0.1:18805. Open http://127.0.0.1:18805/.

The first start seeds history and, with the default interval, begins a live run. AUTO_INTERVAL_SECONDS=0 disables the automatic start. TICK_SECONDS=0 disables the background clock (tests use both).

Dry-run the switch without Docker:

bash deploy/bluegreen.sh --dry-run --image ghcr.io/globex/checkout-api:v2.9.5
      bash deploy/bluegreen.sh --dry-run --status
      bash deploy/rollback.sh --dry-run

Docker Compose

Docker runs the visualizer. It is not required to view the demo, and this box does not need Docker to review the file.

docker compose up -d --build
      curl -fsS http://127.0.0.1:18805/healthz

The image listens on 0.0.0.0:18805 as uid 10001, stores SQLite in the shiplane_data volume, and healthchecks /healthz. Memory cap in Compose is 150M.

Configuration

VariableDefaultRole
PORT18805Listen port
HOST127.0.0.1Bind address. The image sets 0.0.0.0
DATA_DIR./dataSQLite directory (shiplane.db)
AUTO_INTERVAL_SECONDS600How often a new simulated run may start. 0 disables it
TICK_SECONDS1Simulator clock. 0 disables the background loop
SEED10, false, or no skips historical seed
SIM_SEED18805RNG seed for failure selection and durations
RETAIN_RUNS300Newest runs kept. Older runs and their jobs are deleted
RATE_LIMIT_SECONDS60Minimum gap between POST /api/runs from one address
DORA_WINDOW_DAYS45Trailing window for the four DORA figures

POST /api/runs also allows only one in-progress run for the whole process. A second start returns 409 and does not consume the per-address limit. The body is ignored and capped at 8 KB.

API endpoints

MethodPathNotes
GET/Pipeline page
GET/healthz{"status":"ok",...}. No database work
GET/api/runs?limit=Newest runs. limit is clamped
GET/api/runs/{id}One run and its jobs
GET/api/runs/{id}/jobs/{job}/logVisible log text for a known job key
POST/api/runsStart one simulated run. 201, 409 if one is active, 429 if the address is limited, 413 above 8 KB
GET/api/environmentsStaging and production color, traffic, version
GET/api/metrics/doraFrequency, lead time, change failure rate, MTTR, weekly buckets
GET/api/workflowsWhitelist names, or ?name=ci.yml / ?name=release.yml
GET/metricsPrometheus text

Paths in the browser are relative (api/runs, not /api/runs) so the page works both at the bare port and behind a stripped prefix.

DORA definitions used by the page (trailing window, by pipeline start):

Tests

.venv/bin/python -m pytest
      .venv/bin/ruff check app tests
      bash -n deploy/bluegreen.sh deploy/rollback.sh

The suite checks the run state machine (success, test failure, scan block, verify failure with rollback and color restore), live job duration bounds, retention, DORA math, the seed counts, per-address rate limit and the global single-run lock, the 8 KB body cap, workflow path whitelist, relative URLs, HTML escaping, Prometheus text, Compose and systemd and Dockerfile tokens, bash -n, and a --dry-run that must not invoke docker, nginx, or curl.

CI (.github/workflows/ci.yml) runs ruff, bash -n, pytest with coverage on Python 3.10, 3.11, and 3.12, then a local docker build (push: false). The workflow lives with this project. If the git root is the parent monorepo, each job still finds shiplane/requirements-dev.txt.

Deployment notes

The live demo is a systemd service behind nginx on a small VPS, not a container. The unit is deploy/shiplane.service (User=ec2-user, WorkingDirectory=/opt/portfolio/shiplane, PORT=18805, DATA_DIR=/opt/portfolio/shiplane/data, Restart=always, MemoryMax=150M, NoNewPrivileges=yes).

The public URL is https://3.1.79.63/portfolio/shiplane/ with the prefix stripped by nginx (proxy_pass http://127.0.0.1:18805/;). The page links back to the portfolio with ../. deploy/deploy.sh in the portfolio repo installs this unit.

deploy/bluegreen.sh is the production switch for checkout-api on that same class of host: two Compose projects, an atomic symlink for the live color, an upstream file swapped into place, nginx -t before reload, previous color left running. Defaults assume state under /var/lib/shiplane and an upstream at /etc/nginx/conf.d/shiplane-checkout.conf. Staging in release.yml uses a separate state directory and ports. The visualizer's own systemd unit does not call this script.

Screenshots

Desktop, 1440×1000:

Desktop pipeline

Mobile, 390×844:

Mobile pipeline

A run in progress, with the job log open:

Live run

Open the live demo