#!/usr/bin/env sh # 🤖 ops publish / promote / ship — the release path, one verb per stage. # # ./deploy/ops publish "release notes" # build HEAD -> server- release -> edge # ./deploy/ops promote beta # move a channel pointer (re-signs the feed) # ./deploy/ops ship # every fleet box catches up NOW # # The machinery is deploy/release-server.sh (which pulls the Bitwarden signing # key itself, unlocking interactively when the vault is locked — no key setup). # This wrapper adds only the rules that used to live in prose: # # - a release must correspond to LANDED MAIN CODE — publish refuses a HEAD # that origin/main doesn't contain, so a feature branch can't become the # fleet's edge build by accident; # - there are exactly three channels, edge / beta / stable. There is NO dev # channel — the dev mesh is deployed by `ops dev deploy`, never the feed; # - `ship` means every box, not one: it loops the fleet (same aliases doctor # checks), so a second box can't quietly stay behind; # - box SSH sessions incur a why-entry in the public access ledger. set -eu here="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" root="$(CDPATH= cd -- "$here/.." && pwd)" # Fleet box ssh aliases — user/hostname come from ~/.ssh/config (operator SSH # rides WireGuard; specifics in CLAUDE.local.md). Same default as doctor.sh. BOXES="${LCHAT_BOXES:-elevenbox kepler}" usage() { echo "usage: ops publish [notes...] | ops promote edge|beta|stable | ops ship [--expedite] [box...]" >&2 exit 2 } cmd="${1:-}" [ $# -gt 0 ] && shift case "$cmd" in publish) # Refuse to publish code main doesn't have: edge tracks main, and a branch # build published by accident would auto-update the whole edge fleet onto # unreviewed code within the hour. (Branch builds belong on a pinned canary: # `ops canary deploy`.) Fetch first so the check is against today's main, # not a stale ref; offline, fall back to the last-known one. git -C "$root" fetch -q origin main 2>/dev/null \ || echo "ops publish: warning: could not fetch origin/main — checking against the last-known ref" >&2 if ! git -C "$root" merge-base --is-ancestor HEAD origin/main 2>/dev/null; then echo "ops publish: HEAD is not on origin/main — a release must be landed main code." >&2 echo " merge the PR, pull main, and publish from there" >&2 echo " (branch builds go to a pinned canary instead: ops canary deploy )" >&2 exit 1 fi # release-server.sh re-checks this, but 20 minutes earlier is kinder. if [ -n "$(git -C "$root" status --porcelain 2>/dev/null)" ]; then echo "ops publish: working tree is dirty — publish from a clean main" >&2 exit 1 fi exec "$here/release-server.sh" publish "$@" ;; promote) ver="${1:-}"; chan="${2:-}" # Catch the swapped-argument habit before release-server's terser usage line. case "$ver" in edge|beta|stable) echo "ops promote: arguments are then — try: ops promote $chan $ver" >&2 exit 2 ;; esac case "$chan" in edge|beta|stable) ;; dev) echo "ops promote: there is no 'dev' channel — the channels are edge / beta / stable." >&2 echo " the dev mesh is not on the release feed at all; deploy it with: ops dev deploy" >&2 exit 2 ;; *) usage ;; esac [ -n "$ver" ] || usage exec "$here/release-server.sh" promote "$ver" "$chan" ;; ship) # Ship converges the WHOLE fleet: one release-server.sh ship pass per box # (each box runs its own updater). Naming boxes limits the sweep; the # default is every fleet box, so nobody has to remember there are two. # `ship --expedite [box...]` overrides the supply-chain bake window — the # encoded form of the update-run --ignore-bake incident precedent. EXPEDITE="" if [ "${1:-}" = "--expedite" ]; then EXPEDITE="--expedite"; shift; fi [ $# -gt 0 ] && BOXES="$*" failed="" for box in $BOXES; do echo "ops ship: converging $box" # One box lagging (GitHub's CDN serves fresh manifests stale for a few # minutes) must not stop the others from converging — finish the sweep, # then report. LCHAT_BOX="$box" "$here/release-server.sh" ship $EXPEDITE || failed="$failed $box" done echo echo "note: this opened operator SSH sessions — the public access ledger wants a" echo " why-entry for this session: ops ledger \"shipped \" (web/status/ssh/)" if [ -n "$failed" ]; then echo "ops ship: still behind:$failed — usually the CDN window; retry in a few minutes" >&2 exit 1 fi ;; *) usage ;; esac