# P5-03 implementation plan — Docker-only release Spec: `docs/superpowers/specs/2026-05-05-p5-03-docker-only-release.md`. Branch: `p5-03-docker-release`. Do not auto-open a PR (see CLAUDE.md memory: CI runs are expensive on the self-hosted cluster). --- ## Slice 1 — Server config + handler fallback **Goal:** server can serve agent binaries / install scripts from a read-only "bundled assets" path when `` doesn't have them. 1. `internal/server/config/config.go` (or wherever `Cfg` lives) gains a `BundledAssetsDir string` field, defaulting to `/opt/restic-manager/dist`. Wire from `RM_BUNDLED_ASSETS_DIR` env var, mirroring the existing env-var conventions. 2. `internal/server/http/agent_assets.go`: - `handleAgentBinary`: try `/agent-binaries/` first; on `os.Stat` ENOENT, try `/agent-binaries/`; on second ENOENT, existing 404. - `handleInstallAsset`: same dual-path, with `install/` subpath. 3. Tests in `internal/server/http/agent_assets_test.go` (new file): - DataDir hit serves DataDir bytes. - DataDir miss + bundled hit serves bundled bytes. - DataDir hit shadows bundled. - Both miss → 404 + existing error envelope. - Path-traversal still rejected for `install/*` (regression check). **Verify:** `go vet ./...` + `go test ./internal/server/http/...`. --- ## Slice 2 — Version ldflags on both binaries 1. `cmd/server/main.go`: keep `var version`, add `var commit = "none"` and `var date = "unknown"`. Surface via existing version-log line. 2. `cmd/agent/main.go`: same three vars. Agent already reports `agent_version` in the WS hello — extend to include commit if it's already plumbed through `internal/api`; otherwise leave the commit out of the wire and just log it on startup. 3. `Makefile`: extend the `make build` `-ldflags` to set all three from `git describe --tags --always` + `git rev-parse HEAD` + UTC timestamp. Source-build users get real values, not "dev". 4. `deploy/Dockerfile.server`: add `ARG COMMIT=none` and `ARG DATE=unknown`; pass through `-ldflags`. **Verify:** `make build && ./bin/restic-manager-server -version` (or whatever the existing flag is) prints non-`dev` values. --- ## Slice 3 — Dockerfile bakes agents + install assets 1. Build stage cross-compiles three agents: ```dockerfile RUN go build -trimpath -ldflags="-s -w \ -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" \ -o /out/agent/restic-manager-agent-linux-amd64 ./cmd/agent ENV GOARCH=arm64 RUN go build ... -o /out/agent/restic-manager-agent-linux-arm64 ./cmd/agent ENV GOOS=windows GOARCH=amd64 RUN go build ... -o /out/agent/restic-manager-agent-windows-amd64.exe ./cmd/agent ``` (Reset `GOOS`/`GOARCH` between layers via `ENV`. Server build stays at `GOOS=linux GOARCH=$TARGETARCH`.) 2. Final stage `COPY --from=build`: - `/out/restic-manager-server` → `/usr/local/bin/` - `/out/agent/*` → `/opt/restic-manager/dist/agent-binaries/` - `deploy/install/install.sh` → `/opt/restic-manager/dist/install/install.sh` - `deploy/install/install.ps1` → `/opt/restic-manager/dist/install/install.ps1` - `deploy/install/restic-manager-agent.service` → `/opt/restic-manager/dist/install/restic-manager-agent.service` 3. Set `--chmod=0755` on the agent binaries and `install.sh`, `--chmod=0644` on the unit file and `install.ps1`. Distroless final stage runs as `nonroot`; bundled assets are readable by anyone (mode `o+r`), so the user switch doesn't break reads. **Verify:** ```sh docker build -f deploy/Dockerfile.server -t rm:dev . docker run --rm -d -p 18080:8080 \ -e RM_LISTEN=:8080 -e RM_DATA_DIR=/data \ -e RM_BASE_URL=http://127.0.0.1:18080 \ -v rm-test:/data rm:dev curl -fsSL "http://127.0.0.1:18080/agent/binary?os=linux&arch=amd64" | wc -c curl -fsSL "http://127.0.0.1:18080/install/install.sh" | head -1 ``` Both should succeed against a fresh volume (no operator staging). --- ## Slice 4 — Release workflow `.gitea/workflows/release.yml` per the spec. Two jobs: 1. **`image`**: checkout → setup-qemu → setup-buildx → login → compute tags → buildx build+push. 2. (Future) `release-notes`: stub left as a TODO comment for now. Operator can hand-write release notes via the Gitea UI on first cut. The `compute tags` shell step is the only non-trivial bit; tested inline by running the script with mocked `GITHUB_REF_TYPE` / `GITHUB_REF_NAME` env vars before committing. **Verify on first dispatch:** trigger `workflow_dispatch` from the Gitea UI, check the runner produces `:snapshot-` and pushes multi-arch. --- ## Slice 5 — Tasks.md + commit + push 1. `tasks.md`: tick P5-03; add a one-line note that goreleaser was dropped in favour of Docker-only after a 2026-05-05 design pass (link the spec). 2. `git add -A && git commit -m "p5-03: docker-only release path"` (no Co-Authored-By trailer — CLAUDE.md rule). 3. `git push -u origin p5-03-docker-release`. 4. **Stop.** Do not open a PR. Wait for operator review.