@@ -358,24 +358,85 @@ model:
358358- ** Per-chain config** — separate RPC endpoints, rate-limit tiers, resource
359359 requests, Prometheus pod labels.
360360
361- ### Kubernetes (production)
361+ ### Kubernetes
362362
363363Deployed via [ cowprotocol/infrastructure] ( https://github.com/cowprotocol/infrastructure )
364- using Pulumi (DNS, secrets) + Flux (k8s manifests). One ` Deployment ` + ` Service `
365- per chain in the ` balances-watcher ` namespace, with a shared ` Ingress ` routing
366- ` /<chain_id>/... ` and ` /sse/<chain_id>/... ` to the matching service.
364+ using Pulumi (DNS) + Flux (k8s manifests):
367365
368- Docker images are built and pushed to GHCR by ` .github/workflows/build-image.yml `
369- on push to ` main ` or on semver tags (` vX.Y.Z ` ). Flux picks up new image tags
370- from ` ghcr.io/cowprotocol/balances-watcher ` .
366+ - ** Staging** : ` balances-watcher.barn.cow.fi ` . Flux ` ImagePolicy ` watches GHCR
367+ and auto-opens a bump PR when a new semver tag lands (see the
368+ ` staging-auto-pr ` workflow in the infra repo).
369+ - ** Prod** : ` balances-watcher.cow.fi ` . ** No Flux image automation** on the prod
370+ cluster — the tag is pinned manually in ` cluster/prod/balances-watcher/…/kustomization.yaml ` ,
371+ bumped via an infra PR when someone decides to promote a version from staging.
372+ This is the deliberate promotion gate between the two environments.
371373
372- ### Releases
374+ One ` Deployment ` + ` Service ` per chain in the ` balances-watcher ` namespace,
375+ with a shared ` Ingress ` routing ` /<chain_id>/... ` and ` /sse/<chain_id>/... ` to
376+ the matching service. Each chain is a kustomize overlay (` nameSuffix: -mainnet `
377+ etc.) over one shared ` flux-apps/balances-watcher/ ` template.
373378
374- Versioning is fully automatic. Every merge to ` main ` triggers the ` release ` job
375- which bumps the minor version from the latest git tag (` v0.1.0 ` → ` v0.2.0 ` → …)
376- and pushes the new tag. The tag push re-triggers the build pipeline, producing
377- a GHCR image tagged with the semver version (` v0.2.0 ` , ` 0.2 ` ) alongside ` sha-xxx `
378- and ` latest ` .
379+ ### Release flow
380+
381+ Versioning is semantic and label-driven. Two workflows split the work:
382+
383+ - ** ` release.yml ` ** — fires on ` pull_request_target: closed ` when a PR is merged
384+ into ` main ` . Reads the merged PR's labels and picks the bump:
385+
386+ | PR label | Bump | Example |
387+ | ---| ---| ---|
388+ | ` breaking ` | MAJOR + 1 | ` v1.4.7 → v2.0.0 ` |
389+ | ` hotfix ` | PATCH + 1 | ` v1.4.7 → v1.4.8 ` |
390+ | (none) | MINOR + 1 (default) | ` v1.4.7 → v1.5.0 ` |
391+
392+ Then it creates the git tag, pushes it, and creates a GitHub Release with
393+ auto-generated notes (PR titles + authors between the previous Release and
394+ the new one).
395+
396+ - ** ` build-image.yml ` ** — fires on push to ` main ` (produces ` :sha-<7> ` +
397+ ` :latest ` ) and on push of a ` v* ` git tag (produces the semver tags
398+ ` :vX.Y.Z ` and ` :vX.Y ` ). Buildx multi-stage build → GHCR.
399+
400+ The two are decoupled: ` release.yml ` tags, tag push triggers ` build-image.yml ` .
401+
402+ #### Two gotchas worth knowing
403+
404+ 1 . ** Tag push must use a PAT, not ` GITHUB_TOKEN ` .** GitHub silently exempts
405+ ` GITHUB_TOKEN ` -driven pushes from triggering downstream workflows (loop
406+ protection). To make the tag push fire ` build-image.yml ` , ` release.yml `
407+ uses the ` RELEASE_TOKEN ` repo secret (fine-grained PAT with
408+ ` Contents: read/write ` on this repo). Rotate the PAT before its expiration
409+ or CI will silently stop bumping images.
410+
411+ 2 . ** Docker tag ` v ` -prefix is preserved intentionally.** ` docker/metadata-action `
412+ by default strips the ` v ` from semver docker tags (so ` v1.6.0 ` becomes
413+ ` :1.6.0 ` ), but our overlays and history use ` v1.5.0 ` , ` v1.4.0 ` etc. with
414+ the prefix. We keep the prefix via ` pattern=v{{version}} ` in the
415+ metadata-action config — if you touch that step, keep the ` v ` .
416+
417+ #### Skip-empty guard
418+
419+ ` release.yml ` short-circuits if ` HEAD ` already sits on the latest tag (`git
420+ diff --quiet $LATEST..HEAD`). This handles two cases: re-runs after a
421+ transient failure, and no-op merges (revert-then-reapply). Without it we'd
422+ tag the same commit under two versions.
423+
424+ #### Promoting staging → prod
425+
426+ Manual and intentional:
427+
428+ ``` bash
429+ # In cowprotocol/infrastructure
430+ git checkout -b promote/balances-watcher-v1.7.0
431+ # Bump: cluster/prod/balances-watcher/balances-watcher/kustomization.yaml
432+ # newTag: v1.6.1 → newTag: v1.7.0
433+ git commit -am " promote balances-watcher to v1.7.0 in prod"
434+ git push -u origin promote/balances-watcher-v1.7.0
435+ gh pr create --title " promote balances-watcher v1.7.0 → prod"
436+ ```
437+
438+ Reviewer approves, merges, Flux applies. No automation between the version
439+ existing on staging and it landing in prod — that's the gate.
379440
380441### docker-compose (local dev)
381442
0 commit comments