130 lines
5.0 KiB
Bash
Executable File
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"
|