-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeploy.sh
More file actions
executable file
·303 lines (264 loc) · 14 KB
/
Copy pathdeploy.sh
File metadata and controls
executable file
·303 lines (264 loc) · 14 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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
#!/usr/bin/env bash
#
# 오늘폼 — 배포.
#
# ./deploy.sh 검사 → 배포 → 배포 후 확인 → 되돌릴 지점 기록
# ./deploy.sh --check 검사만. 아무것도 배포하지 않습니다.
# ./deploy.sh --only hosting,functions 배포 대상을 직접 지정
#
# 왜 firebase deploy 를 직접 안 쓰고 이걸 쓰는가:
#
# CLAUDE.md 에 배포 전 확인할 것이 적혀 있는데, 세 번 다 까먹어서 사고가 났습니다.
# 적어두는 걸로는 안 잡힙니다. 여기서는 까먹으면 배포가 멈춥니다.
#
# 그리고 firebase deploy 는 "성공" 이라고 말한 뒤에도 사이트가 하얗게 뜰 수 있습니다.
# 업로드에 성공한 것과 화면이 뜨는 것은 다른 일입니다. 마지막 단계에서 실제
# 배포 주소를 받아와서 확인합니다.
#
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"
SITE="https://onform.web.app"
CHECK_ONLY=0
FORCE_TARGETS=""
while [ $# -gt 0 ]; do
case "$1" in
--check) CHECK_ONLY=1 ;;
--only) shift; FORCE_TARGETS="${1:-}" ;;
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
*) echo "모르는 옵션: $1"; exit 2 ;;
esac
shift
done
bold() { printf '\n\033[1m%s\033[0m\n' "$1"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
bad() { printf ' \033[1;31m✗\033[0m %s\n' "$1"; }
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; }
hr() { printf '\033[90m%s\033[0m\n' "──────────────────────────────────────────────────────────"; }
STOP=0
stop() { bad "$1"; STOP=1; }
# ── 0. 선수 폼 파일 다시 굽기 ────────────────────────────────────────
#
# public/players.json 은 손으로 굽는 정적 파일입니다. 굽지 않으면 낡습니다.
#
# ⚠️ 실제로 이틀 동안 낡아 있었습니다. 그 사이 표가 168개 쌓였는데 파일에는
# 전부 0 으로 박혀 있어서, 들어온 사람은 선수 화면에서
# "아직 아무도 평점을 받지 않았습니다" 만 봤습니다. 데이터는 있었는데요.
#
# ⚠️ **1번(되돌릴 지점)보다 먼저** 굽습니다. 뒤에서 구우면 "작업 폴더 깨끗함"
# 을 확인한 다음에 파일이 바뀌어서, 기록에 없는 것이 배포됩니다. 여기서
# 구우면 바뀐 게 1번의 dirty 검사에 걸려서 커밋하고 가게 됩니다.
#
# 실패해도 배포를 막지 않습니다 — 옛 파일이 나갈 뿐 사이트가 깨지진 않습니다.
bold "0) 선수 폼 파일"
if [ "$CHECK_ONLY" = "1" ]; then
ok "검사만 — 배포할 때 굽습니다"
elif PL="$(python3 tools/build_players.py 2>&1 | tail -1)"; then
ok "${PL:-players.json 갱신}"
else
warn "선수 폼 파일을 굽지 못했습니다. 옛 파일이 그대로 나갑니다"
fi
# ⚠️ updatedAt 은 구울 때마다 바뀝니다. 내용이 같은데도 파일이 달라지니까
# 1번(되돌릴 지점)이 매번 걸리고, 커밋해도 다음 실행에서 또 걸립니다.
# 즉 배포가 스스로를 영원히 막습니다 — 실제로 그래서 한 번 못 나갔습니다.
# 내용이 그대로면 되돌립니다. 진짜로 바뀌었으면 그대로 두고 1번이 잡습니다.
if ! git diff --quiet -- public/players.json 2>/dev/null; then
if python3 - <<'SAME'
import json, subprocess, sys
old = subprocess.run(['git', 'show', 'HEAD:public/players.json'],
capture_output=True, text=True).stdout
try:
a, b = json.loads(old), json.load(open('public/players.json'))
except Exception:
sys.exit(1)
a.pop('updatedAt', None)
b.pop('updatedAt', None)
sys.exit(0 if a == b else 1)
SAME
then
git checkout -- public/players.json
ok "내용은 그대로 — updatedAt 만 바뀌어서 되돌렸습니다"
fi
fi
# ── 1. 되돌릴 수 있는 상태인가 ───────────────────────────────────────
#
# 배포 자체보다 먼저 봅니다. 뒤에서 무슨 일이 나든, 돌아올 곳이 있어야 합니다.
bold "1) 되돌릴 지점"
if ! git rev-parse --git-dir >/dev/null 2>&1; then
stop "git 저장소가 아닙니다. 배포 후 문제가 생겨도 되돌릴 방법이 없습니다."
else
DIRTY="$(git status --porcelain)"
if [ -n "$DIRTY" ]; then
warn "아직 저장 안 된 변경이 있습니다:"
git status --short | sed 's/^/ /'
echo ""
warn "지금 배포하면 '무엇을 배포했는지' 가 기록에 안 남습니다."
warn "먼저 저장하세요: git add -A && git commit -m \"무엇을 했는지\""
STOP=1
else
ok "작업 폴더 깨끗함 — 지금 상태가 그대로 기록돼 있습니다"
fi
LAST_TAG="$(git tag -l 'live-*' --sort=-creatordate | head -1)"
[ -n "$LAST_TAG" ] && ok "마지막 배포 지점: $LAST_TAG"
fi
# ── 2. CSP 스크립트 해시 ─────────────────────────────────────────────
#
# index.html 의 <script> 를 한 글자만 고쳐도 어긋납니다.
# 어긋난 채로 배포하면 사이트가 하얗게 뜹니다. 이미 두 번 그랬습니다.
bold "2) CSP 스크립트 해시"
if python3 security/csp.py >/tmp/onform-csp.$$ 2>&1; then
ok "$(tail -1 /tmp/onform-csp.$$ | cut -c1-90)"
else
cat /tmp/onform-csp.$$ | sed 's/^/ /'
stop "해시가 어긋났습니다. 이대로 배포하면 화면이 하얗게 뜹니다."
warn "고치기: python3 security/csp.py --write"
fi
rm -f /tmp/onform-csp.$$
# ── 3. config.js ─────────────────────────────────────────────────────
#
# 2026-08-08: 테스트 reCAPTCHA 키가 그대로 나가서 방문자가 24시간씩 잠겼습니다.
# TRAFFIC_URL: index.html 이 찾는 이름이 없어서 모듈이 통째로 죽었습니다.
# 둘 다 형식은 멀쩡했습니다. 값이 제자리에 있는지는 따로 봐야 합니다.
bold "3) config.js — 임시값·빠진 값"
if python3 security/scan.py --only release --min high --fail-on high --no-color \
>/tmp/onform-rel.$$ 2>&1; then
ok "임시값 없음, index.html 이 찾는 이름 전부 있음"
else
sed -n '/CRITICAL\|HIGH\|왜:\|기대:\|│/p' /tmp/onform-rel.$$ | sed 's/^/ /'
stop "config.js 를 이대로 배포하면 안 됩니다."
fi
rm -f /tmp/onform-rel.$$
# ── 4. 무엇을 같이 배포해야 하는가 ───────────────────────────────────
#
# hosting · rules · functions 는 서로 물려 있습니다. 한 쪽만 올리면
# 나머지와 짝이 안 맞아서 서비스가 깨집니다 (CLAUDE.md 의 표).
# 그래서 "바뀐 것" 이 아니라 "같이 올려야 하는 것" 을 계산합니다.
bold "4) 배포 대상"
changed_since() { # $1 = 경로 패턴들
[ -z "${LAST_TAG:-}" ] && { echo "yes"; return; }
if git diff --quiet "$LAST_TAG" -- "$@" 2>/dev/null; then echo ""; else echo "yes"; fi
}
H="$(changed_since public firebase.json)"
R="$(changed_since firestore.rules)"
# ⚠️ 색인도 firebase deploy 의 기본 대상이 아닙니다 — `firestore:rules` 는
# 규칙만 올립니다. 2026-08-24 까지 이 줄이 firestore.indexes.json 을
# R 에 묶어두고 있어서, 색인을 고쳐도 **규칙만 올라갔습니다.**
# 색인이 없으면 collectionGroup 질의가 '인덱스 없음' 으로 죽습니다
# (지금은 커뮤니티 글의 베스트 댓글 미리보기가 거기 걸립니다).
# 그리고 색인은 만들어지는 데 시간이 걸려서 **먼저** 올려야 합니다.
I="$(changed_since firestore.indexes.json)"
F="$(changed_since functions/index.js functions/package.json)"
# ⚠️ 경기 동기화 스케줄 함수는 **별도 코드베이스**입니다(파이썬).
# hosting·rules·functions 셋과 달리 클라이언트와 짝이 맞아야 하는 관계가
# 아니라서, storage 처럼 바뀌었을 때만 붙입니다.
# ⚠️ 다만 `--only functions` 는 두 코드베이스를 **모두** 올립니다. 그래서
# 아래에서는 `functions:default` / `functions:sync` 로 나눠 부릅니다.
Y="$(changed_since functions-sync)"
# ⚠️ storage 는 firebase deploy 의 기본 대상이 **아닙니다.**
# 2026-08-23 실제로 놓쳤습니다 — 커뮤니티 이미지 업로드를 붙이면서
# storage.rules 를 새로 썼는데, hosting/rules/functions 만 올라가서
# 저장소는 며칠 전 규칙(= 설정 안 됨)으로 남아 있었습니다.
# 그 상태로 업로드를 열면 아무나 아무 파일이나 올릴 수 있습니다.
S="$(changed_since storage.rules)"
N=0
[ -n "$H" ] && { echo " hosting 바뀜"; N=$((N+1)); }
[ -n "$R" ] && { echo " rules 바뀜"; N=$((N+1)); }
[ -n "$I" ] && { echo " indexes 바뀜"; N=$((N+1)); }
[ -n "$F" ] && { echo " functions 바뀜"; N=$((N+1)); }
[ -n "$Y" ] && { echo " sync함수 바뀜"; N=$((N+1)); }
[ -n "$S" ] && { echo " storage 바뀜"; N=$((N+1)); }
if [ -n "$FORCE_TARGETS" ]; then
TARGETS="$FORCE_TARGETS"
warn "대상을 직접 지정했습니다: $TARGETS"
elif [ "$N" = "0" ]; then
ok "마지막 배포 이후 바뀐 게 없습니다"
TARGETS=""
elif [ "$N" = "1" ]; then
T=""; [ -n "$H" ] && T="hosting"; [ -n "$R" ] && T="firestore:rules"
[ -n "$F" ] && T="functions:default"
[ -n "$Y" ] && T="functions:sync"
[ -n "$S" ] && T="storage"
[ -n "$I" ] && T="firestore:indexes"
TARGETS="$T"
ok "한 곳만 바뀌었습니다 → $TARGETS 만 올립니다"
else
# storage 는 나머지 셋과 물려 있지 않아서(짝이 안 맞아 깨지는 관계가 아님)
# 바뀌었을 때만 붙입니다. 다만 **빠뜨리면 안 됩니다** — 위 주석 참고.
TARGETS="hosting,firestore:rules,functions:default"
[ -n "$Y" ] && TARGETS="$TARGETS,functions:sync"
[ -n "$S" ] && TARGETS="$TARGETS,storage"
[ -n "$I" ] && TARGETS="firestore:indexes,$TARGETS"
warn "두 곳 이상 바뀌었습니다 → 같이 올립니다: $TARGETS"
warn "(한 쪽만 올리면 짝이 안 맞아서 한마디 작성이나 로그인이 통째로 실패합니다)"
fi
# ── 검사 결과 ────────────────────────────────────────────────────────
hr
if [ "$STOP" = "1" ]; then
printf '\n\033[1;31m배포를 멈췄습니다.\033[0m 위의 ✗ 를 먼저 해결하세요.\n\n'
exit 1
fi
printf '\n\033[1;32m검사 통과.\033[0m\n'
if [ "$CHECK_ONLY" = "1" ]; then
echo "(--check 이므로 여기서 끝냅니다. 배포하지 않았습니다.)"
exit 0
fi
if [ -z "$TARGETS" ]; then
echo "올릴 것이 없습니다."
exit 0
fi
# ── 5. 배포 ──────────────────────────────────────────────────────────
bold "5) 배포 — $TARGETS"
echo ""
read -r -p " 진행할까요? [y/N] " ANS
case "$ANS" in y|Y|yes) ;; *) echo " 취소했습니다."; exit 0 ;; esac
if ! firebase deploy --only "$TARGETS"; then
printf '\n\033[1;31m배포가 실패했습니다.\033[0m 사이트는 이전 상태 그대로입니다.\n\n'
exit 1
fi
# ── 6. 배포 후 — 진짜로 화면이 뜨는가 ────────────────────────────────
#
# firebase 가 "성공" 이라고 해도 방문자 화면은 하얄 수 있습니다.
# 배포한 파일을 실제 주소에서 다시 받아와서 확인합니다.
bold "6) 배포 후 확인 — $SITE"
sleep 3
POST=0
CODE="$(curl -s -o /tmp/onform-idx.$$ -w '%{http_code}' "$SITE/" || echo 000)"
if [ "$CODE" = "200" ]; then ok "첫 화면 응답 $CODE"; else bad "첫 화면 응답 $CODE"; POST=1; fi
# 살아 있는 주소에서 CSP 를 봅니다. 헤더가 있는지만이 아니라, **배포된
# index.html 의 인라인 스크립트 해시가 헤더에 실제로 들어 있는지**까지.
# 그게 하얀 화면의 조건 그 자체입니다.
#
# ⚠️ 예전에는 여기서 `grep -q "Content-Security-Policy"` 를 했습니다. HTTP/2 는
# 헤더 이름을 소문자로 내려서 이 검사는 **한 번도 통과한 적이 없습니다** —
# 배포할 때마다 "헤더가 없습니다" 라고 했고, 그래서 되돌릴 지점(태그)도
# 매번 안 남았습니다. 거짓 경보는 진짜 경보를 가립니다.
if CSPOUT="$(python3 security/csp.py --live "$SITE/" 2>&1)"; then
ok "$CSPOUT"
else
bad "$CSPOUT"; POST=1
fi
# 배포된 index.html 이 찾는 이름을, 배포된 config.js 가 실제로 갖고 있는가.
# 캐시 때문에 둘의 버전이 어긋나는 게 하얀 화면의 단골 원인입니다.
curl -s "$SITE/config.js" -o /tmp/onform-cfg.$$
MISSING=""
for NAME in $(grep -o 'import {[^}]*} from "/config.js"' /tmp/onform-idx.$$ \
| tr ',{}' '\n' | sed 's/import//;s/from.*//' | tr -d ' "' | grep -v '^$'); do
grep -q "export const $NAME\|export .*\b$NAME\b" /tmp/onform-cfg.$$ || MISSING="$MISSING $NAME"
done
if [ -n "$MISSING" ]; then
bad "배포된 config.js 에 없는 이름:$MISSING → 화면이 하얗게 뜹니다"; POST=1
else
ok "index.html 과 config.js 짝이 맞음"
fi
rm -f /tmp/onform-idx.$$ /tmp/onform-cfg.$$
hr
if [ "$POST" = "1" ]; then
printf '\n\033[1;31m배포는 됐지만 사이트에 문제가 있습니다.\033[0m\n'
printf '되돌리기: git checkout %s -- . && ./deploy.sh\n\n' "${LAST_TAG:-<지난 태그>}"
exit 1
fi
# ── 7. 되돌아올 지점 기록 ────────────────────────────────────────────
TAG="live-$(date +%Y-%m-%d-%H%M)"
git tag -a "$TAG" -m "배포: $TARGETS" && ok "되돌아올 지점 기록: $TAG"
printf '\n\033[1;32m배포 완료.\033[0m %s\n' "$SITE"
printf '문제가 생기면: git checkout %s -- . && ./deploy.sh\n\n' "${LAST_TAG:-$TAG}"