cyb/harness/ship.sh

#!/bin/bash
# ship โ€” every version becomes a GitHub release, the erga way.
#
# One command: bump the version, commit through the fleet gate, tag,
# build the dmg AND the signed APK from the clean tagged tree (a release
# never wears the dirty star), publish to GitHub with both attached,
# install locally. One binary, every body: apple silicon + android in
# every release; ubuntu and windows come from the cybernode build node.
#
#   make ship                       # bump minor, title = last commit line
#   make ship V=0.3.0 T="headline"  # explicit version and title
#   N="extra notes" make ship       # prepended to the auto notes
#
# Notes are the commit log since the previous tag โ€” the release IS the
# changelog, nothing to write twice.

set -euo pipefail
cd "$(dirname "$")/.."
# apksigner is a java program; gradle found its jdk through the Makefile,
# the signer gets the same one.
export JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk@17}"
export PATH="$JAVA_HOME/bin:$PATH"

# A release is a statement about a tree, so the tree must be fully told.
if [ -n "$(git status --porcelain)" ]; then
  echo "ship: tree is dirty โ€” commit or stash first"; git status --short | head; exit 1
fi

CUR=$(grep -m1 '^version' shell/Cargo.toml | sed 's/.*"\(.*\)"/\1/')
if [ "${V:-auto}" = "auto" ]; then
  V=$(echo "$CUR" | awk -F. '{printf "%d.%d.0", $1, $2 + 1}')
fi
TAG="v$V"
TITLE="${T:-$(git log -1 --pretty=%s)}"

if git rev-parse "$TAG" >/dev/null 2>&1; then
  echo "ship: $TAG already exists"; exit 1
fi

echo "ship: $CUR -> $V  ($TITLE)"

# โ”€โ”€ bump + commit (the pre-commit fleet gate runs here) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Idempotent: a ship that died after the bump resumes without re-bumping.
if [ "$V" != "$CUR" ]; then
  sed -i '' "s/^version = \"$CUR\"/version = \"$V\"/" shell/Cargo.toml
  T_BIN="$HOME/.rustup/toolchains/stable-aarch64-apple-darwin/bin"
  RUSTC="$T_BIN/rustc" "$T_BIN/cargo" check -p cyb >/dev/null 2>&1 || true # refresh lock
  git add shell/Cargo.toml Cargo.lock
  git commit -m "cyb $V"
else
  echo "ship: version already $V - resuming"
fi

# โ”€โ”€ build the artifacts from the tagged, clean tree โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
make dmg
DMG="target/release/cyb-$V.dmg"
cp target/release/cyb.dmg "$DMG"
ASSETS=("$DMG")

# Android: build, align, sign with the local release key. The keystore is
# self-signed and lives outside the repo (~/.cyb-release.keystore) โ€” same
# trust model as the unsigned dmg, but installable on a phone.
KS="$HOME/.cyb-release.keystore"
if [ -f "$KS" ]; then
  echo "ship: building the android body..."
  make android
  BT="$HOME/Library/Android/sdk/build-tools/34.0.0"
  RAW="shell/gen/android/app/build/outputs/apk/release/app-release-unsigned.apk"
  APK="target/release/cyb-$V.apk"
  "$BT/zipalign" -f 4 "$RAW" "$APK.aligned"
  "$BT/apksigner" sign --ks "$KS" --ks-key-alias cyb \
    --ks-pass "file:$HOME/.cyb-release.keystore.pass" \
    --out "$APK" "$APK.aligned"
  rm -f "$APK.aligned" "$APK.idsig"
  "$BT/apksigner" verify "$APK"
  echo "ship: apk signed and verified"
  ASSETS+=("$APK")
else
  echo "ship: no $KS - skipping the android body"
fi

# Linux + Windows: built on the cybernode (deimos, the quiet one) โ€” the
# workspace spans sibling repos GitHub never sees, so CI cannot build it,
# a node can. CYB_NODE overrides; CYB_SKIP_NODE=1 ships mac+android only.
NODE="${CYB_NODE:-deimos}"
if [ "${CYB_SKIP_NODE:-0}" != "1" ]; then
  echo "ship: ubuntu + windows on $NODE..."
  if bash harness/build-node.sh "$NODE" "$V" all; then
    ASSETS+=("target/release/cyb-$V-linux-x86_64.tar.gz")
    ASSETS+=("target/release/cyb-$V-windows-x86_64.zip")
  else
    echo "ship: node build FAILED - shipping without linux/windows"
  fi
fi

# Every body built โ€” NOW the version is real.
git tag -a "$TAG" -m "cyb $V โ€” $TITLE"

# โ”€โ”€ notes: what actually changed since the last release โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
PREV=$(git describe --tags --abbrev=0 "$TAG"^ 2>/dev/null || echo "")
NOTES_FILE=$(mktemp)
{
  [ -n "${N:-}" ] && printf '%s\n\n' "$N"
  echo "## changes"
  if [ -n "$PREV" ]; then
    git log --pretty='- %s' "$PREV".."$TAG" | grep -v '^- cyb [0-9]'
  else
    git log --pretty='- %s' -20
  fi
  echo
  echo "**macOS (apple silicon)**: mount the dmg, then"
  echo '```'
  echo "xattr -cr /Applications/cyb.app && codesign --force --deep -s - /Applications/cyb.app"
  echo '```'
  echo "**android**: \`adb install cyb-$V.apk\` (self-signed; allow unknown sources)"
  echo "**ubuntu (x86_64, 20.04+)**: untar, then ./cyb"
  echo "**windows (x86_64)**: unzip, run cyb.exe - cross-built, testers welcome"
} > "$NOTES_FILE"

# โ”€โ”€ publish โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
git push origin master --tags
gh release create "$TAG" "${ASSETS[@]}" \
  --title "cyb $V โ€” $TITLE" \
  --notes-file "$NOTES_FILE"
rm -f "$NOTES_FILE"

# โ”€โ”€ and run what we shipped โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
rm -rf ~/Applications/cyb.app /Applications/cyb.app
cp -R target/release/cyb.app ~/Applications/cyb.app
cp -R target/release/cyb.app /Applications/cyb.app
xattr -cr ~/Applications/cyb.app /Applications/cyb.app
codesign --force --deep -s - ~/Applications/cyb.app 2>/dev/null || true
codesign --force --deep -s - /Applications/cyb.app 2>/dev/null || true

echo "ship: cyb $V is live โ€” $(gh release view "$TAG" --json url -q .url)"

Graph