Files
buzz/deploy/charts/buzz
ravarora2 4142d2b84e Use worker snapshots for relay storage metrics (#7845)
## Summary

Make the relay read completed storage-accounting snapshots from
PostgreSQL by default. An environment with no snapshot emits no storage
metrics. When a worker publishes its first result, the relay picks it up
on the next successful leader usage tick, without a restart or
configuration change.

This changes how storage usage is measured and reported. **S3 remains
the object store. PostgreSQL stores the completed counts, byte totals,
community breakdown, and calculation metadata—not the objects
themselves.**

### Why move relay storage accounting entirely to database reads?

Today the relay supports two sources: its own S3 scan (`inline`, the
default) and the worker's saved snapshot (`external`). That requires
operators to deploy a worker and then coordinate a separate relay-mode
change in each environment.

The worker already owns the S3 listing and calculation. It runs in a
separate process with its own object cap, memory limit, and deadline.
Keeping a second scan inside the serving relay retains the resource
pressure and failure modes that this isolation is meant to remove.

PostgreSQL provides a durable handoff. The worker replaces one completed
snapshot atomically after a successful calculation. The relay reads that
row and exposes its values through the existing metrics endpoint. A
relay restart or leadership change can load the last completed result
without scanning the bucket again.

There is deliberately no S3 fallback when a snapshot is absent or a read
fails. Such a fallback would put the expensive scan back in the relay
precisely when a worker is absent or unhealthy. It would also leave two
calculation paths with different resource limits and freshness behavior.

### What happens if no worker exists?

**A worker that has never published is a normal, successful inactive
state.** The snapshot table must exist through the normal database
schema, but it may contain zero rows. The reader returns success, emits
no storage totals or failed-load gauge, and checks again on the next
usage tick. It does not exit the relay, start a worker, or list S3.

The relay checks for a completed database row; it does not inspect
Kubernetes Jobs or CronJobs. Worker presence and snapshot presence are
therefore different:

| Situation | Storage-reader behavior |
|---|---|
| No worker and no snapshot row | Return success, emit no storage
metrics, and query again next tick. |
| Worker starts later and publishes its first row | Read and emit it on
the next successful leader usage tick; no restart or mode change. |
| Worker is removed or fails, but its last valid row remains | Continue
reporting the last completed totals with their increasing age. |
| Query succeeds but a previously present row has been removed | Clear
the cache and stop refreshing storage series; existing exporter series
expire through the idle timeout. |
| Database read fails, times out, or returns an invalid snapshot | Log a
warning, set `buzz_storage_snapshot_load_ok=0`, retain any last-good
cached totals, and retry next tick. |
| Snapshot table is missing | Treat this as a schema error, not normal
worker absence; follow the failed-read behavior above. |
| `BUZZ_STORAGE_METRICS=off` | Skip the snapshot query and all storage
metric emission. |

No row means “not measured,” not zero bytes. A valid completed snapshot
of an empty bucket can report zero. If a read fails before any good
snapshot has been cached, only failed-load health is emitted.

These reads happen in the existing background metrics task, not the
relay startup path. A storage-read failure does not request process exit
or change readiness directly. A wider database outage can still affect
the relay's existing database-dependent operations and readiness.

### Read, parse, and emit path

```text
Worker: S3 listing -> BucketSnapshot -> JSON -> PostgreSQL
Relay: PostgreSQL -> JSON -> BucketSnapshot -> existing metric gauges
Collector: relay /metrics endpoint -> Datadog
```

1. `main.rs::run_storage_sweep_tick` calls the reader from the existing
leader-only usage task. The default interval remains 300 seconds.
2. `storage_sweep.rs::run_storage_metrics_tick` coordinates the read,
cache update, failure health, and metric emission.
3. `refresh_persisted_snapshot` calls the existing
`Db::load_storage_accounting_snapshot()` method. A five-second timeout
covers pool acquisition and the query. It uses the relay's existing
pool.
4. The existing query in `buzz-db/src/store/storage_accounting.rs` reads
`snapshot`, `completed_at`, `duration_ms`, `max_objects`, and `code_sha`
from the singleton row.
5. `serde_json::from_value(stored.snapshot)` decodes the JSON into the
existing `buzz_media::BucketSnapshot` type. Rust infers that type from
`cache_persisted_snapshot`'s argument. The generated `Deserialize`
implementation handles all fields, including the UUID-keyed
`per_community` map.
6. `emit_cached_storage_metrics` sets the existing physical/logical
totals and community byte/object gauges. The existing Prometheus
exporter exposes them for Datadog collection; this code does not send
snapshot JSON to Datadog.

The worker's `BucketSnapshot`/`CommunityStorage` structures, JSON field
names, SQL publication method, and database schema are unchanged. The
old external-mode JSON conversion moves out of `main.rs` into the reader
helper. No new format or hand-written field parser is needed. Duration
and object-cap metadata are validated before replacing the cache.

The five-second relay read timeout is separate from the worker's
30-second startup acquisition budget introduced in #7770. This PR does
not change the worker's pool or startup logic.

### Configuration and code changes

| Configuration | Before | After |
|---|---|---|
| Unset, `inline`, or `on` | Relay-local S3 scan | Read completed
database snapshots. `inline` logs one migration warning. |
| `external` or `snapshot` | Read completed database snapshots |
Continue reading snapshots. |
| `off` | Disabled | Disabled. |
| Unknown or empty value | Disabled with a configuration error | Same. |

Existing `inline` settings intentionally become reader aliases. Merely
changing the default would leave older deployments on their explicit
`inline` setting and preserve the need for coordinated configuration
PRs.

Remove relay-local scan scheduling, in-flight scan state, scan
configuration, and obsolete scan-attempt tests and health metrics.
Preserve the physical/logical total and per-community metric names,
leader-only emission, community scope filtering, and cleanup of old
community labels.

The shared relay-state field remains in place; its comment now describes
a cached worker result. The Helm chart defaults to `relayMode: external`
while leaving `storageAccounting.enabled: false`. Its rendering test
verifies that the reader is configured even when no worker is created.
The reader regression tests cover missing rows, later publication,
updates, invalid data, timeout, removal, reactivation, and the explicit
off switch. `docs/storage-accounting.md` describes this operating model.

### Freshness and rollout impact

`buzz_storage_snapshot_age_seconds` uses the worker's original
completion timestamp. Re-reading an old row never makes it fresh.
`buzz_storage_snapshot_load_ok=1` means the row was read and decoded; it
does not prove that the worker's latest attempt succeeded.

Monitor snapshot age against the worker's schedule and allowed runtime,
and monitor Job failures separately. The old relay scan-attempt health
gauges are retired because the relay no longer runs those scans.

Deploy this relay version through the usual release process. Once it
reaches an environment, workers can be enabled independently.
Already-released charts that still set `inline` work with the new
binary; future chart releases default explicitly to `external`. An
intentional `off` override still requires an operator to enable the
reader.

**Environments with no completed snapshot lose relay-calculated storage
totals after this upgrade.** That is the intended tradeoff for removing
scans from the serving process. Environments with a prior snapshot
continue showing its last completed totals and age. This PR does not
install workers in additional environments or deploy the new relay
image.

### Related issue

Related to #4601. Follows the worker startup fix in #7770. The separate
fleet-usage collector work in #7176 overlaps this code and may need
reconciliation when it merges.

### Testing

Built and ran the relay locally against isolated PostgreSQL and Redis
with the legacy `inline` setting, five-second usage ticks, and a
15-second metric idle timeout. The same process was exercised through
these steps:

1. Start with an empty snapshot table: relay becomes ready and exposes
no storage series.
2. Publish a snapshot containing 123 bytes: the community gauge becomes
123.
3. Replace it with 456 bytes: the gauge becomes 456.
4. Remove the row: storage series expire without fabricating a zero-byte
measurement.
5. Publish 789 bytes: metrics reactivate without restarting the relay;
readiness remains healthy.

The local S3 endpoint recorded zero requests. The unrelated Git
conformance startup probe was disabled for this isolated reader check;
the test covers storage accounting, not every other use of S3 in the
relay. No new relay image was deployed to staging or production during
this test.

The broad local integration run also hit an existing
`p0_pool_acquisitions_use_typed_operation_pairs_without_other` failure
in `buzz-db/tests/observability_source.rs`. The same source-only test
fails on unmodified `main` at `0ef7a2222`; this PR changes neither the
test nor its failing source.

The full local `just ci` run stopped at mobile native-asset setup: the
`objective_c` build hook received no SDK path from `xcrun`. Mobile tests
did not run.

Generated with Codex

Signed-off-by: Ravneet Arora <rarora@squareup.com>
2026-09-23 14:39:39 -04:00
..

Buzz Helm Chart

Buzz is a Nostr-based messaging platform for human–agent collaboration: a single relay binary serving WebSocket + REST + web UI, backed by PostgreSQL, Redis, and S3-compatible object storage.

This chart has two operating profiles selected by values:

Profile When What you get
Production (default) Self-hosted multi-tenant, regulated, or GitOps-managed External managed Postgres/Redis/S3, secrets.existingSecret:, no chart-side autogen, HA-capable (replicaCount ≥ 2)
Quickstart (eval) Eval, single-node, one-off demo In-cluster Postgres + Redis + MinIO subcharts/Deployments, chart auto-generates relay + service secrets, single replica

Quickstart (eval only)

helm install buzz oci://ghcr.io/block/buzz/charts/buzz --version 0.1.8 \
  --create-namespace --namespace buzz \
  --set quickstart=true \
  --set postgresql.enabled=true \
  --set redis.enabled=true \
  --set minio.enabled=true \
  --set relayUrl=wss://buzz.example.com \
  --set ownerPubkey=<64-char-hex-pubkey>

This brings up everything in-cluster — Postgres, Redis, and MinIO (with its bucket created by a post-install Job) — and composes the relay's BUZZ_S3_ENDPOINT plus autogenerated credentials automatically. No external services required. The quickstart=true flag is an intent marker surfaced in NOTES.txt; the bundled services are opted in via the four *.enabled flags above (see ci/quickstart-values.yaml for the exact set CI installs). Eval-only: every bundled service is a single replica with no HA.

For immutable delivery, pin the OCI digest instead of a tag. image.digest overrides image.tag when both are present:

image:
  repository: ghcr.io/block/buzz
  digest: sha256:<64-lowercase-hex-characters>

Production (GitOps)

The chart is designed for ArgoCD and Flux. Both render charts with helm template, in which mode Helm's lookup function returns empty — any chart-side randAlphaNum call would regenerate secrets on every sync. The chart-managed Secret path is only safe for helm install / helm upgrade.

Production deploys MUST use secrets.existingSecret:. The Secret is consumed for any keys present and ignored for keys missing — extras are harmless.

To enable relay-proxied KLIPY search, add BUZZ_KLIPY_API_KEY to that Secret. The key stays in the relay pod; clients discover the public buzz-gif extension and gif descriptor in NIP-11, then receive KLIPY-hosted media URLs. See docs/gif-search.md for the protocol and security boundaries.

See:

Required inputs

Key What When required
relayUrl Public wss:// URL clients connect to Always
ownerPubkey 64-char lowercase hex Nostr pubkey of the relay operator When relay.requireRelayMembership=true (default)
secrets.existingSecret Name of pre-created Secret Production / GitOps
externalPostgresql.url / externalRedis.url / s3.endpoint External service URLs Production — when the matching bundled service is disabled (the default)

The chart fails at helm install / helm template time with a clear message if any of these are missing or malformed (see templates/_validate.tpl).

S3 URL addressing

Buzz uses one URL style for both media and Git/CAS object-store requests:

s3.addressingStyle Request shape Use for
path (default) https://endpoint/bucket/key Bundled MinIO and endpoints whose DNS does not resolve bucket subdomains
virtual https://bucket.endpoint/key AWS-style providers and new Railway Storage Buckets

The chart always renders s3.addressingStyle as BUZZ_S3_ADDRESSING_STYLE and s3.region as BUZZ_S3_REGION. The region defaults to us-east-1, keeping bundled MinIO and the in-pod buzz-admin deletions workflow operable without an ambient AWS_REGION. Production providers must set their credential region explicitly when it differs. Existing releases that previously omitted s3.region will begin rendering BUZZ_S3_REGION=us-east-1 after upgrade, even if an image or relay.extraEnv entry supplied AWS_REGION; set s3.region to the provider's actual credential region before upgrading. Only path and virtual addressing styles are accepted; invalid values fail chart rendering and relay startup. The bundled MinIO quickstart deliberately keeps path because its Service DNS resolves one endpoint hostname, not arbitrary <bucket>.<service> names.

For a Railway Storage Bucket, map its variables to chart values in the service or generated Helm configuration:

s3:
  endpoint: "${{Object Storage.ENDPOINT}}"
  bucket: "${{Object Storage.BUCKET}}"
  region: "${{Object Storage.REGION}}"
  addressingStyle: virtual

Store BUZZ_S3_ACCESS_KEY=${{Object Storage.ACCESS_KEY_ID}} and BUZZ_S3_SECRET_KEY=${{Object Storage.SECRET_ACCESS_KEY}} in the Secret named by secrets.existingSecret. Railway's Credentials tab is authoritative for older buckets, which may still require path. The setting changes request routing and SigV4 signing, so do not put the bucket into s3.endpoint; pass Railway's base ENDPOINT and BUCKET separately.

Object storage is contacted during relay startup only when BUZZ_GIT_CONFORMANCE_PROBE is enabled (the relay default). A probe failure is startup-fatal, so Kubernetes readiness never opens. If an operator explicitly disables that probe through relay.extraEnv, /_readiness does not test object storage; configuration is still parsed strictly, but reachability and addressing errors surface on the first storage operation.

Early-startup telemetry contract

buzz_process_lifecycle JSON records are the authoritative history for the fixed phases crypto_init, tracing_init, config_load, key_load, and metrics_bind, plus the aggregate process_telemetry result. They use bounded status/reason values and never contain raw configuration, keys, URLs, or errors. These phases intentionally do not emit metrics. Most run before the Prometheus exporter exists, and one uniform log-only contract preserves every phase's real event time and failure without assigning an eventual scrape time to earlier work.

Readiness telemetry contract

Only requests served by the private health listener (BUZZ_HEALTH_PORT) emit rollout readiness telemetry. The compatibility /_readiness route on the public app listener returns health but does not change these metrics.

Metric Type Labels
buzz_readiness_checks_total counter reason from the closed readiness-reason set
buzz_readiness_dependency_checks_total counter dependency, typed bounded outcome
buzz_readiness_check_duration_seconds histogram check only
buzz_readiness_state gauge check only; latest publishable generation

The schema has a ceiling of 99 raw Prometheus series per pod: 12 overall reasons, 11 valid dependency/outcome pairs, 72 histogram series, and 4 gauges. Do not add pod, ReplicaSet, version, rollout, error text, SQL, URL, tenant, user, community, pubkey, header, query, or other request-controlled labels. Shutdown without dependency evaluation increments only buzz_readiness_checks_total{reason="shutting_down"} and sets the overall state to zero; it does not fabricate dependency failures or latency samples.

Operation-aware database pool acquisition contract

The operation-aware families separate four questions: when an operation asked for a connection, who is waiting now, how completed/abandoned attempts ended, and how long checkout waits took. Outcome remains on the terminal counter for historical deployment comparison; it is intentionally absent from the expensive duration histogram.

These families cover the explicitly routed deployment-critical operations listed below; they are not a count of every SQLx checkout in Buzz. In particular, a zero operation waiter does not prove that the shared SQLx pool has no uninstrumented waiter. Interpret it beside the pool active, idle, and maximum gauges when diagnosing total capacity pressure.

Metric Type Labels
buzz_db_pool_acquire_started_total counter pool_role, operation
buzz_db_pool_acquire_duration_seconds histogram pool_role, operation
buzz_db_pool_acquire_attempts_total counter pool_role, operation, outcome
buzz_db_pool_waiters gauge pool_role, operation; tracked operations only, periodically refreshed including zero

Outcomes are success, timeout, error, and cancelled. Operations are bootstrap, readiness, tenant_resolution, authentication, authorization, subscription_history, event_write, and maintenance. Only the following eleven pairs are valid:

writer/bootstrap                 reader/bootstrap
writer/readiness
writer/tenant_resolution
writer/authentication
writer/authorization             reader/authorization
writer/subscription_history      reader/subscription_history
writer/event_write
writer/maintenance

Nine finite checkout buckets plus +Inf, sum, and count yield 12 histogram series per valid pair. The new contract therefore has a hard ceiling of 198 raw Prometheus series per pod: 11 × (1 + 12 + 4 + 1). The two legacy acquisition families remain temporarily for dashboard compatibility and are not part of that new-family budget. No other operation or request-controlled/sensitive label is valid.

Writer connection setup contract

Writer connection setup is separate from checkout. At boot, SQLx constructs the writer pool and creates its minimum physical connections. Later it may create more when the pool grows or replaces a broken or expired connection. Every connected writer session must install the created-at floor, install the session timeouts, verify READ COMMITTED isolation, and reach ready before SQLx can give it to a caller.

Metric Type Labels
buzz_db_connection_step_started_total counter pool_role, step
buzz_db_connection_step_duration_seconds histogram pool_role, step
buzz_db_connection_step_attempts_total counter pool_role, step, outcome

The fixed steps are writer_pool, physical_connect, created_at_floor, session_timeouts, isolation, and ready. Measurable phases emit start, duration, and terminal evidence. physical_connect and ready are success milestones: SQLx 0.9 exposes after_connect only after DNS, network, TLS, and authentication finish, so Buzz does not invent separate timings for those internal phases. Raw connect failures before after_connect are classified on the aggregate writer_pool phase during initial construction. Outcomes are succeeded, failed, timed_out, and cancelled where valid.

For a measurable phase on one pod, subtract all terminal outcomes from its start counter to derive the number of attempts currently in progress. The session phases run in order, so a later phase start also proves the earlier phases succeeded for that attempt.

Four start counters, four 13-series histograms, and fifteen terminal counters create a hard ceiling of 71 raw Prometheus series per pod. Connection ordinals, database URLs, hosts, usernames, SQL, and raw errors are forbidden as metric labels.

When audit logging is enabled, the buzz_db_connection_* metric totals combine the main writer pool and the separate audit writer pool.

Physical database pool utilization and configuration contract

Every physical Postgres pool the relay owns reports current utilization, capacity, and configuration under role-labelled families. The role vocabulary is closed — writer, reader, audit, search — and all four roles are always present, so a series never appears or disappears when an optional pool is enabled or disabled. An unconfigured optional pool reports buzz_db_pool_configured 0 with zero utilization and capacity rather than going missing.

Metric Type Labels
buzz_db_pool_connections gauge pool_role, state
buzz_db_pool_max_connections gauge pool_role
buzz_db_pool_configured gauge pool_role; 1 configured, 0 not configured

States are idle and active; their sum is the pool's current size. buzz_db_pool_max_connections reports capacity separately, so summing the connections family counts each current connection exactly once. The three families are fixed at 16 raw Prometheus series per pod (4 × 2 current utilization plus 4 maximum capacity plus 4 configured). The audit and search roles are statistics-only: pool ownership, capacities, timeouts, and query routing are unchanged, and these roles emit no acquisition telemetry.

The pre-existing unlabelled buzz_db_pool_* (writer) and buzz_db_read_pool_* (reader) gauges are still exported unchanged for dashboard compatibility. The reader family remains absent when no read replica is configured; use buzz_db_pool_configured{pool_role="reader"} for the explicit signal.

Pool sizing and the aggregate connection budget across all four roles remain deployment configuration concerns. The relay does not enforce a combined connection ceiling.

Relay Pod extensions

The chart exposes narrow extension points for init containers, volumes, relay volume mounts, and image command/argument overrides. extraManifests creates independent Kubernetes resources but cannot modify the chart-managed relay Deployment. These extension values insert fields into that Deployment, avoiding duplication of its environment, probes, security context, secrets, and chart-owned volumes.

For example, an init container can copy a wrapper binary into a shared volume and make that wrapper the relay entrypoint:

extraInitContainers:
  - name: install-wrapper
    image: example.com/wrapper-init:v1
    args: [/opt/wrapper/wrapper]
    securityContext:
      runAsNonRoot: true
      runAsUser: 65532
      runAsGroup: 65532
      allowPrivilegeEscalation: false
      capabilities:
        drop: [ALL]
    resources:
      requests:
        cpu: 10m
        memory: 16Mi
    volumeMounts:
      - name: wrapper
        mountPath: /opt/wrapper

extraVolumes:
  - name: wrapper
    emptyDir: {}

relay:
  command: [/opt/wrapper/wrapper]
  args: [/usr/local/bin/buzz-relay]
  extraVolumeMounts:
    - name: wrapper
      mountPath: /opt/wrapper

These values are raw Kubernetes fragments rendered with toYaml, not tpl. The chart does not validate cross-field relationships: extension names must not collide with chart-owned containers or volumes, mounts must reference existing volumes, and each init container must define an appropriate security context and resources. Empty relay.command and relay.args arrays preserve the image defaults; non-empty values override its entrypoint and arguments respectively.

Device pairing relay

The chart can run Buzz's stateless pairing WebSocket relay as an independent Deployment and Service using the same image as the main relay:

pairingRelay:
  enabled: true
  url: wss://pairing.example.com

pairingRelay.url is advertised in the main relay's NIP-11 document so Buzz clients connect directly to the dedicated endpoint. The chart does not create an Ingress or HTTPRoute for the pairing Service; route the public hostname to <release>-buzz-pairing:5000 with your platform's ingress configuration.

HA (production)

replicaCount > 1 hard-requires Redis:

  • Redis (redis.enabled=true, externalRedis.url, or REDIS_URL in existingSecret) — for buzz-pubsub fan-out

It does not require ReadWriteMany git storage. Git ref/object state is object-store-backed (each request hydrates an ephemeral repo from S3-compatible storage; writer serialization is the object-store pointer CAS — see docs/git-on-object-storage.md), and repo-name uniqueness lives in Postgres. Each replica can use its own ReadWriteOnce volume; no shared filesystem is needed.

The chart template-fails if the Redis invariant is broken at replicaCount > 1. No silent degradation.

Relay autoscaling

The optional HPA scales the relay on the larger recommendation from CPU or average active WebSockets per pod:

autoscaling:
  enabled: true
  minReplicas: 5
  maxReplicas: 15
  targetCPUUtilizationPercentage: 65
  websocketMetricEnabled: true
  websocketMetricName: buzz_ws_connections_active
  targetWebsocketConnections: 5000

CPU scaling requires Kubernetes Metrics Server. Set websocketMetricEnabled: false for a CPU-only HPA. WebSocket scaling additionally requires a custom-metrics adapter (for example Prometheus Adapter) configured to expose the relay's buzz_ws_connections_active gauge as a pod metric with the name in websocketMetricName. The chart creates the HPA but deliberately does not install or configure a cluster-wide metrics adapter. Scale-down is gradual by default so long-lived WebSocket connections have time to drain.

Upgrades

Schema migrations are embedded in the relay binary via sqlx::migrate! and run at startup, gated by BUZZ_AUTO_MIGRATE (default true). Multiple replicas race-safely behind a Postgres advisory lock. helm upgrade is the entire upgrade procedure.

Migration 0032 is a hard compatibility boundary for relay versions that publish repaired channel rosters. The relay verifies the roster-fence trigger catalog and behavior before opening listeners and refuses to start if 0032 is missing or inert. Apply migrations before rolling the relay; for large installations, prefer a controlled buzz-admin migrate job with PostgreSQL lock monitoring before the code rollout.

If you prefer decoupling migrations from serving, set migrate.autoMigrate=false. In that mode the chart does not run migrations for you — you own running buzz-admin migrate (separate Pod / one-shot Job) against the database before every helm install / helm upgrade. Readiness probes only verify DB connectivity, not schema freshness, so a pod will appear healthy against an unmigrated schema and fail under load. A pre-upgrade Helm Job for this is on the chart roadmap; the values knob migrate.preUpgradeJob.enabled is reserved.

Backups

Save these. Losing any of them is data loss. See NOTES.txt printed by helm install for the live list:

  1. BUZZ_RELAY_PRIVATE_KEY — relay identity. Rotating it = new identity (federation peers will not recognize the relay).
  2. PostgreSQL database — the canonical event store.
  3. S3 bucket — media blobs (chart default bucket: buzz-media).
  4. Git PVC — repo on-disk state served by the relay's git endpoint.
  5. Owner private key — held by the operator, not by this chart. Restore by re-installing with the same ownerPubkey.

Honest limitations (v1)

  • Bundled MinIO is eval-only. The quickstart profile runs an in-cluster MinIO (single replica, no HA, lookup-autogenerated credentials) so the relay starts with zero external object storage. Production leaves minio.enabled off and points s3.endpoint (or BUZZ_S3_* in existingSecret) at managed S3-compatible storage. The bundled Deployment is not GitOps-safe and is not intended for production traffic.
  • Minimal-mode is not yet supported. The relay's BUZZ_PUBSUB=local / filesystem media paths are upstream work in progress — even quickstart currently stands up real Redis and S3 rather than the relay's single-node fallbacks. (Full-text search already runs in Postgres, so no separate search service is provisioned.)
  • Cosign signing of the published chart is a follow-up (the relay image is attested via actions/attest-build-provenance; the chart is not yet). The chart itself is published to GHCR — see Releasing.

Releasing

The chart is published to GHCR as an OCI artifact at oci://ghcr.io/block/buzz/charts/buzz by the helm chart workflow (.github/workflows/helm-chart.yml), versioned independently of the desktop app and the relay image via its own chart-v* tags. Every PR/main push still lints, unit-tests, and render-checks the chart; only a chart-v* tag publishes, so an in-progress main can never overwrite a released version.

To cut a release, push a chart-release/<version> branch whose <version> matches Chart.yaml's version; merging it auto-tags chart-v<version> and dispatches the publish job (same lane machinery as the desktop and relay releases — see .github/workflows/auto-tag-on-release-pr-merge.yml). The publish job fails loudly if the tag version and Chart.yaml version disagree.

Development

# Render every fixture
for f in ci/*-values.yaml tests/fixtures/*-values.yaml; do
  helm template buzz . -f "$f" >/dev/null && echo "ok: $f"
done

# Unit tests
helm plugin install https://github.com/helm-unittest/helm-unittest
helm unittest .

# Lint
helm dependency build .
ct lint --config ../../../ct.yaml --charts .