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 # Production postgrest always owns port 3010. No dynamic allocation. POSTGREST_HOST_PORT=3010 export NEXT_PUBLIC_API_URL="http://localhost:$POSTGREST_HOST_PORT" # Tear down previous stack: kill containers by name (two separate calls # because multiple --filter name= flags are ANDed, not ORed). { docker ps -aq --filter "name=route_commerce_" docker ps -aq --filter "name=route-commerce-" } | sort -u | xargs -r docker rm -f || true # Remove network after containers are gone so Docker allocates a fresh subnet. docker network rm route-commerce_default 2>/dev/null || true # Write fresh .env for docker compose cp docker-compose.yml $APP_DIR/docker-compose.yml cat > $APP_DIR/.env < /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 # PAGER= prevents psql launching less/more in a non-interactive shell. # Each migration runs in its own psql call so an error in one file # doesn't abort the transaction and block all subsequent migrations. export PAGER= export PGPASSWORD="${POSTGRES_PASSWORD}" PG="psql -h 127.0.0.1 -U ${POSTGRES_USER} -d ${POSTGRES_DB} --no-psqlrc -v ON_ERROR_STOP=0 -q" $PG -f supabase/migrations/000_preflight_supabase_compat.sql || true [ -f supabase/captured_schema.sql ] && $PG -f supabase/captured_schema.sql || true for f in supabase/migrations/[0-9]*.sql; do $PG -f "$f" || true 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: | export NEXT_PUBLIC_API_URL="http://localhost:3010" 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 POSTGREST_HOST_PORT=3010 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"