This doc maps the three remaining “go to sea” checks to automation tiers. Phase 11 covered Docker-bridge negatives; Phase 12 closes TLS, gRPC in CI, and remote/Tailscale as far as GitHub-hosted runners allow.
| Gap | Automate in CI? | What we ship |
|---|---|---|
| TLS | Yes (weekly + local) | Caddy + mesh-lab-tls-certs.sh + mesh-lab-verify-tls.sh |
| gRPC transport | Yes (every PR via PrimeTime) | Nexo.Tests.Transport Category=ProdStyle + optional dedicated workflow |
| Two-host / Tailscale | Partial | mesh-lab-verify-remote.sh + self-hosted runner recipe |
Idea: Nexo stays HTTP on peer-a:8080; Caddy terminates HTTPS on the lab bridge (docker-compose.mesh-lab-tls.override.yml).
make mesh-lab-e2e-tls
# or:
./scripts/run-mesh-lab-e2e-tls.shmesh-lab-verify-tls.sh checks:
GET https://127.0.0.1:18443/health(self-signed cert;-kunlessMESH_LAB_TLS_CACERTset; HTTP/1.1 for macOS LibreSSL)GET/POST /api/mesh/*over HTTPS (register, schedule → Assigned)- Mutating mesh without API key → 401/403
CI: .github/workflows/mesh-lab-tls-gate.yml — weekly + workflow_dispatch.
Production parity: Replace self-signed certs with real DNS + ACME (friend-mesh.Caddyfile.example); same curl paths, trust store from Let’s Encrypt.
Local macOS note: Host curl (LibreSSL) against Docker-published HTTPS can fail with bad decrypt; mesh-lab-tls-curl.sh retries via OpenSSL curl in a container (host.docker.internal). CI on ubuntu-latest uses host curl directly.
Idea: GrpcAgentTransportTests already spin a real Kestrel gRPC server (GrpcServerFixture) — not mocks.
Ensure CI runs Transport:
dotnet test src/Nexo.Tests.Transport/Nexo.Tests.Transport.csproj --filter "Category=ProdStyle"
# or full prime-time:
make test-prime-timeOptional dedicated gate: .github/workflows/grpc-transport-gate.yml — fast, no Docker.
No extra hardware required.
GitHub ubuntu-latest is a single host; it cannot join your production tailnet without secrets and a second node. Automate in three layers:
Deploy mesh lab (or friend-mesh) on machine A; run worker on machine B. From a third machine or CI runner with routes:
export NEXO_MESH_DIRECTOR_BASE_URL=https://100.64.0.1:8080 # director tailnet IP
export NEXO_MESH_API_KEY=...
export MESH_LAB_PEER_REGISTRATION_KEY=... # ≠ API key
export MESH_LAB_REMOTE_WORKER_URL=http://100.64.0.2:8080 # worker tailnet IP
export NEXO_MESH_TLS_INSECURE=1 # if using self-signed TLS
./scripts/mesh-lab-verify-remote.sh- Install runner on a host that is always on tailnet.
- Register runner with label
nexo-tailnet. - Workflow uses
runs-on: [self-hosted, nexo-tailnet]+tailscale/github-actiononly if the runner is not already on the tailnet. - Job runs
mesh-lab-verify-remote.shagainst secrets:NEXO_MESH_DIRECTOR_BASE_URLNEXO_MESH_API_KEYMESH_LAB_REMOTE_WORKER_URL
Reuse scripts/bootstrap-cloud-mesh-lab.sh on a VM with Tailscale installed; second peer is a laptop also on tailnet. Document IPs in .env and run remote verify from the VM cron.
What we do not fake in Docker: true cross-NAT tailnet ACLs — use real Tailscale for that.
| Workflow | When | Covers |
|---|---|---|
mesh-lab-gate.yml |
PR | Bridge HTTP + negatives + persistence + Phase 13 data plane |
mesh-lab-stress-gate.yml |
Weekly | Stress + post-stress |
mesh-lab-tls-gate.yml |
Weekly | HTTPS director |
grpc-transport-gate.yml |
PR | gRPC Kestrel round-trip |
friend-mesh-prefab-gate.yml |
PR | Single-hub auth smoke |
PrimeTime test-prime-time |
Pre-release | Transport + full ProdStyle |
| Date | Change |
|---|---|
| 2026-05-19 | TLS override + remote verify script + automation doc. |