| 1 | #!/usr/bin/env sh |
| 2 | # π€ ops publish / promote / ship β the release path, one verb per stage. |
| 3 | # |
| 4 | # ./deploy/ops publish "release notes" # build HEAD -> server-<sha> release -> edge |
| 5 | # ./deploy/ops promote <ver> beta # move a channel pointer (re-signs the feed) |
| 6 | # ./deploy/ops ship # every fleet box catches up NOW |
| 7 | # |
| 8 | # The machinery is deploy/release-server.sh (which pulls the Bitwarden signing |
| 9 | # key itself, unlocking interactively when the vault is locked β no key setup). |
| 10 | # This wrapper adds only the rules that used to live in prose: |
| 11 | # |
| 12 | # - a release must correspond to LANDED MAIN CODE β publish refuses a HEAD |
| 13 | # that origin/main doesn't contain, so a feature branch can't become the |
| 14 | # fleet's edge build by accident; |
| 15 | # - there are exactly three channels, edge / beta / stable. There is NO dev |
| 16 | # channel β the dev mesh is deployed by `ops dev deploy`, never the feed; |
| 17 | # - `ship` means every box, not one: it loops the fleet (same aliases doctor |
| 18 | # checks), so a second box can't quietly stay behind; |
| 19 | # - box SSH sessions incur a why-entry in the public access ledger. |
| 20 | set -eu |
| 21 | |
| 22 | here="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" |
| 23 | root="$(CDPATH= cd -- "$here/.." && pwd)" |
| 24 | # Fleet box ssh aliases β user/hostname come from ~/.ssh/config (operator SSH |
| 25 | # rides WireGuard; specifics in CLAUDE.local.md). Same default as doctor.sh. |
| 26 | BOXES="${LCHAT_BOXES:-elevenbox kepler}" |
| 27 | |
| 28 | usage() { |
| 29 | echo "usage: ops publish [notes...] | ops promote <ver> edge|beta|stable | ops ship [--expedite] [box...]" >&2 |
| 30 | exit 2 |
| 31 | } |
| 32 | |
| 33 | cmd="${1:-}" |
| 34 | [ $# -gt 0 ] && shift |
| 35 | case "$cmd" in |
| 36 | |
| 37 | publish) |
| 38 | # Refuse to publish code main doesn't have: edge tracks main, and a branch |
| 39 | # build published by accident would auto-update the whole edge fleet onto |
| 40 | # unreviewed code within the hour. (Branch builds belong on a pinned canary: |
| 41 | # `ops canary deploy`.) Fetch first so the check is against today's main, |
| 42 | # not a stale ref; offline, fall back to the last-known one. |
| 43 | git -C "$root" fetch -q origin main 2>/dev/null \ |
| 44 | || echo "ops publish: warning: could not fetch origin/main β checking against the last-known ref" >&2 |
| 45 | if ! git -C "$root" merge-base --is-ancestor HEAD origin/main 2>/dev/null; then |
| 46 | echo "ops publish: HEAD is not on origin/main β a release must be landed main code." >&2 |
| 47 | echo " merge the PR, pull main, and publish from there" >&2 |
| 48 | echo " (branch builds go to a pinned canary instead: ops canary deploy <slug>)" >&2 |
| 49 | exit 1 |
| 50 | fi |
| 51 | # release-server.sh re-checks this, but 20 minutes earlier is kinder. |
| 52 | if [ -n "$(git -C "$root" status --porcelain 2>/dev/null)" ]; then |
| 53 | echo "ops publish: working tree is dirty β publish from a clean main" >&2 |
| 54 | exit 1 |
| 55 | fi |
| 56 | exec "$here/release-server.sh" publish "$@" |
| 57 | ;; |
| 58 | |
| 59 | promote) |
| 60 | ver="${1:-}"; chan="${2:-}" |
| 61 | # Catch the swapped-argument habit before release-server's terser usage line. |
| 62 | case "$ver" in edge|beta|stable) |
| 63 | echo "ops promote: arguments are <version> then <channel> β try: ops promote $chan $ver" >&2 |
| 64 | exit 2 ;; |
| 65 | esac |
| 66 | case "$chan" in |
| 67 | edge|beta|stable) ;; |
| 68 | dev) |
| 69 | echo "ops promote: there is no 'dev' channel β the channels are edge / beta / stable." >&2 |
| 70 | echo " the dev mesh is not on the release feed at all; deploy it with: ops dev deploy" >&2 |
| 71 | exit 2 ;; |
| 72 | *) usage ;; |
| 73 | esac |
| 74 | [ -n "$ver" ] || usage |
| 75 | exec "$here/release-server.sh" promote "$ver" "$chan" |
| 76 | ;; |
| 77 | |
| 78 | ship) |
| 79 | # Ship converges the WHOLE fleet: one release-server.sh ship pass per box |
| 80 | # (each box runs its own updater). Naming boxes limits the sweep; the |
| 81 | # default is every fleet box, so nobody has to remember there are two. |
| 82 | # `ship --expedite [box...]` overrides the supply-chain bake window β the |
| 83 | # encoded form of the update-run --ignore-bake incident precedent. |
| 84 | EXPEDITE="" |
| 85 | if [ "${1:-}" = "--expedite" ]; then EXPEDITE="--expedite"; shift; fi |
| 86 | [ $# -gt 0 ] && BOXES="$*" |
| 87 | failed="" |
| 88 | for box in $BOXES; do |
| 89 | echo "ops ship: converging $box" |
| 90 | # One box lagging (GitHub's CDN serves fresh manifests stale for a few |
| 91 | # minutes) must not stop the others from converging β finish the sweep, |
| 92 | # then report. |
| 93 | LCHAT_BOX="$box" "$here/release-server.sh" ship $EXPEDITE || failed="$failed $box" |
| 94 | done |
| 95 | echo |
| 96 | echo "note: this opened operator SSH sessions β the public access ledger wants a" |
| 97 | echo " why-entry for this session: ops ledger \"shipped <ver>\" (web/status/ssh/)" |
| 98 | if [ -n "$failed" ]; then |
| 99 | echo "ops ship: still behind:$failed β usually the CDN window; retry in a few minutes" >&2 |
| 100 | exit 1 |
| 101 | fi |
| 102 | ;; |
| 103 | |
| 104 | *) usage ;; |
| 105 | esac |
| 106 | |