feat: multi-server deployment topology and CI fan-out #5
@@ -78,12 +78,18 @@ curl -sH "Authorization: Bearer $UPDATER_API_KEY" \
|
|||||||
with a permission error against a worker-only node. This is an operator/deployment
|
with a permission error against a worker-only node. This is an operator/deployment
|
||||||
concern (point `DOCKER_HOST` at a manager, or schedule the updater on a manager),
|
concern (point `DOCKER_HOST` at a manager, or schedule the updater on a manager),
|
||||||
not something the code can detect or work around.
|
not something the code can detect or work around.
|
||||||
|
- **`tag` is cosmetic in compose mode but load-bearing in swarm mode.** `handlers.go` folds it
|
||||||
|
into the requested image; compose discovery strips it via `NormaliseImage`, but
|
||||||
|
`SwarmExecutor` assigns it directly to `ContainerSpec.Image`. A request without a tag updates
|
||||||
|
a Swarm service to `:latest`.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- Design spec: `docs/superpowers/specs/2026-05-22-package-updater-design.md` (489 lines, authoritative)
|
- 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`
|
- Implementation plan: `docs/superpowers/plans/2026-05-22-package-updater-implementation.md`
|
||||||
- Consumer-side CI integration: `gitea-action/`
|
- Consumer-side CI integration: `gitea-action/`
|
||||||
|
- Multi-server topology spec: `docs/superpowers/specs/2026-08-04-multi-server-fanout-design.md`
|
||||||
|
- Multi-server fan-out plan: `docs/superpowers/plans/2026-08-04-multi-server-fanout.md`
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# package-updater
|
# package-updater
|
||||||
|
|
||||||
Webhook-driven Docker Compose service updater. Fills the gap between Watchtower (polling, no CI integration) and full GitOps (Argo CD, Flux) for a self-hosted, single-host environment.
|
Webhook-driven Docker service updater — Compose stacks or Swarm services. Fills the gap between Watchtower (polling, no CI integration) and full GitOps (Argo CD, Flux) for self-hosted environments, one instance per server.
|
||||||
|
|
||||||
**Trigger flow:**
|
**Trigger flow:**
|
||||||
|
|
||||||
1. Gitea workflow builds and pushes a new image to your registry.
|
1. Gitea workflow builds and pushes a new image to your registry.
|
||||||
2. Workflow calls `POST /update` on this service with the image name.
|
2. Workflow calls `POST /update` on this service with the image name.
|
||||||
3. Service finds the matching Compose-managed container(s) on the host via Docker labels.
|
3. Service finds the matching container(s) or Swarm service(s) via Docker labels.
|
||||||
4. Runs `docker compose pull` + `up -d` for the relevant service(s).
|
4. Runs `docker compose pull` + `up -d`, or `docker service update`, depending on `MODE`.
|
||||||
|
|
||||||
See [design spec](docs/superpowers/specs/2026-05-22-package-updater-design.md) and [implementation plan](docs/superpowers/plans/2026-05-22-package-updater-implementation.md) for full design and rationale.
|
See [design spec](docs/superpowers/specs/2026-05-22-package-updater-design.md) and [implementation plan](docs/superpowers/plans/2026-05-22-package-updater-implementation.md) for full design and rationale.
|
||||||
|
|
||||||
@@ -25,7 +25,12 @@ A container is eligible for update only if it has **both**:
|
|||||||
- An image name matching the request (tag-agnostic), AND
|
- An image name matching the request (tag-agnostic), AND
|
||||||
- The opt-in label `se.shcizo.auto-update=true`.
|
- The opt-in label `se.shcizo.auto-update=true`.
|
||||||
|
|
||||||
Defense in depth: a valid bearer token AND the opt-in label must both be present before any container is touched.
|
Defense in depth, compose mode: a valid bearer token AND the opt-in label AND a working
|
||||||
|
directory inside `STACKS_ROOT` must all hold before a container is touched. A stack outside
|
||||||
|
`STACKS_ROOT` comes back as `refused` rather than being updated.
|
||||||
|
|
||||||
|
Swarm mode's gate is the token and the opt-in label only — a Swarm service has no local
|
||||||
|
compose file to anchor a path check against.
|
||||||
|
|
||||||
## Swarm mode
|
## Swarm mode
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Deploy via package-updater (composite action)
|
# Deploy via package-updater (composite action)
|
||||||
|
|
||||||
Notifies `package-updater` to `docker compose pull` + `up -d` for the matching service(s) after a CI build.
|
Notifies one or more `package-updater` instances to update the matching service(s) after a CI build. Each instance does whatever its own `MODE` dictates — `docker compose pull` + `up -d`, or `docker service update` for swarm.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ input is sent to every endpoint.
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `endpoint` | yes | — | Full URL to `/update`. Several may be given, one per line, to update a fleet. |
|
| `endpoint` | yes | — | Full URL to `/update`. Several may be given, one per line, to update a fleet. |
|
||||||
| `image` | yes | — | Image reference without tag |
|
| `image` | yes | — | Image reference without tag |
|
||||||
| `tag` | no | `""` | Tag that was just pushed (logged for audit) |
|
| `tag` | no | `""` | Tag that was just pushed. Compose mode ignores it (the compose file pins the reference); **swarm mode sets the service image to it**, so omitting it deploys `:latest`. Always pass it. |
|
||||||
| `token` | yes | — | Bearer token configured in package-updater |
|
| `token` | yes | — | Bearer token configured in package-updater |
|
||||||
|
|
||||||
## Failure modes
|
## Failure modes
|
||||||
@@ -46,3 +46,7 @@ which hosts actually succeeded.
|
|||||||
The step exits non-zero if any endpoint returned 4xx/5xx or was unreachable. The
|
The step exits non-zero if any endpoint returned 4xx/5xx or was unreachable. The
|
||||||
log lists each endpoint with its HTTP status and response body, so a partial
|
log lists each endpoint with its HTTP status and response body, so a partial
|
||||||
deploy is visible at a glance.
|
deploy is visible at a glance.
|
||||||
|
|
||||||
|
Endpoints are contacted sequentially, so worst-case wall time is the number of endpoints times
|
||||||
|
how long one update takes. Each request allows 10s to connect and 15 minutes to complete —
|
||||||
|
`/update` is synchronous and waits for the deploy to finish.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ inputs:
|
|||||||
description: "Image reference without tag (e.g. registry.example.com/myapp)"
|
description: "Image reference without tag (e.g. registry.example.com/myapp)"
|
||||||
required: true
|
required: true
|
||||||
tag:
|
tag:
|
||||||
description: "Tag that was just pushed (for logging)"
|
description: "Tag that was just pushed. Required in practice for swarm instances — it becomes the image the service is set to. Omit it and swarm deploys :latest."
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
token:
|
token:
|
||||||
@@ -31,7 +31,7 @@ runs:
|
|||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
payload=$(jq -nc --arg image "$IMAGE" --arg tag "$TAG" \
|
payload=$(jq -nc --arg image "$IMAGE" --arg tag "$TAG" \
|
||||||
'{image: $image, tag: $tag}')
|
'{image: $image, tag: $tag}') || { echo "jq is required but failed"; exit 1; }
|
||||||
|
|
||||||
attempted=0
|
attempted=0
|
||||||
failed=0
|
failed=0
|
||||||
@@ -46,6 +46,7 @@ runs:
|
|||||||
echo "--- $endpoint"
|
echo "--- $endpoint"
|
||||||
|
|
||||||
if ! response=$(curl -sS -w "\n%{http_code}" \
|
if ! response=$(curl -sS -w "\n%{http_code}" \
|
||||||
|
--connect-timeout 10 --max-time 900 \
|
||||||
-X POST "$endpoint" \
|
-X POST "$endpoint" \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|||||||
Reference in New Issue
Block a user