HASH bcb96bdb3d56
DATE 2025-12-04
SUBJECT ci: bucket database
FILES 6 CHANGED
HASH bcb96bdb3d56
DATE 2025-12-04
SUBJECT ci: bucket database
FILES 6 CHANGED
┌─ .github/workflows/deploy-preview.yml ─────────────────────────────────────┐
│ diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-previ │
│ ew.yml │
│ new file mode 100644 │
│ index 0000000..c2595e0 │
│ --- /dev/null │
│ +++ b/.github/workflows/deploy-preview.yml │
│ @@ -0,0 +1,58 @@ │
│ +name: Deploy Preview │
│ + │
│ +on: │
│ + workflow_dispatch: │
│ + │
│ +concurrency: │
│ + group: database-builder-preview │
│ + cancel-in-progress: false │
│ + │
│ +permissions: │
│ + contents: read │
│ + │
│ +env: │
│ + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} │
│ + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} │
│ + │
│ +jobs: │
│ + deploy: │
│ + runs-on: ubuntu-latest │
│ + steps: │
│ + - name: Checkout repository │
│ + uses: actions/checkout@v4 │
│ + with: │
│ + token: ${{ secrets.GITHUB_TOKEN }} │
│ + │
│ + - name: Install Nix │
│ + uses: cachix/install-nix-action@v27 │
│ + │
│ + - name: Authenticate with Blizzard API │
│ + id: blizzard_auth │
│ + run: | │
│ + echo "Authenticating with Blizzard API..." │
│ + │
│ + RESPONSE=$(curl -s -X POST "https://oauth.battle.net/token" \ │
│ + -H "Content-Type: application/x-www-form-urlencoded" \ │
│ + -d "grant_type=client_credentials" \ │
│ + -u "${{ secrets.BLIZZARD_CLIENT_ID }}:${{ secrets.BLIZZARD_CLIENT_SEC │
│ RET }}") │
│ + │
│ + ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token') │
│ + │
│ + if [ "$ACCESS_TOKEN" = "null" ] || [ -z "$ACCESS_TOKEN" ]; then │
│ + echo "Failed to authenticate with Blizzard API" │
│ + exit 1 │
│ + fi │
│ + │
│ + echo "::add-mask::$ACCESS_TOKEN" │
│ + echo "Successfully authenticated with Blizzard API" │
│ + echo "BLIZZARD_API_TOKEN=$ACCESS_TOKEN" >> $GITHUB_ENV │
│ + │
│ + - name: Deploy ookstats (preview) │
│ + env: │
│ + BLIZZARD_API_TOKEN: ${{ env.BLIZZARD_API_TOKEN }} │
│ + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} │
│ + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} │
│ + AWS_ENDPOINT_URL: ${{ vars.AWS_ENDPOINT_URL }} │
│ + AWS_BUCKET: ${{ vars.AWS_BUCKET }} │
│ + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} │
│ + run: nix run .#ookstats-deploy -- --preview │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../workflows/deploy-preview.yml ───┐
│ diff --git a/.github/workflows/deploy-previe │
│ w.yml b/.github/workflows/deploy-preview.yml │
│ new file mode 100644 │
│ index 0000000..c2595e0 │
│ --- /dev/null │
│ +++ b/.github/workflows/deploy-preview.yml │
│ @@ -0,0 +1,58 @@ │
│ +name: Deploy Preview │
│ + │
│ +on: │
│ + workflow_dispatch: │
│ + │
│ +concurrency: │
│ + group: database-builder-preview │
│ + cancel-in-progress: false │
│ + │
│ +permissions: │
│ + contents: read │
│ + │
│ +env: │
│ + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID │
│ }} │
│ + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PRO │
│ JECT_ID }} │
│ + │
│ +jobs: │
│ + deploy: │
│ + runs-on: ubuntu-latest │
│ + steps: │
│ + - name: Checkout repository │
│ + uses: actions/checkout@v4 │
│ + with: │
│ + token: ${{ secrets.GITHUB_TOKEN } │
│ } │
│ + │
│ + - name: Install Nix │
│ + uses: cachix/install-nix-action@v27 │
│ + │
│ + - name: Authenticate with Blizzard AP │
│ I │
│ + id: blizzard_auth │
│ + run: | │
│ + echo "Authenticating with Blizzar │
│ d API..." │
│ + │
│ + RESPONSE=$(curl -s -X POST "https │
│ ://oauth.battle.net/token" \ │
│ + -H "Content-Type: application/x │
│ -www-form-urlencoded" \ │
│ + -d "grant_type=client_credentia │
│ ls" \ │
│ + -u "${{ secrets.BLIZZARD_CLIENT │
│ _ID }}:${{ secrets.BLIZZARD_CLIENT_SECRET }} │
│ ") │
│ + │
│ + ACCESS_TOKEN=$(echo "$RESPONSE" | │
│ jq -r '.access_token') │
│ + │
│ + if [ "$ACCESS_TOKEN" = "null" ] | │
│ | [ -z "$ACCESS_TOKEN" ]; then │
│ + echo "Failed to authenticate wi │
│ th Blizzard API" │
│ + exit 1 │
│ + fi │
│ + │
│ + echo "::add-mask::$ACCESS_TOKEN" │
│ + echo "Successfully authenticated │
│ with Blizzard API" │
│ + echo "BLIZZARD_API_TOKEN=$ACCESS_ │
│ TOKEN" >> $GITHUB_ENV │
│ + │
│ + - name: Deploy ookstats (preview) │
│ + env: │
│ + BLIZZARD_API_TOKEN: ${{ env.BLIZZ │
│ ARD_API_TOKEN }} │
│ + AWS_ACCESS_KEY_ID: ${{ secrets.AW │
│ S_ACCESS_KEY_ID }} │
│ + AWS_SECRET_ACCESS_KEY: ${{ secret │
│ s.AWS_SECRET_ACCESS_KEY }} │
│ + AWS_ENDPOINT_URL: ${{ vars.AWS_EN │
│ DPOINT_URL }} │
│ + AWS_BUCKET: ${{ vars.AWS_BUCKET } │
│ } │
│ + VERCEL_TOKEN: ${{ secrets.VERCEL_ │
│ TOKEN }} │
│ + run: nix run .#ookstats-deploy -- - │
│ -preview │
└──────────────────────────────────────────────┘
┌─ .github/workflows/deploy-prod.yml ────────────────────────────────────────┐
│ diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml │
│ new file mode 100644 │
│ index 0000000..5820e8c │
│ --- /dev/null │
│ +++ b/.github/workflows/deploy-prod.yml │
│ @@ -0,0 +1,60 @@ │
│ +name: Deploy Production │
│ + │
│ +on: │
│ + workflow_dispatch: │
│ + # schedule: │
│ + # - cron: "0 */2 * * *" │
│ + │
│ +concurrency: │
│ + group: database-builder │
│ + cancel-in-progress: false │
│ + │
│ +permissions: │
│ + contents: read │
│ + │
│ +env: │
│ + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} │
│ + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} │
│ + │
│ +jobs: │
│ + deploy: │
│ + runs-on: ubuntu-latest │
│ + steps: │
│ + - name: Checkout repository │
│ + uses: actions/checkout@v4 │
│ + with: │
│ + token: ${{ secrets.GITHUB_TOKEN }} │
│ + │
│ + - name: Install Nix │
│ + uses: cachix/install-nix-action@v27 │
│ + │
│ + - name: Authenticate with Blizzard API │
│ + id: blizzard_auth │
│ + run: | │
│ + echo "Authenticating with Blizzard API..." │
│ + │
│ + RESPONSE=$(curl -s -X POST "https://oauth.battle.net/token" \ │
│ + -H "Content-Type: application/x-www-form-urlencoded" \ │
│ + -d "grant_type=client_credentials" \ │
│ + -u "${{ secrets.BLIZZARD_CLIENT_ID }}:${{ secrets.BLIZZARD_CLIENT_SEC │
│ RET }}") │
│ + │
│ + ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token') │
│ + │
│ + if [ "$ACCESS_TOKEN" = "null" ] || [ -z "$ACCESS_TOKEN" ]; then │
│ + echo "Failed to authenticate with Blizzard API" │
│ + exit 1 │
│ + fi │
│ + │
│ + echo "::add-mask::$ACCESS_TOKEN" │
│ + echo "Successfully authenticated with Blizzard API" │
│ + echo "BLIZZARD_API_TOKEN=$ACCESS_TOKEN" >> $GITHUB_ENV │
│ + │
│ + - name: Deploy ookstats │
│ + env: │
│ + BLIZZARD_API_TOKEN: ${{ env.BLIZZARD_API_TOKEN }} │
│ + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} │
│ + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} │
│ + AWS_ENDPOINT_URL: ${{ vars.AWS_ENDPOINT_URL }} │
│ + AWS_BUCKET: ${{ vars.AWS_BUCKET }} │
│ + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} │
│ + run: nix run .#ookstats-deploy │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...hub/workflows/deploy-prod.yml ───┐
│ diff --git a/.github/workflows/deploy-prod.y │
│ ml b/.github/workflows/deploy-prod.yml │
│ new file mode 100644 │
│ index 0000000..5820e8c │
│ --- /dev/null │
│ +++ b/.github/workflows/deploy-prod.yml │
│ @@ -0,0 +1,60 @@ │
│ +name: Deploy Production │
│ + │
│ +on: │
│ + workflow_dispatch: │
│ + # schedule: │
│ + # - cron: "0 */2 * * *" │
│ + │
│ +concurrency: │
│ + group: database-builder │
│ + cancel-in-progress: false │
│ + │
│ +permissions: │
│ + contents: read │
│ + │
│ +env: │
│ + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID │
│ }} │
│ + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PRO │
│ JECT_ID }} │
│ + │
│ +jobs: │
│ + deploy: │
│ + runs-on: ubuntu-latest │
│ + steps: │
│ + - name: Checkout repository │
│ + uses: actions/checkout@v4 │
│ + with: │
│ + token: ${{ secrets.GITHUB_TOKEN } │
│ } │
│ + │
│ + - name: Install Nix │
│ + uses: cachix/install-nix-action@v27 │
│ + │
│ + - name: Authenticate with Blizzard AP │
│ I │
│ + id: blizzard_auth │
│ + run: | │
│ + echo "Authenticating with Blizzar │
│ d API..." │
│ + │
│ + RESPONSE=$(curl -s -X POST "https │
│ ://oauth.battle.net/token" \ │
│ + -H "Content-Type: application/x │
│ -www-form-urlencoded" \ │
│ + -d "grant_type=client_credentia │
│ ls" \ │
│ + -u "${{ secrets.BLIZZARD_CLIENT │
│ _ID }}:${{ secrets.BLIZZARD_CLIENT_SECRET }} │
│ ") │
│ + │
│ + ACCESS_TOKEN=$(echo "$RESPONSE" | │
│ jq -r '.access_token') │
│ + │
│ + if [ "$ACCESS_TOKEN" = "null" ] | │
│ | [ -z "$ACCESS_TOKEN" ]; then │
│ + echo "Failed to authenticate wi │
│ th Blizzard API" │
│ + exit 1 │
│ + fi │
│ + │
│ + echo "::add-mask::$ACCESS_TOKEN" │
│ + echo "Successfully authenticated │
│ with Blizzard API" │
│ + echo "BLIZZARD_API_TOKEN=$ACCESS_ │
│ TOKEN" >> $GITHUB_ENV │
│ + │
│ + - name: Deploy ookstats │
│ + env: │
│ + BLIZZARD_API_TOKEN: ${{ env.BLIZZ │
│ ARD_API_TOKEN }} │
│ + AWS_ACCESS_KEY_ID: ${{ secrets.AW │
│ S_ACCESS_KEY_ID }} │
│ + AWS_SECRET_ACCESS_KEY: ${{ secret │
│ s.AWS_SECRET_ACCESS_KEY }} │
│ + AWS_ENDPOINT_URL: ${{ vars.AWS_EN │
│ DPOINT_URL }} │
│ + AWS_BUCKET: ${{ vars.AWS_BUCKET } │
│ } │
│ + VERCEL_TOKEN: ${{ secrets.VERCEL_ │
│ TOKEN }} │
│ + run: nix run .#ookstats-deploy │
└──────────────────────────────────────────────┘
┌─ .github/workflows/vercel-build-deploy-prod.yml.old ───────────────────────┐
│ diff --git a/.github/workflows/vercel-build-deploy-prod.yml b/.github/workflows/ve │
│ rcel-build-deploy-prod.yml.old │
│ similarity index 100% │
│ rename from .github/workflows/vercel-build-deploy-prod.yml │
│ rename to .github/workflows/vercel-build-deploy-prod.yml.old │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...cel-build-deploy-prod.yml.old ───┐
│ diff --git a/.github/workflows/vercel-build- │
│ deploy-prod.yml b/.github/workflows/vercel-b │
│ uild-deploy-prod.yml.old │
│ similarity index 100% │
│ rename from .github/workflows/vercel-build-d │
│ eploy-prod.yml │
│ rename to .github/workflows/vercel-build-dep │
│ loy-prod.yml.old │
└──────────────────────────────────────────────┘
┌─ nix/apps/default.nix ─────────────────────────────────────────────────────┐
│ diff --git a/nix/apps/default.nix b/nix/apps/default.nix │
│ index 2b45327..32b45e6 100644 │
│ --- a/nix/apps/default.nix │
│ +++ b/nix/apps/default.nix │
│ @@ -42,6 +42,10 @@ │
│ type = "app"; │
│ program = lib.getExe config.packages.ookstats; │
│ }; │
│ + ookstats-deploy = { │
│ + type = "app"; │
│ + program = lib.getExe config.packages.ookstats-deploy; │
│ + }; │
│ default = config.apps.ookstats; │
│ allSimulations = { │
│ type = "app"; │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ nix/apps/default.nix ───────────────┐
│ diff --git a/nix/apps/default.nix b/nix/apps │
│ /default.nix │
│ index 2b45327..32b45e6 100644 │
│ --- a/nix/apps/default.nix │
│ +++ b/nix/apps/default.nix │
│ @@ -42,6 +42,10 @@ │
│ type = "app"; │
│ program = lib.getExe config.packa │
│ ges.ookstats; │
│ }; │
│ + ookstats-deploy = { │
│ + type = "app"; │
│ + program = lib.getExe config.packa │
│ ges.ookstats-deploy; │
│ + }; │
│ default = config.apps.ookstats; │
│ allSimulations = { │
│ type = "app"; │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/default.nix ─────────────────────────────────────────────────────┐
│ diff --git a/nix/pkgs/default.nix b/nix/pkgs/default.nix │
│ index 17bf37a..bc25609 100644 │
│ --- a/nix/pkgs/default.nix │
│ +++ b/nix/pkgs/default.nix │
│ @@ -31,13 +31,14 @@ │
│ }; │
│ │
│ ookstats = callPackage ./ookstats {inherit go-libsql-src wowsims-db;}; │
│ + ookstats-deploy = callPackage ./ookstats-deploy {inherit ookstats;}; │
│ in { │
│ packages = │
│ simulationPackages │
│ // racePackages │
│ // trinketPackages │
│ // { │
│ - inherit ookstats; │
│ + inherit ookstats ookstats-deploy; │
│ allSimulations = simulation.generateAllSimulationsScript pkgs; │
│ simInputs = simulation.generateSimInputs pkgs; │
│ testRaid = callPackage ./testRaid.nix { │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ nix/pkgs/default.nix ───────────────┐
│ diff --git a/nix/pkgs/default.nix b/nix/pkgs │
│ /default.nix │
│ index 17bf37a..bc25609 100644 │
│ --- a/nix/pkgs/default.nix │
│ +++ b/nix/pkgs/default.nix │
│ @@ -31,13 +31,14 @@ │
│ }; │
│ │
│ ookstats = callPackage ./ookstats {inhe │
│ rit go-libsql-src wowsims-db;}; │
│ + ookstats-deploy = callPackage ./ookstat │
│ s-deploy {inherit ookstats;}; │
│ in { │
│ packages = │
│ simulationPackages │
│ // racePackages │
│ // trinketPackages │
│ // { │
│ - inherit ookstats; │
│ + inherit ookstats ookstats-deploy; │
│ allSimulations = simulation.generat │
│ eAllSimulationsScript pkgs; │
│ simInputs = simulation.generateSimI │
│ nputs pkgs; │
│ testRaid = callPackage ./testRaid.n │
│ ix { │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats-deploy/default.nix ─────────────────────────────────────┐
│ diff --git a/nix/pkgs/ookstats-deploy/default.nix b/nix/pkgs/ookstats-deploy/defau │
│ lt.nix │
│ new file mode 100644 │
│ index 0000000..2cc5542 │
│ --- /dev/null │
│ +++ b/nix/pkgs/ookstats-deploy/default.nix │
│ @@ -0,0 +1,415 @@ │
│ +{ │
│ + writeShellApplication, │
│ + ookstats, │
│ + awscli2, │
│ + coreutils, │
│ + jq, │
│ + nodejs, │
│ + nodePackages, │
│ + sqlite, │
│ + gum, │
│ +}: │
│ +writeShellApplication { │
│ + name = "ookstats-deploy"; │
│ + │
│ + runtimeInputs = [ │
│ + ookstats │
│ + awscli2 │
│ + coreutils │
│ + jq │
│ + nodejs │
│ + nodePackages.vercel │
│ + sqlite │
│ + gum │
│ + ]; │
│ + │
│ + text = │
│ + /* │
│ + bash │
│ + */ │
│ + '' │
│ + │
│ + # configuration │
│ + │
│ + # how many entries per leaderboard page │
│ + PAGE_SIZE=25 │
│ + │
│ + # how many concurrent connections │
│ + CONCURRENCY=20 │
│ + │
│ + # player search api shard size │
│ + SHARD_SIZE=5000 │
│ + │
│ + # where to output the static API │
│ + API_OUT=web/public │
│ + │
│ + REGIONS=us,tw,kr,eu │
│ + │
│ + DB_PATH="db/current.db" │
│ + HASH_PATH="db/current.db.sha256" │
│ + LOCK_PATH="db/db.lock" │
│ + BACKUP_PREFIX="db/backups/" │
│ + LOCAL_DB="local.db" │
│ + LOCK_TIMEOUT=7200 # 2 hours in seconds │
│ + │
│ + # log helper function │
│ + log() { │
│ + local type="$1" │
│ + local prefix="$2" │
│ + local message="$3" │
│ + │
│ + if [ "$type" = "verbose" ]; then │
│ + if [ -n "$verbose" ]; then │
│ + gum log --structured --level debug "$message" --prefix "$prefix" │
│ + fi │
│ + return │
│ + fi │
│ + if [ "$type" = "abort" ]; then │
│ + gum log --structured --level error "$message" --prefix "$prefix" │
│ + exit 1 │
│ + else │
│ + gum log --structured --level "$type" "$message" --prefix "$prefix" │
│ + fi │
│ + } │
│ + │
│ + │
│ + # configuration │
│ + │
│ + # parse arguments │
│ + DRY_RUN=false │
│ + ENVIRONMENT="production" │
│ + │
│ + for arg in "$@"; do │
│ + case $arg in │
│ + --dry-run) │
│ + DRY_RUN=true │
│ + shift │
│ + ;; │
│ + --preview) │
│ + ENVIRONMENT="preview" │
│ + shift │
│ + ;; │
│ + --production|--prod) │
│ + ENVIRONMENT="production" │
│ + shift │
│ + ;; │
│ + *) │
│ + echo "Unknown argument: $arg" │
│ + echo "Usage: ookstats-deploy [--dry-run] [--preview|--production]" │
│ + exit 1 │
│ + ;; │
│ + esac │
│ + done │
│ + │
│ + verifyEnv() { │
│ + log info "Environment" "Deploying to: $ENVIRONMENT" │
│ + │
│ + if [ "$DRY_RUN" = true ]; then │
│ + log info "Dry Run" "DRY RUN MODE - No uploads will be performed" │
│ + fi │
│ + │
│ + # verify required environment variables │
│ + if [ -z "''${BLIZZARD_API_TOKEN:-}" ]; then │
│ + log abort "Environment" "BLIZZARD_API_TOKEN not set" │
│ + fi │
│ + │
│ + if [ "$DRY_RUN" = false ]; then │
│ + if [ -z "''${AWS_ENDPOINT_URL:-}" ]; then │
│ + log abort "Environment" "AWS_ENDPOINT_URL not set" │
│ + fi │
│ + if [ -z "''${AWS_BUCKET:-}" ]; then │
│ + log abort "Environment" "AWS_BUCKET not set" │
│ + fi │
│ + if [ -z "''${AWS_ACCESS_KEY_ID:-}" ]; then │
│ + log abort "Environment" "AWS_ACCESS_KEY_ID not set" │
│ + fi │
│ + if [ -z "''${AWS_SECRET_ACCESS_KEY:-}" ]; then │
│ + log abort "Environment" "AWS_SECRET_ACCESS_KEY not set" │
│ + fi │
│ + if [ -z "''${VERCEL_TOKEN:-}" ]; then │
│ + log abort "Environment" "VERCEL_TOKEN not set" │
│ + fi │
│ + if [ -z "''${VERCEL_ORG_ID:-}" ]; then │
│ + log abort "Environment" "VERCEL_ORG_ID not set" │
│ + fi │
│ + if [ -z "''${VERCEL_PROJECT_ID:-}" ]; then │
│ + log abort "Environment" "VERCEL_PROJECT_ID not set" │
│ + fi │
│ + fi │
│ + } │
│ + │
│ + # s3 helper function │
│ + s3() { │
│ + aws s3 "$@" │
│ + } │
│ + │
│ + # cleanup function for trap │
│ + cleanup() { │
│ + local exit_code=$? │
│ + if [ "$DRY_RUN" = false ] && [ -f ".lock_acquired" ]; then │
│ + log info "Lock File" "Releasing lock" │
│ + s3 rm "s3://$AWS_BUCKET/$LOCK_PATH" 2>/dev/null || true │
│ + rm -f .lock_acquired │
│ + fi │
│ + exit $exit_code │
│ + } │
│ + trap cleanup EXIT INT TERM │
│ + │
│ + # === STEP 1: LOCK ACQUISITION === │
│ + getLock() { │
│ + log info "Lock File" "Acquiring lock..." │
│ + │
│ + if [ "$DRY_RUN" = false ]; then │
│ + # check if lock exists │
│ + if s3 ls "s3://$AWS_BUCKET/$LOCK_PATH" >/dev/null 2>&1; then │
│ + log info "Lock File" "Lock file exists, checking age..." │
│ + │
│ + # download and check lock timestamp │
│ + s3 cp "s3://$AWS_BUCKET/$LOCK_PATH" .lock_check || true │
│ + if [ -f .lock_check ]; then │
│ + lock_time=$(cat .lock_check) │
│ + current_time=$(date +%s) │
│ + age=$((current_time - lock_time)) │
│ + │
│ + if [ "$age" -lt "$LOCK_TIMEOUT" ]; then │
│ + log error "Lock File" "Another build is running (lock age: $age s │
│ econds)" │
│ + log error "Lock File" "Lock was acquired at: $(date -d @"$lock_ti │
│ me" 2>/dev/null || date -r "$lock_time")" │
│ + rm -f .lock_check │
│ + exit 1 │
│ + else │
│ + log warn "Lock File" "Stale lock detected (age: $age seconds), ov │
│ erriding..." │
│ + rm -f .lock_check │
│ + fi │
│ + fi │
│ + fi │
│ + │
│ + # create lock │
│ + current_timestamp=$(date +%s) │
│ + echo "$current_timestamp" > .lock_temp │
│ + s3 cp .lock_temp "s3://$AWS_BUCKET/$LOCK_PATH" │
│ + rm -f .lock_temp │
│ + touch .lock_acquired │
│ + log info "Lock File" "Lock acquired at: $(date)" │
│ + else │
│ + log info "Dry Run" "Skipping lock acquisition" │
│ + fi │
│ + } │
│ + │
│ + # === STEP 2: DATABASE DOWNLOAD === │
│ + downloadDB() { │
│ + log info "Database" "Downloading database..." │
│ + │
│ + if [ "$DRY_RUN" = false ]; then │
│ + # download database if exists │
│ + if s3 ls "s3://$AWS_BUCKET/$DB_PATH" >/dev/null 2>&1; then │
│ + log info "Database" "Downloading from R2..." │
│ + s3 cp "s3://$AWS_BUCKET/$DB_PATH" "$LOCAL_DB" │
│ + │
│ + # download and verify hash │
│ + if s3 ls "s3://$AWS_BUCKET/$HASH_PATH" >/dev/null 2>&1; then │
│ + s3 cp "s3://$AWS_BUCKET/$HASH_PATH" "$LOCAL_DB.sha256" │
│ + │
│ + log info "Database" "Verifying integrity..." │
│ + if ! sha256sum -c "$LOCAL_DB.sha256"; then │
│ + log abort "Database" "Hash mismatch - corrupted download" │
│ + fi │
│ + log info "Database" "Integrity verified" │
│ + else │
│ + log warn "Database" "No hash file found, skipping verification" │
│ + fi │
│ + │
│ + db_size=$(stat -c%s "$LOCAL_DB" 2>/dev/null || stat -f%z "$LOCAL_DB") │
│ + log info "Database" "Downloaded size: $((db_size / 1024 / 1024)) MB" │
│ + else │
│ + log info "Database" "No existing database found, starting fresh" │
│ + rm -f "$LOCAL_DB" │
│ + fi │
│ + else │
│ + log info "Dry Run" "Using existing local database or starting fresh" │
│ + fi │
│ + │
│ + # record pre-build database size │
│ + if [ -f "$LOCAL_DB" ]; then │
│ + PRE_BUILD_SIZE=$(stat -c%s "$LOCAL_DB" 2>/dev/null || stat -f%z "$LOCAL │
│ _DB") │
│ + else │
│ + PRE_BUILD_SIZE=0 │
│ + fi │
│ + } │
│ + │
│ + # === STEP 3: BACKUP CREATION === │
│ + │
│ + backupDB() { │
│ + log info "Backup" "Creating backup..." │
│ + │
│ + if [ "$DRY_RUN" = false ] && [ -f "$LOCAL_DB" ]; then │
│ + # cleanup old backups FIRST (>24h) │
│ + log info "Backup" "Cleaning up old backups..." │
│ + cutoff_timestamp=$(date -u -d '24 hours ago' +%s 2>/dev/null || date -u │
│ -v-24H +%s) │
│ + │
│ + # check if backup directory exists │
│ + if s3 ls "s3://$AWS_BUCKET/$BACKUP_PREFIX" >/dev/null 2>&1; then │
│ + s3 ls "s3://$AWS_BUCKET/$BACKUP_PREFIX" | awk '{print $4}' | while re │
│ ad -r backup_file; do │
│ + if [ -n "$backup_file" ]; then │
│ + # extract timestamp from filename: 2025-12-04T14:00:00Z.db │
│ + file_timestamp=$(echo "$backup_file" | sed 's/\.db$//' | xargs -I │
│ {} date -d {} +%s 2>/dev/null || echo "0") │
│ + │
│ + if [ "$file_timestamp" -gt 0 ] && [ "$file_timestamp" -lt "$cutof │
│ f_timestamp" ]; then │
│ + log info "Backup" "Deleting old backup: $backup_file" │
│ + s3 rm "s3://$AWS_BUCKET/$BACKUP_PREFIX$backup_file" || true │
│ + fi │
│ + fi │
│ + done │
│ + else │
│ + log info "Backup" "No existing backups found" │
│ + fi │
│ + │
│ + # now create new backup │
│ + backup_timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ) │
│ + backup_path="$BACKUP_PREFIX$backup_timestamp.db" │
│ + │
│ + log info "Backup" "Uploading to $backup_path" │
│ + s3 cp "$LOCAL_DB" "s3://$AWS_BUCKET/$backup_path" │
│ + │
│ + log info "Backup" "Backup created successfully" │
│ + else │
│ + log info "Dry Run" "Skipping backup (dry run or no database)" │
│ + fi │
│ + } │
│ + │
│ + # === STEP 4: BUILD PIPELINE === │
│ + │
│ + buildPipeline() { │
│ + log info "Build" "Running ookstats build" │
│ + │
│ + export OOKSTATS_DB="$LOCAL_DB" │
│ + │
│ + ookstats build \ │
│ + --out "$API_OUT" \ │
│ + --regions "$REGIONS" \ │
│ + --page-size "$PAGE_SIZE" \ │
│ + --shard-size "$SHARD_SIZE" \ │
│ + --concurrency "$CONCURRENCY" │
│ + │
│ + log info "Build" "Build completed successfully" │
│ + } │
│ + │
│ + # === STEP 5: DATABASE INTEGRITY CHECK === │
│ + │
│ + dbIntegrityCheck() { │
│ + log info "Integrity" "Checking database integrity" │
│ + │
│ + if [ -f "$LOCAL_DB" ]; then │
│ + POST_BUILD_SIZE=$(stat -c%s "$LOCAL_DB" 2>/dev/null || stat -f%z "$LOCA │
│ L_DB") │
│ + │
│ + log info "Integrity" "Pre-build size: $((PRE_BUILD_SIZE / 1024 / 1024) │
│ ) MB" │
│ + log info "Integrity" "Post-build size: $((POST_BUILD_SIZE / 1024 / 1024 │
│ )) MB" │
│ + │
│ + # check for significant shrinkage │
│ + if [ "$PRE_BUILD_SIZE" -gt 0 ]; then │
│ + size_ratio=$((POST_BUILD_SIZE * 100 / PRE_BUILD_SIZE)) │
│ + if [ "$size_ratio" -lt 90 ]; then │
│ + log abort "Integrity" "Database shrunk by >10% - possible data loss │
│ (Ratio: $size_ratio%)" │
│ + fi │
│ + fi │
│ + │
│ + # basic sanity check - count players │
│ + player_count=$(sqlite3 "$LOCAL_DB" "SELECT COUNT(*) FROM players WHERE │
│ is_valid = 1" 2>/dev/null || echo "0") │
│ + log info "Integrity" "Valid players in database: $player_count" │
│ + │
│ + if [ "$player_count" -eq 0 ] && [ "$PRE_BUILD_SIZE" -gt 0 ]; then │
│ + log warn "Integrity" "No valid players found in database" │
│ + fi │
│ + else │
│ + log abort "Integrity" "Database file not found after build" │
│ + fi │
│ + } │
│ + │
│ + # === STEP 6: HASH & UPLOAD DATABASE === │
│ + uploadDB() { │
│ + log info "Upload" "Uploading database to R2..." │
│ + │
│ + if [ "$DRY_RUN" = false ]; then │
│ + log info "Upload" "Calculating database hash..." │
│ + sha256sum "$LOCAL_DB" > "$LOCAL_DB.sha256" │
│ + │
│ + log info "Upload" "Uploading database..." │
│ + s3 cp "$LOCAL_DB" "s3://$AWS_BUCKET/$DB_PATH" │
│ + │
│ + log info "Upload" "Uploading hash..." │
│ + s3 cp "$LOCAL_DB.sha256" "s3://$AWS_BUCKET/$HASH_PATH" │
│ + │
│ + # verify upload │
│ + if ! s3 ls "s3://$AWS_BUCKET/$DB_PATH" >/dev/null 2>&1; then │
│ + log abort "Upload" "Database upload verification failed" │
│ + fi │
│ + │
│ + log info "Upload" "Database uploaded successfully" │
│ + else │
│ + log info "Dry Run" "Skipping database upload" │
│ + sha256sum "$LOCAL_DB" > "$LOCAL_DB.sha256" │
│ + log info "Dry Run" "Hash calculated (not uploaded):" │
│ + cat "$LOCAL_DB.sha256" │
│ + fi │
│ + } │
│ + │
│ + # VERCEL DEPLOYMENT │
│ + vercelDeploy() { │
│ + log info "Vercel" "Deploying to Vercel ($ENVIRONMENT)" │
│ + │
│ + if [ "$DRY_RUN" = false ]; then │
│ + cd web │
│ + │
│ + log info "Vercel" "Installing dependencies..." │
│ + npm ci │
│ + │
│ + log info "Vercel" "Pulling configuration..." │
│ + vercel pull --yes --environment="$ENVIRONMENT" --token="$VERCEL_TOKEN" │
│ + │
│ + if [ "$ENVIRONMENT" = "production" ]; then │
│ + log info "Vercel" "Building for production..." │
│ + vercel build --prod --token="$VERCEL_TOKEN" │
│ + │
│ + log info "Vercel" "Deploying to production..." │
│ + deployment_url=$(vercel deploy --prebuilt --prod --archive=tgz --toke │
│ n="$VERCEL_TOKEN") │
│ + else │
│ + log info "Vercel" "Building for preview..." │
│ + vercel build --token="$VERCEL_TOKEN" │
│ + │
│ + log info "Vercel" "Deploying to preview..." │
│ + deployment_url=$(vercel deploy --prebuilt --archive=tgz --token="$VER │
│ CEL_TOKEN") │
│ + fi │
│ + │
│ + log info "Vercel" "Deployment complete: $deployment_url" │
│ + │
│ + cd .. │
│ + else │
│ + log info "Dry Run" "Skipping Vercel deployment" │
│ + log info "Dry Run" "Would deploy $API_OUT to $ENVIRONMENT" │
│ + fi │
│ + } │
│ + │
│ + # === STEP 8: LOCK RELEASE === │
│ + # handled by cleanup trap │
│ + │
│ + finish() { │
│ + log info "Deploy" "Deployment Complete" │
│ + if [ "$DRY_RUN" = true ]; then │
│ + log info "Dry Run" "This was a dry run - no uploads or deployments were │
│ performed" │
│ + fi │
│ + } │
│ + │
│ + main() { │
│ + verifyEnv │
│ + getLock │
│ + downloadDB │
│ + backupDB │
│ + buildPipeline │
│ + dbIntegrityCheck │
│ + uploadDB │
│ + vercelDeploy │
│ + finish │
│ + } │
│ + │
│ + main │
│ + ''; │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...s/ookstats-deploy/default.nix ───┐
│ diff --git a/nix/pkgs/ookstats-deploy/defaul │
│ t.nix b/nix/pkgs/ookstats-deploy/default.nix │
│ new file mode 100644 │
│ index 0000000..2cc5542 │
│ --- /dev/null │
│ +++ b/nix/pkgs/ookstats-deploy/default.nix │
│ @@ -0,0 +1,415 @@ │
│ +{ │
│ + writeShellApplication, │
│ + ookstats, │
│ + awscli2, │
│ + coreutils, │
│ + jq, │
│ + nodejs, │
│ + nodePackages, │
│ + sqlite, │
│ + gum, │
│ +}: │
│ +writeShellApplication { │
│ + name = "ookstats-deploy"; │
│ + │
│ + runtimeInputs = [ │
│ + ookstats │
│ + awscli2 │
│ + coreutils │
│ + jq │
│ + nodejs │
│ + nodePackages.vercel │
│ + sqlite │
│ + gum │
│ + ]; │
│ + │
│ + text = │
│ + /* │
│ + bash │
│ + */ │
│ + '' │
│ + │
│ + # configuration │
│ + │
│ + # how many entries per leaderboard pa │
│ ge │
│ + PAGE_SIZE=25 │
│ + │
│ + # how many concurrent connections │
│ + CONCURRENCY=20 │
│ + │
│ + # player search api shard size │
│ + SHARD_SIZE=5000 │
│ + │
│ + # where to output the static API │
│ + API_OUT=web/public │
│ + │
│ + REGIONS=us,tw,kr,eu │
│ + │
│ + DB_PATH="db/current.db" │
│ + HASH_PATH="db/current.db.sha256" │
│ + LOCK_PATH="db/db.lock" │
│ + BACKUP_PREFIX="db/backups/" │
│ + LOCAL_DB="local.db" │
│ + LOCK_TIMEOUT=7200 # 2 hours in secon │
│ ds │
│ + │
│ + # log helper function │
│ + log() { │
│ + local type="$1" │
│ + local prefix="$2" │
│ + local message="$3" │
│ + │
│ + if [ "$type" = "verbose" ]; then │
│ + if [ -n "$verbose" ]; then │
│ + gum log --structured --level de │
│ bug "$message" --prefix "$prefix" │
│ + fi │
│ + return │
│ + fi │
│ + if [ "$type" = "abort" ]; then │
│ + gum log --structured --level erro │
│ r "$message" --prefix "$prefix" │
│ + exit 1 │
│ + else │
│ + gum log --structured --level "$ty │
│ pe" "$message" --prefix "$prefix" │
│ + fi │
│ + } │
│ + │
│ + │
│ + # configuration │
│ + │
│ + # parse arguments │
│ + DRY_RUN=false │
│ + ENVIRONMENT="production" │
│ + │
│ + for arg in "$@"; do │
│ + case $arg in │
│ + --dry-run) │
│ + DRY_RUN=true │
│ + shift │
│ + ;; │
│ + --preview) │
│ + ENVIRONMENT="preview" │
│ + shift │
│ + ;; │
│ + --production|--prod) │
│ + ENVIRONMENT="production" │
│ + shift │
│ + ;; │
│ + *) │
│ + echo "Unknown argument: $arg" │
│ + echo "Usage: ookstats-deploy [- │
│ -dry-run] [--preview|--production]" │
│ + exit 1 │
│ + ;; │
│ + esac │
│ + done │
│ + │
│ + verifyEnv() { │
│ + log info "Environment" "Deploying t │
│ o: $ENVIRONMENT" │
│ + │
│ + if [ "$DRY_RUN" = true ]; then │
│ + log info "Dry Run" "DRY RUN MODE │
│ - No uploads will be performed" │
│ + fi │
│ + │
│ + # verify required environment varia │
│ bles │
│ + if [ -z "''${BLIZZARD_API_TOKEN:-}" │
│ ]; then │
│ + log abort "Environment" "BLIZZARD │
│ _API_TOKEN not set" │
│ + fi │
│ + │
│ + if [ "$DRY_RUN" = false ]; then │
│ + if [ -z "''${AWS_ENDPOINT_URL:-}" │
│ ]; then │
│ + log abort "Environment" "AWS_EN │
│ DPOINT_URL not set" │
│ + fi │
│ + if [ -z "''${AWS_BUCKET:-}" ]; th │
│ en │
│ + log abort "Environment" "AWS_BU │
│ CKET not set" │
│ + fi │
│ + if [ -z "''${AWS_ACCESS_KEY_ID:-} │
│ " ]; then │
│ + log abort "Environment" "AWS_AC │
│ CESS_KEY_ID not set" │
│ + fi │
│ + if [ -z "''${AWS_SECRET_ACCESS_KE │
│ Y:-}" ]; then │
│ + log abort "Environment" "AWS_SE │
│ CRET_ACCESS_KEY not set" │
│ + fi │
│ + if [ -z "''${VERCEL_TOKEN:-}" ]; │
│ then │
│ + log abort "Environment" "VERCEL │
│ _TOKEN not set" │
│ + fi │
│ + if [ -z "''${VERCEL_ORG_ID:-}" ]; │
│ then │
│ + log abort "Environment" "VERCEL │
│ _ORG_ID not set" │
│ + fi │
│ + if [ -z "''${VERCEL_PROJECT_ID:-} │
│ " ]; then │
│ + log abort "Environment" "VERCEL │
│ _PROJECT_ID not set" │
│ + fi │
│ + fi │
│ + } │
│ + │
│ + # s3 helper function │
│ + s3() { │
│ + aws s3 "$@" │
│ + } │
│ + │
│ + # cleanup function for trap │
│ + cleanup() { │
│ + local exit_code=$? │
│ + if [ "$DRY_RUN" = false ] && [ -f " │
│ .lock_acquired" ]; then │
│ + log info "Lock File" "Releasing l │
│ ock" │
│ + s3 rm "s3://$AWS_BUCKET/$LOCK_PAT │
│ H" 2>/dev/null || true │
│ + rm -f .lock_acquired │
│ + fi │
│ + exit $exit_code │
│ + } │
│ + trap cleanup EXIT INT TERM │
│ + │
│ + # === STEP 1: LOCK ACQUISITION === │
│ + getLock() { │
│ + log info "Lock File" "Acquiring loc │
│ k..." │
│ + │
│ + if [ "$DRY_RUN" = false ]; then │
│ + # check if lock exists │
│ + if s3 ls "s3://$AWS_BUCKET/$LOCK_ │
│ PATH" >/dev/null 2>&1; then │
│ + log info "Lock File" "Lock file │
│ exists, checking age..." │
│ + │
│ + # download and check lock times │
│ tamp │
│ + s3 cp "s3://$AWS_BUCKET/$LOCK_P │
│ ATH" .lock_check || true │
│ + if [ -f .lock_check ]; then │
│ + lock_time=$(cat .lock_check) │
│ + current_time=$(date +%s) │
│ + age=$((current_time - lock_ti │
│ me)) │
│ + │
│ + if [ "$age" -lt "$LOCK_TIMEOU │
│ T" ]; then │
│ + log error "Lock File" "Anot │
│ her build is running (lock age: $age seconds │
│ )" │
│ + log error "Lock File" "Lock │
│ was acquired at: $(date -d @"$lock_time" 2> │
│ /dev/null || date -r "$lock_time")" │
│ + rm -f .lock_check │
│ + exit 1 │
│ + else │
│ + log warn "Lock File" "Stale │
│ lock detected (age: $age seconds), overridi │
│ ng..." │
│ + rm -f .lock_check │
│ + fi │
│ + fi │
│ + fi │
│ + │
│ + # create lock │
│ + current_timestamp=$(date +%s) │
│ + echo "$current_timestamp" > .lock │
│ _temp │
│ + s3 cp .lock_temp "s3://$AWS_BUCKE │
│ T/$LOCK_PATH" │
│ + rm -f .lock_temp │
│ + touch .lock_acquired │
│ + log info "Lock File" "Lock acquir │
│ ed at: $(date)" │
│ + else │
│ + log info "Dry Run" "Skipping lock │
│ acquisition" │
│ + fi │
│ + } │
│ + │
│ + # === STEP 2: DATABASE DOWNLOAD === │
│ + downloadDB() { │
│ + log info "Database" "Downloading da │
│ tabase..." │
│ + │
│ + if [ "$DRY_RUN" = false ]; then │
│ + # download database if exists │
│ + if s3 ls "s3://$AWS_BUCKET/$DB_PA │
│ TH" >/dev/null 2>&1; then │
│ + log info "Database" "Downloadin │
│ g from R2..." │
│ + s3 cp "s3://$AWS_BUCKET/$DB_PAT │
│ H" "$LOCAL_DB" │
│ + │
│ + # download and verify hash │
│ + if s3 ls "s3://$AWS_BUCKET/$HAS │
│ H_PATH" >/dev/null 2>&1; then │
│ + s3 cp "s3://$AWS_BUCKET/$HASH │
│ _PATH" "$LOCAL_DB.sha256" │
│ + │
│ + log info "Database" "Verifyin │
│ g integrity..." │
│ + if ! sha256sum -c "$LOCAL_DB. │
│ sha256"; then │
│ + log abort "Database" "Hash │
│ mismatch - corrupted download" │
│ + fi │
│ + log info "Database" "Integrit │
│ y verified" │
│ + else │
│ + log warn "Database" "No hash │
│ file found, skipping verification" │
│ + fi │
│ + │
│ + db_size=$(stat -c%s "$LOCAL_DB" │
│ 2>/dev/null || stat -f%z "$LOCAL_DB") │
│ + log info "Database" "Downloaded │
│ size: $((db_size / 1024 / 1024)) MB" │
│ + else │
│ + log info "Database" "No existin │
│ g database found, starting fresh" │
│ + rm -f "$LOCAL_DB" │
│ + fi │
│ + else │
│ + log info "Dry Run" "Using existin │
│ g local database or starting fresh" │
│ + fi │
│ + │
│ + # record pre-build database size │
│ + if [ -f "$LOCAL_DB" ]; then │
│ + PRE_BUILD_SIZE=$(stat -c%s "$LOCA │
│ L_DB" 2>/dev/null || stat -f%z "$LOCAL_DB") │
│ + else │
│ + PRE_BUILD_SIZE=0 │
│ + fi │
│ + } │
│ + │
│ + # === STEP 3: BACKUP CREATION === │
│ + │
│ + backupDB() { │
│ + log info "Backup" "Creating backup. │
│ .." │
│ + │
│ + if [ "$DRY_RUN" = false ] && [ -f " │
│ $LOCAL_DB" ]; then │
│ + # cleanup old backups FIRST (>24h │
│ ) │
│ + log info "Backup" "Cleaning up ol │
│ d backups..." │
│ + cutoff_timestamp=$(date -u -d '24 │
│ hours ago' +%s 2>/dev/null || date -u -v-24 │
│ H +%s) │
│ + │
│ + # check if backup directory exist │
│ s │
│ + if s3 ls "s3://$AWS_BUCKET/$BACKU │
│ P_PREFIX" >/dev/null 2>&1; then │
│ + s3 ls "s3://$AWS_BUCKET/$BACKUP │
│ _PREFIX" | awk '{print $4}' | while read -r │
│ backup_file; do │
│ + if [ -n "$backup_file" ]; the │
│ n │
│ + # extract timestamp from fi │
│ lename: 2025-12-04T14:00:00Z.db │
│ + file_timestamp=$(echo "$bac │
│ kup_file" | sed 's/\.db$//' | xargs -I {} da │
│ te -d {} +%s 2>/dev/null || echo "0") │
│ + │
│ + if [ "$file_timestamp" -gt │
│ 0 ] && [ "$file_timestamp" -lt "$cutoff_time │
│ stamp" ]; then │
│ + log info "Backup" "Deleti │
│ ng old backup: $backup_file" │
│ + s3 rm "s3://$AWS_BUCKET/$ │
│ BACKUP_PREFIX$backup_file" || true │
│ + fi │
│ + fi │
│ + done │
│ + else │
│ + log info "Backup" "No existing │
│ backups found" │
│ + fi │
│ + │
│ + # now create new backup │
│ + backup_timestamp=$(date -u +%Y-%m │
│ -%dT%H:%M:%SZ) │
│ + backup_path="$BACKUP_PREFIX$backu │
│ p_timestamp.db" │
│ + │
│ + log info "Backup" "Uploading to $ │
│ backup_path" │
│ + s3 cp "$LOCAL_DB" "s3://$AWS_BUCK │
│ ET/$backup_path" │
│ + │
│ + log info "Backup" "Backup created │
│ successfully" │
│ + else │
│ + log info "Dry Run" "Skipping back │
│ up (dry run or no database)" │
│ + fi │
│ + } │
│ + │
│ + # === STEP 4: BUILD PIPELINE === │
│ + │
│ + buildPipeline() { │
│ + log info "Build" "Running ookstats │
│ build" │
│ + │
│ + export OOKSTATS_DB="$LOCAL_DB" │
│ + │
│ + ookstats build \ │
│ + --out "$API_OUT" \ │
│ + --regions "$REGIONS" \ │
│ + --page-size "$PAGE_SIZE" \ │
│ + --shard-size "$SHARD_SIZE" \ │
│ + --concurrency "$CONCURRENCY" │
│ + │
│ + log info "Build" "Build completed s │
│ uccessfully" │
│ + } │
│ + │
│ + # === STEP 5: DATABASE INTEGRITY CHEC │
│ K === │
│ + │
│ + dbIntegrityCheck() { │
│ + log info "Integrity" "Checking data │
│ base integrity" │
│ + │
│ + if [ -f "$LOCAL_DB" ]; then │
│ + POST_BUILD_SIZE=$(stat -c%s "$LOC │
│ AL_DB" 2>/dev/null || stat -f%z "$LOCAL_DB") │
│ + │
│ + log info "Integrity" "Pre-build s │
│ ize: $((PRE_BUILD_SIZE / 1024 / 1024)) MB" │
│ + log info "Integrity" "Post-build │
│ size: $((POST_BUILD_SIZE / 1024 / 1024)) MB" │
│ + │
│ + # check for significant shrinkage │
│ + if [ "$PRE_BUILD_SIZE" -gt 0 ]; t │
│ hen │
│ + size_ratio=$((POST_BUILD_SIZE * │
│ 100 / PRE_BUILD_SIZE)) │
│ + if [ "$size_ratio" -lt 90 ]; th │
│ en │
│ + log abort "Integrity" "Databa │
│ se shrunk by >10% - possible data loss (Rati │
│ o: $size_ratio%)" │
│ + fi │
│ + fi │
│ + │
│ + # basic sanity check - count play │
│ ers │
│ + player_count=$(sqlite3 "$LOCAL_DB │
│ " "SELECT COUNT(*) FROM players WHERE is_val │
│ id = 1" 2>/dev/null || echo "0") │
│ + log info "Integrity" "Valid playe │
│ rs in database: $player_count" │
│ + │
│ + if [ "$player_count" -eq 0 ] && [ │
│ "$PRE_BUILD_SIZE" -gt 0 ]; then │
│ + log warn "Integrity" "No valid │
│ players found in database" │
│ + fi │
│ + else │
│ + log abort "Integrity" "Database f │
│ ile not found after build" │
│ + fi │
│ + } │
│ + │
│ + # === STEP 6: HASH & UPLOAD DATABASE │
│ === │
│ + uploadDB() { │
│ + log info "Upload" "Uploading databa │
│ se to R2..." │
│ + │
│ + if [ "$DRY_RUN" = false ]; then │
│ + log info "Upload" "Calculating da │
│ tabase hash..." │
│ + sha256sum "$LOCAL_DB" > "$LOCAL_D │
│ B.sha256" │
│ + │
│ + log info "Upload" "Uploading data │
│ base..." │
│ + s3 cp "$LOCAL_DB" "s3://$AWS_BUCK │
│ ET/$DB_PATH" │
│ + │
│ + log info "Upload" "Uploading hash │
│ ..." │
│ + s3 cp "$LOCAL_DB.sha256" "s3://$A │
│ WS_BUCKET/$HASH_PATH" │
│ + │
│ + # verify upload │
│ + if ! s3 ls "s3://$AWS_BUCKET/$DB_ │
│ PATH" >/dev/null 2>&1; then │
│ + log abort "Upload" "Database up │
│ load verification failed" │
│ + fi │
│ + │
│ + log info "Upload" "Database uploa │
│ ded successfully" │
│ + else │
│ + log info "Dry Run" "Skipping data │
│ base upload" │
│ + sha256sum "$LOCAL_DB" > "$LOCAL_D │
│ B.sha256" │
│ + log info "Dry Run" "Hash calculat │
│ ed (not uploaded):" │
│ + cat "$LOCAL_DB.sha256" │
│ + fi │
│ + } │
│ + │
│ + # VERCEL DEPLOYMENT │
│ + vercelDeploy() { │
│ + log info "Vercel" "Deploying to Ver │
│ cel ($ENVIRONMENT)" │
│ + │
│ + if [ "$DRY_RUN" = false ]; then │
│ + cd web │
│ + │
│ + log info "Vercel" "Installing dep │
│ endencies..." │
│ + npm ci │
│ + │
│ + log info "Vercel" "Pulling config │
│ uration..." │
│ + vercel pull --yes --environment=" │
│ $ENVIRONMENT" --token="$VERCEL_TOKEN" │
│ + │
│ + if [ "$ENVIRONMENT" = "production │
│ " ]; then │
│ + log info "Vercel" "Building for │
│ production..." │
│ + vercel build --prod --token="$V │
│ ERCEL_TOKEN" │
│ + │
│ + log info "Vercel" "Deploying to │
│ production..." │
│ + deployment_url=$(vercel deploy │
│ --prebuilt --prod --archive=tgz --token="$VE │
│ RCEL_TOKEN") │
│ + else │
│ + log info "Vercel" "Building for │
│ preview..." │
│ + vercel build --token="$VERCEL_T │
│ OKEN" │
│ + │
│ + log info "Vercel" "Deploying to │
│ preview..." │
│ + deployment_url=$(vercel deploy │
│ --prebuilt --archive=tgz --token="$VERCEL_TO │
│ KEN") │
│ + fi │
│ + │
│ + log info "Vercel" "Deployment com │
│ plete: $deployment_url" │
│ + │
│ + cd .. │
│ + else │
│ + log info "Dry Run" "Skipping Verc │
│ el deployment" │
│ + log info "Dry Run" "Would deploy │
│ $API_OUT to $ENVIRONMENT" │
│ + fi │
│ + } │
│ + │
│ + # === STEP 8: LOCK RELEASE === │
│ + # handled by cleanup trap │
│ + │
│ + finish() { │
│ + log info "Deploy" "Deployment Compl │
│ ete" │
│ + if [ "$DRY_RUN" = true ]; then │
│ + log info "Dry Run" "This was a dr │
│ y run - no uploads or deployments were perfo │
│ rmed" │
│ + fi │
│ + } │
│ + │
│ + main() { │
│ + verifyEnv │
│ + getLock │
│ + downloadDB │
│ + backupDB │
│ + buildPipeline │
│ + dbIntegrityCheck │
│ + uploadDB │
│ + vercelDeploy │
│ + finish │
│ + } │
│ + │
│ + main │
│ + ''; │
│ +} │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET