eleven / messenger Public

deploy: release-server.sh ship — the manual-deploy button (#500)

publish moves the feed; ship makes the box catch up NOW instead of on the
hourly timer. It loops `lchat update run` until every un-pinned instance is
current on its channel (absorbing GitHub's stale-CDN window on a fresh
manifest — the pass that quietly no-ops right after a publish), prints the
fleet, and calls out pinned canaries without waiting on them. Sidecars ride
along for free: post-#418, every instance update try-restarts its bot.

LCHAT_BOX (e.g. root@<box-ip>) names the box — environment specifics stay in
CLAUDE.local.md, never the repo. `publish … && ship` is a full deploy; with
the fleet on edge that's the one-command release Paul asked for.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 1f9021cea3962db7d20d8ecda4a7c1d9b20a30e6 parent 54d95be
2 files changed, +53 −1
  • deploy/release-server.sh +43 −1
  • docs/server-releases.md +10 −0
diff --git a/deploy/release-server.sh b/deploy/release-server.sh
index cce77ee..69f3000 100755
--- a/deploy/release-server.sh
+++ b/deploy/release-server.sh
@@ -9,6 +9,15 @@
#
# ./deploy/release-server.sh publish [notes...] # build HEAD -> release server-<sha> -> point edge at it
# ./deploy/release-server.sh promote <ver> <chan> # point beta/stable at an already-published version
+# ./deploy/release-server.sh ship [ver] # run the box updater until every instance is current
+#
+# `ship` is the manual-deploy button: publish moves the feed, ship makes the
+# box catch up NOW instead of on the hourly timer — it loops `lchat update run`
+# until every un-pinned instance is current on its channel (absorbing GitHub's
+# stale-CDN window on a fresh manifest); each pass restarts the updated
+# instances' bot sidecars in lock-step (deploy/systemd/lchat-bot@.service).
+# Needs LCHAT_BOX (e.g. root@<box-ip> — environment specifics live in
+# CLAUDE.local.md, never here). `publish ... && ship` is a full deploy.
#
# Needs: gh (authed, write access to $LCHAT_RELEASES_REPO), jq, go, and the
# signing key at $LCHAT_RELEASE_KEY_FILE (1Password: "Eleven Messenger server
@@ -153,8 +162,41 @@ promote)
echo "promoted $VER to $CHAN"
;;
+ship)
+ VER="${1:-$(git rev-parse --short HEAD)}"
+ BOX="${LCHAT_BOX:-}"
+ [ -n "$BOX" ] || { echo "release-server: set LCHAT_BOX (e.g. root@<box>) to ship" >&2; exit 1; }
+ echo "ship: updating instances on $BOX until current (feed target: $VER)"
+ DEADLINE=$(( $(date +%s) + 600 ))
+ while :; do
+ ssh "$BOX" /opt/lchat/lchat update run || true
+ STATUS="$(ssh "$BOX" /opt/lchat/lchat update status || true)"
+ # Done when no un-pinned instance with a known channel head is behind, and
+ # at least one row proves a feed landed at all. Pinned rows are someone's
+ # canary: reported, never waited on.
+ BEHIND="$(printf '%s\n' "$STATUS" | awk 'NR>1 && $3!="pinned" && $5!="-" && $0 !~ /\(current\)/ {print $1}')"
+ if [ -z "$BEHIND" ] && printf '%s\n' "$STATUS" | grep -q "(current)"; then
+ printf '%s\n' "$STATUS"
+ printf '%s\n' "$STATUS" | awk 'NR>1 && $3=="pinned" {print "note: " $1 " is pinned (a canary) - left alone"}'
+ if printf '%s\n' "$STATUS" | grep -q "$VER (current)"; then
+ echo "shipped $VER - every un-pinned instance is current"
+ else
+ echo "shipped - every un-pinned instance is current on its channel (none rides a channel pointing at $VER yet)"
+ fi
+ exit 0
+ fi
+ if [ "$(date +%s)" -ge "$DEADLINE" ]; then
+ printf '%s\n' "$STATUS"
+ echo "ship: gave up after 10m - still behind: $BEHIND (GitHub CDN can serve a fresh manifest stale; retry in a few minutes)" >&2
+ exit 1
+ fi
+ echo "ship: waiting for the feed/instances (behind: ${BEHIND:-feed})..."
+ sleep 30
+ done
+ ;;
+
*)
- echo "usage: release-server.sh publish [notes...] | promote <ver> edge|beta|stable" >&2
+ echo "usage: release-server.sh publish [notes...] | promote <ver> edge|beta|stable | ship [ver]" >&2
exit 1
;;
esac
diff --git a/docs/server-releases.md b/docs/server-releases.md
index d53339f..a54b9e1 100644
--- a/docs/server-releases.md
+++ b/docs/server-releases.md
@@ -112,11 +112,21 @@ export LCHAT_RELEASE_KEY_FILE=~/secrets/lchat-release-key.b64
# cut a release from clean main and point edge at it
./deploy/release-server.sh publish "what changed, one line"
+# make the fleet catch up NOW instead of on the hourly timer (the manual
+# "deploy" button; LCHAT_BOX names the box, e.g. root@<ip>):
+./deploy/release-server.sh ship
+
# promote when it's proven
./deploy/release-server.sh promote a1b2c3d beta
./deploy/release-server.sh promote a1b2c3d stable
```
+`ship` loops the box's `lchat update run` until every un-pinned instance is
+current on its channel — absorbing GitHub's stale-CDN window on a fresh
+manifest — and each pass restarts updated instances' bot sidecars in
+lock-step. Pinned instances (canaries) are reported and left alone. With the
+whole fleet riding `edge`, `publish … && ship` is a complete manual deploy.
+
Same rule as deploys and TestFlight: **publish from landed `main`**, never a
feature branch (the script refuses a dirty tree). Publishing is manual for now;
a CI job cutting `edge` on every push to `main` is the natural next step.