#!/usr/bin/env bash # Upload web/ and verify every published artifact (GUI app and installers, # terminal client and server) over the public URL. # # Usage: # ./scripts/deploy.sh user@host:/srv/zagan.bet https://zagan.bet/ # ./scripts/deploy.sh /srv/staging http://127.0.0.1:8899/ # ./scripts/deploy.sh --verify-only user@host:/srv/zagan.bet https://zagan.bet/ # # The target may be a local directory, which is handy for staging and for # testing the verification itself. Old versions are left in place on purpose; # prune them by hand when you want to drop them. # # Environment: # DEPLOY_TARGET, DEPLOY_URL same as the two arguments # SSH_PORT ssh port for a remote target set -euo pipefail ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) cd "$ROOT" die() { echo "deploy: $*" >&2; exit 1; } VERIFY_ONLY=0 if [ "${1:-}" = "--verify-only" ]; then VERIFY_ONLY=1 shift fi TARGET=${1:-${DEPLOY_TARGET:-}} URL=${2:-${DEPLOY_URL:-}} if [ -z "$TARGET" ] || [ -z "$URL" ]; then cat >&2 <<'EOF' usage: scripts/deploy.sh [--verify-only] target user@host:/srv/zagan.bet or a local directory url public URL of the page, e.g. https://zagan.bet/ --verify-only skips the upload and only checks the live page environment: DEPLOY_TARGET, DEPLOY_URL same as the two arguments SSH_PORT ssh port for a remote target EOF exit 2 fi for tool in rsync curl sha256sum; do command -v "$tool" >/dev/null || die "$tool is required" done [ -f web/index.php ] || die "web/index.php is missing" shopt -s nullglob artifacts=( web/bin/gui/zagan-bet-* web/bin/client/zagan-bet-* web/bin/server/zagan-bet-* ) shopt -u nullglob [ ${#artifacts[@]} -gt 0 ] || die "no artifacts in web/bin; run scripts/publish.sh first" if [ "$VERIFY_ONLY" -eq 0 ]; then echo "==> uploading web/ to $TARGET" # --delay-updates writes to temp names and renames at the end, so a visitor # never sees a half-written binary. Old versions are kept: no --delete. RSYNC_FLAGS=( -a --delay-updates --human-readable --chmod=D755,F644 --exclude=README.md --exclude=.gitignore --exclude=play/node_modules ) if [[ "$TARGET" == *:* ]]; then host=${TARGET%%:*} path=${TARGET#*:} ssh_args=() if [ -n "${SSH_PORT:-}" ]; then ssh_args+=(-p "$SSH_PORT") fi ssh ${ssh_args[@]+"${ssh_args[@]}"} "$host" "mkdir -p $(printf '%q' "$path")" rsync "${RSYNC_FLAGS[@]}" -e "ssh ${ssh_args[*]:-}" web/ "$host:$path/" else mkdir -p "$TARGET" rsync "${RSYNC_FLAGS[@]}" web/ "$TARGET/" fi fi echo "==> verifying $URL" tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT curl -fsSL --max-time 30 "$URL" -o "$tmp/page.html" \ || die "could not fetch $URL" origin=$(printf '%s' "$URL" | sed -E 's#^(https?://[^/]+).*#\1#') grep -oE '[0-9a-f]{64} [A-Za-z0-9._-]+' "$tmp/page.html" > "$tmp/sums" || true [ -s "$tmp/sums" ] || die "no SHA-256 lines on the page (is index.php being served?)" for script in install.sh install.ps1; do grep -qE "href=\"[^\"]*/$script\"" "$tmp/page.html" \ || die "$script is not linked from the page" curl -fsSL --max-time 30 "$origin/$script" -o "$tmp/$script" \ || die "could not fetch $script" cmp -s "web/$script" "$tmp/$script" \ || die "$script served from the site differs from the local file" done curl -fsSL --max-time 30 "$origin/latest.php" -o "$tmp/latest.txt" \ || die "could not fetch latest.php" newest=$(awk '$1 == "version" { print $2; exit }' "$tmp/latest.txt") [ -n "$newest" ] || die "latest.php has no version line" failed=0 verified=0 for local in "${artifacts[@]}"; do [ -f "$local" ] || continue file=$(basename "$local") kind=$(basename "$(dirname "$local")") local_hash=$(sha256sum "$local" | cut -d' ' -f1) href=$(grep -oE "href=\"[^\"]*/bin/$kind/$file\"" "$tmp/page.html" | head -1 \ | sed 's/^href="//;s/"$//' || true) if [ -z "$href" ]; then echo " $file: not listed on the page" failed=1 continue fi shown=$(awk -v name="$file" '$2 == name { print $1 }' "$tmp/sums" | head -1) if [ "$shown" != "$local_hash" ]; then echo " $file: page shows ${shown:-nothing}, local is $local_hash" failed=1 continue fi if ! curl -fsSL --max-time 120 "$origin$href" -o "$tmp/$file"; then echo " $file: download failed" failed=1 continue fi remote_hash=$(sha256sum "$tmp/$file" | cut -d' ' -f1) if [ "$remote_hash" != "$local_hash" ]; then echo " $file: downloaded bytes differ from local" failed=1 continue fi version=$(printf '%s' "$file" | sed -E 's/^zagan-bet-[a-z]+-([0-9]+\.[0-9]+\.[0-9]+)-.*/\1/') if [ "$version" = "$newest" ]; then latest_hash=$(awk -v kind="$kind" -v name="$file" '$1 == kind && $5 == name { print $6 }' "$tmp/latest.txt") if [ "$latest_hash" != "$local_hash" ]; then echo " $file: latest.php shows ${latest_hash:-nothing}, local is $local_hash" failed=1 continue fi fi echo " $file: ok" verified=$((verified + 1)) done curl -fsSL --max-time 30 "$origin/play/" -o "$tmp/play.html" \ || die "could not fetch the browser app at /play/" grep -q 'app.js' "$tmp/play.html" || die "/play/ does not reference app.js" for asset in app.js style.css; do curl -fsSL --max-time 30 "$origin/play/$asset" -o "$tmp/$asset" \ || die "could not fetch /play/$asset" cmp -s "web/play/$asset" "$tmp/$asset" \ || die "/play/$asset served from the site differs from the local file" done [ "$failed" -eq 0 ] || die "verification failed" echo "==> $verified artifact(s) and the browser app verified at $URL"