#!/usr/bin/env bash # Build, send and switch the server (and the agent/runner downloads) on the VPS, checking sha256 at every step, # then tag the commit that went out (deploy-N) and push the tag. # scripts/deploy.sh build + deploy the server # scripts/deploy.sh server agent also publish the agent binaries (arm64 and amd64) in /opt/wxch/dl # scripts/deploy.sh all server + agent + runner # scripts/deploy.sh --no-build ... use what is in dist/ already # scripts/deploy.sh --src DIR ... build and deploy the source tree in DIR (used by scripts/rollback.sh); no tag # # Settings come from scripts/deploy.env (not in git; see scripts/deploy.env.example): # WXCH_DEPLOY_HOST=root@ WXCH_DEPLOY_KEY= # # What it does NOT do: update the agent on a machine (that restarts the agent and kills running downloads: use the # Update button on the Models page when nothing is downloading) or run the installer on a machine. # It stops at the first mismatch and never switches the server unless the uploaded file is byte for byte the built one. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" [ -f scripts/deploy.env ] && . scripts/deploy.env : "${WXCH_DEPLOY_HOST:?set WXCH_DEPLOY_HOST in scripts/deploy.env (see scripts/deploy.env.example)}" : "${WXCH_DEPLOY_KEY:?set WXCH_DEPLOY_KEY in scripts/deploy.env (see scripts/deploy.env.example)}" [ -f "$WXCH_DEPLOY_KEY" ] || { echo "ssh key not found: $WXCH_DEPLOY_KEY" >&2; exit 1; } SITE="${WXCH_DEPLOY_SITE:-https://control.wxch.org}" BUILD=1; SRC="$ROOT"; WHAT=() while [ $# -gt 0 ]; do case "$1" in --no-build) BUILD=0 ;; --src) shift; SRC="$(cd "${1:?--src needs a folder}" && pwd)" ;; server|agent|runner) WHAT+=("$1") ;; all) WHAT+=(server agent runner) ;; *) echo "unknown argument: $1 (use: server agent runner all --no-build --src DIR)" >&2; exit 2 ;; esac shift done [ ${#WHAT[@]} -gt 0 ] || WHAT=(server) want() { for w in "${WHAT[@]}"; do [ "$w" = "$1" ] && return 0; done; return 1; } SSH=(ssh -i "$WXCH_DEPLOY_KEY" -o BatchMode=yes -o ConnectTimeout=15) SCP=(scp -i "$WXCH_DEPLOY_KEY" -o BatchMode=yes -o ConnectTimeout=15) say() { printf '\033[1m[deploy]\033[0m %s\n' "$*"; } sum() { sha256sum "$1" | cut -d' ' -f1; } SUMS=() # "name hash" lines for the tag message if [ "$BUILD" = 1 ]; then say "building $([ "$SRC" = "$ROOT" ] || echo "$SRC ")(vet, tests, binaries)" (cd "$SRC" && bash scripts/build.sh >/dev/null) fi DIST="$SRC/dist" [ -f "$DIST/wxch-server" ] || { echo "$DIST is empty: run without --no-build" >&2; exit 1; } "${SSH[@]}" "$WXCH_DEPLOY_HOST" "mkdir -p /tmp/wxch-new" || { echo "cannot reach $WXCH_DEPLOY_HOST with that key" >&2; exit 1; } # remote hash of a file, empty if missing rsum() { "${SSH[@]}" "$WXCH_DEPLOY_HOST" "sha256sum '$1' 2>/dev/null | cut -d' ' -f1" || true; } if want server; then LOCAL="$(sum "$DIST/wxch-server")" say "server ${LOCAL:0:12}: uploading" "${SCP[@]}" "$DIST/wxch-server" "$WXCH_DEPLOY_HOST:/tmp/wxch-new/wxch-server" "${SCP[@]}" "$ROOT/scripts/vps-swap-server.sh" "$WXCH_DEPLOY_HOST:/tmp/wxch-new/swap-server.sh" [ "$(rsum /tmp/wxch-new/wxch-server)" = "$LOCAL" ] || { echo "the uploaded server differs from the built one: not switching" >&2; exit 1; } say "hash matches, switching (everybody is logged out once)" "${SSH[@]}" "$WXCH_DEPLOY_HOST" "bash /tmp/wxch-new/swap-server.sh" [ "$(rsum /opt/wxch/wxch-server)" = "$LOCAL" ] || { echo "after the switch /opt/wxch/wxch-server is not the built file" >&2; exit 1; } SUMS+=("wxch-server $LOCAL") fi for kind in agent runner; do want "$kind" || continue for arch in arm64 amd64; do f="$DIST/dl/wxch-$kind-$arch" LOCAL="$(sum "$f")" say "$kind-$arch ${LOCAL:0:12}: uploading" "${SCP[@]}" "$f" "$WXCH_DEPLOY_HOST:/opt/wxch/dl/wxch-$kind-$arch" "${SSH[@]}" "$WXCH_DEPLOY_HOST" "chown root:root /opt/wxch/dl/wxch-$kind-$arch && chmod 755 /opt/wxch/dl/wxch-$kind-$arch" [ "$(rsum "/opt/wxch/dl/wxch-$kind-$arch")" = "$LOCAL" ] || { echo "$f was not stored intact" >&2; exit 1; } SUMS+=("wxch-$kind-$arch $LOCAL") done done say "checking from outside (read-only)" code() { curl -s -o /dev/null -m 15 -w '%{http_code}' "$1" || true; } [ "$(code "$SITE/get")" = 200 ] || { echo "$SITE/get did not answer 200" >&2; exit 1; } [ "$(code "$SITE/api/me")" = 401 ] || { echo "$SITE/api/me should answer 401 when not logged in" >&2; exit 1; } say "done: $(IFS=,; echo "${WHAT[*]}") deployed and verified" # ---- tag the commit that went out: deploy-N, with what was deployed and the hashes ---- [ "$SRC" = "$ROOT" ] || { say "deployed from $SRC: no tag (this was a rollback or a special build)"; exit 0; } if [ -n "$(git status --porcelain)" ]; then say "WARNING: the working tree has uncommitted changes, so no tag was made (a tag must mean exactly this commit)" exit 0 fi LAST="$(git tag -l 'deploy-*' | sed 's/^deploy-//' | grep -E '^[0-9]+$' | sort -n | tail -1 || true)" N=$(( ${LAST:-0} + 1 )) TAG="deploy-$N" COMMIT="$(git rev-parse --short HEAD)" { echo "Deploy $N: $(git log -1 --format=%s)" echo echo "date: $(date -u '+%Y-%m-%d %H:%M UTC')" echo "commit: $COMMIT" echo "deployed: $(IFS=,; echo "${WHAT[*]}") to $WXCH_DEPLOY_HOST" [ -n "$LAST" ] && echo "previous: deploy-$LAST" echo "sha256:" printf ' %s\n' "${SUMS[@]}" echo [ -n "$LAST" ] && echo "roll back to the previous deploy: bash scripts/rollback.sh deploy-$LAST" echo "roll back to this deploy later: bash scripts/rollback.sh $TAG" } > "${TMPDIR:-/tmp}/wxch-tag-msg.$$" git tag -a "$TAG" -F "${TMPDIR:-/tmp}/wxch-tag-msg.$$" rm -f "${TMPDIR:-/tmp}/wxch-tag-msg.$$" say "tagged $TAG ($COMMIT)" if git push origin "$TAG" >/dev/null 2>&1; then say "tag $TAG pushed"; else say "WARNING: could not push $TAG: run git push origin $TAG"; fi [ -n "$LAST" ] && say "roll back to the previous deploy: bash scripts/rollback.sh deploy-$LAST" exit 0