From 50e6afea2a5965cacc8f8ab19e144440fa66eff2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:52:32 +0000 Subject: [PATCH 1/3] chore: begin Unicorn Hetzner-only migration Agent-Logs-Url: https://github.com/ruffy80/ZeusAI/sessions/e1ffc6c3-a08e-4082-b80e-d1d535811fbc Co-authored-by: ruffy80 <29306714+ruffy80@users.noreply.github.com> --- UNICORN_FINAL/package-lock.json | 34 --------------------------------- 1 file changed, 34 deletions(-) diff --git a/UNICORN_FINAL/package-lock.json b/UNICORN_FINAL/package-lock.json index 44c9d7c0..85c51168 100644 --- a/UNICORN_FINAL/package-lock.json +++ b/UNICORN_FINAL/package-lock.json @@ -85,40 +85,6 @@ "node": ">=16" } }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", From b974a46cc3a3952d58b5d494a91a0393982763ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:56:23 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20migrate=20Unicorn=20to=20GitHub+Het?= =?UTF-8?q?zner=20only=20=E2=80=94=20disable=20Vercel,=20improve=20Nginx?= =?UTF-8?q?=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/ruffy80/ZeusAI/sessions/e1ffc6c3-a08e-4082-b80e-d1d535811fbc Co-authored-by: ruffy80 <29306714+ruffy80@users.noreply.github.com> --- .github/workflows/unicorn-keepalive.yml | 36 ++++--------- .github/workflows/vercel-deploy.yml | 60 +++++++--------------- UNICORN_FINAL/scripts/nginx-unicorn.conf | 64 +++++++++++++++++++++++- 3 files changed, 90 insertions(+), 70 deletions(-) diff --git a/.github/workflows/unicorn-keepalive.yml b/.github/workflows/unicorn-keepalive.yml index d68f1c6e..c51da1f1 100644 --- a/.github/workflows/unicorn-keepalive.yml +++ b/.github/workflows/unicorn-keepalive.yml @@ -1,7 +1,7 @@ -name: ๐Ÿ”— Unicorn Keep-Alive (Platform Health Check) +name: ๐Ÿ”— Unicorn Keep-Alive (Hetzner Health Check) -# Runs every 30 minutes to verify Vercel + Hetzner are reachable. -# If either is down, triggers the main deploy workflow to heal the platform. +# Runs every 30 minutes to verify Hetzner is reachable. +# If the server is down, triggers the main deploy workflow to heal the platform. on: schedule: @@ -26,19 +26,6 @@ jobs: outputs: healthy: ${{ steps.health.outputs.healthy }} steps: - - name: Check Vercel health - id: vercel_check - continue-on-error: true - run: | - URL="${{ secrets.VERCEL_HEALTH_URL || 'https://zeusai.pro/health' }}" - STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$URL" || echo "000") - echo "Vercel health URL โ†’ HTTP $STATUS" - if [ "$STATUS" = "200" ]; then - echo "vercel_ok=true" >> "$GITHUB_OUTPUT" - else - echo "vercel_ok=false" >> "$GITHUB_OUTPUT" - fi - - name: Check Hetzner health id: hetzner_check continue-on-error: true @@ -65,16 +52,14 @@ jobs: - name: Evaluate platform health id: health run: | - VERCEL="${{ steps.vercel_check.outputs.vercel_ok || 'unknown' }}" HETZNER="${{ steps.hetzner_check.outputs.hetzner_ok || 'unknown' }}" FORCE="${{ inputs.force_redeploy || 'false' }}" - echo "Vercel status : $VERCEL" echo "Hetzner status : $HETZNER" echo "Force redeploy : $FORCE" - if [ "$VERCEL" = "true" ] && [ "$HETZNER" = "true" ] && [ "$FORCE" != "true" ]; then - echo "โœ… Platform fully healthy โ€” no redeploy needed" + if [ "$HETZNER" = "true" ] && [ "$FORCE" != "true" ]; then + echo "โœ… Platform healthy โ€” no redeploy needed" echo "healthy=true" >> "$GITHUB_OUTPUT" else echo "โš ๏ธ Platform needs attention โ€” triggering redeploy" @@ -88,7 +73,6 @@ jobs: echo "" >> "$GITHUB_STEP_SUMMARY" echo "| Platform | Status |" >> "$GITHUB_STEP_SUMMARY" echo "|----------|--------|" >> "$GITHUB_STEP_SUMMARY" - echo "| Vercel | ${{ steps.vercel_check.outputs.vercel_ok == 'true' && 'โœ… OK' || 'โŒ DOWN' }} |" >> "$GITHUB_STEP_SUMMARY" echo "| Hetzner | ${{ steps.hetzner_check.outputs.hetzner_ok == 'true' && 'โœ… OK' || 'โŒ DOWN' }} |" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "Checked at: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_STEP_SUMMARY" @@ -218,15 +202,13 @@ jobs: fi # Allow services 30 s more to stabilise after the SSH restart (server-side # health probe already ran inside the SSH command; this wait covers propagation - # to the public-facing Nginx/Vercel layer before external re-probing). + # before external re-probing). sleep 30 HEALTH_TIMEOUT=15 - # Check both Vercel and Hetzner from the runner - VERCEL_URL="${{ secrets.VERCEL_HEALTH_URL || 'https://zeusai.pro/health' }}" - VERCEL_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$HEALTH_TIMEOUT" "$VERCEL_URL" || echo "000") + # Check Hetzner from the runner HETZNER_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$HEALTH_TIMEOUT" "http://${DEPLOY_HOST}/api/health" || echo "000") - echo "Vercel re-check: $VERCEL_STATUS | Hetzner re-check: $HETZNER_STATUS" - if [ "$VERCEL_STATUS" = "200" ] && [ "$HETZNER_STATUS" = "200" ]; then + echo "Hetzner re-check: $HETZNER_STATUS" + if [ "$HETZNER_STATUS" = "200" ]; then echo "โœ… Platform recovered via fast SSH restart โ€” skipping full redeploy" echo "recovered=true" >> "$GITHUB_OUTPUT" else diff --git a/.github/workflows/vercel-deploy.yml b/.github/workflows/vercel-deploy.yml index f77817a0..0ecd69dc 100644 --- a/.github/workflows/vercel-deploy.yml +++ b/.github/workflows/vercel-deploy.yml @@ -1,4 +1,4 @@ -name: ๐Ÿฆ„ Deploy ZeusAI (GitHub โ†’ Vercel + Hetzner) +name: ๐Ÿฆ„ Deploy ZeusAI (GitHub โ†’ Hetzner) on: push: @@ -28,7 +28,7 @@ on: default: 'false' env: - VERCEL_PROD_URL: https://zeusai.pro + SITE_DOMAIN_DEFAULT: zeusai.pro concurrency: group: ${{ github.event_name == 'pull_request' && format('pr-validate-{0}', github.event.pull_request.number) || 'zeusai-production' }} @@ -153,17 +153,17 @@ jobs: run: npm run innovation:report || true # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - # JOB 2: Deploy to Vercel (runs src/index.js - the Node HTTP server) + # JOB 2: Vercel deploy โ€” DISABLED (platform runs exclusively on GitHub + Hetzner) # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ deploy-vercel: - name: ๐Ÿš€ Deploy โ†’ Vercel + name: ๐Ÿšซ Deploy โ†’ Vercel (disabled โ€” Hetzner only) runs-on: ubuntu-latest needs: validate - # Skip Vercel deploy on scheduled runs โ€“ cron is for Hetzner keep-alive only. - # Deploying on schedule quickly exhausts the 100 free deployments/day limit. - # Exception: force_deploy=true overrides the schedule skip (e.g. keepalive or setup-secrets triggering a full redeploy). - # Skip on pull_request โ€“ PRs only run the validate job. - if: (github.event_name != 'schedule' || inputs.force_deploy == 'true') && github.event_name != 'pull_request' + # Vercel deployment is permanently disabled โ€” the platform runs exclusively + # on GitHub Actions + Hetzner. This stub job is kept so downstream jobs + # (deploy-summary, smoke-tests) that reference `needs: deploy-vercel` continue + # to resolve without needing YAML surgery. + if: false defaults: run: working-directory: UNICORN_FINAL @@ -1123,23 +1123,17 @@ jobs: steps: - name: Print summary env: - DEPLOY_URL: ${{ needs.deploy-vercel.outputs.deploy_url }} SITE_DOMAIN: ${{ secrets.SITE_DOMAIN || 'zeusai.pro' }} run: | - echo "## ๐Ÿฆ„ ZeusAI Deploy Summary" >> "$GITHUB_STEP_SUMMARY" + echo "## ๐Ÿฆ„ ZeusAI Deploy Summary (GitHub โ†’ Hetzner)" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "| Target | Result |" >> "$GITHUB_STEP_SUMMARY" echo "|-----------|--------|" >> "$GITHUB_STEP_SUMMARY" echo "| Validate | ${{ needs.validate.result }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Vercel | ${{ needs.deploy-vercel.result }} |" >> "$GITHUB_STEP_SUMMARY" echo "| Hetzner | ${{ needs.deploy-hetzner.result }} |" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "Commit: \`${{ github.sha }}\` by **${{ github.actor }}**" >> "$GITHUB_STEP_SUMMARY" echo "Triggered: ${{ github.event_name }}" >> "$GITHUB_STEP_SUMMARY" - if [ -n "$DEPLOY_URL" ]; then - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "**๐Ÿ”— Vercel deploy URL:** [$DEPLOY_URL]($DEPLOY_URL)" >> "$GITHUB_STEP_SUMMARY" - fi echo "" >> "$GITHUB_STEP_SUMMARY" SITE_DOMAIN="${SITE_DOMAIN:-zeusai.pro}" @@ -1173,27 +1167,13 @@ jobs: needs: [deploy-vercel, deploy-hetzner] if: always() steps: - - name: Check Vercel and Hetzner health + - name: Check Hetzner health run: | - VERCEL_URL='${{ needs.deploy-vercel.outputs.deploy_url }}' HETZNER_HOST="${{ secrets.HETZNER_HOST || secrets.SSH_HOST || '204.168.230.142' }}" ALL_OK=true - if [ -z "$VERCEL_URL" ]; then - echo "โ„น๏ธ Vercel not deployed โ€“ skipping Vercel smoke test." - else - VERCEL_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${VERCEL_URL}/health" || echo "000") - echo "Vercel /health: $VERCEL_STATUS" - if [ "$VERCEL_STATUS" != "200" ]; then - echo "โš ๏ธ Vercel smoke test: expected 200, got $VERCEL_STATUS" - ALL_OK=false - else - echo "โœ… Vercel smoke test passed" - fi - fi - if [ -z "$HETZNER_HOST" ]; then - echo "โ„น๏ธ Hetzner not configured โ€“ skipping Hetzner smoke test." + echo "โ„น๏ธ Hetzner not configured โ€“ skipping smoke test." else # Prefer Nginx port 80 (proxied to backend:3000); port 3000 is firewall-restricted HETZNER_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 "http://${HETZNER_HOST}/api/health" || echo "000") @@ -1215,27 +1195,23 @@ jobs: echo "โš ๏ธ Some smoke tests reported non-200 responses (see above)" fi - # โ”€โ”€ Production Vercel domain check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - PROD_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${{ env.VERCEL_PROD_URL }}/health" || echo "000") - echo "Production ${{ env.VERCEL_PROD_URL }}/health: $PROD_STATUS" - [ "$PROD_STATUS" = "200" ] && echo "โœ… ${{ env.VERCEL_PROD_URL }} is live" || echo "โ„น๏ธ Production domain not yet reachable (DNS/alias may still be propagating)" + # โ”€โ”€ Production domain check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + PROD_DOMAIN="${{ secrets.SITE_DOMAIN || env.SITE_DOMAIN_DEFAULT }}" + PROD_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://${PROD_DOMAIN}/health" || echo "000") + echo "Production https://${PROD_DOMAIN}/health: $PROD_STATUS" + [ "$PROD_STATUS" = "200" ] && echo "โœ… https://${PROD_DOMAIN} is live" || echo "โ„น๏ธ Production domain not yet reachable (DNS/SSL may still be propagating)" - name: Quantum Integrity Shield check continue-on-error: true run: | HETZNER_HOST="${{ secrets.HETZNER_HOST || secrets.SSH_HOST || '204.168.230.142' }}" - VERCEL_URL='${{ needs.deploy-vercel.outputs.deploy_url }}' QIS_STATUS="" - # Try Hetzner first (port 80 via Nginx, fallback to 3000) + # Check Hetzner (port 80 via Nginx, fallback to 3000) if [ -n "$HETZNER_HOST" ]; then QIS_STATUS=$(curl -s --max-time 15 "http://${HETZNER_HOST}/api/quantum-integrity/status" || \ curl -s --max-time 15 "http://${HETZNER_HOST}:3000/api/quantum-integrity/status" || echo "") fi - # Try Vercel if Hetzner didn't respond - if [ -z "$QIS_STATUS" ] && [ -n "$VERCEL_URL" ]; then - QIS_STATUS=$(curl -s --max-time 15 "${VERCEL_URL}/api/quantum-integrity/status" || echo "") - fi if [ -z "$QIS_STATUS" ]; then echo "โ„น๏ธ Quantum Integrity Shield endpoint not reachable โ€” skipping check." diff --git a/UNICORN_FINAL/scripts/nginx-unicorn.conf b/UNICORN_FINAL/scripts/nginx-unicorn.conf index b423c792..c159e4c7 100644 --- a/UNICORN_FINAL/scripts/nginx-unicorn.conf +++ b/UNICORN_FINAL/scripts/nginx-unicorn.conf @@ -11,6 +11,11 @@ map $http_upgrade $connection_upgrade { "" close; } +# Rate limiting zones +limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m; +limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/m; +limit_conn_zone $binary_remote_addr zone=conn_limit:10m; + # Redirect HTTP โ†’ HTTPS (certbot va adauga blocul HTTPS la prima rulare) server { listen 80; @@ -20,13 +25,25 @@ server { # Permite certbot ACME challenge location /.well-known/acme-challenge/ { root /var/www/certbot; + default_type "text/plain"; } # Compresie gzip pentru raspunsuri text/JSON gzip on; - gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + gzip_vary on; + gzip_proxied any; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; gzip_min_length 256; + # Limita conexiuni simultane per IP + limit_conn conn_limit 50; + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + # SSE (Server-Sent Events) โ€“ dezactiveaza buffering-ul si extinde timeout-ul location /stream { proxy_pass http://127.0.0.1:3000; @@ -44,6 +61,47 @@ server { chunked_transfer_encoding on; } + # Auth endpoints โ€“ stricter rate limiting + location ~ ^/api/auth/ { + limit_req zone=auth burst=5 nodelay; + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } + + # API endpoints โ€“ moderate rate limiting + location ~ ^/api/ { + limit_req zone=api burst=20 nodelay; + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Static assets caching (React build) + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp)$ { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_valid 200 7d; + add_header Cache-Control "public, max-age=604800, immutable"; + expires 7d; + } + # API si orice alta ruta โ€“ proxy catre Node pe portul 3000 location / { proxy_pass http://127.0.0.1:3000; @@ -58,6 +116,10 @@ server { proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; + # Buffer settings for better performance + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; } } # Nota: certbot --nginx va adauga automat blocul server pentru HTTPS (port 443) From 0d20dda6d96a64011ecb94d1e644affaf489e4ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:01:39 +0000 Subject: [PATCH 3/3] fix: remove deploy-vercel stub entirely, fix nginx burst/cache review comments Agent-Logs-Url: https://github.com/ruffy80/ZeusAI/sessions/e1ffc6c3-a08e-4082-b80e-d1d535811fbc Co-authored-by: ruffy80 <29306714+ruffy80@users.noreply.github.com> --- .github/workflows/vercel-deploy.yml | 339 +---------------------- UNICORN_FINAL/scripts/nginx-unicorn.conf | 3 +- 2 files changed, 3 insertions(+), 339 deletions(-) diff --git a/.github/workflows/vercel-deploy.yml b/.github/workflows/vercel-deploy.yml index 0ecd69dc..053d551a 100644 --- a/.github/workflows/vercel-deploy.yml +++ b/.github/workflows/vercel-deploy.yml @@ -152,341 +152,6 @@ jobs: - name: Innovation report (pre-deploy snapshot) run: npm run innovation:report || true - # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - # JOB 2: Vercel deploy โ€” DISABLED (platform runs exclusively on GitHub + Hetzner) - # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - deploy-vercel: - name: ๐Ÿšซ Deploy โ†’ Vercel (disabled โ€” Hetzner only) - runs-on: ubuntu-latest - needs: validate - # Vercel deployment is permanently disabled โ€” the platform runs exclusively - # on GitHub Actions + Hetzner. This stub job is kept so downstream jobs - # (deploy-summary, smoke-tests) that reference `needs: deploy-vercel` continue - # to resolve without needing YAML surgery. - if: false - defaults: - run: - working-directory: UNICORN_FINAL - outputs: - deploy_url: ${{ steps.deploy.outputs.deploy_url }} - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Validate Vercel credentials - id: vercel_guard - run: | - # Check if secrets exist (not empty string) - # VERCEL_ORG_ID and VERCEL_PROJECT_ID have known defaults embedded in the codebase. - # Only VERCEL_TOKEN is a true authentication secret that cannot be defaulted. - if [ -z "${{ secrets.VERCEL_TOKEN }}" ]; then - echo "ready=false" >> "$GITHUB_OUTPUT" - echo "โ„น๏ธ Missing VERCEL_TOKEN secret. Skipping Vercel deploy job." - exit 0 - fi - # Validate token with Vercel API to avoid noisy failures in later steps - if ! curl -fsS -H "Authorization: Bearer ${{ secrets.VERCEL_TOKEN }}" https://api.vercel.com/v2/user >/dev/null; then - echo "ready=false" >> "$GITHUB_OUTPUT" - echo "โ„น๏ธ VERCEL_TOKEN is invalid. Skipping Vercel deploy job." - exit 0 - fi - echo "ready=true" >> "$GITHUB_OUTPUT" - echo "โœ… Vercel credentials validated" - - - name: Setup Node 22 - if: steps.vercel_guard.outputs.ready == 'true' - uses: actions/setup-node@v5 - with: - node-version: '22' - - - name: Install Vercel CLI - if: steps.vercel_guard.outputs.ready == 'true' - run: npm i -g vercel@latest - - - name: Resolve & link Vercel project (auto-sync zeusai project ID) - if: steps.vercel_guard.outputs.ready == 'true' - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID || 'team_wes3fQvKjdfOMKXe7f4fFQoL' }} - VERCEL_PROJECT_ID_SECRET: ${{ secrets.VERCEL_PROJECT_ID || 'prj_YNIHsyltyZUV7HQA3VyQhhGDvKD3' }} - run: | - # Query Vercel API for the 'zeusai' project to get the authoritative project ID - TEAM_PARAM="" - [ -n "$VERCEL_ORG_ID" ] && TEAM_PARAM="?teamId=${VERCEL_ORG_ID}" - RESOLVED_ID=$(curl -fsS \ - -H "Authorization: Bearer ${VERCEL_TOKEN}" \ - "https://api.vercel.com/v9/projects/zeusai${TEAM_PARAM}" \ - 2>/dev/null \ - | jq -r '.id // empty' 2>/dev/null || echo "") - if [ -n "$RESOLVED_ID" ]; then - echo "โœ… Resolved zeusai Vercel project ID: ${RESOLVED_ID}" - PROJECT_ID="$RESOLVED_ID" - else - # Fallback: try old project name 'zeusai-rust' (legacy project slug) - echo "โ„น๏ธ zeusai project not found โ€“ trying legacy name zeusai-rust..." - RESOLVED_ID=$(curl -fsS \ - -H "Authorization: Bearer ${VERCEL_TOKEN}" \ - "https://api.vercel.com/v9/projects/zeusai-rust${TEAM_PARAM}" \ - 2>/dev/null \ - | jq -r '.id // empty' 2>/dev/null || echo "") - if [ -n "$RESOLVED_ID" ]; then - echo "โœ… Resolved legacy zeusai-rust project ID: ${RESOLVED_ID}" - PROJECT_ID="$RESOLVED_ID" - else - echo "โ„น๏ธ Could not resolve project from Vercel API โ€“ falling back to VERCEL_PROJECT_ID secret" - PROJECT_ID="$VERCEL_PROJECT_ID_SECRET" - fi - fi - mkdir -p .vercel - cat > .vercel/project.json <> "$GITHUB_ENV" - echo "๐Ÿ”— Linked to Vercel project: ${PROJECT_ID}" - - - name: Rename Vercel project to 'zeusai' (fix zeusai-rust.vercel.app URL) - if: steps.vercel_guard.outputs.ready == 'true' - continue-on-error: true - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID || secrets.VERCEL_ORG_ID || 'team_wes3fQvKjdfOMKXe7f4fFQoL' }} - VERCEL_PROJECT_ID: ${{ env.RESOLVED_VERCEL_PROJECT_ID || secrets.VERCEL_PROJECT_ID || 'prj_YNIHsyltyZUV7HQA3VyQhhGDvKD3' }} - run: | - TEAM_PARAM="" - [ -n "$VERCEL_TEAM_ID" ] && TEAM_PARAM="?teamId=${VERCEL_TEAM_ID}" - # Get current project name - CURRENT_NAME=$(curl -fsS \ - -H "Authorization: Bearer ${VERCEL_TOKEN}" \ - "https://api.vercel.com/v9/projects/${VERCEL_PROJECT_ID}${TEAM_PARAM}" \ - 2>/dev/null | jq -r '.name // empty' 2>/dev/null || echo "") - echo "Current Vercel project name: '${CURRENT_NAME}'" - if [ "$CURRENT_NAME" = "zeusai" ]; then - echo "โœ… Project already named 'zeusai' โ€“ no rename needed" - elif [ -n "$CURRENT_NAME" ]; then - HTTP=$(curl -s -o /dev/null -w "%{http_code}" \ - -X PATCH \ - "https://api.vercel.com/v9/projects/${VERCEL_PROJECT_ID}${TEAM_PARAM}" \ - -H "Authorization: Bearer ${VERCEL_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"name":"zeusai"}') - if [ "$HTTP" = "200" ]; then - echo "โœ… Vercel project renamed from '${CURRENT_NAME}' to 'zeusai' โ€“ URL will be zeusai.vercel.app" - else - echo "โš ๏ธ Could not rename project (HTTP ${HTTP}) โ€“ project may already have the name or a conflict exists" - fi - else - echo "โš ๏ธ Could not determine current project name โ€“ skipping rename" - fi - - - name: Pull Vercel environment - if: steps.vercel_guard.outputs.ready == 'true' - continue-on-error: true - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - run: vercel pull --yes --environment=production - - - name: Build - if: steps.vercel_guard.outputs.ready == 'true' - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - CI: "false" - run: vercel build --prod - - - name: Deploy to Vercel Production - if: steps.vercel_guard.outputs.ready == 'true' - id: deploy - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - run: | - DEPLOY_OUTPUT=$(vercel deploy --prebuilt --prod 2>&1) || DEPLOY_EXIT=$? - # Gracefully handle the free-plan 100-deployments/day rate limit. - # The existing production deployment continues to serve traffic normally. - if echo "$DEPLOY_OUTPUT" | grep -q 'api-deployments-free-per-day'; then - echo "โš ๏ธ Vercel daily deployment limit reached (100/day on free plan)." - echo " The current production deployment remains active and serving traffic." - echo " Skipping โ€“ will retry on next push." - exit 0 - fi - if [ "${DEPLOY_EXIT:-0}" -ne 0 ]; then - echo "โŒ Vercel deploy failed: $DEPLOY_OUTPUT" - exit 1 - fi - URL=$(echo "$DEPLOY_OUTPUT" | grep -oE 'https://[a-zA-Z0-9._-]+\.vercel\.app' | tail -1) - echo "deploy_url=$URL" >> "$GITHUB_OUTPUT" - echo "โœ… Vercel deploy: $URL" - - - name: Ensure Vercel custom domains (optional) - if: steps.vercel_guard.outputs.ready == 'true' - continue-on-error: true - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_PROJECT_ID: ${{ env.RESOLVED_VERCEL_PROJECT_ID || secrets.VERCEL_PROJECT_ID || 'prj_YNIHsyltyZUV7HQA3VyQhhGDvKD3' }} - VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID || secrets.VERCEL_ORG_ID || 'team_wes3fQvKjdfOMKXe7f4fFQoL' }} - SITE_DOMAIN: ${{ secrets.SITE_DOMAIN || 'zeusai.pro' }} - UNICORN_DOMAIN: ${{ secrets.UNICORN_DOMAIN || 'www.zeusai.pro' }} - run: | - attach_domain () { - local domain="$1" - [ -z "$domain" ] && return 0 - echo "๐ŸŒ Ensuring domain: $domain" - curl --silent --show-error --fail \ - -X POST "https://api.vercel.com/v10/projects/${VERCEL_PROJECT_ID}/domains?teamId=${VERCEL_TEAM_ID}" \ - -H "Authorization: Bearer ${VERCEL_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"name\":\"${domain}\"}" \ - || echo "โ„น๏ธ Could not attach domain now (may already exist or DNS verification pending): ${domain}" - } - - attach_domain "$SITE_DOMAIN" - attach_domain "$UNICORN_DOMAIN" - - - name: Alias production deploy โ†’ zeusai.pro - if: steps.vercel_guard.outputs.ready == 'true' && steps.deploy.outputs.deploy_url != '' - continue-on-error: true - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID || secrets.VERCEL_ORG_ID || 'team_wes3fQvKjdfOMKXe7f4fFQoL' }} - DEPLOY_URL: ${{ steps.deploy.outputs.deploy_url }} - SITE_DOMAIN: ${{ secrets.SITE_DOMAIN || 'zeusai.pro' }} - run: | - # Set zeusai.pro as production alias so it points to the latest deployment - TEAM_PARAM="" - [ -n "$VERCEL_TEAM_ID" ] && TEAM_PARAM="--scope ${VERCEL_TEAM_ID}" - echo "๐Ÿ”— Aliasing ${DEPLOY_URL} โ†’ ${SITE_DOMAIN}" - vercel alias set "${DEPLOY_URL}" "${SITE_DOMAIN}" ${TEAM_PARAM} --token "${VERCEL_TOKEN}" \ - && echo "โœ… zeusai.pro now points to ${DEPLOY_URL}" \ - || echo "โ„น๏ธ Alias command failed (DNS may not be verified yet; domain is still attached as target)" - # Also alias www. subdomain - WWW_DOMAIN="www.${SITE_DOMAIN}" - vercel alias set "${DEPLOY_URL}" "${WWW_DOMAIN}" ${TEAM_PARAM} --token "${VERCEL_TOKEN}" \ - && echo "โœ… ${WWW_DOMAIN} aliased" \ - || echo "โ„น๏ธ www alias pending DNS verification" - - - name: Update GitHub repo homepage โ†’ https://zeusai.pro - if: steps.vercel_guard.outputs.ready == 'true' - continue-on-error: true - env: - GH_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} - run: | - echo "๐Ÿ  Setting GitHub repo homepage to https://zeusai.pro..." - HTTP=$(curl -s -o /dev/null -w "%{http_code}" \ - -X PATCH "https://api.github.com/repos/ruffy80/ZeusAI" \ - -H "Authorization: Bearer ${GH_TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - -H "Content-Type: application/json" \ - -d '{"homepage":"https://zeusai.pro","description":"ZeusAI โ€“ Autonomous AI Business Platform"}') - if [ "$HTTP" = "200" ]; then - echo "โœ… GitHub repo homepage updated to https://zeusai.pro (was zeusai-rust.vercel.app)" - else - echo "โ„น๏ธ Could not update repo homepage (HTTP ${HTTP}) โ€“ GH_PAT may need repo scope" - fi - - - name: Disable Vercel Password Protection - if: steps.vercel_guard.outputs.ready == 'true' - continue-on-error: true - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_PROJECT_ID: ${{ env.RESOLVED_VERCEL_PROJECT_ID || secrets.VERCEL_PROJECT_ID || 'prj_YNIHsyltyZUV7HQA3VyQhhGDvKD3' }} - VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID || secrets.VERCEL_ORG_ID || 'team_wes3fQvKjdfOMKXe7f4fFQoL' }} - run: | - echo "๐Ÿ”“ Disabling Vercel Password Protection..." - curl --silent --show-error \ - -X PATCH "https://api.vercel.com/v9/projects/${VERCEL_PROJECT_ID}?teamId=${VERCEL_TEAM_ID}" \ - -H "Authorization: Bearer ${VERCEL_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"passwordProtection": null, "ssoProtection": null}' \ - && echo "โœ… Password protection disabled" \ - || echo "โ„น๏ธ Could not disable password protection (may already be off)" - - - name: Set BACKEND_API_URL in Vercel environment (proxy /api/* to Hetzner) - if: steps.vercel_guard.outputs.ready == 'true' - continue-on-error: true - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_PROJECT_ID: ${{ env.RESOLVED_VERCEL_PROJECT_ID || secrets.VERCEL_PROJECT_ID || 'prj_YNIHsyltyZUV7HQA3VyQhhGDvKD3' }} - VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID || secrets.VERCEL_ORG_ID || 'team_wes3fQvKjdfOMKXe7f4fFQoL' }} - HETZNER_HOST: ${{ secrets.HETZNER_HOST || secrets.SSH_HOST }} - run: | - if [ -z "$HETZNER_HOST" ]; then - echo "โ„น๏ธ HETZNER_HOST not set โ€” skipping BACKEND_API_URL configuration" - exit 0 - fi - BACKEND_URL="http://${HETZNER_HOST}" - TEAM_PARAM="" - [ -n "$VERCEL_TEAM_ID" ] && TEAM_PARAM="?teamId=${VERCEL_TEAM_ID}" - HTTP=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST "https://api.vercel.com/v10/projects/${VERCEL_PROJECT_ID}/env${TEAM_PARAM}" \ - -H "Authorization: Bearer ${VERCEL_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"key\":\"BACKEND_API_URL\",\"value\":\"${BACKEND_URL}\",\"type\":\"plain\",\"target\":[\"production\"],\"upsert\":true}") - if [ "$HTTP" = "200" ] || [ "$HTTP" = "201" ]; then - echo "โœ… BACKEND_API_URL set to ${BACKEND_URL} (Vercel proxies /api/* to Hetzner)" - else - echo "โš ๏ธ Could not set BACKEND_API_URL (HTTP ${HTTP}) โ€” site will load but API calls may fail" - fi - - - name: Smoke test Vercel URL - if: steps.vercel_guard.outputs.ready == 'true' - continue-on-error: true - run: | - sleep 10 - URL="${{ steps.deploy.outputs.deploy_url }}" - if [ -n "$URL" ]; then - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL/health" || echo "000") - echo "Vercel /health โ†’ HTTP $STATUS" - [ "$STATUS" = "200" ] && echo "โœ… Vercel /health OK" || echo "โš ๏ธ Could not verify (may need warmup)" - STATUS_ROOT=$(curl -s -o /dev/null -w "%{http_code}" "$URL/" || echo "000") - echo "Vercel / โ†’ HTTP $STATUS_ROOT" - [ "$STATUS_ROOT" = "200" ] && echo "โœ… Vercel / OK" || echo "โš ๏ธ Root path not serving (frontend may be broken)" - fi - - - name: Smoke test production Vercel domain - if: steps.vercel_guard.outputs.ready == 'true' - continue-on-error: true - run: | - PROD_URL="${{ env.VERCEL_PROD_URL }}" - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$PROD_URL/health" || echo "000") - echo "Production $PROD_URL/health โ†’ HTTP $STATUS" - [ "$STATUS" = "200" ] && echo "โœ… Production domain OK" || echo "โ„น๏ธ Production domain not yet reachable (DNS propagation may be pending)" - STATUS_ROOT=$(curl -s -o /dev/null -w "%{http_code}" "$PROD_URL/" || echo "000") - echo "Production $PROD_URL/ โ†’ HTTP $STATUS_ROOT" - [ "$STATUS_ROOT" = "200" ] && echo "โœ… Production root OK" || echo "โš ๏ธ Production root not serving" - - - name: Delete old unicorn-final Vercel project (cleanup) - if: steps.vercel_guard.outputs.ready == 'true' - continue-on-error: true - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID || secrets.VERCEL_ORG_ID || 'team_wes3fQvKjdfOMKXe7f4fFQoL' }} - run: | - echo "๐Ÿ—‘๏ธ Looking up and removing old 'unicorn-final' Vercel project..." - TEAM_PARAM="" - [ -n "$VERCEL_TEAM_ID" ] && TEAM_PARAM="?teamId=${VERCEL_TEAM_ID}" - # Find project ID by name - OLD_PROJECT_ID=$(curl -fsS \ - -H "Authorization: Bearer ${VERCEL_TOKEN}" \ - "https://api.vercel.com/v9/projects/unicorn-final${TEAM_PARAM}" \ - 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || echo "") - if [ -z "$OLD_PROJECT_ID" ]; then - echo "โ„น๏ธ Project 'unicorn-final' not found or already deleted โ€“ nothing to do." - else - HTTP=$(curl -s -o /dev/null -w "%{http_code}" \ - -X DELETE \ - -H "Authorization: Bearer ${VERCEL_TOKEN}" \ - "https://api.vercel.com/v9/projects/${OLD_PROJECT_ID}${TEAM_PARAM}") - if [ "$HTTP" = "204" ] || [ "$HTTP" = "200" ]; then - echo "โœ… Vercel project 'unicorn-final' (${OLD_PROJECT_ID}) deleted successfully." - else - echo "โš ๏ธ Could not delete 'unicorn-final' (HTTP ${HTTP}). It may have already been removed." - fi - fi - - # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - # JOB: Configure DNS records for zeusai.pro (idempotent, non-blocking) # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ setup-dns: name: ๐ŸŒ Configure DNS โ†’ zeusai.pro @@ -1118,7 +783,7 @@ jobs: deploy-summary: name: ๐Ÿ“Š Deploy Summary runs-on: ubuntu-latest - needs: [validate, deploy-vercel, deploy-hetzner] + needs: [validate, deploy-hetzner] if: always() steps: - name: Print summary @@ -1164,7 +829,7 @@ jobs: smoke-tests: name: ๐Ÿงช Post-deploy Smoke Tests runs-on: ubuntu-latest - needs: [deploy-vercel, deploy-hetzner] + needs: [deploy-hetzner] if: always() steps: - name: Check Hetzner health diff --git a/UNICORN_FINAL/scripts/nginx-unicorn.conf b/UNICORN_FINAL/scripts/nginx-unicorn.conf index c159e4c7..e6550cd1 100644 --- a/UNICORN_FINAL/scripts/nginx-unicorn.conf +++ b/UNICORN_FINAL/scripts/nginx-unicorn.conf @@ -63,7 +63,7 @@ server { # Auth endpoints โ€“ stricter rate limiting location ~ ^/api/auth/ { - limit_req zone=auth burst=5 nodelay; + limit_req zone=auth burst=15 nodelay; proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; @@ -97,7 +97,6 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_valid 200 7d; add_header Cache-Control "public, max-age=604800, immutable"; expires 7d; }