eleven / messenger Public

deploy: release ergonomics — legible releases page, batched uploads, loud Sparkle guard (#1261)

- server-<sha> releases are created --prerelease: build artifacts wear the
  grey badge so the releases page reads as the THINGS (Mac app, the rolling
  feed) instead of a wall of shas. Belt+braces with the existing
  --latest=false (prereleases can never take Latest).
- The rolling 'server' release becomes the human surface: every
  publish/promote rewrites its notes as a current-stack table (channel
  pointers + latest Mac tag + services note) — the whole-stack view.
- refresh_release_page also asserts the invariant the 'enterprise' release
  broke on 2026-07-19: whatever holds GitHub's Latest must carry
  appcast.xml, or Sparkle's releases/latest/download/appcast.xml is 404ing
  every Mac's update check — now a loud warning on every release op instead
  of silent breakage (found + hand-fixed 2026-07-22: Latest restored to
  v0.7.3).
- Service assets upload in ONE gh call (was: ten calls, ten anonymous
  'Successfully uploaded 1 asset' lines), with a step line naming the count.
- bw unlock gets stdin pinned to the tty — the 'press Enter to see the
  password prompt' papercut (Node inquirer inheriting a consumed stdin).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 8c1544a53df3602703461cfa17549784a8c5d151 parent 23388ae
1 file changed, +53 −5
  • deploy/release-server.sh +53 −5
diff --git a/deploy/release-server.sh b/deploy/release-server.sh
index e90fc80..d6e9d43 100755
--- a/deploy/release-server.sh
+++ b/deploy/release-server.sh
@@ -104,7 +104,10 @@ case "${1:-}" in publish|promote)
# session): unlock — prompts for the master password on the tty — then
# read again.
echo "release-server: unlocking Bitwarden to read the signing key…" >&2
- BW_SESSION="$(bw unlock --raw)" && export BW_SESSION || {
+ # stdin pinned to the tty: without it bw's Node prompt sits invisible
+ # until a stray Enter nudges it (inquirer inherits whatever stdin state
+ # the pipeline left behind — the "I had to press Enter" papercut).
+ BW_SESSION="$(bw unlock --raw </dev/tty)" && export BW_SESSION || {
echo "release-server: Bitwarden unlock failed — unlock it (or set LCHAT_RELEASE_KEY), then retry" >&2; exit 1; }
_t0="$(date +%s)"
LCHAT_RELEASE_KEY="$(bw_key)"
@@ -187,6 +190,42 @@ sign_and_upload() {
# Both publish and promote come through here, so the mirror's feed tracks
# every channel move, not just new builds.
mirror_upload "$TMP/manifest.json" manifest.json
+ refresh_release_page "$1"
+}
+
+# refresh_release_page: keep the releases repo human-readable. The per-version
+# server-<sha> releases are mechanical build artifacts (created --prerelease,
+# below, so they wear the grey badge and stay out of the way); the rolling
+# `server` release is the HUMAN surface, so on every publish/promote its notes
+# become a current-stack table: the channel pointers plus the latest Mac app.
+# Also asserts the repo invariant the enterprise release broke on 2026-07-19:
+# GitHub's "Latest" belongs to the MAC releases — Sparkle's baked-in SUFeedURL
+# is releases/latest/download/appcast.xml, so a Latest without appcast.xml
+# silently 404s every Mac's update check. Any release minted OUTSIDE this
+# script must pass --latest=false (or --prerelease); this check turns that
+# mistake from silent breakage into a loud warning on the next release op.
+refresh_release_page() {
+ LATEST_TAG="$(gh api "repos/$REPO/releases/latest" -q .tag_name 2>/dev/null || true)"
+ if [ -n "$LATEST_TAG" ] && ! gh release view "$LATEST_TAG" -R "$REPO" --json assets \
+ -q '.assets[].name' 2>/dev/null | grep -qx "appcast.xml"; then
+ echo "release-server: ⚠️ '$LATEST_TAG' holds GitHub's Latest badge but has no appcast.xml —" >&2
+ echo " Mac auto-update (releases/latest/download/appcast.xml) is 404ing RIGHT NOW." >&2
+ echo " Fix: gh release edit <newest-mac-tag> -R $REPO --latest" >&2
+ fi
+ TABLE="$(jq -r '
+ "Current stack (refreshed on every publish/promote):\n\n" +
+ "| Piece | Version |\n|---|---|\n" +
+ ([.channels | to_entries[] | "| Server (" + .key + ") | " + .value.version + " |"] | join("\n"))
+ ' "$1" 2>/dev/null || true)"
+ MAC_TAG="$(gh release list -R "$REPO" --limit 40 --json tagName -q \
+ '[.[].tagName | select(startswith("v"))][0]' 2>/dev/null || true)"
+ [ -n "$MAC_TAG" ] && TABLE="$TABLE
+| Mac app | $MAC_TAG |"
+ TABLE="$TABLE
+| Services (connect/push/home) | ride the server version |
+
+manifest.json on this release is the signed channel feed instances update from — docs/server-releases.md."
+ [ -n "$TABLE" ] && gh release edit server -R "$REPO" --notes "$TABLE" >/dev/null 2>&1 || true
}
cmd="${1:-}"; shift || true
@@ -245,12 +284,21 @@ publish)
# GitHub hands "latest" to the newest plain release by default, so every
# server publish was silently 404ing every Mac's update check (found
# 2026-07-14: the feed pointed at server-414bdeb0, no appcast there).
- gh release create "server-$VER" -R "$REPO" --title "Server $VER" --notes "$NOTES" --latest=false \
+ # --prerelease: build artifacts, not the headline — they wear the grey badge
+ # so the releases page reads as "the things" (Mac app, the rolling feed)
+ # instead of a wall of shas. Prereleases also can never take "Latest"
+ # (which belongs to the Mac releases — see refresh_release_page).
+ echo "release-server: creating server-$VER + uploading $((8 + $(echo $SVC_ASSETS | wc -w | tr -d ' '))) assets…" >&2
+ gh release create "server-$VER" -R "$REPO" --title "Server $VER" --notes "$NOTES" \
+ --prerelease --latest=false \
"$TMP/$BIN" "$TMP/$BIN.sha256" "$TMP/$BIN_ARM" "$TMP/$BIN_ARM.sha256" \
"$TMP/$BIN_MAC" "$TMP/$BIN_MAC.sha256" "$TMP/$BIN_MAC_X64" "$TMP/$BIN_MAC_X64.sha256"
- for A in $SVC_ASSETS; do
- gh release upload "server-$VER" "$TMP/$A" -R "$REPO"
- done
+ # One call for the service binaries — a call per asset printed ten anonymous
+ # "Successfully uploaded 1 asset" lines and paid ten round trips.
+ SVC_PATHS=""
+ for A in $SVC_ASSETS; do SVC_PATHS="$SVC_PATHS $TMP/$A"; done
+ # shellcheck disable=SC2086 # word-splitting the paths is the point
+ gh release upload "server-$VER" $SVC_PATHS -R "$REPO"
# Binaries mirror before the manifest that points at them, so the mirror
# never serves a feed whose mirror-side binaries don't exist yet. Keys copy
# the GitHub layout (server-<ver>/<asset>) — that's what the updater's