Files
esports/scripts/deploy-remote.sh

130 lines
5.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
sha=${1:?commit SHA required}
directory=${2:?deployment directory required}
origin=${3:?public origin required}
case "$sha" in
*[!0-9a-f]*|'') echo 'Invalid image tag' >&2; exit 2 ;;
esac
case "$directory:$origin" in
/opt/esports:https://esports.vash.software)
environment=/etc/esports/prod.env
host_port=3100 ;;
/opt/esports-staging:https://esports-staging.vash.software)
environment=/etc/esports-staging/staging.env
host_port=3101 ;;
*) echo 'Unrecognized deployment target' >&2; exit 2 ;;
esac
image="git.stanrunge.dev/stan/esports:$sha"
compose="$directory/compose.prod.yml"
if [ "$directory" = /opt/esports-staging ]; then
environment_name=staging
else
environment_name=production
fi
sudo install -d -m 0755 "$directory"
if [ ! -f "$directory/release.env" ]; then
printf 'IMAGE_TAG=%s\nHOST_PORT=%s\n' "$sha" "$host_port" | sudo tee "$directory/release.env" >/dev/null
fi
if ! grep -q '^HOST_PORT=' "$directory/release.env"; then
echo "HOST_PORT=$host_port" | sudo tee -a "$directory/release.env" >/dev/null
fi
sudo tee "$compose" >/dev/null <<EOF
services:
app:
image: git.stanrunge.dev/stan/esports:\${IMAGE_TAG}
restart: unless-stopped
stop_grace_period: 30s
env_file:
- path: $environment
format: raw
environment:
PORT: 3000
HOST: 0.0.0.0
BODY_SIZE_LIMIT: 6M
PROTOCOL_HEADER: x-forwarded-proto
HOST_HEADER: x-forwarded-host
ADDRESS_HEADER: CF-Connecting-IP
ports:
- 127.0.0.1:\${HOST_PORT}:3000
networks:
- backend
networks:
backend:
external: true
EOF
previous_tag=$(sed -n 's/^IMAGE_TAG=//p' "$directory/release.env" | head -n 1)
switched=0
rollback() {
if [ "$switched" -eq 1 ] && [ -n "$previous_tag" ]; then
echo "Health check failed; restoring prior image tag $previous_tag" >&2
sudo sed -i "s/^IMAGE_TAG=.*/IMAGE_TAG=$previous_tag/" "$directory/release.env"
sudo docker compose --env-file "$directory/release.env" -f "$compose" up -d
fi
}
trap rollback ERR
port=$(sed -n 's/^HOST_PORT=//p' "$directory/release.env" | head -n 1)
if [ "${FORCE_DEPLOY:-}" != 1 ] && curl --fail --silent --show-error "http://127.0.0.1:${port}/api/health" > /tmp/esports-health.json; then
python3 - <<'PY'
import json
data = json.load(open("/tmp/esports-health.json"))
active = int(data.get("activeMatches") or data.get("irc", {}).get("activeLobbies") or 0)
if active:
raise SystemExit(f"{active} matches are still live. Drain them from the admin page, or set FORCE_DEPLOY=1.")
PY
fi
# The old release reports lobbies rather than active matches, and its health
# endpoint may be unavailable. The database check covers both releases.
if [ "${FORCE_DEPLOY:-}" != 1 ]; then
active_count=$(sudo docker run --rm --network backend --env-file "$environment" "$image" \
bun -e 'const sql = new Bun.SQL(process.env.DB_URL || process.env.DATABASE_URL, { max: 1 }); const states = ["LOBBY", "ROLLING", "CHOOSING", "BANNING", "PICKING", "TB_SELECT", "PLAYING", "CLOSING"]; const [row] = await sql`select count(*)::int as count from match where state in ${sql(states)}`; console.log(row.count); await sql.end();')
if [ "$active_count" -gt 0 ]; then
echo "$active_count matches are live in the database. Drain them before deploying." >&2
exit 1
fi
fi
db_version=$(sudo docker run --rm --network backend --env-file "$environment" "$image" \
bun -e 'const sql = new Bun.SQL(process.env.DB_URL || process.env.DATABASE_URL, { max: 1 }); const [row] = await sql`show server_version_num`; console.log(row.server_version_num); await sql.end();')
case "$db_version" in
*[!0-9]*|'') echo 'Could not determine the PostgreSQL server version' >&2; exit 1 ;;
esac
if [ "$db_version" -ge 100000 ]; then
db_major=$((db_version / 10000))
else
db_major="$((db_version / 10000)).$(((db_version / 100) % 100))"
fi
backup_image="postgres:${db_major}-alpine"
backup_dir=/var/backups/esports
backup_stamp=$(date -u +%Y%m%dT%H%M%SZ)
backup_name="${environment_name}-${backup_stamp}-${sha}.dump"
sudo docker pull "$backup_image"
sudo install -d -m 0700 "$backup_dir"
sudo docker run --rm --network backend --env-file "$environment" \
--mount "type=bind,source=$backup_dir,target=/backups" \
--env "BACKUP_FILE=$backup_name" "$backup_image" \
sh -eu -c 'pg_dump --format=custom --no-owner --no-acl --file="/backups/$BACKUP_FILE" "${DB_URL:-$DATABASE_URL}"'
sudo docker run --rm --mount "type=bind,source=$backup_dir,target=/backups" \
"$backup_image" pg_restore --list "/backups/$backup_name" >/dev/null
sudo chmod 0600 "$backup_dir/$backup_name"
echo "Verified $environment_name database backup at $backup_dir/$backup_name"
sudo docker run --rm --network backend --env-file "$environment" "$image" \
bun scripts/migrate.ts
sudo sed -i "s/^IMAGE_TAG=.*/IMAGE_TAG=$sha/" "$directory/release.env"
switched=1
sudo docker compose --env-file "$directory/release.env" -f "$compose" up -d
curl --retry 12 --retry-delay 2 --retry-all-errors --fail --silent --show-error \
"http://127.0.0.1:${port}/api/health" >/dev/null
trap - ERR
echo "Deployed $sha to $origin"