diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 00000000..da56da5e --- /dev/null +++ b/deploy.sh @@ -0,0 +1,187 @@ +#!/bin/bash +set -e + +# ────────────────────────────────────────── +# Crewli Deploy Script +# Run on VPS: ./deploy.sh [tag] +# Examples: +# ./deploy.sh → deploys latest main +# ./deploy.sh v1.2.0 → deploys specific tag +# ────────────────────────────────────────── + +APP_DIR="/home/crewli/crewli" +PHP="/usr/local/php84/bin/php" +COMPOSER="/usr/local/bin/composer" +HEALTHCHECK_URL="https://api.crewli.app/up" +TAG="${1:-}" + +# Load fnm (Node version manager) +export PATH="$HOME/.local/share/fnm:$PATH" +eval "$(fnm env)" + +echo "══════════════════════════════════════" +echo " Crewli — Deploy" +echo "══════════════════════════════════════" + +cd "$APP_DIR" + +# ────────────────────────────────────────── +# 0. Preflight checks +# ────────────────────────────────────────── + +# Prevent concurrent deploys (race conditions on git + npm) +exec 200>/tmp/crewli-deploy.lock +flock -n 200 || { echo "❌ Another deploy is already running. Aborting."; exit 1; } + +# Verify .env exists — without it config:cache will crash the app +if [ ! -f .env ]; then + echo "❌ .env missing — aborting before any changes" + exit 1 +fi + +# Capture current commit for rollback hint on failure +PREVIOUS_COMMIT=$(git rev-parse HEAD) +echo "→ Current commit: $PREVIOUS_COMMIT" + +# Rollback hint on any failure after this point +trap 'echo ""; echo "❌ Deploy failed. To roll back run:"; echo " cd $APP_DIR && git reset --hard $PREVIOUS_COMMIT && ./deploy.sh"; echo ""; $PHP artisan up 2>/dev/null || true' ERR + +# ────────────────────────────────────────── +# 1. Maintenance mode +# ────────────────────────────────────────── +echo "→ Enabling maintenance mode..." +$PHP artisan down --retry=30 || true + +# ────────────────────────────────────────── +# 2. Pull latest code +# ────────────────────────────────────────── +echo "→ Pulling from Gitea..." +git fetch --all --tags + +if [ -n "$TAG" ]; then + echo "→ Checking out tag: $TAG" + git checkout "$TAG" +else + echo "→ Checking out latest main" + git checkout main + git pull origin main +fi + +NEW_COMMIT=$(git rev-parse HEAD) +echo "→ New commit: $NEW_COMMIT" + +# ────────────────────────────────────────── +# 3. Install PHP dependencies +# ────────────────────────────────────────── +# Only run composer install if composer.lock actually changed +if ! git diff --quiet "$PREVIOUS_COMMIT" "$NEW_COMMIT" -- composer.lock; then + echo "→ composer.lock changed — installing Composer dependencies..." + $PHP $COMPOSER install --no-dev --optimize-autoloader --no-interaction +else + echo "→ composer.lock unchanged — skipping composer install" + # Still regenerate autoloader in case new classes were added + $PHP $COMPOSER dump-autoload --optimize --no-dev --no-interaction +fi + +# ────────────────────────────────────────── +# 4. Install Node dependencies and build +# ────────────────────────────────────────── +if ! git diff --quiet "$PREVIOUS_COMMIT" "$NEW_COMMIT" -- package-lock.json; then + echo "→ package-lock.json changed — running npm ci..." + npm ci --production=false +else + echo "→ package-lock.json unchanged — skipping npm ci" +fi + +echo "→ Building frontend assets (apps/app + apps/portal)..." +# Explicit per-workspace build to avoid silent single-app builds +npm run build -w apps/app +npm run build -w apps/portal + +# Verify both dist folders exist and are non-empty +for app in app portal; do + if [ ! -f "apps/$app/dist/index.html" ]; then + echo "❌ Build failed: apps/$app/dist/index.html missing" + exit 1 + fi +done + +# ────────────────────────────────────────── +# 5. Run migrations +# ────────────────────────────────────────── +echo "→ Running migrations..." +$PHP artisan migrate --force + +# ────────────────────────────────────────── +# 6. Clear and rebuild caches +# ────────────────────────────────────────── +echo "→ Clearing all caches..." +$PHP artisan optimize:clear + +echo "→ Rebuilding caches..." +$PHP artisan config:cache +$PHP artisan route:cache +$PHP artisan event:cache +$PHP artisan view:cache # Needed for Blade email templates (TransactionalMail) + +# ────────────────────────────────────────── +# 7. File permissions +# ────────────────────────────────────────── +# Ensure storage/cache dirs are writable by both crewli user and www-data +echo "→ Fixing permissions..." +chown -R crewli:crewli storage bootstrap/cache +chmod -R ug+rwX storage bootstrap/cache + +# ────────────────────────────────────────── +# 8. Restart queue (process pending jobs with new code) +# ────────────────────────────────────────── +# NB: Supervisor/systemd must be configured to restart workers after signal +echo "→ Signaling queue workers to restart..." +$PHP artisan queue:restart + +# ────────────────────────────────────────── +# 9. Storage link (idempotent) +# ────────────────────────────────────────── +$PHP artisan storage:link 2>/dev/null || true + +# ────────────────────────────────────────── +# 10. Disable maintenance mode +# ────────────────────────────────────────── +echo "→ Going live!" +$PHP artisan up + +# ────────────────────────────────────────── +# 11. Health check +# ────────────────────────────────────────── +echo "→ Running health check..." +sleep 2 # Give PHP-FPM a moment to pick up new code + +HEALTH_CODE=$(curl -sf -o /dev/null -w "%{http_code}" --max-time 10 "$HEALTHCHECK_URL" || echo "000") + +if [ "$HEALTH_CODE" != "200" ]; then + echo "" + echo "⚠️ Health check failed — HTTP $HEALTH_CODE on $HEALTHCHECK_URL" + echo " Site is marked 'up' but may be broken." + echo " Investigate immediately or roll back:" + echo " cd $APP_DIR && git reset --hard $PREVIOUS_COMMIT && ./deploy.sh" + exit 1 +fi + +echo "✓ Health check passed (HTTP 200)" + +# ────────────────────────────────────────── +# Done +# ────────────────────────────────────────── + +# Clear the rollback trap — we made it +trap - ERR + +echo "" +echo "══════════════════════════════════════" +if [ -n "$TAG" ]; then + echo " ✓ Deployed: $TAG" +else + echo " ✓ Deployed: main (latest)" +fi +echo " ✓ Commit: $NEW_COMMIT" +echo "══════════════════════════════════════" \ No newline at end of file