Compare commits

..

2 Commits

Author SHA1 Message Date
331507ee40 fix: migrate with the owner role and a private health file
The app's database role cannot create tables in public, so migrations
failed with "permission denied for schema public". They now also load
/etc/esports/{prod,staging}-migrations.env, whose DATABASE_URL overrides
the app's. The health pre-check wrote a fixed /tmp file that another user
already owned, which silently skipped the live-match check; it uses mktemp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:47:58 +00:00
6ff5fe53c5 fix: deploy with only sudo tee and docker
Some checks failed
Test and deploy staging / staging (push) Failing after 16s
The deploy user has passwordless sudo for tee and docker only, so the
script stopped at its first sudo install. release.env is rewritten with
tee, and the backup container creates the backup directory and sets the
file modes itself. HOST_PORT comes from the target, not release.env.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:37:50 +00:00

View File

@@ -10,9 +10,11 @@ esac
case "$directory:$origin" in case "$directory:$origin" in
/opt/esports:https://esports.vash.software) /opt/esports:https://esports.vash.software)
environment=/etc/esports/prod.env environment=/etc/esports/prod.env
migration_environment=/etc/esports/prod-migrations.env
host_port=3100 ;; host_port=3100 ;;
/opt/esports-staging:https://esports-staging.vash.software) /opt/esports-staging:https://esports-staging.vash.software)
environment=/etc/esports-staging/staging.env environment=/etc/esports-staging/staging.env
migration_environment=/etc/esports/staging-migrations.env
host_port=3101 ;; host_port=3101 ;;
*) echo 'Unrecognized deployment target' >&2; exit 2 ;; *) echo 'Unrecognized deployment target' >&2; exit 2 ;;
esac esac
@@ -25,13 +27,15 @@ else
environment_name=production environment_name=production
fi fi
sudo install -d -m 0755 "$directory" # The deploy user only has passwordless sudo for tee and docker.
if [ ! -f "$directory/release.env" ]; then if [ ! -d "$directory" ]; then
printf 'IMAGE_TAG=%s\nHOST_PORT=%s\n' "$sha" "$host_port" | sudo tee "$directory/release.env" >/dev/null echo "$directory does not exist; create it once on the host" >&2
fi exit 1
if ! grep -q '^HOST_PORT=' "$directory/release.env"; then
echo "HOST_PORT=$host_port" | sudo tee -a "$directory/release.env" >/dev/null
fi fi
previous_tag=$(sed -n 's/^IMAGE_TAG=//p' "$directory/release.env" 2>/dev/null | head -n 1 || true)
write_release() {
printf 'IMAGE_TAG=%s\nHOST_PORT=%s\n' "$1" "$host_port" | sudo tee "$directory/release.env" >/dev/null
}
sudo tee "$compose" >/dev/null <<EOF sudo tee "$compose" >/dev/null <<EOF
services: services:
@@ -59,22 +63,22 @@ networks:
external: true external: true
EOF EOF
previous_tag=$(sed -n 's/^IMAGE_TAG=//p' "$directory/release.env" | head -n 1)
switched=0 switched=0
rollback() { rollback() {
if [ "$switched" -eq 1 ] && [ -n "$previous_tag" ]; then if [ "$switched" -eq 1 ] && [ -n "$previous_tag" ]; then
echo "Health check failed; restoring prior image tag $previous_tag" >&2 echo "Health check failed; restoring prior image tag $previous_tag" >&2
sudo sed -i "s/^IMAGE_TAG=.*/IMAGE_TAG=$previous_tag/" "$directory/release.env" write_release "$previous_tag"
sudo docker compose --env-file "$directory/release.env" -f "$compose" up -d sudo docker compose --env-file "$directory/release.env" -f "$compose" up -d
fi fi
} }
trap rollback ERR trap rollback ERR
port=$(sed -n 's/^HOST_PORT=//p' "$directory/release.env" | head -n 1) port=$host_port
if [ "${FORCE_DEPLOY:-}" != 1 ] && curl --fail --silent --show-error "http://127.0.0.1:${port}/api/health" > /tmp/esports-health.json; then health=$(mktemp)
python3 - <<'PY' if [ "${FORCE_DEPLOY:-}" != 1 ] && curl --fail --silent --show-error "http://127.0.0.1:${port}/api/health" > "$health"; then
import json HEALTH_FILE=$health python3 - <<'PY'
data = json.load(open("/tmp/esports-health.json")) import json, os
data = json.load(open(os.environ["HEALTH_FILE"]))
active = int(data.get("activeMatches") or data.get("irc", {}).get("activeLobbies") or 0) active = int(data.get("activeMatches") or data.get("irc", {}).get("activeLobbies") or 0)
if active: if active:
raise SystemExit(f"{active} matches are still live. Drain them from the admin page, or set FORCE_DEPLOY=1.") raise SystemExit(f"{active} matches are still live. Drain them from the admin page, or set FORCE_DEPLOY=1.")
@@ -107,20 +111,22 @@ backup_dir=/var/backups/esports
backup_stamp=$(date -u +%Y%m%dT%H%M%SZ) backup_stamp=$(date -u +%Y%m%dT%H%M%SZ)
backup_name="${environment_name}-${backup_stamp}-${sha}.dump" backup_name="${environment_name}-${backup_stamp}-${sha}.dump"
sudo docker pull "$backup_image" sudo docker pull "$backup_image"
sudo install -d -m 0700 "$backup_dir" # The container runs as root, so it creates the directory and sets the modes.
sudo docker run --rm --network backend --env-file "$environment" \ sudo docker run --rm --network backend --env-file "$environment" \
--mount "type=bind,source=$backup_dir,target=/backups" \ --mount "type=bind,source=$(dirname "$backup_dir"),target=/backups" \
--env "BACKUP_FILE=$backup_name" "$backup_image" \ --env "BACKUP_FILE=$(basename "$backup_dir")/$backup_name" "$backup_image" \
sh -eu -c 'pg_dump --format=custom --no-owner --no-acl --file="/backups/$BACKUP_FILE" "${DB_URL:-$DATABASE_URL}"' sh -eu -c 'mkdir -p -m 0700 "/backups/${BACKUP_FILE%/*}"
sudo docker run --rm --mount "type=bind,source=$backup_dir,target=/backups" \ pg_dump --format=custom --no-owner --no-acl --file="/backups/$BACKUP_FILE" "${DB_URL:-$DATABASE_URL}"
"$backup_image" pg_restore --list "/backups/$backup_name" >/dev/null chmod 0600 "/backups/$BACKUP_FILE"
sudo chmod 0600 "$backup_dir/$backup_name" pg_restore --list "/backups/$BACKUP_FILE" >/dev/null'
echo "Verified $environment_name database backup at $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" \ # The app's database role cannot create tables; the migrations file overrides
bun scripts/migrate.ts # DATABASE_URL with the owner role and keeps the rest (ROOT_ADMIN_USER_ID).
sudo docker run --rm --network backend --env-file "$environment" \
--env-file "$migration_environment" "$image" bun scripts/migrate.ts
sudo sed -i "s/^IMAGE_TAG=.*/IMAGE_TAG=$sha/" "$directory/release.env" write_release "$sha"
switched=1 switched=1
sudo docker compose --env-file "$directory/release.env" -f "$compose" up -d 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 \ curl --retry 12 --retry-delay 2 --retry-all-errors --fail --silent --show-error \