From 381046190b0568a00145b6f7aa2c03a76a06414a Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Mon, 29 Jun 2026 13:11:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20add=20stage=20tier=20=E2=80=94?= =?UTF-8?q?=20park-buzi=20as=20the=20staging=20booth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Model the staging-vs-production split that fleet-deployment-komodo flagged as open. Three tiers: dev (working, no booth) -> stage (staging booth park-buzi, real-world test) -> main (production, manual + pinned). - build-images.yml: trigger on [dev, stage, main]. The tag computation is already branch-derived, so :stage / :stage- build with no other change. - komodo/resources.toml: park-buzi now branch=stage + TAG=stage- (pinned; no webhook even on staging). BACKUP_KEY already wired as a per-booth secret. - komodo/README.md: a Promotion (dev->stage->main) section; per-booth secret list now includes backup_key; hard-rule #1 generalised to pinned -. - wiki: fleet-deployment-komodo open-item resolved + a Promotion-tiers table; deploy-trigger choice generalised; container-deployment tag list gains :stage. Promotion is a merge: when dev is ready, merge dev->stage, CI builds the image, bump TAG=stage- in resources.toml, deploy from Core. stage is branched from dev HEAD so the first real-world test carries the full current app. Per-booth secrets must pre-exist in Core; migrations run at boot so a promotion auto-migrates the staging ledger (where a bad migration is caught before production). Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V --- .gitea/workflows/build-images.yml | 9 +++-- komodo/README.md | 29 +++++++++++--- komodo/resources.toml | 15 +++++--- wiki/decisions/container-deployment.md | 6 ++- wiki/decisions/fleet-deployment-komodo.md | 47 ++++++++++++++++++----- wiki/log.md | 14 +++++++ 6 files changed, 93 insertions(+), 27 deletions(-) diff --git a/.gitea/workflows/build-images.yml b/.gitea/workflows/build-images.yml index 6ae2ac9..1e81a52 100644 --- a/.gitea/workflows/build-images.yml +++ b/.gitea/workflows/build-images.yml @@ -1,13 +1,14 @@ name: Build & push images # Build the SERVER (API + SPA) and VISION (ANPR) container images and push them to the -# house Gitea registry, tagged by BRANCH + short SHA (branch-aware: dev→:dev, main→:main). -# Separate from ci.yml (checks-only) and release.yml (tag-only desktop bundle). Mirrors the -# house pattern (cf. trm/processor build.yml). See wiki/decisions/container-deployment.md. +# house Gitea registry, tagged by BRANCH + short SHA (branch-aware: dev→:dev, stage→:stage, +# main→:main). Separate from ci.yml (checks-only) and release.yml (tag-only desktop bundle). +# Mirrors the house pattern (cf. trm/processor build.yml). See +# wiki/decisions/container-deployment.md and fleet-deployment-komodo.md (dev→stage→main tiers). on: push: - branches: [dev, main] + branches: [dev, stage, main] paths: - 'apps/server/**' - 'apps/web/**' diff --git a/komodo/README.md b/komodo/README.md index 298d641..9ea6a10 100644 --- a/komodo/README.md +++ b/komodo/README.md @@ -16,7 +16,22 @@ run `booth.sh`. Procedures), synced into Core via a **ResourceSync**. This is the reviewable, version- controlled source of truth for *which booth runs what*. - **`.env.komodo.example`** — the variables a Stack expects, documenting what comes from Core's - **secret store** (per-booth `JWT_SECRET` / `EVENT_SIGNING_KEY`) vs. plain Stack env. + **secret store** (per-booth `JWT_SECRET` / `EVENT_SIGNING_KEY` / `BACKUP_KEY`) vs. plain Stack env. + +## Promotion: dev → stage → main + +Three tiers (see `wiki/decisions/fleet-deployment-komodo.md`): + +- **`dev`** — the working branch. CI builds `:dev` / `:dev-`. No booth deploys off it. +- **`stage`** — what the **staging booth (`park-buzi`)** runs, to test the app in real-world + conditions. When `dev` is confident-ready, **merge `dev → stage`**; CI builds `:stage` / + `:stage-`; then **bump `TAG=stage-` in `resources.toml`** to that build and deploy + from Core (manual, pinned — no webhook even on staging). +- **`main`** — vetted **production** booths: `:main-`, manual + pinned. `main` only gets what + survived staging. + +> The `TAG=stage-` in `resources.toml` is a **pinned pointer**: the moving `:stage` tag exists +> but we deploy the immutable sha so a booth runs a known image. Re-pin on each promotion. ## How Core consumes this (one-time) @@ -43,14 +58,16 @@ booth), and connects **outbound** — the booth opens **no inbound port**. The s ## Adding booth N Copy the `[[stack]]` block, change `name`, `server` (its onboarded name), and the per-booth -secret references (`[[park__jwt_secret]]`, `[[park__event_signing_key]]`). Create -those secrets in Core's store first. +secret references (`[[park__jwt_secret]]`, `[[park__event_signing_key]]`, +`[[park__backup_key]]`). Create those secrets in Core's store first. Set `branch` + +`TAG` for the tier the booth runs (staging → `stage` / `stage-`; production → `main` / +`main-`). ## Hard rules encoded here (do not relax without updating the decision page) -1. **No deploy webhook on a booth Stack.** Deploys are a human action; pin `TAG=dev-` before - a production booth goes live. A moving `:dev` on a production booth is the non-determinism we - rejected. (`TAG=dev` here is fine while staging.) +1. **No deploy webhook on a booth Stack.** Deploys are a human action; pin an immutable + `TAG=-` (staging → `stage-`, production → `main-`). A moving tag on a + booth is the non-determinism we rejected — and we hold that line even on the staging booth. 2. **Onboarding, outbound, mesh-only.** Servers self-register via an onboarding key; Periphery connects outbound to Core's mesh URL and exposes no inbound port. Never a LAN/WAN address. 3. **Secrets are per-booth and unique.** `EVENT_SIGNING_KEY` signs the anti-fraud ledger — one diff --git a/komodo/resources.toml b/komodo/resources.toml index 99594a7..96de3e6 100644 --- a/komodo/resources.toml +++ b/komodo/resources.toml @@ -18,9 +18,12 @@ # EVENT_SIGNING_KEY signs the append-only anti-fraud ledger; BACKUP_KEY encrypts on-site DB # backups (separate from the signing key; escrow it offsite — recovery needs both). # -# Deploys are MANUAL + PINNED in spirit: bump TAG to an immutable dev- before a -# production booth goes live (TAG=dev here is the moving tag, fine while staging). NO -# deploy webhook is attached to a booth Stack. +# Deploys are MANUAL + PINNED: park-buzi is the STAGING booth (real-world test of the app), +# so it tracks the `stage` branch + the `:stage` image, but is still deployed by hand with a +# PINNED immutable TAG=stage- (no webhook). Promotion: merge dev → stage when confident, +# CI builds :stage / :stage-, then bump TAG below to that sha and deploy from Komodo Core. +# A PRODUCTION booth tracks `main` + manual+pinned `:main-`. See +# wiki/decisions/fleet-deployment-komodo.md (dev → stage → main tiers). ############################################################################## # Stack — the deployable unit for booth "park-buzi". One Stack per booth; add a @@ -34,7 +37,7 @@ server = "park-buzi" git_provider = "git.infra.msai.al" git_account = "komodo" repo = "mca/parking_solution" -branch = "dev" +branch = "stage" file_paths = [ "docker-compose.yml", "docker-compose.prod.yml" @@ -43,7 +46,9 @@ registry_provider = "git.infra.msai.al" registry_account = "komodo" environment = """ REGISTRY=git.infra.msai.al/mca/parking_solution -TAG=dev +# Staging booth: pin an immutable stage- per deploy (bump after merging dev → stage and +# CI builds it). The moving `:stage` tag exists as the pointer; we deploy the sha, not the mover. +TAG=stage-84f00db COOKIE_SECURE=0 VISION_ENABLED=1 WS_ALLOWED_ORIGINS= diff --git a/wiki/decisions/container-deployment.md b/wiki/decisions/container-deployment.md index 93f90d5..1d0f625 100644 --- a/wiki/decisions/container-deployment.md +++ b/wiki/decisions/container-deployment.md @@ -35,8 +35,10 @@ The **desktop** app stays on its own tag-only `release.yml` (Tauri installers), ## Branch-aware (the user's hard requirement) - **Image tags = branch + short SHA.** A push to `dev` builds `…/parking-server:dev` + - `…/parking-server:dev-`; `main` builds `:main` + `:main-`. The moving branch tag is the - deploy pointer; the branch-SHA tag is the immutable record. Same for `parking-vision`. + `…/parking-server:dev-`; `stage` builds `:stage` + `:stage-`; `main` builds `:main` + + `:main-`. The moving branch tag is the deploy pointer; the branch-SHA tag is the immutable + record. Same for `parking-vision`. (The `stage` tier — the staging booth — was added 2026-06-29; + see [[fleet-deployment-komodo]] "Promotion tiers".) - **Per-env compose.** A base `docker-compose.yml` + overrides: `docker-compose.dev.yml` (build locally, expose ports, `stub` recognizer) and `docker-compose.prod.yml` (pull pinned images, `restart: always`, `fast_alpr`, vision kept internal). `REGISTRY`/`TAG` come from env, so a deploy diff --git a/wiki/decisions/fleet-deployment-komodo.md b/wiki/decisions/fleet-deployment-komodo.md index 33b3968..69bbbb1 100644 --- a/wiki/decisions/fleet-deployment-komodo.md +++ b/wiki/decisions/fleet-deployment-komodo.md @@ -2,7 +2,7 @@ type: decision tags: [parking, deployment, fleet, komodo, netbird, offline-first, threat-model] sources: [] -updated: 2026-06-27 +updated: 2026-06-29 status: settled --- @@ -49,13 +49,37 @@ Komodo Core (off-site) ──── NetBird mesh ─┼─▶ Periphery @ booth- 1. **Fleet size: many/growing.** Komodo is treated as load-bearing infrastructure, not a convenience. This is what tips the decision away from "SSH-over-NetBird + a playbook". 2. **Deploy trigger: always manual + pinned.** **No deploy webhook on a booth Stack.** A human - deploys a specific immutable `TAG=dev-` from Core. This preserves the determinism we - chose when pinning the booth tag (a moving `:dev` auto-redeploying a production booth is the - surprise we explicitly rejected). A *staging* booth MAY track `:dev`; a production booth - never does. -3. **Secrets: Komodo-managed (per-booth, unique).** Core's secret store injects `JWT_SECRET` - and `EVENT_SIGNING_KEY` into the Stack at deploy. This scales (no SSH-to-N-booths to rotate - a key) — but see the threat-model tension below; the keys MUST be **distinct per booth**. + deploys a specific immutable `TAG=-` from Core. This preserves the determinism we + chose when pinning the booth tag (a moving tag auto-redeploying a booth is the surprise we + explicitly rejected). This holds for **every** booth — including the **staging** booth: it tracks + the `stage` *branch* but still runs a **pinned `stage-`** (an earlier sketch floated a moving + `:dev` + webhook for staging; rejected in favour of pinned-everywhere). See "Promotion tiers". +3. **Secrets: Komodo-managed (per-booth, unique).** Core's secret store injects `JWT_SECRET`, + `EVENT_SIGNING_KEY` and `BACKUP_KEY` into the Stack at deploy. This scales (no SSH-to-N-booths to + rotate a key) — but see the threat-model tension below; the keys MUST be **distinct per booth**. + +## Promotion tiers — dev → stage → main (settled 2026-06-29) + +Three git branches map to three image tags and three booth roles: + +| Branch | Image tag | Role | Deploy | +| --- | --- | --- | --- | +| `dev` | `:dev` / `:dev-` | working branch | no booth runs it | +| `stage` | `:stage` / `:stage-` | **staging booth (`park-buzi`)** — real-world test | **manual + pinned** `stage-` | +| `main` | `:main` / `:main-` | **production** booths | manual + pinned `main-` | + +- **Promotion is a merge, not a build trigger.** When `dev` is confident-ready, **merge `dev → stage`**; + CI (`build-images.yml`, which now triggers on `dev`/`stage`/`main`) builds `:stage` + `:stage-`; + then **bump `TAG=stage-`** in `komodo/resources.toml` and deploy from Core. `main` only ever + receives what survived staging. +- **`stage` was branched from `dev`** (2026-06-29) so the first real-world test carries the full current + app, not a `main` that predates this work. `park-buzi`'s Stack `branch` + `TAG` both point at `stage`. +- **DB migrations run at boot** (`migrate-runtime.mjs`), so a promotion auto-migrates the staging booth's + SQLite ledger — staging is exactly where a bad migration is caught before production. +- **Per-booth secrets must pre-exist** in Core for `park-buzi`: `[[park_buzi_jwt_secret]]`, + `[[park_buzi_event_signing_key]]`, `[[park_buzi_backup_key]]` — distinct, never shared. Once the booth + signs real entries under its `EVENT_SIGNING_KEY`, that key is load-bearing for its ledger forever + (escrow it; see [[backup-recovery]]). ## Why this is safe (against the project's two forces) @@ -136,8 +160,11 @@ here so it isn't re-litigated. then central secrets carry the blast-radius noted above. - **Periphery hardening checklist** folded into [[disk-os-hardening]] (interface binding, passkey, TLS, agent as TCB). -- **Staging vs production booth split** (a staging booth on `:dev` with a webhook; production - manual+pinned) — not yet modelled in `komodo/`. +- **Staging vs production booth split** — ✅ MODELLED 2026-06-29 (see "Promotion tiers" below). + `park-buzi` is the first **staging** booth, tracking the `stage` branch / `:stage` image, deployed + **manual + pinned** (`TAG=stage-`, no webhook — we hold the no-moving-tag-on-a-booth line even + on staging, *not* the webhook-on-staging option the earlier sketch floated). `komodo/resources.toml` + + `komodo/README.md` updated. - **Core backup / DR** — Core is now Tier-0; its loss = no fleet management (operation unaffected, per offline-first). Backup story TBD. diff --git a/wiki/log.md b/wiki/log.md index 3a1cf7d..8b8af4f 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -1962,3 +1962,17 @@ komodo/.env.komodo.example as the ONLY backup env var — target+retention are U trimmed to just BACKUP_KEY. build/lint/test green (218 server tests, incl. retention persist/reset/negative + updated status shape). NOTE: dev API process was down after this round (live process, not code) — verified via the full test harness, not a live click-through this time. Updated [[backup-recovery]] as-built. + +## [2026-06-29] decision | Staging tier: dev → stage → main; park-buzi is the staging booth +Modelled the staging-vs-production split that fleet-deployment-komodo flagged as open. THREE tiers: dev +(working, no booth runs it) → stage (staging booth park-buzi, real-world test) → main (production, manual+ +pinned). park-buzi tracks the `stage` branch + `:stage` image but is deployed MANUAL + PINNED (TAG=stage-, +NO webhook — we hold the no-moving-tag-on-a-booth line even on staging, rejecting the earlier 'webhook on +staging' sketch). Promotion = merge dev→stage when confident → CI builds :stage/:stage- → bump TAG in +resources.toml → deploy from Core. `stage` branched from dev HEAD (84f00db) so the first real-world test +carries the full current app. Changes: build-images.yml triggers on [dev, stage, main] (tagging already +branch-derived, so :stage works with no other change); komodo/resources.toml park-buzi branch=stage + +TAG=stage-84f00db; komodo/README.md promotion section + per-booth secret list now includes backup_key; +fleet-deployment-komodo open-item resolved + new 'Promotion tiers' table; container-deployment tag list + +:stage. Per-booth secrets (jwt/event_signing/backup) must pre-exist in Core for park-buzi; migrations run at +boot so a promotion auto-migrates the staging ledger (where a bad migration is caught before prod).