diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml new file mode 100644 index 0000000..655356a --- /dev/null +++ b/.gitea/workflows/docker-build.yml @@ -0,0 +1,37 @@ +name: Docker Build + +on: + push: + tags: + - 'v*' + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - uses: docker/login-action@v3 + with: + registry: gitea.shcizo.se + username: ${{ gitea.actor }} + password: ${{ secrets.PACKAGES_TOKEN }} + + - uses: docker/metadata-action@v5 + id: meta + with: + images: gitea.shcizo.se/shcizo/package-updater + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7414ea9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,78 @@ +# 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/updater` — FIFO queue + single worker + `docker compose` subprocess executor +- `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. + +## 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.`). + +## 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.