# package-updater — Claude context Webhook-driven Docker Compose updater. Go service, single binary, deployed as a container with `/var/run/docker.sock` mounted. See `README.md` for user-facing docs. This file is for working ON the code. ## Commands ```bash go test ./... # all tests, fast (no docker required) go test -run TestFoo ./internal/api # single test go build ./cmd/server # produces ./server docker build -t package-updater:dev . # multi-stage, builds golang:1.26-alpine # Local smoke test (builds from Dockerfile, runs against /var/run/docker.sock): cp env.sample .env && $EDITOR .env # fill UPDATER_API_KEY docker compose -f docker-compose.local.yml up --build curl -sH "Authorization: Bearer $UPDATER_API_KEY" \ -d '{"image":"foo"}' http://localhost:8080/update | jq ``` ## Architecture (one-line per package) - `cmd/server` — wiring only: config → docker client → discovery → queue → http - `internal/api` — HTTP handlers, bearer-token auth, request-id + access-log middleware - `internal/config` — env-var loading; fails fast if `UPDATER_API_KEY` missing - `internal/discovery`— given an image, return Compose `Job`s to run (label parsing, path check, dedup) - `internal/discovery/swarm.go` — SwarmDiscovery: same FindJobs signature, lists `docker service ls`, gates on service-level opt-in label instead of STACKS_ROOT path-check - `internal/updater` — FIFO queue + single worker + `docker compose` subprocess executor - `internal/updater/swarm_executor.go` — SwarmExecutor: `docker service update --image` via the Docker API instead of a `docker compose` subprocess - `internal/selfupdate` — flush-then-exec wrapper for updating ourselves (NOT wired in live, see gotcha) - `internal/metrics` — Prometheus collectors - `internal/logging` — slog JSON, request-id context propagation ## Design intent (do not break without discussion) - **Single FIFO worker by design** (`internal/updater/queue.go`). Spec §5.7. Never parallelise the queue — two `docker compose` calls against the same stack race. - **Defense in depth: token AND opt-in label AND STACKS_ROOT prefix** must all hold before a container is touched. Weakening any of these breaks the security model — discuss before changing. - **Auth on `/update` only**. `/healthz`, `/metrics`, `/version` are intentionally unauthenticated (internal network, scraper/healthcheck need them). See `cmd/server/main.go` `routeAuth`. - **`metrics *Metrics` parameters may be nil**; constructors and call sites check. Tests rely on this — don't drop the nil-check. - **Stateless**: no DB, no config file, no on-disk audit log. Docker daemon is the source of truth. - **`MODE` is exclusive because each instance owns exactly one Docker daemon.** A composite "handle both at once" mode has been considered and rejected: it would still only reach one daemon, so it buys nothing. Fleets run one instance per server and CI fans out. Reopen this only if a Swarm manager node starts running standalone Compose stacks locally. Rationale and rejected alternatives: `docs/superpowers/specs/2026-08-04-multi-server-fanout-design.md`. - **Swarm mode's security gate is opt-in label only** — there is no STACKS_ROOT-equivalent path check, since Swarm services have no local compose file. Don't add one; don't weaken Compose mode's three-factor gate to match. ## Gotchas - **`internal/selfupdate.Wrapped` is implemented and unit-tested but NOT wired into the live queue.** Spec §15 calls this out as a v1 gap. If you "fix" this, read the package doc — flush-before-exec ordering is subtle. - **Image matching is case-sensitive and tag/digest-agnostic.** The tricky case is `localhost:5000/foo:v1` where the first colon is a port, not a tag. See `internal/discovery/matching.go` `NormaliseImage`. - **Path safety uses `filepath.Rel` + ".." prefix check**, NOT `strings.HasPrefix`. Prevents the `/foo-evil` vs `/foo` confusion. See `internal/discovery/pathcheck.go`. - **`STACKS_ROOT` defaults to `/home/shcizo/self-hosted`** in prod but `env.sample` suggests `/tmp` for smoke tests. The default in `config.go` is the prod value — set explicitly in test/dev envs. - **Go 1.26.3** (`go.mod`). Dockerfile pins `golang:1.26-alpine`. Bumping one without the other has caused a fix commit already. - **`` C# convention from global CLAUDE.md does not apply here** — this is Go. Use idiomatic GoDoc (`// FuncName does X.`). - **Swarm mode requires manager-node API access.** `docker service update` fails with a permission error against a worker-only node. This is an operator/deployment concern (point `DOCKER_HOST` at a manager, or schedule the updater on a manager), not something the code can detect or work around. ## References - Design spec: `docs/superpowers/specs/2026-05-22-package-updater-design.md` (489 lines, authoritative) - Implementation plan: `docs/superpowers/plans/2026-05-22-package-updater-implementation.md` - Consumer-side CI integration: `gitea-action/` ## Conventions - Conventional Commits (`feat:`, `fix:`, `docs:`, `ci:`, `build:`, `chore:`). - One package = one responsibility; interfaces defined at consumer site (`api.Finder`, `api.Submitter`, `api.Pinger`) for testability. - Table-driven tests with `stretchr/testify`. No mocks beyond hand-written fakes.