Files
Filamenteka/.gitea/workflows/deploy.yml
DaX b24a1fea27
Some checks failed
Deploy / tag-deploy (push) Has been cancelled
Deploy / deploy-frontend (push) Has been cancelled
Deploy / detect (push) Successful in 4s
Deploy / deploy-api (push) Failing after 28s
Diagnostic: find node binary path and working directory from running process
2026-03-05 01:59:18 +01:00

289 lines
11 KiB
YAML

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: |
cat > /tmp/migrate-params.json << 'PARAMS'
{"commands":["NODE_PID=$(pgrep -f 'node server.js' | head -1) && echo Node PID: $NODE_PID && ls -la /proc/$NODE_PID/exe && ls -la /proc/$NODE_PID/cwd && cat /proc/$NODE_PID/environ | tr '\\0' '\\n' | grep -E 'PATH|HOME|NODE' || echo 'no node process found'","systemctl cat node-api 2>/dev/null || systemctl list-units --type=service | grep -i node || echo 'no node service'","ls -la /home/ec2-user/*.js /home/ec2-user/*.json 2>/dev/null || echo 'no js in ec2-user home'"]}
PARAMS
CMD_ID=$(aws ssm send-command \
--region $AWS_REGION \
--instance-ids "$INSTANCE_ID" \
--document-name "AWS-RunShellScript" \
--parameters file:///tmp/migrate-params.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: |
cat > /tmp/deploy-params.json << 'PARAMS'
{"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"]}
PARAMS
CMD_ID=$(aws ssm send-command \
--region $AWS_REGION \
--instance-ids "$INSTANCE_ID" \
--document-name "AWS-RunShellScript" \
--parameters file:///tmp/deploy-params.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..."
cat > /tmp/rollback-params.json << 'PARAMS'
{"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"]}
PARAMS
aws ssm send-command \
--region $AWS_REGION \
--instance-ids "$INSTANCE_ID" \
--document-name "AWS-RunShellScript" \
--parameters file:///tmp/rollback-params.json \
--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"