feat(deploy): add stage tier — park-buzi as the staging booth
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-<sha> build with no other change. - komodo/resources.toml: park-buzi now branch=stage + TAG=stage-<sha> (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 <branch>-<sha>. - 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-<sha> 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
This commit is contained in:
@@ -1,13 +1,14 @@
|
|||||||
name: Build & push images
|
name: Build & push images
|
||||||
|
|
||||||
# Build the SERVER (API + SPA) and VISION (ANPR) container images and push them to the
|
# 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).
|
# house Gitea registry, tagged by BRANCH + short SHA (branch-aware: dev→:dev, stage→:stage,
|
||||||
# Separate from ci.yml (checks-only) and release.yml (tag-only desktop bundle). Mirrors the
|
# main→:main). Separate from ci.yml (checks-only) and release.yml (tag-only desktop bundle).
|
||||||
# house pattern (cf. trm/processor build.yml). See wiki/decisions/container-deployment.md.
|
# 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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [dev, main]
|
branches: [dev, stage, main]
|
||||||
paths:
|
paths:
|
||||||
- 'apps/server/**'
|
- 'apps/server/**'
|
||||||
- 'apps/web/**'
|
- 'apps/web/**'
|
||||||
|
|||||||
+23
-6
@@ -16,7 +16,22 @@ run `booth.sh`.
|
|||||||
Procedures), synced into Core via a **ResourceSync**. This is the reviewable, version-
|
Procedures), synced into Core via a **ResourceSync**. This is the reviewable, version-
|
||||||
controlled source of truth for *which booth runs what*.
|
controlled source of truth for *which booth runs what*.
|
||||||
- **`.env.komodo.example`** — the variables a Stack expects, documenting what comes from Core's
|
- **`.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-<sha>`. 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-<sha>`; then **bump `TAG=stage-<sha>` in `resources.toml`** to that build and deploy
|
||||||
|
from Core (manual, pinned — no webhook even on staging).
|
||||||
|
- **`main`** — vetted **production** booths: `:main-<sha>`, manual + pinned. `main` only gets what
|
||||||
|
survived staging.
|
||||||
|
|
||||||
|
> The `TAG=stage-<sha>` 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)
|
## 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
|
## Adding booth N
|
||||||
|
|
||||||
Copy the `[[stack]]` block, change `name`, `server` (its onboarded name), and the per-booth
|
Copy the `[[stack]]` block, change `name`, `server` (its onboarded name), and the per-booth
|
||||||
secret references (`[[park_<site>_jwt_secret]]`, `[[park_<site>_event_signing_key]]`). Create
|
secret references (`[[park_<site>_jwt_secret]]`, `[[park_<site>_event_signing_key]]`,
|
||||||
those secrets in Core's store first.
|
`[[park_<site>_backup_key]]`). Create those secrets in Core's store first. Set `branch` +
|
||||||
|
`TAG` for the tier the booth runs (staging → `stage` / `stage-<sha>`; production → `main` /
|
||||||
|
`main-<sha>`).
|
||||||
|
|
||||||
## Hard rules encoded here (do not relax without updating the decision page)
|
## 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-<sha>` before
|
1. **No deploy webhook on a booth Stack.** Deploys are a human action; pin an immutable
|
||||||
a production booth goes live. A moving `:dev` on a production booth is the non-determinism we
|
`TAG=<branch>-<sha>` (staging → `stage-<sha>`, production → `main-<sha>`). A moving tag on a
|
||||||
rejected. (`TAG=dev` here is fine while staging.)
|
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
|
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.
|
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
|
3. **Secrets are per-booth and unique.** `EVENT_SIGNING_KEY` signs the anti-fraud ledger — one
|
||||||
|
|||||||
+10
-5
@@ -18,9 +18,12 @@
|
|||||||
# EVENT_SIGNING_KEY signs the append-only anti-fraud ledger; BACKUP_KEY encrypts on-site DB
|
# 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).
|
# backups (separate from the signing key; escrow it offsite — recovery needs both).
|
||||||
#
|
#
|
||||||
# Deploys are MANUAL + PINNED in spirit: bump TAG to an immutable dev-<sha> before a
|
# Deploys are MANUAL + PINNED: park-buzi is the STAGING booth (real-world test of the app),
|
||||||
# production booth goes live (TAG=dev here is the moving tag, fine while staging). NO
|
# so it tracks the `stage` branch + the `:stage` image, but is still deployed by hand with a
|
||||||
# deploy webhook is attached to a booth Stack.
|
# PINNED immutable TAG=stage-<sha> (no webhook). Promotion: merge dev → stage when confident,
|
||||||
|
# CI builds :stage / :stage-<sha>, then bump TAG below to that sha and deploy from Komodo Core.
|
||||||
|
# A PRODUCTION booth tracks `main` + manual+pinned `:main-<sha>`. 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
|
# 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_provider = "git.infra.msai.al"
|
||||||
git_account = "komodo"
|
git_account = "komodo"
|
||||||
repo = "mca/parking_solution"
|
repo = "mca/parking_solution"
|
||||||
branch = "dev"
|
branch = "stage"
|
||||||
file_paths = [
|
file_paths = [
|
||||||
"docker-compose.yml",
|
"docker-compose.yml",
|
||||||
"docker-compose.prod.yml"
|
"docker-compose.prod.yml"
|
||||||
@@ -43,7 +46,9 @@ registry_provider = "git.infra.msai.al"
|
|||||||
registry_account = "komodo"
|
registry_account = "komodo"
|
||||||
environment = """
|
environment = """
|
||||||
REGISTRY=git.infra.msai.al/mca/parking_solution
|
REGISTRY=git.infra.msai.al/mca/parking_solution
|
||||||
TAG=dev
|
# Staging booth: pin an immutable stage-<sha> 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
|
COOKIE_SECURE=0
|
||||||
VISION_ENABLED=1
|
VISION_ENABLED=1
|
||||||
WS_ALLOWED_ORIGINS=
|
WS_ALLOWED_ORIGINS=
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ The **desktop** app stays on its own tag-only `release.yml` (Tauri installers),
|
|||||||
## Branch-aware (the user's hard requirement)
|
## Branch-aware (the user's hard requirement)
|
||||||
|
|
||||||
- **Image tags = branch + short SHA.** A push to `dev` builds `…/parking-server:dev` +
|
- **Image tags = branch + short SHA.** A push to `dev` builds `…/parking-server:dev` +
|
||||||
`…/parking-server:dev-<sha>`; `main` builds `:main` + `:main-<sha>`. The moving branch tag is the
|
`…/parking-server:dev-<sha>`; `stage` builds `:stage` + `:stage-<sha>`; `main` builds `:main` +
|
||||||
deploy pointer; the branch-SHA tag is the immutable record. Same for `parking-vision`.
|
`:main-<sha>`. 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
|
- **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,
|
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
|
`restart: always`, `fast_alpr`, vision kept internal). `REGISTRY`/`TAG` come from env, so a deploy
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
type: decision
|
type: decision
|
||||||
tags: [parking, deployment, fleet, komodo, netbird, offline-first, threat-model]
|
tags: [parking, deployment, fleet, komodo, netbird, offline-first, threat-model]
|
||||||
sources: []
|
sources: []
|
||||||
updated: 2026-06-27
|
updated: 2026-06-29
|
||||||
status: settled
|
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
|
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".
|
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
|
2. **Deploy trigger: always manual + pinned.** **No deploy webhook on a booth Stack.** A human
|
||||||
deploys a specific immutable `TAG=dev-<sha>` from Core. This preserves the determinism we
|
deploys a specific immutable `TAG=<branch>-<sha>` from Core. This preserves the determinism we
|
||||||
chose when pinning the booth tag (a moving `:dev` auto-redeploying a production booth is the
|
chose when pinning the booth tag (a moving tag auto-redeploying a booth is the surprise we
|
||||||
surprise we explicitly rejected). A *staging* booth MAY track `:dev`; a production booth
|
explicitly rejected). This holds for **every** booth — including the **staging** booth: it tracks
|
||||||
never does.
|
the `stage` *branch* but still runs a **pinned `stage-<sha>`** (an earlier sketch floated a moving
|
||||||
3. **Secrets: Komodo-managed (per-booth, unique).** Core's secret store injects `JWT_SECRET`
|
`:dev` + webhook for staging; rejected in favour of pinned-everywhere). See "Promotion tiers".
|
||||||
and `EVENT_SIGNING_KEY` into the Stack at deploy. This scales (no SSH-to-N-booths to rotate
|
3. **Secrets: Komodo-managed (per-booth, unique).** Core's secret store injects `JWT_SECRET`,
|
||||||
a key) — but see the threat-model tension below; the keys MUST be **distinct per booth**.
|
`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-<sha>` | working branch | no booth runs it |
|
||||||
|
| `stage` | `:stage` / `:stage-<sha>` | **staging booth (`park-buzi`)** — real-world test | **manual + pinned** `stage-<sha>` |
|
||||||
|
| `main` | `:main` / `:main-<sha>` | **production** booths | manual + pinned `main-<sha>` |
|
||||||
|
|
||||||
|
- **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-<sha>`;
|
||||||
|
then **bump `TAG=stage-<sha>`** 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)
|
## 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.
|
then central secrets carry the blast-radius noted above.
|
||||||
- **Periphery hardening checklist** folded into [[disk-os-hardening]] (interface binding, passkey,
|
- **Periphery hardening checklist** folded into [[disk-os-hardening]] (interface binding, passkey,
|
||||||
TLS, agent as TCB).
|
TLS, agent as TCB).
|
||||||
- **Staging vs production booth split** (a staging booth on `:dev` with a webhook; production
|
- **Staging vs production booth split** — ✅ MODELLED 2026-06-29 (see "Promotion tiers" below).
|
||||||
manual+pinned) — not yet modelled in `komodo/`.
|
`park-buzi` is the first **staging** booth, tracking the `stage` branch / `:stage` image, deployed
|
||||||
|
**manual + pinned** (`TAG=stage-<sha>`, 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
|
- **Core backup / DR** — Core is now Tier-0; its loss = no fleet management (operation
|
||||||
unaffected, per offline-first). Backup story TBD.
|
unaffected, per offline-first). Backup story TBD.
|
||||||
|
|
||||||
|
|||||||
+14
@@ -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
|
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
|
+ 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.
|
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-<sha>,
|
||||||
|
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-<sha> → 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).
|
||||||
|
|||||||
Reference in New Issue
Block a user