Files
package-updater/CLAUDE.md
T

5.3 KiB

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

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 Jobs 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.
  • <summary> 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.