Files
package-updater/gitea-action/action.yml
shcizo 2d668ddc63 docs: fix multi-server fanout review findings
Documentation fixes from the final branch review, plus small curl/jq
hardening in the gitea-action script:

- tag was documented as cosmetic ("for logging") but is 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. Omitting it deploys :latest, silently
  diverging from what CI just built. Fixed in action.yml, gitea-action's
  README, and added to CLAUDE.md's Gotchas since it's invisible from
  either mode's code alone.
- gitea-action/README.md's opening line and root README.md's intro/trigger
  flow described compose-only behavior even though both docs' bodies now
  cover swarm mode too.
- README.md's defense-in-depth section described a two-factor gate; compose
  mode is actually three factors (token, label, STACKS_ROOT prefix), and
  swarm mode is genuinely two (no local compose file to path-check against).
- action.yml: curl now has --connect-timeout 10 --max-time 900 so a host
  that accepts TCP but never answers can't block the fan-out loop forever;
  the jq payload build now fails loudly instead of silently sending an
  empty payload to every endpoint.
- CLAUDE.md References section now lists this branch's spec and plan.

Claude-Session: https://claude.ai/code/session_01S3aqJ4tvaPezQhsGNCybut
2026-08-04 19:48:16 +02:00

81 lines
2.7 KiB
YAML

name: "Deploy via package-updater"
description: "Notifies one or more package-updater instances to pull & restart a service"
inputs:
endpoint:
description: "Full URL to /update. Give several, one per line, to update a fleet."
required: true
image:
description: "Image reference without tag (e.g. registry.example.com/myapp)"
required: true
tag:
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
default: ""
token:
description: "Bearer token for package-updater"
required: true
runs:
using: "composite"
steps:
- name: Trigger update
shell: bash
env:
ENDPOINTS: ${{ inputs.endpoint }}
IMAGE: ${{ inputs.image }}
TAG: ${{ inputs.tag }}
TOKEN: ${{ inputs.token }}
run: |
# No `set -e`: a failing endpoint must not abort the loop, or one dead
# server leaves the rest of the fleet un-updated and hides which hosts
# actually succeeded.
set -uo pipefail
payload=$(jq -nc --arg image "$IMAGE" --arg tag "$TAG" \
'{image: $image, tag: $tag}') || { echo "jq is required but failed"; exit 1; }
attempted=0
failed=0
while IFS= read -r endpoint; do
# URLs never contain whitespace, so stripping all of it safely
# handles indentation, blank lines and CRLF line endings.
endpoint=$(printf '%s' "$endpoint" | tr -d '[:space:]')
[ -z "$endpoint" ] && continue
attempted=$((attempted + 1))
echo "--- $endpoint"
if ! response=$(curl -sS -w "\n%{http_code}" \
--connect-timeout 10 --max-time 900 \
-X POST "$endpoint" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$payload"); then
echo "unreachable"
failed=$((failed + 1))
continue
fi
# Split on the last newline: curl's -w appended the status code
# there. Pure bash — `head -n -1` is GNU-only and fails on
# BSD/macOS, so the fan-out could not be tested locally.
code="${response##*$'\n'}"
body="${response%$'\n'*}"
echo "HTTP $code"
printf '%s' "$body" | jq . || printf '%s\n' "$body"
if [ "$code" -ge 400 ]; then
failed=$((failed + 1))
fi
done <<< "$ENDPOINTS"
if [ "$attempted" -eq 0 ]; then
echo "no endpoints given"
exit 1
fi
if [ "$failed" -gt 0 ]; then
echo "$failed of $attempted endpoint(s) failed"
exit 1
fi
echo "all $attempted endpoint(s) updated"