-
Notifications
You must be signed in to change notification settings - Fork 2
176 lines (161 loc) · 7.2 KB
/
Copy pathdeploy-app.yml
File metadata and controls
176 lines (161 loc) · 7.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
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