eleven / messenger Public

deploy: release-server.sh pulls the signing key from Bitwarden automatically (#1192)

Bitwarden is the standing store for operator secrets now, so publish/promote
shouldn't require hand-exporting LCHAT_RELEASE_KEY. When no key is provided
any other way and the `bw` CLI is present, the script pulls the signing seed
from the "Eleven Messenger server release signing key" item itself — unlocking
interactively if the vault is locked. The seed stays in the env only, never on
disk (same property as the explicit flow). Explicitly setting LCHAT_RELEASE_KEY
/ _KEY_FILE / - still wins and bypasses bw.

Applies to both publish and promote (both re-sign the feed, so both need the
key) — so `promote <ver> stable` now just works with bw installed, no manual
key setup.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 209fb034c595ad348a1e028303984ba48350f07c parent d7ed42b
1 file changed, +41 −13
  • deploy/release-server.sh +41 −13
diff --git a/deploy/release-server.sh b/deploy/release-server.sh
index 8158e95..bd51616 100755
--- a/deploy/release-server.sh
+++ b/deploy/release-server.sh
@@ -22,22 +22,24 @@
# 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. Preferred: the seed VALUE in $LCHAT_RELEASE_KEY — it goes
-# secret manager -> shell -> the signing tool's memory and never touches disk
-# (one fetch covers a whole publish && promote chain):
+# signing key. Both publish AND promote re-sign the feed, so both need the key.
#
-# export LCHAT_RELEASE_KEY="$(bw get item "Eleven Messenger server release signing key" \
-# | jq -r '.fields[]? | select((.value|length)>40).value' | head -1)"
-# ./deploy/release-server.sh publish "notes"
+# The easy path — nothing to set up: with the Bitwarden CLI (`bw`) installed,
+# just run the command and it pulls the seed from Bitwarden itself (unlocking
+# interactively if the vault is locked). One unlock covers a publish && promote
+# chain; the seed stays in the env only, never on disk.
#
-# (The Bitwarden item has no password field — the seed rides a hidden custom
-# field, which that jq picks out by being the only long value.)
+# ./deploy/release-server.sh publish "notes"
+# ./deploy/release-server.sh promote <ver> stable
#
-# Fallbacks: $LCHAT_RELEASE_KEY_FILE as a file path, or `-` to read the key
-# from stdin (parked in this run's tmpdir, wiped on exit).
+# (The Bitwarden item — "Eleven Messenger server release signing key" — has no
+# password field; the seed rides a hidden custom field, picked out as the only
+# >40-char value. Legacy copy in 1Password.)
#
-# (The key lives in Bitwarden — legacy copy in 1Password — as "Eleven
-# Messenger server release signing key".)
+# Override the auto-pull by providing the key yourself: the seed VALUE in
+# $LCHAT_RELEASE_KEY (env, never touches disk), $LCHAT_RELEASE_KEY_FILE as a
+# file path, or `-` to read it from stdin (parked in this run's tmpdir, wiped
+# on exit). Any of these set → Bitwarden is not consulted.
#
# Optional S3 mirror: set LCHAT_RELEASE_MIRROR_BUCKET (needs the aws CLI) and
# every feed artifact is also copied to s3://<bucket>/releases/… — GitHub's
@@ -67,6 +69,32 @@ trap 'rm -rf "$TMP"' EXIT
# and the gh/jq toolchain are required only where the feed is touched
# (publish/promote, checked below).
case "${1:-}" in publish|promote)
+ # No key provided any other way? If the Bitwarden CLI is present, pull the
+ # signing seed from there — bw is the standing store for operator secrets, so
+ # neither publish NOR promote should need a hand-exported key (both re-sign the
+ # feed, so both need it). Unlocks interactively if the vault is locked; the
+ # seed lands in the env only, never on disk (same property as the explicit
+ # LCHAT_RELEASE_KEY flow). No bw and no key set → the original error below.
+ if [ -z "${LCHAT_RELEASE_KEY:-}" ] && [ -z "$KEY" ] \
+ && 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 "Eleven Messenger server release signing key" 2>/dev/null \
+ | jq -r '.fields[]? | select((.value|length)>40).value' | head -1; }
+ 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.
+ 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; }
+ LCHAT_RELEASE_KEY="$(bw_key)"
+ fi
+ [ -n "$LCHAT_RELEASE_KEY" ] || {
+ echo "release-server: couldn't read 'Eleven Messenger server release signing key' from Bitwarden" >&2; exit 1; }
+ export LCHAT_RELEASE_KEY # the signing tool (a child process) reads it from the env
+ echo "release-server: signing key sourced from Bitwarden" >&2
+ fi
if [ -n "${LCHAT_RELEASE_KEY:-}" ]; then
# The seed itself is in the environment — the preferred flow: it goes
# secret manager -> shell -> the signing tool's memory and never touches
@@ -81,7 +109,7 @@ case "${1:-}" in publish|promote)
KEY="$TMP/signing.key"
else
[ -n "$KEY" ] && [ -f "$KEY" ] || {
- echo "release-server: set LCHAT_RELEASE_KEY to the base64 seed (or LCHAT_RELEASE_KEY_FILE to a key file, or - for stdin)" >&2; exit 1; }
+ echo "release-server: set LCHAT_RELEASE_KEY to the base64 seed — or install the Bitwarden CLI 'bw' to pull it automatically (LCHAT_RELEASE_KEY_FILE to a key file, or - for stdin, also work)" >&2; exit 1; }
fi
command -v jq >/dev/null || { echo "release-server: jq is required" >&2; exit 1; }
command -v gh >/dev/null || { echo "release-server: gh is required" >&2; exit 1; }