Collects Pararius rent/buy listings into Postgres via a worker that runs on a residential connection, sends Discord/ntfy alerts for new listings matching rules, and shows an admin-only map, market stats and worker log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013HfF5tnChi8tkCx9svJP6D
100 lines
4.1 KiB
Bash
100 lines
4.1 KiB
Bash
#!/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/personal-website:https://stanrunge.dev)
|
|
environment=/etc/personal-website/prod.env ;;
|
|
/opt/personal-website-staging:https://staging.stanrunge.dev)
|
|
environment=/etc/personal-website-staging/staging.env ;;
|
|
*) echo 'Unrecognized deployment target' >&2; exit 2 ;;
|
|
esac
|
|
|
|
image="git.stanrunge.dev/stan/personal-website:$sha"
|
|
compose="$directory/compose.prod.yml"
|
|
if [ "$directory" = /opt/personal-website-staging ]; then
|
|
environment_name=staging
|
|
else
|
|
environment_name=production
|
|
fi
|
|
previous_tag=$(sed -n 's/^IMAGE_TAG=//p' "$directory/.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
|
|
printf 'IMAGE_TAG=%s\n' "$previous_tag" | sudo tee "$directory/.env" >/dev/null
|
|
sudo docker compose --env-file "$directory/.env" -f "$compose" up -d
|
|
fi
|
|
}
|
|
trap rollback ERR
|
|
|
|
db_version=$(sudo docker run --rm --network backend --env-file "$environment" "$image" \
|
|
bun scripts/print-db-server-version.ts)
|
|
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/personal-website
|
|
backup_stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
|
backup_name="${environment_name}-${backup_stamp}-${sha}.dump"
|
|
sudo docker pull "$backup_image"
|
|
# The deploy account can run Docker, but has no general host-file sudo access.
|
|
# A bind volume creates the root-owned directory; restrict it before dumping.
|
|
sudo docker run --rm --network backend --env-file "$environment" \
|
|
--volume "$backup_dir:/backups" \
|
|
--env "BACKUP_FILE=$backup_name" "$backup_image" \
|
|
sh -eu -c 'chmod 0700 /backups; umask 077; pg_dump --format=custom --no-owner --no-acl --file="/backups/$BACKUP_FILE" "$DB_URL"'
|
|
sudo docker run --rm --mount "type=bind,source=$backup_dir,target=/backups" \
|
|
"$backup_image" pg_restore --list "/backups/$backup_name" >/dev/null
|
|
echo "Verified $environment_name database backup at $backup_dir/$backup_name"
|
|
|
|
run_database_script() {
|
|
echo "Running $1"
|
|
sudo docker run --rm --network backend --env-file "$environment" "$image" bun "$1"
|
|
}
|
|
|
|
run_database_script scripts/migrate-auth-catalog.ts
|
|
run_database_script scripts/migrate-foundation.ts
|
|
run_database_script scripts/migrate-money-tracker.ts
|
|
run_database_script scripts/migrate-game-tracker.ts
|
|
run_database_script scripts/migrate-tracker-journal.ts
|
|
run_database_script scripts/migrate-house-tracker.ts
|
|
run_database_script scripts/seed-catalog.ts
|
|
run_database_script scripts/seed-cod-catalog.ts
|
|
run_database_script scripts/seed-pokemon-research.ts
|
|
run_database_script scripts/seed-cod-challenges.ts
|
|
run_database_script scripts/seed-cod-art.ts
|
|
|
|
sudo docker run --rm --network backend --env-file "$environment" "$image" \
|
|
bun scripts/deploy-preflight.ts "$origin"
|
|
|
|
printf 'IMAGE_TAG=%s\n' "$sha" | sudo tee "$directory/.env" >/dev/null
|
|
switched=1
|
|
sudo docker compose --env-file "$directory/.env" -f "$compose" up -d
|
|
|
|
container=$(sudo docker compose --env-file "$directory/.env" -f "$compose" ps -q app)
|
|
expected_image=$(sudo docker image inspect -f '{{.Id}}' "$image")
|
|
actual_image=$(sudo docker inspect -f '{{.Image}}' "$container")
|
|
if [ "$actual_image" != "$expected_image" ]; then
|
|
echo 'Compose did not start the intended image' >&2
|
|
false
|
|
fi
|
|
curl --retry 12 --retry-delay 2 --retry-all-errors --fail --silent --show-error \
|
|
"$origin/login" >/dev/null
|
|
curl --retry 6 --retry-delay 2 --retry-all-errors --fail --silent --show-error \
|
|
"$origin/projects/game-tracker/pokemon" >/dev/null
|
|
curl --retry 6 --retry-delay 2 --retry-all-errors --fail --silent --show-error \
|
|
"$origin/projects/game-tracker/call-of-duty/black-ops-2" >/dev/null
|
|
trap - ERR
|
|
echo "Deployed $sha to $origin"
|