eleven / messenger Public

deploy: release-server never hangs on Bitwarden + narrates its steps (#1233)

A promote sat 14 silent minutes inside `bw get item` today (stale
session/network — bw has no timeout of its own and its stderr was
discarded), violating the verb's own never-a-hang promise. The signing-key
read now runs bw under perl's alarm (30s): a fast empty read (locked vault)
still falls through to the interactive unlock; a genuine stall kills bw and
prints the fix (bw sync / LCHAT_RELEASE_KEY). The seed stays in the pipe,
never on disk. And the slow steps say what they are: key read, tool build
(the cold go-build minutes), feed fetch, sign+upload, the stable
rolling-binary refresh.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 820f15e014c8437b482d94ce6e30a84b1cf4c7e1 parent fe1f65a
1 file changed, +25 −4
  • deploy/release-server.sh +25 −4
diff --git a/deploy/release-server.sh b/deploy/release-server.sh
index 58ca4ec..e90fc80 100755
--- a/deploy/release-server.sh
+++ b/deploy/release-server.sh
@@ -83,16 +83,32 @@ case "${1:-}" in publish|promote)
&& command -v bw >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
# The item has no password field — the seed rides a hidden custom field,
# picked out as the only >40-char value (matches the header's incantation).
- bw_key() { bw get item "$BW_ITEM_SIGNING_KEY" 2>/dev/null \
+ # bw's API calls carry no timeout of their own and can stall for good on a
+ # bad connection or stale session (observed 2026-07-21: a promote sat 14
+ # silent minutes inside `bw get`), so bw runs under perl's alarm — the
+ # never-a-hang promise, enforced. The seed stays in the pipe, never on
+ # disk. bw_stalled distinguishes a killed 30s read from a fast empty one
+ # (vault locked), which still falls through to the unlock prompt below.
+ bw_key() { perl -e 'alarm 30; exec @ARGV' bw get item "$BW_ITEM_SIGNING_KEY" 2>/dev/null \
| jq -r '.fields[]? | select((.value|length)>40).value' | head -1; }
+ bw_stalled() { # $1 = start epoch; empty result + ≥30s elapsed = the watchdog fired
+ [ $(( $(date +%s) - $1 )) -ge 30 ] || return 1
+ echo "release-server: the Bitwarden CLI stalled for 30s reading '$BW_ITEM_SIGNING_KEY' — run 'bw sync' and retry (or export LCHAT_RELEASE_KEY)" >&2
+ }
+ echo "release-server: reading the signing key from Bitwarden…" >&2
+ _t0="$(date +%s)"
LCHAT_RELEASE_KEY="$(bw_key)"
if [ -z "$LCHAT_RELEASE_KEY" ]; then
- # Empty usually means the vault is locked (or this shell has no session):
- # unlock — prompts for the master password on the tty — then read again.
+ bw_stalled "$_t0" && exit 1
+ # A fast empty read means the vault is locked (or this shell has no
+ # 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 || {
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)"
+ [ -n "$LCHAT_RELEASE_KEY" ] || { bw_stalled "$_t0" && exit 1 || true; }
fi
[ -n "$LCHAT_RELEASE_KEY" ] || {
echo "release-server: couldn't read '$BW_ITEM_SIGNING_KEY' from Bitwarden" >&2; exit 1; }
@@ -126,7 +142,9 @@ esac
# can't drift on the format. Build a local tool from this checkout (only the
# feed-touching commands need it).
TOOL="$TMP/lchat-tool"
-case "${1:-}" in publish|promote) go build -o "$TOOL" . ;; esac
+case "${1:-}" in publish|promote)
+ echo "release-server: building the signing tool (go build — a cold build takes a few minutes)…" >&2
+ go build -o "$TOOL" . ;; esac
# fetch_payload: current feed payload JSON (verified), or a fresh skeleton.
fetch_payload() {
@@ -155,6 +173,7 @@ mirror_upload() {
# With LCHAT_RELEASE_KEY in the environment, KEY is empty and the tool reads
# the seed straight from the env — no --key, no file.
sign_and_upload() {
+ echo "release-server: signing + uploading the feed…" >&2
if [ -n "$KEY" ]; then
"$TOOL" release sign --key "$KEY" --in "$1" --out "$TMP/manifest.json"
else
@@ -283,6 +302,7 @@ promote)
case "$CHAN" in edge|beta|stable) ;; *)
echo "usage: release-server.sh promote <ver> edge|beta|stable" >&2; exit 1;; esac
+ echo "promote: fetching the current feed…" >&2
fetch_payload > "$TMP/payload.json"
# Prefer the entry already on a channel (usually promoting edge -> beta ->
# stable); otherwise rebuild it from the server-<ver> release's own assets
@@ -319,6 +339,7 @@ promote)
# Stable also refreshes the rolling release's "latest stable" binaries — the
# fixed, JSON-free URLs deploy/install.sh downloads.
if [ "$CHAN" = stable ]; then
+ echo "promote: refreshing the rolling latest-stable binaries (8 asset copies)…" >&2
mkdir -p "$TMP/stable"
for A in "$BIN" "$BIN.sha256" "$BIN_ARM" "$BIN_ARM.sha256" \
"$BIN_MAC" "$BIN_MAC.sha256" "$BIN_MAC_X64" "$BIN_MAC_X64.sha256"; do