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>
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 |