Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/helm-chart.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: helm chart

on:
workflow_dispatch:
push:
branches: [main]
paths:
- "deploy/charts/buzz/**"
- ".github/workflows/helm-chart.yml"
- "ct.yaml"
pull_request:
paths:
- "deploy/charts/buzz/**"
- ".github/workflows/helm-chart.yml"
- "ct.yaml"

jobs:
lint-and-unittest:
name: lint + unittest + render matrix
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
with:
version: v3.16.4

- name: Install helm-unittest plugin
run: helm plugin install --version 0.8.2 https://github.com/helm-unittest/helm-unittest

- name: Set up Python (for chart-testing)
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Set up chart-testing
uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0

- name: Build chart dependencies
run: helm dependency build deploy/charts/buzz

- name: ct lint
run: ct lint --config ct.yaml --all

- name: helm-unittest
run: helm unittest deploy/charts/buzz

- name: helm template (render every fixture)
run: |
set -euo pipefail
for f in deploy/charts/buzz/ci/*-values.yaml deploy/charts/buzz/tests/fixtures/*-values.yaml; do
echo "::group::render $f"
helm template buzz deploy/charts/buzz -f "$f"
echo "::endgroup::"
done

install-on-kind:
# Full end-to-end install requires the public ghcr.io/block/buzz image to
# exist and to embed Max's startup migrations. Runs only after Sami's
# image PR merges (`workflow_dispatch`) or on a schedule once main carries
# both prerequisites. Render/lint above is the per-PR signal.
name: install on kind (gated)
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
needs: lint-and-unittest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
with:
version: v3.16.4

- name: Set up Python (for chart-testing)
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Set up chart-testing
uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0

- name: Create kind cluster
uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0
with:
version: v0.24.0
node_image: kindest/node:v1.31.0

- name: Build chart dependencies
run: helm dependency build deploy/charts/buzz

- name: ct install (quickstart profile)
run: ct install --config ct.yaml --charts deploy/charts/buzz --helm-extra-args "--timeout 600s"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@ identity.key

# mesh-llm build cache
.cache/

# Helm dependency tarballs — regenerable from Chart.lock via `helm dependency build`
deploy/charts/*/charts/*.tgz
9 changes: 9 additions & 0 deletions ct.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
chart-dirs:
- deploy/charts
chart-repos: []
helm-extra-args: --timeout 600s
target-branch: main
# Skip OCI subchart fetches that need docker.io anonymous access — ct
# resolves these locally via `helm dependency build` in the workflow.
remote: origin
validate-maintainers: false
9 changes: 9 additions & 0 deletions deploy/charts/buzz/Chart.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
dependencies:
- name: postgres
repository: oci://registry-1.docker.io/cloudpirates
version: 0.19.5
- name: redis
repository: oci://registry-1.docker.io/cloudpirates
version: 0.30.3
digest: sha256:9c0df32008f782064104ecec8552f348841f120852d49ace66966a56ba904348
generated: "2026-06-11T15:45:41.764379-04:00"
42 changes: 42 additions & 0 deletions deploy/charts/buzz/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
apiVersion: v2
name: buzz
description: |
Buzz — a Nostr-based messaging platform for human–agent collaboration.

A single relay binary serving WebSocket + REST + web UI, backed by
PostgreSQL, Redis, and Typesense. Configurable for single-node evaluation
(subcharts on) and HA production (external services, existingSecret).
type: application
version: 0.1.0
appVersion: "0.1.0"
home: https://github.com/block/buzz
sources:
- https://github.com/block/buzz
keywords:
- nostr
- relay
- messaging
- websocket
- chat
maintainers:
- name: Block
url: https://github.com/block
annotations:
artifacthub.io/changes: |
- kind: added
description: Initial chart for Buzz relay.
artifacthub.io/license: Apache-2.0

# Optional eval-only subcharts. Production deploys disable both and point
# externalPostgresql / externalRedis (or secrets.existingSecret) at managed
# services.
dependencies:
- name: postgres
version: "0.19.x"
repository: oci://registry-1.docker.io/cloudpirates
condition: postgresql.enabled
alias: postgresql
- name: redis
version: "0.30.x"
repository: oci://registry-1.docker.io/cloudpirates
condition: redis.enabled
97 changes: 97 additions & 0 deletions deploy/charts/buzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Buzz Helm Chart

[Buzz](https://github.com/block/buzz) is a Nostr-based messaging platform for human–agent collaboration: a single relay binary serving WebSocket + REST + web UI, backed by PostgreSQL, Redis, Typesense, 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/Typesense/S3, `secrets.existingSecret:`, no chart-side autogen, HA-capable (`replicaCount ≥ 2`) |
| **Quickstart** (`--set quickstart=true`) | Eval, single-node, one-off demo | In-cluster Postgres + Redis subcharts ([CloudPirates](https://github.com/cloudpirates)), chart auto-generates relay secrets, single replica |

## Quickstart (eval only)

```sh
helm install buzz oci://ghcr.io/block/buzz/charts/buzz --version 0.1.0 \
--create-namespace --namespace buzz \
--set quickstart=true \
--set postgresql.enabled=true \
--set redis.enabled=true \
--set relayUrl=wss://buzz.example.com \
--set ownerPubkey=<64-char-hex-pubkey> \
--set typesense.url=http://typesense.buzz.svc.cluster.local:8108 \
--set typesense.apiKey=<typesense-key>
```

Quickstart still requires an externally managed Typesense in v1; bring up a minimal Typesense Pod/StatefulSet in your namespace, or set `typesense.url` and `typesense.apiKey` to a hosted instance. See the open question in `OPEN_QUESTIONS` at the bottom of this README.

## 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.

See:

- [`examples/argocd-app.yaml`](examples/argocd-app.yaml) — ArgoCD Application
- [`examples/flux-helmrelease.yaml`](examples/flux-helmrelease.yaml) — Flux HelmRelease v2
- [`examples/secret-sample.yaml`](examples/secret-sample.yaml) — Secret schema

## 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` / `typesense.url` | External service URLs | When the matching subchart is disabled (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`).

## HA (production)

`replicaCount > 1` hard-requires both:

- Redis (`redis.enabled=true`, `externalRedis.url`, or `REDIS_URL` in `existingSecret`) — for `buzz-pubsub` fan-out
- ReadWriteMany git PVC — `persistence.git.accessMode: ReadWriteMany` with a RWX storage class (e.g. `efs-sc` on AWS, `azurefile-csi` on Azure)

The chart **template-fails** if either invariant is broken. No silent degradation.

## 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.

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)

- **Typesense has no in-chart subchart.** Bring your own Typesense; the chart wires it via `typesense.url` + `typesense.apiKey` (or `TYPESENSE_URL` / `TYPESENSE_API_KEY` in `existingSecret`). The roadmap depends on either an upstream community chart hitting our quality bar or a minimal in-chart StatefulSet behind a quickstart flag.
- **Minimal-mode is not yet supported.** The relay's `BUZZ_PUBSUB=local` / `BUZZ_SEARCH=pg` / filesystem media paths are upstream work in progress. Until then, "quickstart" still needs Typesense.
- **OCI publish to GHCR + cosign signing** is a follow-up PR. For now, install the chart from source: `helm install buzz ./deploy/charts/buzz` after cloning the repo.

## Development

```sh
# 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 .
```
19 changes: 19 additions & 0 deletions deploy/charts/buzz/ci/quickstart-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Quickstart / eval: subcharts on, autogen secrets, single replica.
# This is the scenario `ct install` exercises against a kind cluster — it
# spins up postgres + redis in-cluster so the relay can actually start.
quickstart: true
postgresql:
enabled: true
redis:
enabled: true
relayUrl: wss://buzz.test.local
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000001"
typesense:
url: "http://typesense.default.svc.cluster.local:8108"
apiKey: "ci-fake-key"
relay:
# Don't enforce membership in CI — we're testing the chart renders and the
# Pod starts, not relay business logic.
requireRelayMembership: false
podDisruptionBudget:
enabled: false
76 changes: 76 additions & 0 deletions deploy/charts/buzz/examples/argocd-app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# ArgoCD Application — GitOps-safe Buzz install.
#
# Prerequisite: a Secret named `buzz-secrets` in namespace `buzz` containing
# (any subset of) the keys consumed by `secrets.existingSecret`. See
# `secret-sample.yaml` for the schema.
#
# Why `existingSecret` and not chart autogen: ArgoCD renders manifests with
# `helm template`, in which mode Helm's `lookup` function returns empty and
# any chart-side `randAlphaNum` call regenerates on every sync. The
# chart-managed Secret path is for `helm install` / `helm upgrade` only.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: buzz
namespace: argocd
spec:
project: default
source:
repoURL: oci://ghcr.io/block/buzz/charts
chart: buzz
targetRevision: 0.1.0
helm:
releaseName: buzz
values: |
relayUrl: wss://buzz.example.com
ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" # replace
replicaCount: 3

secrets:
existingSecret: buzz-secrets

externalPostgresql:
# DATABASE_URL also lives in buzz-secrets; this is here only if you
# prefer the URL stored in values vs. the Secret. Pick one.
url: ""

externalRedis:
url: ""

typesense:
url: "http://typesense.buzz.svc.cluster.local:8108"
# apiKey lives in buzz-secrets

s3:
endpoint: "https://s3.us-east-1.amazonaws.com"
bucket: "buzz-media"
# accessKey / secretKey live in buzz-secrets

persistence:
git:
enabled: true
accessMode: ReadWriteMany # required: replicaCount > 1
storageClass: efs-sc # provider-specific RWX class
size: 50Gi

ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
# WebSocket: long-lived; raise proxy timeouts.
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
tls:
- hosts: [buzz.example.com]
secretName: buzz-tls
destination:
server: https://kubernetes.default.svc
namespace: buzz
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
Loading
Loading