project-auto-wizard 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/bin/project-auto-wizard.js +19 -0
  4. package/package.json +36 -0
  5. package/payload/coderabbit.yaml +19 -0
  6. package/payload/config/breaking-changes.json +1 -0
  7. package/payload/config/wizard-prompts.yml +62 -0
  8. package/payload/scripts/changelog_manager.py +726 -0
  9. package/payload/scripts/version_manager.py +617 -0
  10. package/payload/version.yml.template +48 -0
  11. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +303 -0
  12. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +293 -0
  13. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +289 -0
  14. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +192 -0
  15. package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +209 -0
  16. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +591 -0
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +700 -0
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +308 -0
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +992 -0
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +689 -0
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +987 -0
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +471 -0
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml +500 -0
  24. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +185 -0
  25. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +271 -0
  26. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +81 -0
  27. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +2191 -0
  28. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +386 -0
  29. package/payload/workflows/react/PROJECT-REACT-CI.yaml +194 -0
  30. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +255 -0
  31. package/payload/workflows/spring/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +84 -0
  32. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +316 -0
  33. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +58 -0
  34. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +535 -0
  35. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +437 -0
  36. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +2277 -0
  37. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +425 -0
  38. package/src/cli/args.js +95 -0
  39. package/src/cli/help.js +27 -0
  40. package/src/commands/full.js +53 -0
  41. package/src/commands/interactive.js +276 -0
  42. package/src/commands/revert.js +60 -0
  43. package/src/commands/version.js +31 -0
  44. package/src/commands/workflows.js +49 -0
  45. package/src/context.js +26 -0
  46. package/src/core/assets.js +47 -0
  47. package/src/core/branches.js +60 -0
  48. package/src/core/branding.js +15 -0
  49. package/src/core/breaking-check.js +65 -0
  50. package/src/core/breaking.js +26 -0
  51. package/src/core/copy/coderabbit.js +26 -0
  52. package/src/core/copy/gitignore.js +55 -0
  53. package/src/core/copy/readme.js +30 -0
  54. package/src/core/copy/simple.js +23 -0
  55. package/src/core/copy/workflows.js +233 -0
  56. package/src/core/detect-fs.js +106 -0
  57. package/src/core/detect.js +62 -0
  58. package/src/core/fsutil.js +46 -0
  59. package/src/core/options-ask.js +99 -0
  60. package/src/core/paths-resolve.js +261 -0
  61. package/src/core/paths.js +16 -0
  62. package/src/core/version-yml.js +190 -0
  63. package/src/core/wizard-env.js +101 -0
  64. package/src/core/wizard-labels.js +107 -0
  65. package/src/index.js +162 -0
  66. package/src/ui/ansi.js +26 -0
  67. package/src/ui/banner.js +29 -0
  68. package/src/ui/env-plan.js +207 -0
  69. package/src/ui/prompts.js +99 -0
  70. package/src/ui/readline-engine.js +257 -0
  71. package/src/ui/status-cards.js +56 -0
  72. package/src/ui/summary.js +127 -0
@@ -0,0 +1,437 @@
1
+ # ===================================================================
2
+ # Spring Boot 무중단 배포 (Traefik Blue-Green, SSH + Docker)
3
+ # ===================================================================
4
+ #
5
+ # deploy 브랜치 푸시 시 Docker 이미지를 빌드하고
6
+ # Traefik 라벨 기반 Blue-Green 토글로 서버(SSH 접속 가능한 호스트)에 무중단 배포합니다.
7
+ #
8
+ # 본 워크플로우는 무중단 배포 옵션입니다.
9
+ # 기본 배포는 PROJECT-SPRING-SIMPLE-CICD.yaml 입니다.
10
+ # 전환하려면 아래 push.branches 주석 해제 + SIMPLE-CICD 의 push 트리거 주석 처리.
11
+ #
12
+ # ===================================================================
13
+ # 🔑 필수 GitHub Secrets
14
+ # ===================================================================
15
+ # APPLICATION_PROD_YML (선택): application-prod.yml 내용
16
+ # DOCKERHUB_USERNAME: DockerHub 사용자명
17
+ # DOCKERHUB_TOKEN: DockerHub 액세스 토큰
18
+ # SERVER_HOST: 배포 서버 주소
19
+ # SERVER_USER: SSH 사용자명
20
+ # SERVER_PASSWORD: SSH 비밀번호 (SSH_AUTH_METHOD=password일 때)
21
+ # SSH_KEY: SSH 개인키 .pem 내용 (SSH_AUTH_METHOD=key일 때, AWS EC2 등)
22
+ # ===================================================================
23
+ #
24
+ # 🔧 환경변수 설정 (env 섹션에서 설정)
25
+ # ===================================================================
26
+ #
27
+ # 🌐 포트 및 컨테이너:
28
+ # PROJECT_NAME: 프로젝트명 (컨테이너/이미지/Traefik router 이름)
29
+ # SERVICE_DOMAIN: 외부 진입 도메인
30
+ # CONTAINER_INTERNAL_PORT: Spring Boot 내부 포트 (기본: 8080)
31
+ #
32
+ # 🚦 Traefik:
33
+ # TRAEFIK_NETWORK: Traefik docker 네트워크 (기본: traefik-network)
34
+ # TRAEFIK_ENTRYPOINT: Traefik entrypoint 이름 (기본: web)
35
+ # TRAEFIK_INTERNAL_PORT: NAS 호스트의 Traefik 진입 포트 (기본: 8079)
36
+ # EXTRA_NETWORKS: 추가 docker 네트워크 (공백 구분, 예: selenium-chrome-network)
37
+ #
38
+ # 📦 볼륨/스토리지:
39
+ # ENABLE_VOLUME_MOUNT: 볼륨 마운트 여부 (true/false)
40
+ # VOLUME_HOST_PATH: 호스트 볼륨 경로
41
+ # VOLUME_CONTAINER_PATH: 컨테이너 내부 마운트 경로
42
+ #
43
+ # 🔌 SSH 연결:
44
+ # SSH_PORT: SSH 포트 (기본: 2022)
45
+ # SSH_COMMAND_TIMEOUT: 명령 타임아웃 (기본: 600s)
46
+ # SSH_CONNECTION_TIMEOUT: 연결 타임아웃 (기본: 60s)
47
+ #
48
+ # ⏱️ 헬스체크:
49
+ # HEALTHCHECK_PATH: Traefik 통한 GET 호출 path (기본: /)
50
+ # HEALTHCHECK_ACCEPT_CODES: 통과 인정 HTTP 코드 (| 구분, 기본: 200|301|302|308)
51
+ # HEALTHCHECK_MAX_RETRIES: 최대 시도 횟수 (기본: 36)
52
+ # HEALTHCHECK_RETRY_INTERVAL: 재시도 간격 초 (기본: 5)
53
+ # IN_FLIGHT_WAIT: old 컨테이너 제거 전 대기 초 (기본: 10)
54
+ #
55
+ # 💡 사전 조건:
56
+ # - 배포 서버에 Traefik 컨테이너가 traefik-network 에 띄워져 있어야 함
57
+ # - 역방향 프록시: ${SERVICE_DOMAIN}:443 → localhost:${TRAEFIK_INTERNAL_PORT}
58
+ # (예: Synology DSM 역방향 프록시, Nginx, Caddy 등)
59
+ # ===================================================================
60
+
61
+ name: PROJECT-SPRING-NONSTOP-TRAEFIK-CICD
62
+
63
+ # ===================================================================
64
+ # 트리거 설정
65
+ # ===================================================================
66
+ # deploy 브랜치 push 자동 트리거는 주석 처리되어 있습니다.
67
+ # 본 워크플로우로 전환하려면 아래 push.branches 주석을 해제하세요.
68
+ on:
69
+ # push:
70
+ # branches:
71
+ # - {{MAIN_BRANCH}}
72
+ workflow_dispatch:
73
+
74
+ # ===================================================================
75
+ # 환경 변수 설정
76
+ # ===================================================================
77
+ env:
78
+ # 🔧 프로젝트 설정 - 실제 프로젝트명으로 변경하세요
79
+ PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
80
+ SERVICE_DOMAIN: "__SERVICE_DOMAIN__" # @wizard ask:example.com
81
+
82
+ # ☁️ Spring Boot 설정
83
+ SPRING_PROFILE: "prod"
84
+ JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:21
85
+ GRADLE_OPTS: "-Dspring.profiles.active=prod"
86
+
87
+ # 📁 경로 설정
88
+ APPLICATION_YML_DIR: "__APPLICATION_YML_DIR__" # @wizard auto:spring-app-yml-dir
89
+ DOCKERFILE_PATH: "./Dockerfile"
90
+
91
+ # 🌐 포트 및 컨테이너 설정
92
+ CONTAINER_INTERNAL_PORT: "8080"
93
+
94
+ # 🚦 Traefik 설정 (서버의 Traefik 환경 구축 후 수정 금지)
95
+ TRAEFIK_NETWORK: "traefik-network"
96
+ TRAEFIK_ENTRYPOINT: "web"
97
+ TRAEFIK_INTERNAL_PORT: "8079"
98
+ EXTRA_NETWORKS: ""
99
+
100
+ # 📦 볼륨 마운트 설정
101
+ ENABLE_VOLUME_MOUNT: "false"
102
+ VOLUME_HOST_PATH: "/volume1/projects/__PROJECT_NAME__" # @wizard ask:/volume1/projects/__PROJECT_NAME__
103
+ VOLUME_CONTAINER_PATH: "/mnt/__PROJECT_NAME__" # @wizard ask:/mnt/__PROJECT_NAME__
104
+
105
+ # 🔐 SSH 인증 방식: "password"(Synology·일반 서버) | "key"(AWS EC2·.pem)
106
+ # password → SERVER_PASSWORD secret 사용
107
+ # key → SSH_KEY secret 사용 (.pem 파일 전체 내용을 secret 값으로)
108
+ SSH_AUTH_METHOD: "password" # @wizard ask:password
109
+
110
+ # 🔌 SSH 연결 설정
111
+ SSH_PORT: "2022"
112
+ SSH_COMMAND_TIMEOUT: "600s"
113
+ SSH_CONNECTION_TIMEOUT: "60s"
114
+
115
+ # ⏱️ 헬스체크 설정
116
+ HEALTHCHECK_PATH: "/"
117
+ HEALTHCHECK_ACCEPT_CODES: "200|301|302|308"
118
+ HEALTHCHECK_MAX_RETRIES: "36"
119
+ HEALTHCHECK_RETRY_INTERVAL: "5"
120
+ IN_FLIGHT_WAIT: "10"
121
+
122
+ jobs:
123
+ # ===================================================================
124
+ # 빌드 작업
125
+ # ===================================================================
126
+ build:
127
+ name: Spring Boot 애플리케이션 빌드
128
+ runs-on: ubuntu-latest
129
+
130
+ steps:
131
+ - name: 코드 체크아웃
132
+ uses: actions/checkout@v5
133
+
134
+ - name: Java 설정
135
+ uses: actions/setup-java@v4
136
+ with:
137
+ java-version: ${{ env.JAVA_VERSION }}
138
+ distribution: 'temurin'
139
+ cache: 'gradle'
140
+
141
+ - name: Gradle Wrapper 실행권한 부여
142
+ run: chmod +x gradlew
143
+
144
+ - name: application-prod.yml 생성
145
+ run: |
146
+ mkdir -p ${{ env.APPLICATION_YML_DIR }}
147
+ cat << 'EOF' > ./${{ env.APPLICATION_YML_DIR }}/application-prod.yml
148
+ ${{ secrets.APPLICATION_PROD_YML }}
149
+ EOF
150
+ echo "✅ application-prod.yml 파일 생성 완료 (${{ env.APPLICATION_YML_DIR }})"
151
+
152
+ - name: Build with Gradle
153
+ run: ./gradlew clean build -x test ${{ env.GRADLE_OPTS }}
154
+
155
+ - name: Docker 빌드환경 설정
156
+ uses: docker/setup-buildx-action@v3
157
+
158
+ - name: DockerHub 로그인
159
+ uses: docker/login-action@v3
160
+ with:
161
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
162
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
163
+
164
+ - name: Cache Docker layers
165
+ uses: actions/cache@v4
166
+ with:
167
+ path: /tmp/.buildx-cache
168
+ key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }}
169
+ restore-keys: |
170
+ ${{ runner.os }}-buildx-
171
+
172
+ - name: Docker 이미지 빌드 및 푸시
173
+ uses: docker/build-push-action@v5
174
+ with:
175
+ context: .
176
+ file: ${{ env.DOCKERFILE_PATH }}
177
+ push: true
178
+ tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}:${{ github.ref_name }}
179
+ cache-from: type=local,src=/tmp/.buildx-cache
180
+ cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
181
+
182
+ - name: Move Docker cache
183
+ run: |
184
+ rm -rf /tmp/.buildx-cache
185
+ mv /tmp/.buildx-cache-new /tmp/.buildx-cache
186
+
187
+ # ===================================================================
188
+ # 배포 작업 (Blue-Green 무중단)
189
+ # ===================================================================
190
+ deploy:
191
+ name: Blue-Green 무중단 배포
192
+ needs: build
193
+ runs-on: ubuntu-latest
194
+
195
+ steps:
196
+ - name: Deploy
197
+ uses: appleboy/ssh-action@v1.0.3
198
+ env:
199
+ PROJECT_NAME: ${{ env.PROJECT_NAME }}
200
+ SERVICE_DOMAIN: ${{ env.SERVICE_DOMAIN }}
201
+ CONTAINER_INTERNAL_PORT: ${{ env.CONTAINER_INTERNAL_PORT }}
202
+ SPRING_PROFILE: ${{ env.SPRING_PROFILE }}
203
+ ENABLE_VOLUME_MOUNT: ${{ env.ENABLE_VOLUME_MOUNT }}
204
+ VOLUME_HOST_PATH: ${{ env.VOLUME_HOST_PATH }}
205
+ VOLUME_CONTAINER_PATH: ${{ env.VOLUME_CONTAINER_PATH }}
206
+ EXTRA_NETWORKS: ${{ env.EXTRA_NETWORKS }}
207
+ TRAEFIK_NETWORK: ${{ env.TRAEFIK_NETWORK }}
208
+ TRAEFIK_ENTRYPOINT: ${{ env.TRAEFIK_ENTRYPOINT }}
209
+ TRAEFIK_INTERNAL_PORT: ${{ env.TRAEFIK_INTERNAL_PORT }}
210
+ HEALTHCHECK_PATH: ${{ env.HEALTHCHECK_PATH }}
211
+ HEALTHCHECK_ACCEPT_CODES: ${{ env.HEALTHCHECK_ACCEPT_CODES }}
212
+ HEALTHCHECK_MAX_RETRIES: ${{ env.HEALTHCHECK_MAX_RETRIES }}
213
+ HEALTHCHECK_RETRY_INTERVAL: ${{ env.HEALTHCHECK_RETRY_INTERVAL }}
214
+ IN_FLIGHT_WAIT: ${{ env.IN_FLIGHT_WAIT }}
215
+ BRANCH_REF: ${{ github.ref_name }}
216
+ DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
217
+ SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
218
+ SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
219
+ with:
220
+ host: ${{ secrets.SERVER_HOST }}
221
+ username: ${{ secrets.SERVER_USER }}
222
+ password: ${{ secrets.SERVER_PASSWORD }}
223
+ key: ${{ secrets.SSH_KEY }}
224
+ port: ${{ env.SSH_PORT }}
225
+ command_timeout: ${{ env.SSH_COMMAND_TIMEOUT }}
226
+ timeout: ${{ env.SSH_CONNECTION_TIMEOUT }}
227
+ envs: PROJECT_NAME,SERVICE_DOMAIN,CONTAINER_INTERNAL_PORT,SPRING_PROFILE,ENABLE_VOLUME_MOUNT,VOLUME_HOST_PATH,VOLUME_CONTAINER_PATH,EXTRA_NETWORKS,TRAEFIK_NETWORK,TRAEFIK_ENTRYPOINT,TRAEFIK_INTERNAL_PORT,HEALTHCHECK_PATH,HEALTHCHECK_ACCEPT_CODES,HEALTHCHECK_MAX_RETRIES,HEALTHCHECK_RETRY_INTERVAL,IN_FLIGHT_WAIT,BRANCH_REF,DOCKERHUB_USERNAME,SERVER_PASSWORD,SSH_AUTH_METHOD
228
+ script: |
229
+ set -e
230
+ export PATH=$PATH:/usr/local/bin
231
+ export PW="${SERVER_PASSWORD}"
232
+
233
+ # 🔐 SSH 인증 방식에 따른 sudo 추상화
234
+ SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
235
+ if [ "${SSH_AUTH_METHOD}" = "key" ]; then
236
+ SUDO() { sudo "$@"; }
237
+ else
238
+ SUDO() { echo "$PW" | sudo -S "$@"; }
239
+ fi
240
+ echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
241
+
242
+ IMAGE="${DOCKERHUB_USERNAME}/${PROJECT_NAME}:${BRANCH_REF}"
243
+ ROUTER_NAME="${PROJECT_NAME}"
244
+
245
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
246
+ echo "🚀 Blue-Green 무중단 배포 시작"
247
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
248
+ echo "📦 프로젝트: ${PROJECT_NAME}"
249
+ echo "🌐 도메인: ${SERVICE_DOMAIN}"
250
+ echo "🖼️ 이미지: ${IMAGE}"
251
+ echo "🔧 Traefik 진입: localhost:${TRAEFIK_INTERNAL_PORT} (entrypoint=${TRAEFIK_ENTRYPOINT})"
252
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
253
+
254
+ # ============================================================
255
+ # 1. 최신 이미지 Pull
256
+ # ============================================================
257
+ echo "📥 이미지 Pull: ${IMAGE}"
258
+ SUDO docker pull "${IMAGE}"
259
+
260
+ # ============================================================
261
+ # 2. 현재 active 컨테이너 색 판별 (Traefik 라벨 부착 여부 기준)
262
+ # ============================================================
263
+ ACTIVE=$(SUDO docker ps \
264
+ --filter "label=traefik.http.routers.${ROUTER_NAME}.rule" \
265
+ --filter "name=${PROJECT_NAME}-" \
266
+ --format "{{.Names}}" | head -1 || echo "")
267
+
268
+ if [ "$ACTIVE" = "${PROJECT_NAME}-blue" ]; then
269
+ NEW_COLOR="green"
270
+ OLD_COLOR="blue"
271
+ elif [ "$ACTIVE" = "${PROJECT_NAME}-green" ]; then
272
+ NEW_COLOR="blue"
273
+ OLD_COLOR="green"
274
+ else
275
+ # 최초 배포 또는 active 없음 → blue 부터 시작
276
+ NEW_COLOR="blue"
277
+ OLD_COLOR="green"
278
+ fi
279
+
280
+ NEW_NAME="${PROJECT_NAME}-${NEW_COLOR}"
281
+ OLD_NAME="${PROJECT_NAME}-${OLD_COLOR}"
282
+
283
+ echo "🎨 active=${ACTIVE:-none} → new=${NEW_NAME}, old=${OLD_NAME}"
284
+
285
+ # ============================================================
286
+ # 3. 동일 슬롯(idle 쪽) 잔존 컨테이너 정리
287
+ # ============================================================
288
+ echo "🗑️ idle 슬롯 정리: ${NEW_NAME}"
289
+ SUDO docker rm -f "${NEW_NAME}" 2>/dev/null || true
290
+
291
+ # ============================================================
292
+ # 4. 볼륨 마운트 옵션 구성
293
+ # ============================================================
294
+ VOLUME_MOUNT_OPTION="-v /etc/localtime:/etc/localtime:ro"
295
+ if [ "${ENABLE_VOLUME_MOUNT}" = "true" ]; then
296
+ if [ ! -d "${VOLUME_HOST_PATH}" ]; then
297
+ echo "📁 볼륨 마운트 디렉토리 생성: ${VOLUME_HOST_PATH}"
298
+ SUDO mkdir -p "${VOLUME_HOST_PATH}"
299
+ fi
300
+ VOLUME_MOUNT_OPTION="${VOLUME_MOUNT_OPTION} -v ${VOLUME_HOST_PATH}:${VOLUME_CONTAINER_PATH}"
301
+ echo "📦 볼륨 마운트 활성화: ${VOLUME_HOST_PATH} → ${VOLUME_CONTAINER_PATH}"
302
+ else
303
+ echo "ℹ️ 볼륨 마운트 비활성화됨"
304
+ fi
305
+
306
+ # ============================================================
307
+ # 5. 새 컨테이너 기동 (Traefik 라벨 포함)
308
+ # ============================================================
309
+ echo "🐳 새 컨테이너 기동: ${NEW_NAME}"
310
+ SUDO docker run -d \
311
+ --name "${NEW_NAME}" \
312
+ --network "${TRAEFIK_NETWORK}" \
313
+ --label "traefik.enable=true" \
314
+ --label "traefik.http.routers.${ROUTER_NAME}.rule=Host(\`${SERVICE_DOMAIN}\`)" \
315
+ --label "traefik.http.routers.${ROUTER_NAME}.entrypoints=${TRAEFIK_ENTRYPOINT}" \
316
+ --label "traefik.http.services.${ROUTER_NAME}.loadbalancer.server.port=${CONTAINER_INTERNAL_PORT}" \
317
+ -e TZ=Asia/Seoul \
318
+ -e "SPRING_PROFILES_ACTIVE=${SPRING_PROFILE}" \
319
+ ${VOLUME_MOUNT_OPTION} \
320
+ "${IMAGE}"
321
+
322
+ # ============================================================
323
+ # 6. 추가 네트워크 연결 (EXTRA_NETWORKS, 공백 구분)
324
+ # ============================================================
325
+ if [ -n "${EXTRA_NETWORKS}" ]; then
326
+ for net in ${EXTRA_NETWORKS}; do
327
+ echo "🔗 추가 네트워크 연결: ${net}"
328
+ SUDO docker network connect "${net}" "${NEW_NAME}" 2>/dev/null || true
329
+ done
330
+ fi
331
+
332
+ # ============================================================
333
+ # 7. Traefik 라벨 디버그 출력 (escape 검증용)
334
+ # ============================================================
335
+ echo "🔍 Traefik 라벨 확인:"
336
+ SUDO docker inspect "${NEW_NAME}" --format '{{range $k, $v := .Config.Labels}} {{$k}} = {{$v}}{{"\n"}}{{end}}' | grep traefik || true
337
+
338
+ # ============================================================
339
+ # 8. 헬스체크 — Traefik 통한 라우팅 확인
340
+ # ============================================================
341
+ MAX_WAIT=$((HEALTHCHECK_MAX_RETRIES * HEALTHCHECK_RETRY_INTERVAL))
342
+ echo "⏳ 헬스체크 시작 (최대 ${MAX_WAIT}초, ${HEALTHCHECK_MAX_RETRIES}회 × ${HEALTHCHECK_RETRY_INTERVAL}초)"
343
+
344
+ RETRY_COUNT=0
345
+ HEALTHCHECK_SUCCESS=false
346
+
347
+ while [ $RETRY_COUNT -lt $HEALTHCHECK_MAX_RETRIES ]; do
348
+ sleep ${HEALTHCHECK_RETRY_INTERVAL}
349
+ RETRY_COUNT=$((RETRY_COUNT + 1))
350
+
351
+ STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${NEW_NAME}" 2>/dev/null || echo "not_found")
352
+
353
+ if [ "$STATUS" = "exited" ]; then
354
+ echo "❌ 컨테이너 비정상 종료 (exited). 로그 200줄:"
355
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
356
+ SUDO docker logs --tail 200 "${NEW_NAME}"
357
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
358
+ echo "ℹ️ 컨테이너 ${NEW_NAME} 유지 (자동 삭제 안 함) — 수동 검증/디버깅"
359
+ echo "ℹ️ old=${OLD_NAME} 존재 시 그대로 유지 → 사실상 자동 롤백"
360
+ exit 1
361
+ fi
362
+
363
+ if [ "$STATUS" = "running" ]; then
364
+ HTTP_CODE=$(curl -sm 5 -o /dev/null -w "%{http_code}" \
365
+ -H "Host: ${SERVICE_DOMAIN}" \
366
+ "http://localhost:${TRAEFIK_INTERNAL_PORT}${HEALTHCHECK_PATH}" 2>/dev/null || echo "000")
367
+
368
+ if echo "$HTTP_CODE" | grep -Eq "^(${HEALTHCHECK_ACCEPT_CODES})$"; then
369
+ echo "✅ 헬스체크 통과 (${RETRY_COUNT}/${HEALTHCHECK_MAX_RETRIES}) HTTP=${HTTP_CODE}"
370
+ HEALTHCHECK_SUCCESS=true
371
+ break
372
+ fi
373
+ fi
374
+
375
+ echo "⏳ 대기 (${RETRY_COUNT}/${HEALTHCHECK_MAX_RETRIES}) status=${STATUS} http=${HTTP_CODE:-?}"
376
+ done
377
+
378
+ if [ "$HEALTHCHECK_SUCCESS" = "false" ]; then
379
+ echo "❌ 헬스체크 타임아웃 (${MAX_WAIT}초)"
380
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
381
+ echo "🔍 ${NEW_NAME} 최근 200줄 로그:"
382
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
383
+ SUDO docker logs --tail 200 "${NEW_NAME}"
384
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
385
+ echo "ℹ️ 컨테이너 ${NEW_NAME} 유지 (자동 삭제 안 함) — 수동 검증/디버깅"
386
+ echo "ℹ️ old=${OLD_NAME} 존재 시 그대로 유지 → 사실상 자동 롤백"
387
+ exit 1
388
+ fi
389
+
390
+ # ============================================================
391
+ # 9. in-flight 처리 + old 컨테이너 자동 제거
392
+ # ============================================================
393
+ if SUDO docker ps -a --format '{{.Names}}' | grep -Eq "^${OLD_NAME}$"; then
394
+ echo "⏱️ in-flight 요청 처리 대기 (${IN_FLIGHT_WAIT}초)"
395
+ sleep ${IN_FLIGHT_WAIT}
396
+ echo "🗑️ old=${OLD_NAME} 제거"
397
+ SUDO docker rm -f "${OLD_NAME}"
398
+ else
399
+ echo "ℹ️ old=${OLD_NAME} 없음 — 정리 생략"
400
+ fi
401
+
402
+ # ============================================================
403
+ # 10. dangling 이미지 정리
404
+ # ============================================================
405
+ echo "🧹 dangling(<none>) 이미지 정리..."
406
+ SUDO docker image prune -af
407
+
408
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
409
+ echo "✅ 무중단 배포 완료. active=${NEW_NAME}"
410
+ echo "🌐 도메인: https://${SERVICE_DOMAIN}"
411
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
412
+
413
+ # ===================================================================
414
+ # 📝 사용 방법
415
+ # ===================================================================
416
+ #
417
+ # 1) GitHub Secrets 등록:
418
+ # APPLICATION_PROD_YML, DOCKERHUB_USERNAME, DOCKERHUB_TOKEN,
419
+ # SERVER_HOST, SERVER_USER, SERVER_PASSWORD
420
+ #
421
+ # 2) 본 워크플로우의 env 블록에서 프로젝트별 설정 수정:
422
+ # - PROJECT_NAME, SERVICE_DOMAIN
423
+ # - APPLICATION_YML_DIR, CONTAINER_INTERNAL_PORT
424
+ # - ENABLE_VOLUME_MOUNT, VOLUME_HOST_PATH, VOLUME_CONTAINER_PATH
425
+ # - EXTRA_NETWORKS (예: selenium-chrome-network)
426
+ # - HEALTHCHECK_PATH, HEALTHCHECK_ACCEPT_CODES
427
+ #
428
+ # 3) 서버 사전 준비:
429
+ # - Traefik 컨테이너가 traefik-network 에 띄워져 있어야 함
430
+ # - 역방향 프록시: ${SERVICE_DOMAIN}:443 → localhost:${TRAEFIK_INTERNAL_PORT}
431
+ # (예: Synology DSM 역방향 프록시, Nginx, Caddy 등)
432
+ #
433
+ # 4) 본 워크플로우로 전환:
434
+ # - 본 파일의 push.branches 주석 해제
435
+ # - PROJECT-SPRING-SIMPLE-CICD.yaml 의 push 트리거 주석 처리
436
+ #
437
+ # ===================================================================