Skip to content

Restrict Android builds to arm64-v8a #189

Restrict Android builds to arm64-v8a

Restrict Android builds to arm64-v8a #189

Workflow file for this run

name: Deploy to VPS
on:
push:
branches: [ "main", "dev" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build_and_deploy:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: pip
cache-dependency-path: requirements.txt
- name: Syntax Check & Smoke Test
env:
APP_ENV: testing
run: |
pip install -r requirements.txt
python3 -m compileall app fetcher.py server.py
python3 -c "from app import create_app; app = create_app(); print(f'✅ Flask loaded with {len(app.url_map._rules)} routes')"
- name: Set Environment Variables based on Branch
run: |
if [ "${{ github.ref_name }}" = "main" ]; then
echo "TAG_PREFIX=sha-" >> $GITHUB_ENV
echo "LATEST_TAG=latest" >> $GITHUB_ENV
else
echo "TAG_PREFIX=dev-sha-" >> $GITHUB_ENV
echo "LATEST_TAG=dev-latest" >> $GITHUB_ENV
fi
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ env.LATEST_TAG }}
type=sha,format=long,prefix=${{ env.TAG_PREFIX }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VITE_COMMIT_HASH=${{ github.sha }}
- name: Deploy to VPS via SSH
uses: appleboy/ssh-action@v1.0.3
env:
GHCR_PAT: ${{ secrets.GHCR_PAT }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
CORS_ORIGINS: ${{ secrets.CORS_ORIGINS }}
TURNSTILE_SITE_KEY: ${{ secrets.TURNSTILE_SITE_KEY }}
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
TUNNEL_TOKEN_PROD: ${{ secrets.TUNNEL_TOKEN }}
TUNNEL_TOKEN_DEV: ${{ secrets.TUNNEL_TOKEN_DEV }}
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USERNAME }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT }}
envs: GHCR_PAT,SECRET_KEY,CORS_ORIGINS,TURNSTILE_SITE_KEY,TURNSTILE_SECRET_KEY,TUNNEL_TOKEN_PROD,TUNNEL_TOKEN_DEV
script: |
# ===== 環境判斷 =====
if [ "${{ github.ref_name }}" = "main" ]; then
APP_SERVICE_NAME="school_grades_app"
TUNNEL_SERVICE_NAME="school_grades_tunnel"
CURRENT_TUNNEL_TOKEN="$TUNNEL_TOKEN_PROD"
IMAGE_TAG_PREFIX="sha-"
else
APP_SERVICE_NAME="app_dev"
TUNNEL_SERVICE_NAME="school_grades_tunnel_dev"
CURRENT_TUNNEL_TOKEN="$TUNNEL_TOKEN_DEV"
IMAGE_TAG_PREFIX="dev-sha-"
fi
TARGET_IMAGE="ghcr.io/${{ github.repository }}:${IMAGE_TAG_PREFIX}${{ github.sha }}"
# ===== Step 1: 登入 GHCR =====
echo "🔑 登入 GHCR..."
echo "$GHCR_PAT" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin
# ===== Step 2: 確保 overlay 網路 =====
if ! docker network inspect app_network > /dev/null 2>&1; then
echo "🌐 建立 overlay 網路 app_network..."
docker network create -d overlay app_network
fi
# ===== Step 3: 確保 Redis 就緒 =====
echo "📦 部署 Redis..."
if docker service inspect redis > /dev/null 2>&1; then
docker service update --image redis:7-alpine redis
else
docker service create \
--name redis \
--network app_network \
redis:7-alpine
fi
echo "⏳ 等待 Redis 就緒..."
for i in $(seq 1 30); do
REDIS_CONTAINER=$(docker ps -q -f name=redis 2>/dev/null | head -1)
if [ -n "$REDIS_CONTAINER" ] && docker exec "$REDIS_CONTAINER" redis-cli ping 2>/dev/null | grep -q PONG; then
echo "✅ Redis 已就緒 (PONG)"
break
fi
if [ "$i" = "30" ]; then
echo "❌ Redis 等待逾時!"
exit 1
fi
sleep 2
done
# ===== Step 4: 建立版本化 Docker Swarm Secrets =====
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
echo "🔐 建立 Swarm Secrets (版本: ${SHORT_SHA})..."
echo "$SECRET_KEY" | docker secret create "SECRET_KEY_${SHORT_SHA}" -
echo "$TURNSTILE_SECRET_KEY" | docker secret create "TURNSTILE_SECRET_KEY_${SHORT_SHA}" -
# ===== Step 5: 部署 App(含 healthcheck + rollback + secrets) =====
echo "🚀 部署 App: $TARGET_IMAGE"
if docker service inspect $APP_SERVICE_NAME > /dev/null 2>&1; then
# 找出目前掛載的 secrets 以便移除
OLD_SECRETS=$(docker service inspect $APP_SERVICE_NAME \
--format '{{range .Spec.TaskTemplate.ContainerSpec.Secrets}}{{.SecretName}} {{end}}' 2>/dev/null || echo "")
SECRET_RM_ARGS=""
for s in $OLD_SECRETS; do
SECRET_RM_ARGS="$SECRET_RM_ARGS --secret-rm $s"
done
# 同時清理可能殘留的 env 版 secrets
docker service update \
--with-registry-auth \
--image "$TARGET_IMAGE" \
--env-rm REDIS_URL \
--env-add REDIS_URL=redis://redis:6379/0 \
--env-rm SECRET_KEY \
--env-rm TURNSTILE_SECRET_KEY \
--env-rm CORS_ORIGINS \
--env-add CORS_ORIGINS="$CORS_ORIGINS" \
--env-rm TURNSTILE_SITE_KEY \
--env-add TURNSTILE_SITE_KEY="$TURNSTILE_SITE_KEY" \
$SECRET_RM_ARGS \
--secret-add source=SECRET_KEY_${SHORT_SHA},target=SECRET_KEY \
--secret-add source=TURNSTILE_SECRET_KEY_${SHORT_SHA},target=TURNSTILE_SECRET_KEY \
--health-cmd "curl -f http://localhost:5000/health || exit 1" \
--health-interval 10s \
--health-timeout 5s \
--health-retries 3 \
--health-start-period 15s \
--update-order start-first \
--update-failure-action rollback \
--rollback-order stop-first \
--rollback-max-failure-ratio 0 \
$APP_SERVICE_NAME
# 清理舊版 secrets
for s in $OLD_SECRETS; do
docker secret rm "$s" 2>/dev/null || true
done
else
docker service create \
--with-registry-auth \
--name $APP_SERVICE_NAME \
--network app_network \
--env REDIS_URL=redis://redis:6379/0 \
--env CORS_ORIGINS="$CORS_ORIGINS" \
--env TURNSTILE_SITE_KEY="$TURNSTILE_SITE_KEY" \
--secret source=SECRET_KEY_${SHORT_SHA},target=SECRET_KEY \
--secret source=TURNSTILE_SECRET_KEY_${SHORT_SHA},target=TURNSTILE_SECRET_KEY \
--health-cmd "curl -f http://localhost:5000/health || exit 1" \
--health-interval 10s \
--health-timeout 5s \
--health-retries 3 \
--health-start-period 15s \
--update-order start-first \
--update-failure-action rollback \
--rollback-order stop-first \
--rollback-max-failure-ratio 0 \
"$TARGET_IMAGE"
fi
# ===== Step 6: 部署 Tunnel =====
if [ -n "$CURRENT_TUNNEL_TOKEN" ]; then
echo "🔗 部署 Cloudflare Tunnel..."
if docker service inspect $TUNNEL_SERVICE_NAME > /dev/null 2>&1; then
docker service update \
--env-rm TUNNEL_TOKEN \
--env-add TUNNEL_TOKEN="$CURRENT_TUNNEL_TOKEN" \
$TUNNEL_SERVICE_NAME
docker service update \
--network-add app_network $TUNNEL_SERVICE_NAME > /dev/null 2>&1 || true
else
docker service create \
--name $TUNNEL_SERVICE_NAME \
--network app_network \
--env TUNNEL_TOKEN="$CURRENT_TUNNEL_TOKEN" \
cloudflare/cloudflared:latest tunnel --no-autoupdate run
fi
fi
# ===== Step 7: 部署驗證 =====
echo ""
echo "========================================="
echo " 📋 部署驗證"
echo "========================================="
echo ""
echo "⏳ 等待服務啟動..."
sleep 15
echo "📋 服務總覽:"
docker service ls
echo ""
echo "📋 App 服務任務狀態:"
docker service ps $APP_SERVICE_NAME --no-trunc
echo ""
echo "📋 App 最近日誌:"
docker service logs $APP_SERVICE_NAME --tail 30 --no-task-ids 2>&1 || true
echo ""
echo "🏥 Health Check 驗證..."
HEALTH_OK=false
for i in $(seq 1 6); do
APP_CONTAINER=$(docker ps -q -f "name=${APP_SERVICE_NAME}." -f "status=running" | head -1)
if [ -n "$APP_CONTAINER" ] && docker exec "$APP_CONTAINER" curl -sf http://localhost:5000/health > /dev/null; then
echo ""
HEALTH_OK=true
break
fi
echo " 重試中... ($i/6)"
sleep 5
done
if [ "$HEALTH_OK" = "true" ]; then
echo "✅ 部署驗證通過!"
else
echo ""
echo "❌ Health Check 驗證失敗!"
echo "📋 錯誤日誌:"
docker service logs $APP_SERVICE_NAME --tail 50 --no-task-ids 2>&1 || true
exit 1
fi