summaryrefslogtreecommitdiffstats
path: root/scripts/deploy.sh
blob: 63bd31c896052a9d2994cac56f381e3b89d066a0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/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> <url>

  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
  )

  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

[ "$failed" -eq 0 ] || die "verification failed"

echo "==> $verified artifact(s) verified at $URL"