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
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.
internal/server/config/config.go(or whereverCfglives) gains aBundledAssetsDir stringfield, defaulting to/opt/restic-manager/dist. Wire fromRM_BUNDLED_ASSETS_DIRenv var, mirroring the existing env-var conventions.internal/server/http/agent_assets.go:handleAgentBinary: try<DataDir>/agent-binaries/<name>first; onos.StatENOENT, try<BundledAssetsDir>/agent-binaries/<name>; on second ENOENT, existing 404.handleInstallAsset: same dual-path, withinstall/subpath.
- 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
cmd/server/main.go: keepvar version, addvar commit = "none"andvar date = "unknown". Surface via existing version-log line.cmd/agent/main.go: same three vars. Agent already reportsagent_versionin the WS hello — extend to include commit if it's already plumbed throughinternal/api; otherwise leave the commit out of the wire and just log it on startup.Makefile: extend themake build-ldflagsto set all three fromgit describe --tags --always+git rev-parse HEAD+ UTC timestamp. Source-build users get real values, not "dev".deploy/Dockerfile.server: addARG COMMIT=noneandARG 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
-
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/GOARCHbetween layers viaENV. Server build stays atGOOS=linux GOARCH=$TARGETARCH.) -
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.shdeploy/install/install.ps1→/opt/restic-manager/dist/install/install.ps1deploy/install/restic-manager-agent.service→/opt/restic-manager/dist/install/restic-manager-agent.service
-
Set
--chmod=0755on the agent binaries andinstall.sh,--chmod=0644on the unit file andinstall.ps1. Distroless final stage runs asnonroot; bundled assets are readable by anyone (modeo+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:
image: checkout → setup-qemu → setup-buildx → login → compute tags → buildx build+push.- (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
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).git add -A && git commit -m "p5-03: docker-only release path"(no Co-Authored-By trailer — CLAUDE.md rule).git push -u origin p5-03-docker-release.- Stop. Do not open a PR. Wait for operator review.