eleven / messenger Public

deploy: per-commit server builds publish to elevenmessenger/builds (#1266)

The releases repo's page is the human surface (Mac app + Sparkle appcast,
the rolling feed, enterprise); the per-commit server-<sha> releases are dev
build artifacts, not product releases — they now publish to the new public
builds repo (public because the fleet and self-hosters download tokenless;
the private source repo can't serve that). Manifest URLs follow.

Promote resolves which repo holds a version (builds first, releases for
pre-split builds), so existing pins, rollbacks, and re-promotes of old
versions keep working; nothing already published moves.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 16722b26626e0385fe251fa12176886f74e89ad1 parent 05454ec
2 files changed, +46 −24
  • deploy/release-server.sh +33 −21
  • docs/server-releases.md +13 −3
diff --git a/deploy/release-server.sh b/deploy/release-server.sh
index d6e9d43..d9e8530 100755
--- a/deploy/release-server.sh
+++ b/deploy/release-server.sh
@@ -2,9 +2,10 @@
# Publish server releases + move the channel feed (edge / beta / stable).
#
# The feed model (docs/server-releases.md): every build gets a GitHub release
-# `server-<ver>` on the public releases repo carrying the linux binary; one
-# rolling release tagged `server` carries manifest.json — the Ed25519-signed
-# feed saying which version each channel points at. Boxes only ever read the
+# `server-<ver>` on the public BUILDS repo (machine artifacts, split from the
+# human releases repo 2026-07-22) carrying the linux binary; one rolling
+# release tagged `server` on the RELEASES repo carries manifest.json — the
+# Ed25519-signed feed saying which version each channel points at. Boxes only ever read the
# manifest; promotion is a manifest edit, no rebuild.
#
# ./deploy/release-server.sh publish [notes...] # build HEAD -> release server-<sha> -> point edge at it
@@ -21,7 +22,7 @@
# via `sudo -n` through deploy/sudoers-operators. 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
+# Needs: gh (authed, write access to $LCHAT_RELEASES_REPO AND $LCHAT_BUILDS_REPO), jq, go, and the
# signing key. Both publish AND promote re-sign the feed, so both need the key.
#
# The easy path — nothing to set up: with the Bitwarden CLI (`bw`) installed,
@@ -56,6 +57,13 @@ set -eu
. "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/vault-items.sh"
REPO="${LCHAT_RELEASES_REPO:-elevenmessenger/releases}"
+# Per-commit server-<sha> build artifacts live in their own public repo so the
+# RELEASES repo's page stays human (Mac app, the rolling feed, enterprise) —
+# these are dev builds, not product releases (Paul, 2026-07-22). Public
+# because the fleet and self-hosters download tokenless. Builds published
+# before the split stay on $REPO forever — promote resolves per version, so
+# old pins and rollbacks never break.
+BUILDS_REPO="${LCHAT_BUILDS_REPO:-elevenmessenger/builds}"
KEY="${LCHAT_RELEASE_KEY_FILE:-}"
MIRROR_BUCKET="${LCHAT_RELEASE_MIRROR_BUCKET:-}"
BIN=lchat-linux-amd64
@@ -266,18 +274,18 @@ publish)
echo "$(shasum -a 256 "$TMP/$SBA" | cut -d' ' -f1) $SBA" > "$TMP/$SBA.sha256"
echo "built $VER (service $SNAME, amd64+arm64)"
SVC_JSON="$(printf '%s' "$SVC_JSON" | jq --arg n "$SNAME" \
- --arg url "https://github.com/$REPO/releases/download/server-$VER/$SB" \
+ --arg url "https://github.com/$BUILDS_REPO/releases/download/server-$VER/$SB" \
--arg sha "$(cut -d' ' -f1 < "$TMP/$SB.sha256")" \
--argjson size "$(wc -c < "$TMP/$SB" | tr -d ' ')" \
- --arg aurl "https://github.com/$REPO/releases/download/server-$VER/$SBA" \
+ --arg aurl "https://github.com/$BUILDS_REPO/releases/download/server-$VER/$SBA" \
--arg asha "$(cut -d' ' -f1 < "$TMP/$SBA.sha256")" \
--argjson asize "$(wc -c < "$TMP/$SBA" | tr -d ' ')" \
'.[$n] = {url:$url, sha256:$sha, size:$size, arm64_url:$aurl, arm64_sha256:$asha, arm64_size:$asize}')"
SVC_ASSETS="$SVC_ASSETS $SB $SB.sha256 $SBA $SBA.sha256"
done
- if gh release view "server-$VER" -R "$REPO" >/dev/null 2>&1; then
- echo "release-server: server-$VER already exists on $REPO (promote it, or land a new commit)" >&2; exit 1
+ if gh release view "server-$VER" -R "$BUILDS_REPO" >/dev/null 2>&1; then
+ echo "release-server: server-$VER already exists on $BUILDS_REPO (promote it, or land a new commit)" >&2; exit 1
fi
# --latest=false: the repo's "latest" pointer belongs to the MAC app —
# Sparkle's baked-in SUFeedURL is releases/latest/download/appcast.xml, and
@@ -289,7 +297,7 @@ publish)
# 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" \
+ gh release create "server-$VER" -R "$BUILDS_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"
@@ -298,7 +306,7 @@ publish)
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"
+ gh release upload "server-$VER" $SVC_PATHS -R "$BUILDS_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
@@ -318,8 +326,8 @@ publish)
fetch_payload | jq \
--arg ver "$VER" --arg sha "$SHA" --argjson size "$SIZE" --arg notes "$NOTES" \
--arg sha_arm "$SHA_ARM" --argjson size_arm "$SIZE_ARM" \
- --arg url "https://github.com/$REPO/releases/download/server-$VER/$BIN" \
- --arg url_arm "https://github.com/$REPO/releases/download/server-$VER/$BIN_ARM" \
+ --arg url "https://github.com/$BUILDS_REPO/releases/download/server-$VER/$BIN" \
+ --arg url_arm "https://github.com/$BUILDS_REPO/releases/download/server-$VER/$BIN_ARM" \
--arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg semver "$SEMVER" \
--argjson svc "$SVC_JSON" \
'.channels.edge = ({version:$ver, url:$url, sha256:$sha, size:$size,
@@ -352,6 +360,10 @@ promote)
echo "promote: fetching the current feed…" >&2
fetch_payload > "$TMP/payload.json"
+ # Which repo holds this version's build: builds (post-split) or releases
+ # (published before the split — those releases stay put so nothing breaks).
+ VREPO="$BUILDS_REPO"
+ gh release view "server-$VER" -R "$VREPO" >/dev/null 2>&1 || VREPO="$REPO"
# Prefer the entry already on a channel (usually promoting edge -> beta ->
# stable); otherwise rebuild it from the server-<ver> release's own assets
# (arm64 fields only when that release has an arm64 build — pre-arm64
@@ -361,22 +373,22 @@ promote)
# A rebuilt entry can't recover the semver the build was published with —
# major-pinned instances hold semver-less releases, so say so.
echo "release-server: note: rebuilding the $VER entry from release assets — it will carry no semver (major-pinned instances will hold it)" >&2
- gh release download "server-$VER" -R "$REPO" -p "$BIN.sha256" -O "$TMP/$BIN.sha256" || {
- echo "release-server: no server-$VER release on $REPO" >&2; exit 1; }
+ gh release download "server-$VER" -R "$VREPO" -p "$BIN.sha256" -O "$TMP/$BIN.sha256" || {
+ echo "release-server: no server-$VER release on $VREPO" >&2; exit 1; }
SHA="$(cut -d' ' -f1 < "$TMP/$BIN.sha256")"
- SIZE="$(gh release view "server-$VER" -R "$REPO" --json assets \
+ SIZE="$(gh release view "server-$VER" -R "$VREPO" --json assets \
--jq ".assets[] | select(.name == \"$BIN\") | .size")"
- NOTES="$(gh release view "server-$VER" -R "$REPO" --json body --jq .body)"
+ NOTES="$(gh release view "server-$VER" -R "$VREPO" --json body --jq .body)"
ENTRY="$(jq -n --arg ver "$VER" --arg sha "$SHA" --argjson size "${SIZE:-0}" --arg notes "$NOTES" \
- --arg url "https://github.com/$REPO/releases/download/server-$VER/$BIN" \
+ --arg url "https://github.com/$VREPO/releases/download/server-$VER/$BIN" \
--arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{version:$ver, url:$url, sha256:$sha, size:$size, notes:$notes, published_at:$now}')"
- if gh release download "server-$VER" -R "$REPO" -p "$BIN_ARM.sha256" -O "$TMP/$BIN_ARM.sha256" 2>/dev/null; then
+ if gh release download "server-$VER" -R "$VREPO" -p "$BIN_ARM.sha256" -O "$TMP/$BIN_ARM.sha256" 2>/dev/null; then
SHA_ARM="$(cut -d' ' -f1 < "$TMP/$BIN_ARM.sha256")"
- SIZE_ARM="$(gh release view "server-$VER" -R "$REPO" --json assets \
+ SIZE_ARM="$(gh release view "server-$VER" -R "$VREPO" --json assets \
--jq ".assets[] | select(.name == \"$BIN_ARM\") | .size")"
ENTRY="$(printf '%s' "$ENTRY" | jq --arg sha "$SHA_ARM" --argjson size "${SIZE_ARM:-0}" \
- --arg url "https://github.com/$REPO/releases/download/server-$VER/$BIN_ARM" \
+ --arg url "https://github.com/$VREPO/releases/download/server-$VER/$BIN_ARM" \
'. + {arm64_url:$url, arm64_sha256:$sha, arm64_size:$size}')"
fi
fi
@@ -391,7 +403,7 @@ promote)
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
- if gh release download "server-$VER" -R "$REPO" -p "$A" -O "$TMP/stable/$A" 2>/dev/null; then
+ if gh release download "server-$VER" -R "$VREPO" -p "$A" -O "$TMP/stable/$A" 2>/dev/null; then
gh release upload server "$TMP/stable/$A" -R "$REPO" --clobber
else
echo "note: server-$VER has no $A — rolling copy not refreshed" >&2
diff --git a/docs/server-releases.md b/docs/server-releases.md
index c246fb2..15bc55d 100644
--- a/docs/server-releases.md
+++ b/docs/server-releases.md
@@ -11,9 +11,19 @@ that consumes this feed is described in `docs/instance-router.md`
## The shape
The source repo is private, so a box can't read its GitHub releases without a
-credential we don't want to hand out. Releases therefore publish to the
-**public releases repo** (`elevenmessenger/releases` — the same repo the Mac
-app's Sparkle appcast uses, see `ios/release-mac.sh`):
+credential we don't want to hand out. Everything therefore publishes to public
+repos, split by audience (2026-07-22 — before the split, both lived on the
+releases repo, and the per-build wall made its page unreadable):
+
+- the **releases repo** (`elevenmessenger/releases`) is the HUMAN surface: the
+ Mac app's releases + Sparkle appcast (see `ios/release-mac.sh`), the rolling
+ `server` feed release, enterprise. GitHub's "Latest" badge belongs to the
+ newest Mac release — Sparkle's feed URL rides `latest/download/appcast.xml`,
+ and `release-server.sh` warns loudly when that invariant breaks.
+- the **builds repo** (`elevenmessenger/builds`) takes the per-commit
+ `server-<ver>` build artifacts — machine consumers only. Builds published
+ before the split stay on the releases repo; promote resolves per version,
+ so old pins and rollbacks keep working.
- **One release per build**: tag `server-<ver>` (ver = the git short sha that
`deploy/build-linux.sh` stamps into the binary as `main.buildVersion`),