commits
The broker's subscriber map accumulated empty slices for paths that no
longer had any subscribers. Now it deletes the key when the last one
unsubscribes. The Redis buffer key also had no expiry, so it would
stick around forever after all instances shut down — now it gets a 24h
TTL refreshed on each publish.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wicket was single-replica only — the Broker fanned out events directly
to local SSE subscriber channels, so only the replica that received a
POST could deliver to its subscribers. This adds a Backend interface
that decouples event distribution from local subscriber management,
with two implementations:
- MemoryBackend (default): same behavior as before, ring buffer +
in-process fan-out. Extracts RingBuffer and pathMatches from broker.go.
- RedisBackend: uses Redis pub/sub for cross-replica delivery and a
Redis LIST for replay buffering. Pipelined LPUSH+LTRIM+PUBLISH keeps
it atomic. Tested with miniredis, including a two-backend
cross-replica delivery test.
The Broker now takes a Backend and has a Start(ctx) method that reads
from the backend's event channel. A monotonic sequence counter prevents
duplicate delivery when backend-buffered events race with Last-Event-ID
replay. New -backend and -redis-url CLI flags wire it up in main.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Text payloads (text/*, form-urlencoded, XML) are now stored as plain
strings instead of being base64-encoded, which makes them readable on
the subscriber side. Binary payloads still get base64. Events also
carry the HTTP method now, and verification failures log the request
headers to help debug signature mismatches.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LookupSubscribeSecret already walks up the path hierarchy, but
LookupVerification required an exact match. If you configured
verification at `github.com`, a POST to `github.com/org/repo` would
skip signature checking entirely. Now it uses the same walk-up loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Returns 204 No Content, keeping it simple so the kubelet doesn't
need to parse anything.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
go install puts the binary in $GOPATH/bin, which isn't on PATH in the
nixery environment. Adding it explicitly before calling ko.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After tests pass, the CI pipeline now builds an OCI image with ko and
pushes it to atcr.io/guid.foo/wicket. Tags each image as :latest and
with a timestamp+sha tag (e.g. 20260304201500-abc1234) so FluxCD's
image automation can pick up new builds in order.
Uses chainguard/static as the base image via .ko.yaml — same idea as
our scratch Dockerfile but with tzdata and CA certs baked in.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
check-coverage uses grep, awk, and tr, which aren't in a bare nixery
go image. Also stopped running go test twice — check-coverage already
runs it internally.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Runs tests with coverage on push/PR to main, using the free
spindle.tangled.sh instance. This mirrors what the pre-commit hooks
already enforce locally — just making sure it's gated in CI too.
Docker build + push to ATCR is a follow-up once we sort out the
auth model there.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Runs os.ExpandEnv on the raw YAML before unmarshaling, so secrets can
come from environment variables (e.g. ${WEBHOOK_SECRET}) rather than
being hardcoded. Handy for k8s where the config lives in a ConfigMap
but secrets come from Secret objects mapped to env vars.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WatchConfiguration now uses fsnotify to watch for config file changes
and automatically reload. The SSE handler uses comma-ok for the Flusher
type assertion (returns 500 if unsupported) and checks json.Marshal
errors instead of silently discarding them.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GET /test/ and POST /test/topic/ now normalize to test and test/topic.
Without this, subscribing to /test/ wouldn't receive events published
to /test/topic because the path hierarchy walk never matched "test/".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All core components: broker (hierarchical topic tree with ring buffer
replay), configuration (YAML loading with path hierarchy lookups),
signature verification (HMAC-SHA256/SHA1), query filters (dot-path
equality matching), HTTP server (POST to publish, GET SSE to subscribe,
CORS, auth), and the main entrypoint with signal handling.
100% test coverage (excluding main.go) enforced by check-coverage script.
68 tests across 7 test files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set up wicket as a Go module with build tooling: pre-commit hooks
for gofmt, go vet, staticcheck, loq, and go test. Minimal main.go
that parses the planned CLI flags. Two-stage Dockerfile for scratch-
based container builds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The broker's subscriber map accumulated empty slices for paths that no
longer had any subscribers. Now it deletes the key when the last one
unsubscribes. The Redis buffer key also had no expiry, so it would
stick around forever after all instances shut down — now it gets a 24h
TTL refreshed on each publish.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wicket was single-replica only — the Broker fanned out events directly
to local SSE subscriber channels, so only the replica that received a
POST could deliver to its subscribers. This adds a Backend interface
that decouples event distribution from local subscriber management,
with two implementations:
- MemoryBackend (default): same behavior as before, ring buffer +
in-process fan-out. Extracts RingBuffer and pathMatches from broker.go.
- RedisBackend: uses Redis pub/sub for cross-replica delivery and a
Redis LIST for replay buffering. Pipelined LPUSH+LTRIM+PUBLISH keeps
it atomic. Tested with miniredis, including a two-backend
cross-replica delivery test.
The Broker now takes a Backend and has a Start(ctx) method that reads
from the backend's event channel. A monotonic sequence counter prevents
duplicate delivery when backend-buffered events race with Last-Event-ID
replay. New -backend and -redis-url CLI flags wire it up in main.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Text payloads (text/*, form-urlencoded, XML) are now stored as plain
strings instead of being base64-encoded, which makes them readable on
the subscriber side. Binary payloads still get base64. Events also
carry the HTTP method now, and verification failures log the request
headers to help debug signature mismatches.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LookupSubscribeSecret already walks up the path hierarchy, but
LookupVerification required an exact match. If you configured
verification at `github.com`, a POST to `github.com/org/repo` would
skip signature checking entirely. Now it uses the same walk-up loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After tests pass, the CI pipeline now builds an OCI image with ko and
pushes it to atcr.io/guid.foo/wicket. Tags each image as :latest and
with a timestamp+sha tag (e.g. 20260304201500-abc1234) so FluxCD's
image automation can pick up new builds in order.
Uses chainguard/static as the base image via .ko.yaml — same idea as
our scratch Dockerfile but with tzdata and CA certs baked in.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Runs tests with coverage on push/PR to main, using the free
spindle.tangled.sh instance. This mirrors what the pre-commit hooks
already enforce locally — just making sure it's gated in CI too.
Docker build + push to ATCR is a follow-up once we sort out the
auth model there.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Runs os.ExpandEnv on the raw YAML before unmarshaling, so secrets can
come from environment variables (e.g. ${WEBHOOK_SECRET}) rather than
being hardcoded. Handy for k8s where the config lives in a ConfigMap
but secrets come from Secret objects mapped to env vars.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WatchConfiguration now uses fsnotify to watch for config file changes
and automatically reload. The SSE handler uses comma-ok for the Flusher
type assertion (returns 500 if unsupported) and checks json.Marshal
errors instead of silently discarding them.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All core components: broker (hierarchical topic tree with ring buffer
replay), configuration (YAML loading with path hierarchy lookups),
signature verification (HMAC-SHA256/SHA1), query filters (dot-path
equality matching), HTTP server (POST to publish, GET SSE to subscribe,
CORS, auth), and the main entrypoint with signal handling.
100% test coverage (excluding main.go) enforced by check-coverage script.
68 tests across 7 test files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>