OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 418 LINES
 
[ HISTORY ]  [ UP ]
 

{
  writeShellApplication,
  ookstats,
  awscli2,
  coreutils,
  jq,
  nodejs,
  sqlite,
  gum,
}:
writeShellApplication {
  name = "ookstats-deploy";

  runtimeInputs = [
    ookstats
    awscli2
    coreutils
    jq
    nodejs
    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 ]; then
          log info "Lock File" "Releasing lock"
          s3 rm "s3://$AWS_BUCKET/$LOCK_PATH" 2>/dev/null || true
        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 se
conds)"
                log error "Lock File" "Lock was acquired at: $(date -d @"$lock_tim
e" 2>/dev/null || date -r "$lock_time")"
                rm -f .lock_check
                exit 1
              else
                log warn "Lock File" "Stale lock detected (age: $age seconds), ove
rriding..."
                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 rea
d -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 "$cutoff
_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"

        # closed periods are frozen; only the newest 2 per region can yield new ru
ns
        ookstats build \
          --out "$API_OUT" \
          --regions "$REGIONS" \
          --page-size "$PAGE_SIZE" \
          --shard-size "$SHARD_SIZE" \
          --concurrency "$CONCURRENCY" \
          --latest-periods

        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 "$LOCAL
_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 i
s_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
      }

      # Pinned because Vercel's deploy endpoint enforces a minimum CLI
      # version and silently breaks CI when that floor moves. Bump deliberately.
      VERCEL_CLI="[email protected]"

      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..."
          npx --yes "$VERCEL_CLI" pull --yes --environment="$ENVIRONMENT" --token=
"$VERCEL_TOKEN"

          if [ "$ENVIRONMENT" = "production" ]; then
            log info "Vercel" "Building for production..."
            npx --yes "$VERCEL_CLI" build --prod --token="$VERCEL_TOKEN"

            log info "Vercel" "Deploying to production..."
            deployment_url=$(npx --yes "$VERCEL_CLI" deploy --prebuilt --prod --ar
chive=tgz --token="$VERCEL_TOKEN")
          else
            log info "Vercel" "Building for preview..."
            npx --yes "$VERCEL_CLI" build --token="$VERCEL_TOKEN"

            log info "Vercel" "Deploying to preview..."
            deployment_url=$(npx --yes "$VERCEL_CLI" deploy --prebuilt --archive=t
gz --token="$VERCEL_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
    '';
}

{
  writeShellApplication,
  ookstats,
  awscli2,
  coreutils,
  jq,
  nodejs,
  sqlite,
  gum,
}:
writeShellApplication {
  name = "ookstats-deploy";

  runtimeInputs = [
    ookstats
    awscli2
    coreutils
    jq
    nodejs
    sqlite
    gum
  ];

  text =
    /*
    bash
    */
    ''

      # configuration

      # how many entries per leaderboard pag
e
      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 second
s

      # 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 deb
ug "$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 "$typ
e" "$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 variab
les
        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_END
POINT_URL not set"
          fi
          if [ -z "''${AWS_BUCKET:-}" ]; the
n
            log abort "Environment" "AWS_BUC
KET not set"
          fi
          if [ -z "''${AWS_ACCESS_KEY_ID:-}"
 ]; then
            log abort "Environment" "AWS_ACC
ESS_KEY_ID not set"
          fi
          if [ -z "''${AWS_SECRET_ACCESS_KEY
:-}" ]; then
            log abort "Environment" "AWS_SEC
RET_ACCESS_KEY not set"
          fi
          if [ -z "''${VERCEL_TOKEN:-}" ]; t
hen
            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 ]; then
          log info "Lock File" "Releasing lo
ck"
          s3 rm "s3://$AWS_BUCKET/$LOCK_PATH
" 2>/dev/null || true
        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_P
ATH" >/dev/null 2>&1; then
            log info "Lock File" "Lock file 
exists, checking age..."

            # download and check lock timest
amp
            s3 cp "s3://$AWS_BUCKET/$LOCK_PA
TH" .lock_check || true
            if [ -f .lock_check ]; then
              lock_time=$(cat .lock_check)
              current_time=$(date +%s)
              age=$((current_time - lock_tim
e))

              if [ "$age" -lt "$LOCK_TIMEOUT
" ]; then
                log error "Lock File" "Anoth
er 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), overridin
g..."
                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 acquire
d at: $(date)"
        else
          log info "Dry Run" "Skipping lock 
acquisition"
        fi
      }

      # === STEP 2: DATABASE DOWNLOAD ===
      downloadDB() {
        log info "Database" "Downloading dat
abase..."

        if [ "$DRY_RUN" = false ]; then
          # download database if exists
          if s3 ls "s3://$AWS_BUCKET/$DB_PAT
H" >/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.s
ha256"; then
                log abort "Database" "Hash m
ismatch - corrupted download"
              fi
              log info "Database" "Integrity
 verified"
            else
              log warn "Database" "No hash f
ile 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 read -r b
ackup_file; do
              if [ -n "$backup_file" ]; then
                # extract timestamp from fil
ename: 2025-12-04T14:00:00Z.db
                file_timestamp=$(echo "$back
up_file" | sed 's/\.db$//' | xargs -I {} dat
e -d {} +%s 2>/dev/null || echo "0")

                if [ "$file_timestamp" -gt 0
 ] && [ "$file_timestamp" -lt "$cutoff_times
tamp" ]; then
                  log info "Backup" "Deletin
g old backup: $backup_file"
                  s3 rm "s3://$AWS_BUCKET/$B
ACKUP_PREFIX$backup_file" || true
                fi
              fi
            done
          else
            log info "Backup" "No existing b
ackups 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 $b
ackup_path"
          s3 cp "$LOCAL_DB" "s3://$AWS_BUCKE
T/$backup_path"

          log info "Backup" "Backup created 
successfully"
        else
          log info "Dry Run" "Skipping backu
p (dry run or no database)"
        fi
      }

      # === STEP 4: BUILD PIPELINE ===

      buildPipeline() {
        log info "Build" "Running ookstats b
uild"

        export OOKSTATS_DB="$LOCAL_DB"

        # closed periods are frozen; only th
e newest 2 per region can yield new runs
        ookstats build \
          --out "$API_OUT" \
          --regions "$REGIONS" \
          --page-size "$PAGE_SIZE" \
          --shard-size "$SHARD_SIZE" \
          --concurrency "$CONCURRENCY" \
          --latest-periods

        log info "Build" "Build completed su
ccessfully"
      }

      # === STEP 5: DATABASE INTEGRITY CHECK
 ===

      dbIntegrityCheck() {
        log info "Integrity" "Checking datab
ase integrity"

        if [ -f "$LOCAL_DB" ]; then
          POST_BUILD_SIZE=$(stat -c%s "$LOCA
L_DB" 2>/dev/null || stat -f%z "$LOCAL_DB")

          log info "Integrity" "Pre-build si
ze:  $((PRE_BUILD_SIZE / 1024 / 1024)) MB"
          log info "Integrity" "Post-build s
ize: $((POST_BUILD_SIZE / 1024 / 1024)) MB"

          # check for significant shrinkage
          if [ "$PRE_BUILD_SIZE" -gt 0 ]; th
en
            size_ratio=$((POST_BUILD_SIZE * 
100 / PRE_BUILD_SIZE))
            if [ "$size_ratio" -lt 90 ]; the
n
              log abort "Integrity" "Databas
e shrunk by >10% - possible data loss (Ratio
: $size_ratio%)"
            fi
          fi

          # basic sanity check - count playe
rs
          player_count=$(sqlite3 "$LOCAL_DB"
 "SELECT COUNT(*) FROM players WHERE is_vali
d = 1" 2>/dev/null || echo "0")
          log info "Integrity" "Valid player
s in database: $player_count"

          if [ "$player_count" -eq 0 ] && [ 
"$PRE_BUILD_SIZE" -gt 0 ]; then
            log warn "Integrity" "No valid p
layers found in database"
          fi
        else
          log abort "Integrity" "Database fi
le not found after build"
        fi
      }

      # === STEP 6: HASH & UPLOAD DATABASE =
==
      uploadDB() {
        log info "Upload" "Uploading databas
e to R2..."

        if [ "$DRY_RUN" = false ]; then
          log info "Upload" "Calculating dat
abase hash..."
          sha256sum "$LOCAL_DB" > "$LOCAL_DB
.sha256"

          log info "Upload" "Uploading datab
ase..."
          s3 cp "$LOCAL_DB" "s3://$AWS_BUCKE
T/$DB_PATH"

          log info "Upload" "Uploading hash.
.."
          s3 cp "$LOCAL_DB.sha256" "s3://$AW
S_BUCKET/$HASH_PATH"

          # verify upload
          if ! s3 ls "s3://$AWS_BUCKET/$DB_P
ATH" >/dev/null 2>&1; then
            log abort "Upload" "Database upl
oad verification failed"
          fi

          log info "Upload" "Database upload
ed successfully"
        else
          log info "Dry Run" "Skipping datab
ase upload"
          sha256sum "$LOCAL_DB" > "$LOCAL_DB
.sha256"
          log info "Dry Run" "Hash calculate
d (not uploaded):"
          cat "$LOCAL_DB.sha256"
        fi
      }

      # Pinned because Vercel's deploy endpo
int enforces a minimum CLI
      # version and silently breaks CI when 
that floor moves. Bump deliberately.
      VERCEL_CLI="[email protected]"

      vercelDeploy() {
        log info "Vercel" "Deploying to Verc
el ($ENVIRONMENT)"

        if [ "$DRY_RUN" = false ]; then
          cd web

          log info "Vercel" "Installing depe
ndencies..."
          npm ci

          log info "Vercel" "Pulling configu
ration..."
          npx --yes "$VERCEL_CLI" pull --yes
 --environment="$ENVIRONMENT" --token="$VERC
EL_TOKEN"

          if [ "$ENVIRONMENT" = "production"
 ]; then
            log info "Vercel" "Building for 
production..."
            npx --yes "$VERCEL_CLI" build --
prod --token="$VERCEL_TOKEN"

            log info "Vercel" "Deploying to 
production..."
            deployment_url=$(npx --yes "$VER
CEL_CLI" deploy --prebuilt --prod --archive=
tgz --token="$VERCEL_TOKEN")
          else
            log info "Vercel" "Building for 
preview..."
            npx --yes "$VERCEL_CLI" build --
token="$VERCEL_TOKEN"

            log info "Vercel" "Deploying to 
preview..."
            deployment_url=$(npx --yes "$VER
CEL_CLI" deploy --prebuilt --archive=tgz --t
oken="$VERCEL_TOKEN")
          fi

          log info "Vercel" "Deployment comp
lete: $deployment_url"

          cd ..
        else
          log info "Dry Run" "Skipping Verce
l 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 Comple
te"
        if [ "$DRY_RUN" = true ]; then
          log info "Dry Run" "This was a dry
 run - no uploads or deployments were perfor
med"
        fi
      }

      main() {
        verifyEnv
        getLock
        downloadDB
        backupDB
        buildPipeline
        dbIntegrityCheck
        uploadDB
        vercelDeploy
        finish
      }

      main
    '';
}
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET