Replace complex Gitea API + python parsing in SSM with simple repo archive download and extraction. Inline hardcoded paths in SSM commands instead of shell variable expansion through single quotes to avoid quoting issues.
298 lines
10 KiB
YAML
298 lines
10 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: |
|
|
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() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
|
|
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"
|