From fcd590899205ebe5bd21e51bfbf86911ae10c911 Mon Sep 17 00:00:00 2001 From: Samuel Enocsson Date: Tue, 4 Aug 2026 18:47:00 +0200 Subject: [PATCH] docs: design for multi-server deployment and CI fan-out One instance per server, each owning its local Docker daemon; Gitea CI posts /update to every instance. Records why MODE exclusivity is correct rather than a limitation, and the alternatives rejected. Claude-Session: https://claude.ai/code/session_01S3aqJ4tvaPezQhsGNCybut --- .../2026-08-04-multi-server-fanout-design.md | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-04-multi-server-fanout-design.md diff --git a/docs/superpowers/specs/2026-08-04-multi-server-fanout-design.md b/docs/superpowers/specs/2026-08-04-multi-server-fanout-design.md new file mode 100644 index 0000000..6e8ce41 --- /dev/null +++ b/docs/superpowers/specs/2026-08-04-multi-server-fanout-design.md @@ -0,0 +1,111 @@ +# Multi-Server Deployment & CI Fan-Out — Design + +**Date:** 2026-08-04 +**Status:** Approved, ready for implementation planning + +## Problem + +package-updater was built as a single instance talking to a single local Docker +socket. Two things surfaced at once: + +1. Swarm support (branch `worktree-docker-swarm-support`) added `MODE=compose|swarm`, + selecting one paradigm per deployment. This looked like an artificial limitation: + "why can't one instance do both?" +2. The real constraint is narrower and harder: an instance can only reach the Docker + daemon it is configured against. Compose stacks live on other, separate servers. + No amount of generalising the application makes one instance reach them. + +The second point dissolves the first. The limitation was never in the application — +it was in the deployment topology (one instance for a fleet of servers). + +## Decision + +**One package-updater instance per server. CI fans out to all of them.** + +- Each server runs its own instance against its own local Docker socket. +- The Swarm manager node runs an instance with `MODE=swarm`. +- Each standalone Compose host runs an instance with `MODE=compose`. +- The Gitea CI action posts `/update` to every instance. + +`MODE` exclusivity is therefore correct, not a defect: each instance owns exactly one +machine running exactly one paradigm. No composite Finder or routing Executor is built. + +### Rejected alternatives + +| Alternative | Why rejected | +|---|---| +| Composite Finder + routing Executor (both modes in one instance) | Solves nothing — a single instance still cannot reach other servers' daemons. Only justified if a Swarm manager node *also* ran standalone Compose stacks locally, which it does not. | +| One instance holding N Docker clients (`ssh://` / TCP+TLS to each host) | Technically viable (`client.FromEnv` already supports it) but requires `Job` to carry host identity, discovery to iterate clients, and creates a single point of failure with broad network credentials. Buys nothing over per-server instances given CI can reach every server. | +| Controller + agents (pull model) | Only needed if servers cannot accept inbound traffic. They can. Would be a genuinely different application. | + +### Assumptions + +These held at design time; revisit the decision if any changes: + +- Every server is reachable from Gitea CI over HTTP. +- Per-server outcomes are sufficient; no aggregated cross-server report is needed. +- The Swarm manager node runs Swarm services only, no local Compose stacks. +- All instances share one bearer token (org-level Gitea secret `UPDATER_TOKEN`). + +## Scope + +### Part 1 — Merge Swarm support to main + +Branch `worktree-docker-swarm-support` (10 commits) is complete, builds clean, and +its tests pass. No code changes required. Documentation is reframed so `MODE` +describes *what paradigm this machine runs*, not a product-level restriction, and the +one-instance-per-server deployment model is written down — it is the expensive insight +of this design and is not derivable from the code. + +Documentation changes: + +- `README.md` — describe `MODE` per-instance; add a deployment-topology section + covering one instance per server and CI fan-out. +- `CLAUDE.md` — reword the "Compose mode and Swarm mode are selected once per + deployment" design-intent entry to state the reason (each instance owns one daemon), + so a future reader does not re-litigate "make it handle both". + +### Part 2 — CI fan-out in `gitea-action/` + +`gitea-action/action.yml`'s `endpoint` input accepts a **newline-separated list** of +`/update` URLs instead of a single URL. + +Behaviour: + +- Post to each endpoint in list order. +- **Continue through the whole list even when one fails.** With `set -e` and a naive + loop, an unreachable server B leaves server C never updated — a half-deployed fleet + where the failure also hides which hosts succeeded. +- Print HTTP status and response body per endpoint, so the CI log shows the fleet + state at a glance. +- Exit non-zero at the end if any endpoint returned 4xx/5xx or was unreachable. + +A single URL remains a one-element list and keeps working unchanged. This is a +property of the format, not backward-compatibility work. + +Newline rather than comma as separator: URLs may legally contain commas but never +line breaks, so the separator cannot collide with the data. Choosing an impossible +delimiter is cheaper than building escaping. This also matches the Actions convention +for list inputs (`paths`, `files`). + +Blank lines and surrounding whitespace are ignored, so YAML block scalars with +trailing newlines and indentation behave as expected. + +`README.md` in `gitea-action/` documents the list form with a worked multi-server +example. + +## Out of scope + +- Composite/routing discovery within one instance — not needed under this topology. +- Aggregated cross-server reporting — per-endpoint CI output is sufficient. +- Per-endpoint distinct tokens — all instances share one token. +- Wiring `internal/selfupdate` into the live queue — a pre-existing v1 gap, unrelated. + +## Testing + +- Part 1: existing suite (`go test ./...`) must stay green after merge. No new tests — + no new code. +- Part 2: the action is shell in composite YAML with no test harness in this repo. + Verification is manual: run the loop logic against a mix of reachable and + unreachable endpoints and confirm (a) every endpoint is attempted, (b) the step + exits non-zero, (c) each endpoint's status appears in the output.