name: Deploy on: push: branches: [main] env: AWS_REGION: eu-central-1 S3_BUCKET: filamenteka-frontend INSTANCE_ID: i-03956ecf32292d7d9 NEXT_PUBLIC_API_URL: https://api.filamenteka.rs/api NEXT_PUBLIC_MATOMO_URL: https://analytics.demirix.dev NEXT_PUBLIC_MATOMO_SITE_ID: "7" jobs: # ── Job 1: Change Detection ────────────────────────────────────────── detect: runs-on: ubuntu-latest outputs: frontend: ${{ steps.changes.outputs.frontend }} api: ${{ steps.changes.outputs.api }} migrations: ${{ steps.changes.outputs.migrations }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Detect changes since last deploy id: changes run: | # Find the latest deploy tag LAST_TAG=$(git tag -l 'deploy-*' --sort=-creatordate | head -n 1) if [ -z "$LAST_TAG" ]; then echo "No deploy tag found — deploying everything" echo "frontend=true" >> $GITHUB_OUTPUT echo "api=true" >> $GITHUB_OUTPUT echo "migrations=true" >> $GITHUB_OUTPUT exit 0 fi echo "Last deploy tag: $LAST_TAG" DIFF_FILES=$(git diff --name-only "$LAST_TAG"..HEAD) echo "Changed files since $LAST_TAG:" echo "$DIFF_FILES" FRONTEND=false API=false MIGRATIONS=false if echo "$DIFF_FILES" | grep -qvE '^(api/|database/)'; then FRONTEND=true fi if echo "$DIFF_FILES" | grep -qE '^api/'; then API=true fi if echo "$DIFF_FILES" | grep -qE '^database/migrations/'; then MIGRATIONS=true fi echo "frontend=$FRONTEND" >> $GITHUB_OUTPUT echo "api=$API" >> $GITHUB_OUTPUT echo "migrations=$MIGRATIONS" >> $GITHUB_OUTPUT echo "Frontend: $FRONTEND | API: $API | Migrations: $MIGRATIONS" # ── Job 2: Frontend Deploy ─────────────────────────────────────────── deploy-frontend: runs-on: ubuntu-latest needs: detect if: needs.detect.outputs.frontend == 'true' steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: Security check & tests run: | npm run security:check npm test -- --passWithNoTests - name: Build Next.js run: npm run build - name: Verify build output run: | if [ ! -d "out" ]; then echo "ERROR: out/ directory not found after build" exit 1 fi echo "Build output verified: $(find out -type f | wc -l) files" - name: Setup AWS run: | pip3 install -q --break-system-packages awscli aws configure set aws_access_key_id "${{ secrets.AWS_ACCESS_KEY_ID }}" aws configure set aws_secret_access_key "${{ secrets.AWS_SECRET_ACCESS_KEY }}" aws configure set region ${{ env.AWS_REGION }} - name: Deploy to S3 & invalidate CloudFront run: | # HTML files — no cache aws s3 sync out/ s3://$S3_BUCKET/ \ --delete \ --exclude "*" \ --include "*.html" \ --cache-control "public, max-age=0, must-revalidate" \ --content-type "text/html" # Next.js assets — immutable aws s3 sync out/_next/ s3://$S3_BUCKET/_next/ \ --cache-control "public, max-age=31536000, immutable" # Everything else — 24h cache aws s3 sync out/ s3://$S3_BUCKET/ \ --exclude "*.html" \ --exclude "_next/*" \ --cache-control "public, max-age=86400" aws cloudfront create-invalidation \ --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \ --paths "/*" # ── Job 3: API & Migrations Deploy ────────────────────────────────── deploy-api: runs-on: ubuntu-latest needs: detect if: needs.detect.outputs.api == 'true' || needs.detect.outputs.migrations == 'true' steps: - name: Setup AWS run: | pip3 install -q --break-system-packages awscli aws configure set aws_access_key_id "${{ secrets.AWS_ACCESS_KEY_ID }}" aws configure set aws_secret_access_key "${{ secrets.AWS_SECRET_ACCESS_KEY }}" aws configure set region ${{ env.AWS_REGION }} - name: Run database migrations if: needs.detect.outputs.migrations == 'true' run: | CMD_ID=$(aws ssm send-command \ --region $AWS_REGION \ --instance-ids "$INSTANCE_ID" \ --document-name "AWS-RunShellScript" \ --parameters 'commands=[ "set -e", "cd /tmp", "curl -sf -o repo.tar.gz https://git.demirix.dev/dax/Filamenteka/archive/main.tar.gz", "tar xzf repo.tar.gz", "cp -r filamenteka/database /home/ubuntu/filamenteka-api/database", "cp filamenteka/api/migrate.js /home/ubuntu/filamenteka-api/api/migrate.js", "rm -rf repo.tar.gz filamenteka", "echo Migration files:", "ls -la /home/ubuntu/filamenteka-api/database/migrations/", "cd /home/ubuntu/filamenteka-api/api && node migrate.js" ]' \ --output json \ --query "Command.CommandId" --output text) echo "Migration SSM command: $CMD_ID" # Poll for completion (max 2 minutes) for i in $(seq 1 24); do sleep 5 STATUS=$(aws ssm get-command-invocation \ --command-id "$CMD_ID" \ --instance-id "$INSTANCE_ID" \ --query "Status" --output text 2>/dev/null || echo "Pending") echo "Attempt $i: $STATUS" if [ "$STATUS" = "Success" ]; then echo "Migrations completed successfully" aws ssm get-command-invocation \ --command-id "$CMD_ID" \ --instance-id "$INSTANCE_ID" \ --query "StandardOutputContent" --output text break elif [ "$STATUS" = "Failed" ] || [ "$STATUS" = "Cancelled" ] || [ "$STATUS" = "TimedOut" ]; then echo "Migration failed with status: $STATUS" aws ssm get-command-invocation \ --command-id "$CMD_ID" \ --instance-id "$INSTANCE_ID" \ --query "StandardErrorContent" --output text exit 1 fi done if [ "$STATUS" != "Success" ]; then echo "Migration timed out after 2 minutes" exit 1 fi - name: Deploy server.js id: deploy run: | CMD_ID=$(aws ssm send-command \ --region $AWS_REGION \ --instance-ids "$INSTANCE_ID" \ --document-name "AWS-RunShellScript" \ --parameters 'commands=[ "set -e", "cd /home/ubuntu/filamenteka-api", "cp server.js server.js.backup", "curl -sf -o server.js https://git.demirix.dev/dax/Filamenteka/raw/branch/main/api/server.js", "sudo systemctl restart node-api", "echo API deployed and restarted" ]' \ --output json \ --query "Command.CommandId" --output text) echo "Deploy SSM command: $CMD_ID" for i in $(seq 1 24); do sleep 5 STATUS=$(aws ssm get-command-invocation \ --command-id "$CMD_ID" \ --instance-id "$INSTANCE_ID" \ --query "Status" --output text 2>/dev/null || echo "Pending") echo "Attempt $i: $STATUS" if [ "$STATUS" = "Success" ]; then echo "API deploy completed" break elif [ "$STATUS" = "Failed" ] || [ "$STATUS" = "Cancelled" ] || [ "$STATUS" = "TimedOut" ]; then echo "API deploy failed with status: $STATUS" aws ssm get-command-invocation \ --command-id "$CMD_ID" \ --instance-id "$INSTANCE_ID" \ --query "StandardErrorContent" --output text exit 1 fi done if [ "$STATUS" != "Success" ]; then echo "API deploy timed out" exit 1 fi - name: Health check id: health run: | echo "Waiting for API to be ready..." for i in $(seq 1 6); do HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://api.filamenteka.rs/api/filaments || echo "000") echo "Health check attempt $i: HTTP $HTTP_CODE" if [ "$HTTP_CODE" = "200" ]; then echo "API is healthy" exit 0 fi sleep 5 done echo "Health check failed after 6 attempts" exit 1 - name: Rollback on failure if: failure() && steps.deploy.outcome == 'success' run: | echo "Rolling back to server.js.backup..." aws ssm send-command \ --region $AWS_REGION \ --instance-ids "$INSTANCE_ID" \ --document-name "AWS-RunShellScript" \ --parameters 'commands=[ "cd /home/ubuntu/filamenteka-api", "if [ -f server.js.backup ]; then cp server.js.backup server.js && sudo systemctl restart node-api && echo Rollback complete; else echo No backup found; fi" ]' \ --output json echo "Rollback command sent" # ── Job 4: Tag Successful Deploy ──────────────────────────────────── tag-deploy: runs-on: ubuntu-latest needs: [detect, deploy-frontend, deploy-api] if: >- always() && needs.detect.result == 'success' && (needs.deploy-frontend.result == 'success' || needs.deploy-frontend.result == 'skipped') && (needs.deploy-api.result == 'success' || needs.deploy-api.result == 'skipped') && !(needs.deploy-frontend.result == 'skipped' && needs.deploy-api.result == 'skipped') steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 1 token: ${{ github.token }} - name: Create deploy tag run: | TAG="deploy-$(date -u +%Y%m%d-%H%M%S)" git tag "$TAG" git push origin "$TAG" echo "Tagged successful deploy: $TAG"