From 79588a8bba1648ea4d396a1d1e9884af54e497e2 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 30 Oct 2026 00:40:10 +0100 Subject: add deploy script with live verification; cache file hashes by size and mtime --- scripts/deploy.sh | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100755 scripts/deploy.sh (limited to 'scripts/deploy.sh') diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..99129ad --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# Upload web/ and verify every published artifact 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/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 + ) + + 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?)" + +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 + + echo " $file: ok" + verified=$((verified + 1)) +done + +[ "$failed" -eq 0 ] || die "verification failed" + +echo "==> $verified artifact(s) verified at $URL" -- cgit v1.2.3