20c03acdaa
Deploy to route.crispygoat.com / deploy (push) Successful in 3m26s
The script kept failing on deploy (ESM vs CJS issue, plus the script file wasn't being shipped). User decided not to worry about it. The 26 manual rows will remain in the sheet without Entry IDs; the cron will retry them with a manual cleanup of any duplicates if needed.
212 lines
12 KiB
YAML
212 lines
12 KiB
YAML
name: Deploy to route.crispygoat.com
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
workflow_dispatch:
|
|
|
|
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: Install dependencies
|
|
run: npm install
|
|
|
|
- name: Run migrations
|
|
env:
|
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
run: |
|
|
set -e
|
|
node scripts/preflight-check.js
|
|
npm run migrate:one
|
|
node scripts/postflight-check.js
|
|
|
|
- name: Build
|
|
env:
|
|
NODE_ENV: production
|
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
NEXT_PUBLIC_SITE_URL: ${{ secrets.NEXT_PUBLIC_SITE_URL }}
|
|
NEON_AUTH_BASE_URL: ${{ secrets.NEON_AUTH_BASE_URL }}
|
|
NEON_AUTH_COOKIE_SECRET: ${{ secrets.NEON_AUTH_COOKIE_SECRET }}
|
|
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
|
|
AUTH_URL: ${{ secrets.AUTH_URL }}
|
|
NEXT_PUBLIC_AUTH_URL: ${{ secrets.NEXT_PUBLIC_AUTH_URL }}
|
|
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
|
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
|
ALLOW_DEV_LOGIN: ${{ secrets.ALLOW_DEV_LOGIN }}
|
|
ADMIN_ALLOWED_EMAILS: ${{ secrets.ADMIN_ALLOWED_EMAILS }}
|
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
|
|
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
|
STRIPE_PRICE_STARTER: ${{ secrets.STRIPE_PRICE_STARTER }}
|
|
STRIPE_PRICE_FARM: ${{ secrets.STRIPE_PRICE_FARM }}
|
|
STRIPE_PRICE_ENTERPRISE: ${{ secrets.STRIPE_PRICE_ENTERPRISE }}
|
|
STRIPE_PRICE_HARVEST_REACH: ${{ secrets.STRIPE_PRICE_HARVEST_REACH }}
|
|
STRIPE_PRICE_WHOLESALE_PORTAL: ${{ secrets.STRIPE_PRICE_WHOLESALE_PORTAL }}
|
|
STRIPE_PRICE_WATER_LOG: ${{ secrets.STRIPE_PRICE_WATER_LOG }}
|
|
STRIPE_PRICE_AI_TOOLS: ${{ secrets.STRIPE_PRICE_AI_TOOLS }}
|
|
STRIPE_PRICE_SQUARE_SYNC: ${{ secrets.STRIPE_PRICE_SQUARE_SYNC }}
|
|
STRIPE_PRICE_SMS_CAMPAIGNS: ${{ secrets.STRIPE_PRICE_SMS_CAMPAIGNS }}
|
|
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
|
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
|
MINIO_ENDPOINT: ${{ secrets.MINIO_ENDPOINT }}
|
|
MINIO_REGION: ${{ secrets.MINIO_REGION }}
|
|
MINIO_ACCESS_KEY: ${{ secrets.MINIO_ACCESS_KEY }}
|
|
MINIO_SECRET_KEY: ${{ secrets.MINIO_SECRET_KEY }}
|
|
MINIO_PUBLIC_URL: ${{ secrets.MINIO_PUBLIC_URL }}
|
|
MINIO_BUCKET_PRODUCTS: ${{ secrets.MINIO_BUCKET_PRODUCTS }}
|
|
MINIO_BUCKET_BRAND_LOGOS: ${{ secrets.MINIO_BUCKET_BRAND_LOGOS }}
|
|
MINIO_BUCKET_WATER_LOGS: ${{ secrets.MINIO_BUCKET_WATER_LOGS }}
|
|
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
|
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
|
SMARTSHEET_TOKEN_ENC_KEY: ${{ secrets.SMARTSHEET_TOKEN_ENC_KEY }}
|
|
SMARTSHEET_CRON_SECRET: ${{ secrets.SMARTSHEET_CRON_SECRET }}
|
|
run: npm run build
|
|
|
|
- name: Deploy
|
|
env:
|
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
NEXT_PUBLIC_SITE_URL: ${{ secrets.NEXT_PUBLIC_SITE_URL }}
|
|
NEON_AUTH_BASE_URL: ${{ secrets.NEON_AUTH_BASE_URL }}
|
|
NEON_AUTH_COOKIE_SECRET: ${{ secrets.NEON_AUTH_COOKIE_SECRET }}
|
|
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
|
|
AUTH_URL: ${{ secrets.AUTH_URL }}
|
|
NEXT_PUBLIC_AUTH_URL: ${{ secrets.NEXT_PUBLIC_AUTH_URL }}
|
|
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
|
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
|
ALLOW_DEV_LOGIN: ${{ secrets.ALLOW_DEV_LOGIN }}
|
|
ADMIN_ALLOWED_EMAILS: ${{ secrets.ADMIN_ALLOWED_EMAILS }}
|
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
|
|
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
|
STRIPE_PRICE_STARTER: ${{ secrets.STRIPE_PRICE_STARTER }}
|
|
STRIPE_PRICE_FARM: ${{ secrets.STRIPE_PRICE_FARM }}
|
|
STRIPE_PRICE_ENTERPRISE: ${{ secrets.STRIPE_PRICE_ENTERPRISE }}
|
|
STRIPE_PRICE_HARVEST_REACH: ${{ secrets.STRIPE_PRICE_HARVEST_REACH }}
|
|
STRIPE_PRICE_WHOLESALE_PORTAL: ${{ secrets.STRIPE_PRICE_WHOLESALE_PORTAL }}
|
|
STRIPE_PRICE_WATER_LOG: ${{ secrets.STRIPE_PRICE_WATER_LOG }}
|
|
STRIPE_PRICE_AI_TOOLS: ${{ secrets.STRIPE_PRICE_AI_TOOLS }}
|
|
STRIPE_PRICE_SQUARE_SYNC: ${{ secrets.STRIPE_PRICE_SQUARE_SYNC }}
|
|
STRIPE_PRICE_SMS_CAMPAIGNS: ${{ secrets.STRIPE_PRICE_SMS_CAMPAIGNS }}
|
|
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
|
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
|
MINIO_ENDPOINT: ${{ secrets.MINIO_ENDPOINT }}
|
|
MINIO_REGION: ${{ secrets.MINIO_REGION }}
|
|
MINIO_ACCESS_KEY: ${{ secrets.MINIO_ACCESS_KEY }}
|
|
MINIO_SECRET_KEY: ${{ secrets.MINIO_SECRET_KEY }}
|
|
MINIO_PUBLIC_URL: ${{ secrets.MINIO_PUBLIC_URL }}
|
|
MINIO_BUCKET_PRODUCTS: ${{ secrets.MINIO_BUCKET_PRODUCTS }}
|
|
MINIO_BUCKET_BRAND_LOGOS: ${{ secrets.MINIO_BUCKET_BRAND_LOGOS }}
|
|
MINIO_BUCKET_WATER_LOGS: ${{ secrets.MINIO_BUCKET_WATER_LOGS }}
|
|
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
|
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
|
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
|
SMARTSHEET_TOKEN_ENC_KEY: ${{ secrets.SMARTSHEET_TOKEN_ENC_KEY }}
|
|
SMARTSHEET_CRON_SECRET: ${{ secrets.SMARTSHEET_CRON_SECRET }}
|
|
SERVER_SSH_KEY: ${{ secrets.SERVER_SSH_KEY }}
|
|
run: |
|
|
set -e
|
|
APP_DIR=/home/tyler/route-commerce
|
|
|
|
# Setup SSH key - write raw (no printf which can corrupt multi-line keys)
|
|
mkdir -p ~/.ssh
|
|
echo "$SERVER_SSH_KEY" > ~/.ssh/id_ed25519
|
|
chmod 600 ~/.ssh/id_ed25519
|
|
|
|
# Verify key was written correctly
|
|
if ! grep -q "PRIVATE KEY" ~/.ssh/id_ed25519; then
|
|
echo "ERROR: SSH key not found or malformed. Check SERVER_SSH_KEY secret."
|
|
cat ~/.ssh/id_ed25519 || echo "File is empty"
|
|
exit 1
|
|
fi
|
|
|
|
ssh-keyscan -H route.crispygoat.com >> ~/.ssh/known_hosts 2>/dev/null || true
|
|
|
|
# Test SSH connection with verbose output for debugging
|
|
echo "Testing SSH connection..."
|
|
ssh -o ConnectTimeout=15 -o StrictHostKeyChecking=no -o LogLevel=VERBOSE tyler@route.crispygoat.com "echo 'SSH OK' && hostname" 2>&1 || { echo "SSH FAILED"; exit 1; }
|
|
|
|
# Create app dir on server
|
|
ssh tyler@route.crispygoat.com "mkdir -p $APP_DIR/.next $APP_DIR/public"
|
|
|
|
# Write production env file
|
|
ENV_FILE=$(mktemp)
|
|
{
|
|
printf 'DATABASE_URL=%s\n' "$DATABASE_URL"
|
|
printf 'NEXT_PUBLIC_SITE_URL=%s\n' "$NEXT_PUBLIC_SITE_URL"
|
|
printf 'NEON_AUTH_BASE_URL=%s\n' "$NEON_AUTH_BASE_URL"
|
|
printf 'NEON_AUTH_COOKIE_SECRET=%s\n' "$NEON_AUTH_COOKIE_SECRET"
|
|
printf 'AUTH_SECRET=%s\n' "$AUTH_SECRET"
|
|
printf 'AUTH_URL=%s\n' "$AUTH_URL"
|
|
printf 'NEXT_PUBLIC_AUTH_URL=%s\n' "$NEXT_PUBLIC_AUTH_URL"
|
|
printf 'GOOGLE_CLIENT_ID=%s\n' "$GOOGLE_CLIENT_ID"
|
|
printf 'GOOGLE_CLIENT_SECRET=%s\n' "$GOOGLE_CLIENT_SECRET"
|
|
printf 'ALLOW_DEV_LOGIN=%s\n' "$ALLOW_DEV_LOGIN"
|
|
printf 'ADMIN_ALLOWED_EMAILS=%s\n' "$ADMIN_ALLOWED_EMAILS"
|
|
printf 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=%s\n' "$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY"
|
|
printf 'STRIPE_SECRET_KEY=%s\n' "$STRIPE_SECRET_KEY"
|
|
printf 'STRIPE_WEBHOOK_SECRET=%s\n' "$STRIPE_WEBHOOK_SECRET"
|
|
printf 'STRIPE_PRICE_STARTER=%s\n' "$STRIPE_PRICE_STARTER"
|
|
printf 'STRIPE_PRICE_FARM=%s\n' "$STRIPE_PRICE_FARM"
|
|
printf 'STRIPE_PRICE_ENTERPRISE=%s\n' "$STRIPE_PRICE_ENTERPRISE"
|
|
printf 'STRIPE_PRICE_HARVEST_REACH=%s\n' "$STRIPE_PRICE_HARVEST_REACH"
|
|
printf 'STRIPE_PRICE_WHOLESALE_PORTAL=%s\n' "$STRIPE_PRICE_WHOLESALE_PORTAL"
|
|
printf 'STRIPE_PRICE_WATER_LOG=%s\n' "$STRIPE_PRICE_WATER_LOG"
|
|
printf 'STRIPE_PRICE_AI_TOOLS=%s\n' "$STRIPE_PRICE_AI_TOOLS"
|
|
printf 'STRIPE_PRICE_SQUARE_SYNC=%s\n' "$STRIPE_PRICE_SQUARE_SYNC"
|
|
printf 'STRIPE_PRICE_SMS_CAMPAIGNS=%s\n' "$STRIPE_PRICE_SMS_CAMPAIGNS"
|
|
printf 'RESEND_API_KEY=%s\n' "$RESEND_API_KEY"
|
|
printf 'FROM_EMAIL=%s\n' "$FROM_EMAIL"
|
|
printf 'MINIO_ENDPOINT=%s\n' "$MINIO_ENDPOINT"
|
|
printf 'MINIO_REGION=%s\n' "$MINIO_REGION"
|
|
printf 'MINIO_ACCESS_KEY=%s\n' "$MINIO_ACCESS_KEY"
|
|
printf 'MINIO_SECRET_KEY=%s\n' "$MINIO_SECRET_KEY"
|
|
printf 'MINIO_PUBLIC_URL=%s\n' "$MINIO_PUBLIC_URL"
|
|
printf 'MINIO_BUCKET_PRODUCTS=%s\n' "$MINIO_BUCKET_PRODUCTS"
|
|
printf 'MINIO_BUCKET_BRAND_LOGOS=%s\n' "$MINIO_BUCKET_BRAND_LOGOS"
|
|
printf 'MINIO_BUCKET_WATER_LOGS=%s\n' "$MINIO_BUCKET_WATER_LOGS"
|
|
printf 'MINIMAX_API_KEY=%s\n' "$MINIMAX_API_KEY"
|
|
printf 'MINIMAX_BASE_URL=%s\n' "$MINIMAX_BASE_URL"
|
|
printf 'CRON_SECRET=%s\n' "$CRON_SECRET"
|
|
printf 'SMARTSHEET_TOKEN_ENC_KEY=%s\n' "$SMARTSHEET_TOKEN_ENC_KEY"
|
|
printf 'SMARTSHEET_CRON_SECRET=%s\n' "$SMARTSHEET_CRON_SECRET"
|
|
} > "$ENV_FILE"
|
|
|
|
# Upload env file and sync build output
|
|
echo "Uploading env file..."
|
|
scp -o ConnectTimeout=15 -o StrictHostKeyChecking=no "$ENV_FILE" tyler@route.crispygoat.com:$APP_DIR/.env
|
|
|
|
echo "Copying .next/..."
|
|
scp -o ConnectTimeout=30 -o StrictHostKeyChecking=no -r .next tyler@route.crispygoat.com:$APP_DIR/
|
|
echo "Copying public/..."
|
|
scp -o ConnectTimeout=30 -o StrictHostKeyChecking=no -r public tyler@route.crispygoat.com:$APP_DIR/
|
|
echo "Copying package.json..."
|
|
scp -o ConnectTimeout=15 -o StrictHostKeyChecking=no package.json tyler@route.crispygoat.com:$APP_DIR/
|
|
scp -o ConnectTimeout=15 -o StrictHostKeyChecking=no next.config.ts tyler@route.crispygoat.com:$APP_DIR/ 2>/dev/null || true
|
|
|
|
# Ship the migration runner + SQL so the server has a recovery path (scripts/ and db/migrations/ were previously omitted from the artifact).
|
|
# This allows `node scripts/migrate.js` (after sourcing .env.production) to work directly on the target if needed for bootstrap or emergencies.
|
|
# See docs/superpowers/plans/2026-06-prod-db-schema-migration-reliability.md
|
|
echo "Ensuring migration directories on server and copying runner + SQL..."
|
|
ssh -o ConnectTimeout=15 -o StrictHostKeyChecking=no tyler@route.crispygoat.com "mkdir -p $APP_DIR/scripts $APP_DIR/db"
|
|
scp -o ConnectTimeout=15 -o StrictHostKeyChecking=no scripts/migrate.js tyler@route.crispygoat.com:$APP_DIR/scripts/migrate.js 2>/dev/null || true
|
|
scp -o ConnectTimeout=15 -o StrictHostKeyChecking=no scripts/smartsheet-backfill-entry-ids.mjs tyler@route.crispygoat.com:$APP_DIR/scripts/smartsheet-backfill-entry-ids.mjs 2>/dev/null || true
|
|
scp -o ConnectTimeout=15 -o StrictHostKeyChecking=no -r db/migrations tyler@route.crispygoat.com:$APP_DIR/db/ 2>/dev/null || true
|
|
|
|
# Install deps and restart on server
|
|
echo "Installing deps and restarting PM2..."
|
|
# IMPORTANT: --update-env so PM2 re-reads .env. Without it PM2 keeps
|
|
# the original env vars cached at first start and silently ignores
|
|
# newly added ones (like SMARTSHEET_*).
|
|
ssh -o ConnectTimeout=60 -o StrictHostKeyChecking=no tyler@route.crispygoat.com "cd $APP_DIR && npm install --omit=dev 2>&1 | tail -5 && pm2 restart route-commerce --update-env || pm2 start npm --name route-commerce -- start -- -p 3100 && pm2 save && sleep 4 && curl -f -s http://localhost:3100/api/health/db-schema || { echo 'Health check failed after start - schema not applied (see plan)'; exit 1; }"
|
|
|
|
echo "Deployed successfully" |