deploy-epoch: deployment topology — one binary, three blessed shapes (#27)
Some checks failed
CI / fmt (push) Successful in 1m10s
CI / clippy (push) Successful in 8m17s
CI / test (1.85) (push) Failing after 9m49s
CI / test (stable) (push) Failing after 17m18s
CI / deny (push) Failing after 1m51s
CI / sim-bench (stable) (push) Successful in 24m52s
CI / image-build (4 images) (push) Has been skipped
CI / smoke (all-in-one TLS sim call) (push) Has been skipped
CI / smoke (T2 compose four-service) (push) Has been skipped
CI / smoke (caddy reload during live call, zero drops) (push) Has been skipped
CI / twilio-live (manual only) (push) Has been skipped

deploy-epoch: deployment topology — one binary, three blessed shapes

The full deploy-epoch integration: deployment topology spec + 4 implementation slices
(69 files, +14771/-36). Adds Docker image build pipeline (4 named --target stages),
container smoke CI (all-in-one TLS + compose + caddy reload-during-call), rustls
Phase 1 in-process TLS, /metrics endpoint, ValkeyEventSink, engine hygiene fixes
(TCP_NODELAY + TCP_QUICKACK-suppression + WS pings + trunk config), and the slice-F
tag-push image publish workflow. ADR-0011 (deployment topology) Proposed.

Pre-tag verification (PM-executed on integration tip c772485):
- Seam gate intact (loop_driver + rtc_session hashes unchanged)
- cargo fmt/clippy/test/doc all exit 0
- cargo deny bans+licenses FAILED pre-existing at main 6340e63 (CI authoritative
  via cargo-deny-action@v2 with cargo-deny 0.19.x)

PRs merged into this commit: #23 (dev-d/G), #24 (dev-a/A), #25 (dev-c/C+D+E), #26 (dev-b/B+F)
Integration PR: #27

Signed-off-by: Aaron D. Lee <himself@adlee.work>
Co-authored-by: Aaron D. Lee <himself@adlee.work>
Co-committed-by: Aaron D. Lee <himself@adlee.work>
This commit was merged in pull request #27.
This commit is contained in:
2026-07-09 21:11:25 +00:00
committed by A.D.Lee
parent 6340e638d5
commit cc25e80310
69 changed files with 14771 additions and 36 deletions

View File

@@ -29,6 +29,18 @@ jobs:
clippy:
runs-on: ubuntu-latest
services:
valkey:
image: valkey/valkey:latest
ports:
- 6379:6379
options: >-
--health-cmd "valkey-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 3
env:
VALKEY_URL: redis://127.0.0.1:6379/
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
@@ -41,9 +53,21 @@ jobs:
test:
runs-on: ubuntu-latest
services:
valkey:
image: valkey/valkey:latest
ports:
- 6379:6379
options: >-
--health-cmd "valkey-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 3
strategy:
matrix:
toolchain: [stable, "1.85"]
env:
VALKEY_URL: redis://127.0.0.1:6379/
steps:
- uses: actions/checkout@v4
@@ -144,3 +168,243 @@ jobs:
RUTSTER_TWILIO_TEST_TO: ${{ secrets.TWILIO_TEST_TO }}
RUTSTER_TWILIO_TEST_FROM: ${{ secrets.TWILIO_TEST_FROM }}
run: cargo test --all --features=twilio-live -- --include-ignored
# deploy-B §6.1 image-build — builds all four first-party images via
# `docker buildx build --target` against deploy/Dockerfile. Runs AFTER the
# fmt/clippy/test/deny/sim-bench matrix gates: the smoke job downstream
# depends on these built images.
#
# Single builder invocation that builds all four --targets would be ideal
# (shared cache), but buildx's `--target` is one-per-invocation — so four
# build steps share the GHA cache via `cache-from/cache-to: type=gha`.
# Warm caches take image-build from ~6 min to ~90s.
image-build:
name: image-build (4 images)
runs-on: ubuntu-latest
needs: [fmt, clippy, test, deny, sim-bench]
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# libopus-dev — the headers the `opus` crate's build.rs expects.
# (The builder stage of deploy/Dockerfile already installs this
# inside the build image; this is a belt-and-braces step so a local
# buildx cache root has the headers if the cache misses.)
- name: Build-time host deps (belt-and-braces)
run: sudo apt-get update && sudo apt-get install -y libopus-dev
- name: Build rutster-engine image
uses: docker/build-push-action@v5
with:
context: .
file: deploy/Dockerfile
target: rutster-engine
tags: rutster-engine:ci-${{ github.sha }}
push: false
load: true
cache-from: type=gha,scope=rutster-engine
cache-to: type=gha,mode=max,scope=rutster-engine
- name: Build rutster-brain image
uses: docker/build-push-action@v5
with:
context: .
file: deploy/Dockerfile
target: rutster-brain
tags: rutster-brain:ci-${{ github.sha }}
push: false
load: true
cache-from: type=gha,scope=rutster-brain
cache-to: type=gha,mode=max,scope=rutster-brain
- name: Build rutster-edge image
uses: docker/build-push-action@v5
with:
context: .
file: deploy/Dockerfile
target: rutster-edge
tags: rutster-edge:ci-${{ github.sha }}
push: false
load: true
cache-from: type=gha,scope=rutster-edge
cache-to: type=gha,mode=max,scope=rutster-edge
- name: Build rutster-allinone image
uses: docker/build-push-action@v5
with:
context: .
file: deploy/Dockerfile
target: rutster-allinone
tags: rutster-allinone:ci-${{ github.sha }}
push: false
load: true
cache-from: type=gha,scope=rutster-allinone
cache-to: type=gha,mode=max,scope=rutster-allinone
- name: Smoke-tag the images for the downstream smoke job
# The smoke job references images as `:smoke`
# (deploy/smoke/compose_smoke.sh tags images with the
# RUTSTER_IMAGES_TAG env). Tag the freshly-built :ci-<sha> images as
# :smoke too, so the smoke job picks them up.
run: |
for img in rutster-engine rutster-brain rutster-edge rutster-allinone; do
docker tag "${img}:ci-${{ github.sha }}" "${img}:smoke"
done
docker images | grep rutster
- name: caddy validate
# Validate the Caddyfile inside the freshly-built rutster-edge image.
# Cheap regression against malformed Caddyfile (the local `caddy
# validate` in Task 3 is optional; this one is mandatory in CI).
run: |
docker run --rm \
-e RUTSTER_DOMAIN=pbx.example.com \
-e RUTSTER_ACME_EMAIL=test@example.com \
-v "$PWD/deploy/Caddyfile:/etc/caddy/Caddyfile:ro" \
rutster-edge:smoke \
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
# deploy-B §9 all-in-one smoke — boots rutster-allinone with
# RUTSTER_LOCAL_CERTS=true (Caddy internal CA, no ACME in CI), extracts
# the CA root cert from /data, opens wss:// to /twilio/media-stream,
# sends Twilio's connected+start handshake frames, streams 200 PCM-zeroed
# frames at 20ms cadence, asserts the engine replies with at least one
# text frame through the real edge->FOB WS path. Spec §9 acceptance:
# * "boots"
# * "Caddy terminates TLS via its internal CA (no ACME in CI)"
# * "full sim call through the real edge->FOB WS path"
# The reload-during-call smoke (Task 9 below) and compose smoke (Task 9)
# are separate jobs that depend on this one.
smoke:
name: smoke (all-in-one TLS sim call)
runs-on: ubuntu-latest
needs: [image-build]
steps:
- uses: actions/checkout@v4
- name: Set up Python (stdlib-only; no pip)
uses: actions/setup-python@v5
with:
python-version: "3.x"
# Pull the freshly-built image from the image-build job. Since
# jobs run on separate runners + `push: false` in image-build means
# the image isn't in any registry, the smoke job rebuilds from the
# GHA cache (warm — image-build populated it). This adds ~30s vs
# an image-download artifact, but avoids the registry-auth + image-
# save/load plumbing.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Rebuild rutster-allinone:smoke (warm cache from image-build)
uses: docker/build-push-action@v5
with:
context: .
file: deploy/Dockerfile
target: rutster-allinone
tags: rutster-allinone:smoke
push: false
load: true
cache-from: type=gha,scope=rutster-allinone
- name: Run all-in-one smoke (deploy/smoke/allinone_smoke.py)
run: |
python3 deploy/smoke/allinone_smoke.py
env:
RUTSTER_ALLINONE_IMAGE: rutster-allinone:smoke
RUTSTER_ALLINONE_PORT: "18443"
# deploy-B §9 compose smoke — T2 four-service compose stack boots + all
# services healthy + Caddy TLS /healthz + /readyz. Lighter than the
# all-in-one smoke (which already proved the WS path) — its job is the
# orchestrator shape + the network_mode: "service:engine" brain->engine tap
# posture across the four services.
smoke-compose:
name: smoke (T2 compose four-service)
runs-on: ubuntu-latest
needs: [image-build]
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Rebuild all three first-party images from the warm GHA cache
# populated by image-build. Valkey is upstream (no rebuild needed).
- name: Rebuild rutster-edge:smoke
uses: docker/build-push-action@v5
with:
context: .
file: deploy/Dockerfile
target: rutster-edge
tags: rutster-edge:smoke
push: false
load: true
cache-from: type=gha,scope=rutster-edge
- name: Rebuild rutster-engine:smoke
uses: docker/build-push-action@v5
with:
context: .
file: deploy/Dockerfile
target: rutster-engine
tags: rutster-engine:smoke
push: false
load: true
cache-from: type=gha,scope=rutster-engine
- name: Rebuild rutster-brain:smoke
uses: docker/build-push-action@v5
with:
context: .
file: deploy/Dockerfile
target: rutster-brain
tags: rutster-brain:smoke
push: false
load: true
cache-from: type=gha,scope=rutster-brain
# jq is pre-installed on ubuntu-latest GHA runners.
- name: Run compose smoke (deploy/smoke/compose_smoke.sh)
run: bash deploy/smoke/compose_smoke.sh
env:
RUTSTER_IMAGES_TAG: smoke
# deploy-B §9 + spec §3.2 + TLS brief §5 risk 1: Caddy config-reload
# during a live simulated call, asserting zero frame drops. The
# stream_close_delay 24h mitigation in deploy/Caddyfile is the load-
# bearing fixture; caddy #6420 / #7222 are open upstream bugs — the
# smoke is "don't trust untested" made CI-regressed.
smoke-reload:
name: smoke (caddy reload during live call, zero drops)
runs-on: ubuntu-latest
needs: [smoke] # reuses the rutster-allinone:smoke image, warm cache
steps:
- uses: actions/checkout@v4
- name: Set up Python (stdlib-only; no pip)
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Rebuild rutster-allinone:smoke (warm cache)
uses: docker/build-push-action@v5
with:
context: .
file: deploy/Dockerfile
target: rutster-allinone
tags: rutster-allinone:smoke
push: false
load: true
cache-from: type=gha,scope=rutster-allinone
- name: Run reload-during-call smoke (deploy/smoke/reload_during_call.py)
run: python3 deploy/smoke/reload_during_call.py
env:
RUTSTER_ALLINONE_IMAGE: rutster-allinone:smoke
RUTSTER_ALLINONE_PORT: "18444"