df54bbaacf
Captures design intent, gotchas, and references that aren't obvious from code alone — single-worker queue rationale, defense-in-depth security model, the not-yet-wired selfupdate package, and pointers to the design spec and implementation plan in docs/superpowers/.
4.0 KiB
4.0 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 → httpinternal/api— HTTP handlers, bearer-token auth, request-id + access-log middlewareinternal/config— env-var loading; fails fast ifUPDATER_API_KEYmissinginternal/discovery— given an image, return ComposeJobs to run (label parsing, path check, dedup)internal/updater— FIFO queue + single worker +docker composesubprocess executorinternal/selfupdate— flush-then-exec wrapper for updating ourselves (NOT wired in live, see gotcha)internal/metrics— Prometheus collectorsinternal/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 — twodocker composecalls 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
/updateonly./healthz,/metrics,/versionare intentionally unauthenticated (internal network, scraper/healthcheck need them). Seecmd/server/main.gorouteAuth. metrics *Metricsparameters 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.
Gotchas
internal/selfupdate.Wrappedis 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:v1where the first colon is a port, not a tag. Seeinternal/discovery/matching.goNormaliseImage. - Path safety uses
filepath.Rel+ ".." prefix check, NOTstrings.HasPrefix. Prevents the/foo-evilvs/fooconfusion. Seeinternal/discovery/pathcheck.go. STACKS_ROOTdefaults to/home/shcizo/self-hostedin prod butenv.samplesuggests/tmpfor smoke tests. The default inconfig.gois the prod value — set explicitly in test/dev envs.- Go 1.26.3 (
go.mod). Dockerfile pinsgolang: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.).
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.