Files
restic-manager/docs/superpowers/plans/2026-05-05-p5-03-docker-only-release.md
T
steve 7cc17813a9 p5-03: docker-only release path (drop goreleaser)
Single public deliverable per tag: a multi-arch server image, with
cross-compiled agent binaries + install scripts + the systemd unit
baked under /opt/restic-manager/dist/. The /agent/binary and
/install/* handlers fall back from <DataDir>/... to that read-only
path so a fresh container Just Works without first-run staging;
operators can still drop a custom build into <DataDir>/ to override
per-host.

Architecture rationale: agent distribution already routes through
the running server, so the release surface mirrors that — there's
no second source of truth to keep in sync.

Workflow .gitea/workflows/release.yml triggers on v*.*.* tag-push
(fan-out :vX.Y.Z / :X.Y / :X, plus :latest once MAJOR>=1) and
workflow_dispatch (snapshot tag only). Pushes to the Gitea
container registry on this instance.

Both binaries grow main.commit + main.date ldflag targets. Makefile
and Dockerfile fill them; release workflow forwards from gitea.sha
plus a UTC timestamp.

Spec : docs/superpowers/specs/2026-05-05-p5-03-docker-only-release.md
Plan : docs/superpowers/plans/2026-05-05-p5-03-docker-only-release.md
2026-05-05 15:18:48 +01:00

5.0 KiB

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 <DataDir> 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 <DataDir>/agent-binaries/<name> first; on os.Stat ENOENT, try <BundledAssetsDir>/agent-binaries/<name>; 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:

    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:

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-<sha> 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.