Merge pull request #230 from Team-StackUp/fix/ci-playwright-ipv4-retry #202
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: STACK-UP deploy ana-server | |
| # dev 브랜치 업데이트 시 AI / RealTime / Backend(Core) 를 빌드해 self-hosted runner 에서 재시작. | |
| # 상우 원격컴 전용 — 클라우드 이관 시 워크플로우 재설계 필요. | |
| on: | |
| push: | |
| branches: [dev] | |
| paths: | |
| - "ai/**" | |
| - "realtime/**" | |
| - "backend/**" | |
| - "frontend/**" | |
| - "docker-compose.yml" | |
| - "infra/**" | |
| - ".github/workflows/deploy-app.yml" | |
| workflow_dispatch: | |
| concurrency: | |
| group: deploy-app-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| deploy: | |
| runs-on: self-hosted | |
| timeout-minutes: 30 | |
| env: | |
| DEPLOY_DIR: /home/stackup/stackup | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Sync repo to deploy dir (preserve .env and data) | |
| run: | | |
| mkdir -p "$DEPLOY_DIR" | |
| rsync -a --delete \ | |
| --exclude='.git' \ | |
| --exclude='.env' \ | |
| --exclude='var/' \ | |
| --exclude='**/__pycache__' \ | |
| --exclude='**/.venv' \ | |
| --exclude='**/node_modules' \ | |
| "$GITHUB_WORKSPACE/" "$DEPLOY_DIR/" | |
| # 5/22 시점 임시 만든 docker-compose.override.yml 은 backend 가 정식 compose 로 들어와서 불필요. | |
| # rsync --delete 가 자동 제거하지만, 안전하게 명시적 제거. | |
| rm -f "$DEPLOY_DIR/docker-compose.override.yml" | |
| - name: Ensure .env exists and inject Secrets | |
| env: | |
| # GitHub Repository Secrets. 비어있으면 application-local.yml / docker-compose.yml 의 dev default 가 적용됨. | |
| LLM_API_KEY: ${{ secrets.LLM_API_KEY }} | |
| JWT_SECRET: ${{ secrets.JWT_SECRET }} | |
| ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }} | |
| CORE_INTERNAL_API_KEY: ${{ secrets.CORE_INTERNAL_API_KEY }} | |
| GITHUB_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }} | |
| GITHUB_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }} | |
| GOOGLE_OAUTH_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_CLIENT_ID }} | |
| GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }} | |
| GOOGLE_OAUTH_REDIRECT_URI: ${{ secrets.GOOGLE_OAUTH_REDIRECT_URI }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }} | |
| run: | | |
| cd "$DEPLOY_DIR" | |
| if [ ! -f .env ]; then | |
| cp .env.example .env | |
| fi | |
| chmod 600 .env | |
| # .env.example 에 추가됐는데 .env 에 없는 키는 default 와 함께 append. | |
| # 이 단계가 없으면 docker-compose 의 ${VAR:-} 가 빈 문자열을 주입해 Spring 의 | |
| # @NotBlank 검증을 깨뜨림 (예: JWT_SECRET, ENCRYPTION_KEY 누락 → backend exit 1). | |
| appended=$(mktemp) | |
| awk -F= ' | |
| NR==FNR && /^[A-Z_]+=/ { existing[$1] = 1; next } | |
| /^[A-Z_]+=/ && !($1 in existing) { print } | |
| ' .env .env.example > "$appended" | |
| if [ -s "$appended" ]; then | |
| echo " sync new keys from .env.example:" | |
| cut -d= -f1 "$appended" | sed 's/^/ + /' | |
| echo "" >> .env | |
| echo "# ---- synced from .env.example by deploy-app.yml ----" >> .env | |
| cat "$appended" >> .env | |
| chmod 600 .env | |
| fi | |
| rm -f "$appended" | |
| # 같은 키를 .env 에 upsert. 값이 비어있으면 skip (기존 값 유지). | |
| upsert_env() { | |
| local key="$1" | |
| local value="$2" | |
| if [ -z "$value" ]; then | |
| echo " skip $key (secret 미설정)" | |
| return 0 | |
| fi | |
| local tmp | |
| tmp=$(mktemp) | |
| awk -v k="$key" -v v="$value" ' | |
| BEGIN { written = 0 } | |
| $0 ~ ("^" k "=") { print k "=" v; written = 1; next } | |
| { print } | |
| END { if (!written) print k "=" v } | |
| ' .env > "$tmp" | |
| mv "$tmp" .env | |
| chmod 600 .env | |
| echo " upserted $key" | |
| } | |
| upsert_env "LLM_API_KEY" "$LLM_API_KEY" | |
| upsert_env "JWT_SECRET" "$JWT_SECRET" | |
| upsert_env "ENCRYPTION_KEY" "$ENCRYPTION_KEY" | |
| upsert_env "CORE_INTERNAL_API_KEY" "$CORE_INTERNAL_API_KEY" | |
| upsert_env "GOOGLE_OAUTH_CLIENT_ID" "$GOOGLE_OAUTH_CLIENT_ID" | |
| upsert_env "GOOGLE_OAUTH_CLIENT_SECRET" "$GOOGLE_OAUTH_CLIENT_SECRET" | |
| upsert_env "GOOGLE_OAUTH_REDIRECT_URI" "$GOOGLE_OAUTH_REDIRECT_URI" | |
| upsert_env "GITHUB_OAUTH_CLIENT_ID" "$GITHUB_OAUTH_CLIENT_ID" | |
| upsert_env "GITHUB_OAUTH_CLIENT_SECRET" "$GITHUB_OAUTH_CLIENT_SECRET" | |
| upsert_env "GITHUB_OAUTH_REDIRECT_URI" "https://stack-up.shop/auth/callback" | |
| upsert_env "OPENAI_API_KEY" "$OPENAI_API_KEY" | |
| upsert_env "DEEPGRAM_API_KEY" "$DEEPGRAM_API_KEY" | |
| # STT_PROVIDER 자동 선택. auto 로 두면 AI factory 가 보유 키 기준으로 선택 (deepgram > openai_whisper > mock). | |
| # 키가 1개라도 있으면 auto 로 설정해두면 충분 — 명시적 전환 불필요. | |
| if [ -n "$DEEPGRAM_API_KEY" ] || [ -n "$OPENAI_API_KEY" ]; then | |
| upsert_env "STT_PROVIDER" "auto" | |
| fi | |
| - name: Build and restart app services | |
| run: | | |
| cd "$DEPLOY_DIR" | |
| docker compose up -d --build ai realtime backend | |
| - name: Wait for healthy | |
| run: | | |
| cd "$DEPLOY_DIR" | |
| set +e | |
| rc=0 | |
| for svc in ai realtime backend; do | |
| container="stackup-$svc" | |
| ok=0 | |
| # backend 는 Spring 부팅이 길어 36*5=180s 까지 대기. | |
| for i in $(seq 1 36); do | |
| h=$(docker inspect --format '{{ .State.Health.Status }}' "$container" 2>/dev/null || echo "missing") | |
| echo " [$i] $container: $h" | |
| if [ "$h" = "healthy" ]; then ok=1; break; fi | |
| sleep 5 | |
| done | |
| if [ "$ok" -ne 1 ]; then | |
| echo "::error::$container did not become healthy" | |
| docker compose logs --tail=100 "$svc" || true | |
| rc=1 | |
| fi | |
| done | |
| docker compose ps | |
| exit $rc | |
| # 프론트 교체는 백엔드·AI 가 healthy 를 증명한 뒤에만 한다. | |
| # 이전에는 헬스 게이트 앞에 있어서, 새 백엔드가 부팅에 실패해도 새 API 를 전제로 | |
| # 빌드된 프론트가 이미 라이브로 나가 있었다 — 실패한 배포가 "죽은 백엔드 + 새 프론트" | |
| # 라는 어긋난 조합을 남긴다. 이 순서면 실패 시 프론트는 마지막 정상 버전에 머문다. | |
| # (빌드 자체는 위 CI 의 Frontend 잡이 이미 검증했으므로 여기 빌드가 새로 실패할 여지는 작다.) | |
| - name: Build and deploy frontend | |
| env: | |
| VITE_API_BASE_URL: https://stack-up.shop | |
| VITE_REALTIME_BASE_URL: https://stack-up.shop | |
| VITE_SSE_BASE_URL: https://stack-up.shop | |
| run: | | |
| cd "$GITHUB_WORKSPACE/frontend" | |
| npm ci | |
| npm run build | |
| mkdir -p /var/www/stackup/frontend/dist | |
| rsync -a --delete "$GITHUB_WORKSPACE/frontend/dist/" /var/www/stackup/frontend/dist/ | |
| - name: Prune dangling images (best-effort) | |
| if: always() | |
| run: docker image prune -f >/dev/null 2>&1 || true |