WS-7 PR-3 commit 3, RFC §3.5. - deploy.sh: export VITE_SENTRY_RELEASE=crewli-app@<short-sha> before the Vite build so the release identifier is inlined into the bundle via import.meta.env. - New step 4a after the build: when SENTRY_AUTH_TOKEN and VITE_SENTRY_DSN_FRONTEND are present, upload sourcemaps via `npx @sentry/cli@latest sourcemaps upload` to project crewli-app with --url-prefix=~/assets/ matching Vite's default asset path. Soft-fails with a warning so deploy can still succeed if GlitchTip is unreachable. - Always run `find apps/app/dist -name '*.map' -delete` after upload (or after skipped upload). No public-mapped sources reach nginx — RFC §3.5 invariant. - .gitignore: defensive `apps/app/dist/**/*.map` exclusion (dist/ is already broadly ignored; this is belt-and-suspenders against accidental commits of build output). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
207 lines
9.8 KiB
Bash
Executable File
207 lines
9.8 KiB
Bash
Executable File
#!/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)..."
|
|
# WS-7 RFC §3.4 — release identifier is injected at build-time so Vite
|
|
# inlines it into import.meta.env.VITE_SENTRY_RELEASE for the bundle.
|
|
export VITE_SENTRY_RELEASE="crewli-app@$(git rev-parse --short HEAD)"
|
|
npm run build -w apps/app
|
|
|
|
if [ ! -f "apps/app/dist/index.html" ]; then
|
|
echo "❌ Build failed: apps/app/dist/index.html missing"
|
|
exit 1
|
|
fi
|
|
|
|
# ──────────────────────────────────────────
|
|
# 4a. Sourcemap upload to GlitchTip + scrub from dist/
|
|
# ──────────────────────────────────────────
|
|
# WS-7 RFC §3.5: maps generated by Vite, uploaded to GlitchTip so stack
|
|
# traces are readable in the UI, then DELETED from dist/ before nginx
|
|
# serves them. No public-mapped sources on production.
|
|
if [ -n "${SENTRY_AUTH_TOKEN:-}" ] && [ -n "${VITE_SENTRY_DSN_FRONTEND:-}" ]; then
|
|
SENTRY_ORG_VAL="${SENTRY_ORG:-crewli}"
|
|
echo "→ Uploading sourcemaps for release ${VITE_SENTRY_RELEASE} to project crewli-app..."
|
|
npx --yes @sentry/cli@latest sourcemaps upload \
|
|
--org "$SENTRY_ORG_VAL" \
|
|
--project crewli-app \
|
|
--release "$VITE_SENTRY_RELEASE" \
|
|
--url-prefix "~/assets/" \
|
|
apps/app/dist/assets || echo "⚠️ Sourcemap upload failed; continuing deploy."
|
|
else
|
|
echo "→ SENTRY_AUTH_TOKEN or VITE_SENTRY_DSN_FRONTEND unset — skipping sourcemap upload."
|
|
fi
|
|
|
|
echo "→ Stripping *.map files from dist/ (RFC §3.5: no public-mapped sources)..."
|
|
find apps/app/dist -name '*.map' -type f -delete
|
|
|
|
# ──────────────────────────────────────────
|
|
# 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 "══════════════════════════════════════" |