ffed10cd66
Previous attempts hardcoded 3011, but something on tyler's host is holding 3011 too. Instead of guessing ports, make the deploy probe for the first free port starting from 3011 and thread that choice through the rest of the workflow. How it works: 1. Start Docker stack frees 3001 + the previous deploy's chosen port (read from .postgrest-port) so the lowest free port can be picked back up 2. Loops from 3011 upward, checking ss -tln until it finds a free port in the 3011-30200 range 3. Writes the chosen port to .postgrest-port in the workspace 4. Sets NEXT_PUBLIC_API_URL based on that port 5. Writes POSTGREST_HOST_PORT and NEXT_PUBLIC_API_URL to the .env that docker compose reads 6. Build and Deploy steps read .postgrest-port to set their NEXT_PUBLIC_API_URL dynamically — no more hardcoded 3011
232 lines
11 KiB
YAML
232 lines
11 KiB
YAML
name: Deploy to route.crispygoat.com
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '22'
|
|
|
|
- name: Start Docker stack
|
|
env:
|
|
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
|
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
|
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
|
MINIO_ROOT_USER: ${{ secrets.MINIO_ROOT_USER }}
|
|
MINIO_ROOT_PASSWORD: ${{ secrets.MINIO_ROOT_PASSWORD }}
|
|
POSTGREST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
|
run: |
|
|
APP_DIR=/home/tyler/route-commerce
|
|
mkdir -p $APP_DIR
|
|
|
|
# Free the dev-stack port (3001) and the port the previous deploy used
|
|
# (so a new deploy can pick it back up if it's the lowest free port)
|
|
PREV_PORT=$(cat .postgrest-port 2>/dev/null || echo "")
|
|
for port in 3001 $PREV_PORT; do
|
|
if [ -n "$port" ] && ss -tln 2>/dev/null | grep -q ":$port "; then
|
|
echo "Port $port in use, freeing..."
|
|
fuser -k -9 $port/tcp 2>/dev/null || true
|
|
docker ps -aq --filter "publish=$port" 2>/dev/null | xargs -r docker rm -f 2>/dev/null || true
|
|
fi
|
|
done
|
|
docker compose -f $APP_DIR/docker-compose.yml down --remove-orphans 2>/dev/null || true
|
|
sleep 3
|
|
|
|
# Find the first free host port starting from 3011. Persist the choice
|
|
# so the Build and Deploy steps below can use the same URL.
|
|
POSTGREST_HOST_PORT=3011
|
|
while ss -tln 2>/dev/null | grep -q ":$POSTGREST_HOST_PORT "; do
|
|
echo "Port $POSTGREST_HOST_PORT in use, trying next..."
|
|
POSTGREST_HOST_PORT=$((POSTGREST_HOST_PORT + 1))
|
|
if [ $POSTGREST_HOST_PORT -gt 30200 ]; then
|
|
echo "ERROR: no free port in 3011-30200 range"
|
|
exit 1
|
|
fi
|
|
done
|
|
echo "Using PostgREST host port: $POSTGREST_HOST_PORT"
|
|
echo "$POSTGREST_HOST_PORT" > .postgrest-port
|
|
export NEXT_PUBLIC_API_URL="http://localhost:$POSTGREST_HOST_PORT"
|
|
export POSTGREST_HOST_PORT
|
|
|
|
# Seed config files into APP_DIR if missing (they live in the repo, not in APP_DIR)
|
|
[ -f $APP_DIR/.env.example ] || cp .env.example $APP_DIR/.env.example
|
|
[ -f $APP_DIR/docker-compose.yml ] || cp docker-compose.yml $APP_DIR/docker-compose.yml
|
|
cd $APP_DIR
|
|
[ -f .env ] || cp .env.example .env
|
|
# Append production secrets to .env (overriding .env.example defaults)
|
|
{
|
|
echo "POSTGRES_USER=${POSTGRES_USER}"
|
|
echo "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}"
|
|
echo "POSTGRES_DB=${POSTGRES_DB}"
|
|
echo "MINIO_ROOT_USER=${MINIO_ROOT_USER}"
|
|
echo "MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}"
|
|
echo "POSTGREST_JWT_SECRET=${POSTGREST_JWT_SECRET}"
|
|
echo "POSTGREST_HOST_PORT=$POSTGREST_HOST_PORT"
|
|
echo "NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL"
|
|
} >> .env
|
|
# Bring the stack up fresh — --force-recreate ensures no stale
|
|
# network/container references from prior failed attempts
|
|
docker compose up -d --force-recreate db postgrest minio minio_init
|
|
# Wait for Postgres healthcheck
|
|
for i in $(seq 1 30); do
|
|
if docker compose exec -T db pg_isready -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" > /dev/null 2>&1; then
|
|
echo "Postgres is ready"
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
- name: Apply migrations
|
|
env:
|
|
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
|
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
|
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
|
run: |
|
|
APP_DIR=/home/tyler/route-commerce
|
|
# Seed supabase/ into APP_DIR if missing (the deploy step copies it after, but
|
|
# we need it here for migrations)
|
|
[ -d $APP_DIR/supabase ] || cp -r supabase $APP_DIR/supabase
|
|
cd $APP_DIR
|
|
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -f supabase/migrations/000_preflight_supabase_compat.sql || true
|
|
[ -f supabase/captured_schema.sql ] && PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -f supabase/captured_schema.sql || echo "captured_schema.sql not present, skipping"
|
|
for f in supabase/migrations/[0-9]*.sql; do
|
|
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -q -f "$f" || echo "FAIL: $f"
|
|
done
|
|
|
|
- name: Install dependencies
|
|
run: npm install
|
|
|
|
- name: Build
|
|
env:
|
|
NODE_ENV: production
|
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
|
|
BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }}
|
|
NEXT_PUBLIC_BETTER_AUTH_URL: ${{ secrets.NEXT_PUBLIC_BETTER_AUTH_URL }}
|
|
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
|
NEXT_PUBLIC_STORAGE_BASE_URL: ${{ secrets.NEXT_PUBLIC_STORAGE_BASE_URL }}
|
|
STORAGE_ENDPOINT: ${{ secrets.STORAGE_ENDPOINT }}
|
|
STORAGE_REGION: ${{ secrets.STORAGE_REGION }}
|
|
STORAGE_ACCESS_KEY: ${{ secrets.STORAGE_ACCESS_KEY }}
|
|
STORAGE_SECRET_KEY: ${{ secrets.STORAGE_SECRET_KEY }}
|
|
STORAGE_BUCKET_PREFIX: ${{ secrets.STORAGE_BUCKET_PREFIX }}
|
|
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
|
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}
|
|
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
|
RESEND_WEBHOOK_SECRET: ${{ secrets.RESEND_WEBHOOK_SECRET }}
|
|
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
|
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
|
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
|
run: |
|
|
POSTGREST_HOST_PORT=$(cat .postgrest-port)
|
|
export NEXT_PUBLIC_API_URL="http://localhost:$POSTGREST_HOST_PORT"
|
|
npm run build
|
|
|
|
- name: Deploy
|
|
env:
|
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
|
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
|
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
|
MINIO_ROOT_USER: ${{ secrets.MINIO_ROOT_USER }}
|
|
MINIO_ROOT_PASSWORD: ${{ secrets.MINIO_ROOT_PASSWORD }}
|
|
POSTGREST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
|
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
|
|
BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }}
|
|
NEXT_PUBLIC_BETTER_AUTH_URL: ${{ secrets.NEXT_PUBLIC_BETTER_AUTH_URL }}
|
|
STORAGE_ENDPOINT: ${{ secrets.STORAGE_ENDPOINT }}
|
|
STORAGE_REGION: ${{ secrets.STORAGE_REGION }}
|
|
STORAGE_ACCESS_KEY: ${{ secrets.STORAGE_ACCESS_KEY }}
|
|
STORAGE_SECRET_KEY: ${{ secrets.STORAGE_SECRET_KEY }}
|
|
STORAGE_BUCKET_PREFIX: ${{ secrets.STORAGE_BUCKET_PREFIX }}
|
|
NEXT_PUBLIC_STORAGE_BASE_URL: ${{ secrets.NEXT_PUBLIC_STORAGE_BASE_URL }}
|
|
PGRST_SERVER_PORT: ${{ secrets.PGRST_SERVER_PORT }}
|
|
PGRST_DB_URI: ${{ secrets.PGRST_DB_URI }}
|
|
PGRST_DB_ANON_ROLE: ${{ secrets.PGRST_DB_ANON_ROLE }}
|
|
PGRST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
|
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
|
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
|
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}
|
|
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
|
RESEND_WEBHOOK_SECRET: ${{ secrets.RESEND_WEBHOOK_SECRET }}
|
|
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
|
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
|
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
|
run: |
|
|
APP_DIR=/home/tyler/route-commerce
|
|
mkdir -p $APP_DIR
|
|
# Use the port chosen by Start Docker stack (persisted to .postgrest-port)
|
|
POSTGREST_HOST_PORT=$(cat .postgrest-port)
|
|
export NEXT_PUBLIC_API_URL="http://localhost:$POSTGREST_HOST_PORT"
|
|
|
|
# Write env file from secrets (preserves existing .env for docker compose)
|
|
{
|
|
printf "DATABASE_URL=%s\n" "$DATABASE_URL"
|
|
printf "NEXT_PUBLIC_API_URL=%s\n" "$NEXT_PUBLIC_API_URL"
|
|
printf "POSTGRES_USER=%s\n" "$POSTGRES_USER"
|
|
printf "POSTGRES_PASSWORD=%s\n" "$POSTGRES_PASSWORD"
|
|
printf "POSTGRES_DB=%s\n" "$POSTGRES_DB"
|
|
printf "MINIO_ROOT_USER=%s\n" "$MINIO_ROOT_USER"
|
|
printf "MINIO_ROOT_PASSWORD=%s\n" "$MINIO_ROOT_PASSWORD"
|
|
printf "POSTGREST_JWT_SECRET=%s\n" "$POSTGREST_JWT_SECRET"
|
|
printf "BETTER_AUTH_SECRET=%s\n" "$BETTER_AUTH_SECRET"
|
|
printf "BETTER_AUTH_URL=%s\n" "$BETTER_AUTH_URL"
|
|
printf "NEXT_PUBLIC_BETTER_AUTH_URL=%s\n" "$NEXT_PUBLIC_BETTER_AUTH_URL"
|
|
printf "STORAGE_ENDPOINT=%s\n" "$STORAGE_ENDPOINT"
|
|
printf "STORAGE_REGION=%s\n" "$STORAGE_REGION"
|
|
printf "STORAGE_ACCESS_KEY=%s\n" "$STORAGE_ACCESS_KEY"
|
|
printf "STORAGE_SECRET_KEY=%s\n" "$STORAGE_SECRET_KEY"
|
|
printf "STORAGE_BUCKET_PREFIX=%s\n" "$STORAGE_BUCKET_PREFIX"
|
|
printf "NEXT_PUBLIC_STORAGE_BASE_URL=%s\n" "$NEXT_PUBLIC_STORAGE_BASE_URL"
|
|
printf "PGRST_SERVER_PORT=%s\n" "$PGRST_SERVER_PORT"
|
|
printf "PGRST_DB_URI=%s\n" "$PGRST_DB_URI"
|
|
printf "PGRST_DB_ANON_ROLE=%s\n" "$PGRST_DB_ANON_ROLE"
|
|
printf "PGRST_JWT_SECRET=%s\n" "$POSTGREST_JWT_SECRET"
|
|
printf "NEXT_PUBLIC_SUPABASE_URL=%s\n" "$NEXT_PUBLIC_SUPABASE_URL"
|
|
printf "NEXT_PUBLIC_SUPABASE_ANON_KEY=%s\n" "$NEXT_PUBLIC_SUPABASE_ANON_KEY"
|
|
printf "STRIPE_SECRET_KEY=%s\n" "$STRIPE_SECRET_KEY"
|
|
printf "STRIPE_WEBHOOK_SECRET=%s\n" "$STRIPE_WEBHOOK_SECRET"
|
|
printf "STRIPE_PUBLISHABLE_KEY=%s\n" "$STRIPE_PUBLISHABLE_KEY"
|
|
printf "RESEND_API_KEY=%s\n" "$RESEND_API_KEY"
|
|
printf "RESEND_WEBHOOK_SECRET=%s\n" "$RESEND_WEBHOOK_SECRET"
|
|
printf "MINIMAX_API_KEY=%s\n" "$MINIMAX_API_KEY"
|
|
printf "MINIMAX_BASE_URL=%s\n" "$MINIMAX_BASE_URL"
|
|
printf "FROM_EMAIL=%s\n" "$FROM_EMAIL"
|
|
} > $APP_DIR/.env.production
|
|
|
|
# Copy build output and required files
|
|
rsync -a --delete .next/ $APP_DIR/.next/
|
|
rsync -a --delete public/ $APP_DIR/public/
|
|
cp package.json $APP_DIR/
|
|
cp docker-compose.yml $APP_DIR/
|
|
cp -r supabase/ $APP_DIR/
|
|
cp next.config.ts $APP_DIR/ 2>/dev/null || cp next.config.js $APP_DIR/ 2>/dev/null || true
|
|
|
|
# Install production deps only
|
|
cd $APP_DIR
|
|
npm install --omit=dev
|
|
|
|
# Start or restart PM2 process
|
|
if pm2 describe route-commerce > /dev/null 2>&1; then
|
|
pm2 restart route-commerce
|
|
else
|
|
pm2 start npm --name route-commerce -- start -- -p 3100
|
|
pm2 save
|
|
fi
|
|
|
|
echo "Deployed successfully"
|