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,58 @@
1
+ # ===================================================================
2
+ # Spring Nexus 라이브러리 배포 워크플로우
3
+ # ===================================================================
4
+ #
5
+ # 🔑 필수 GitHub Secrets
6
+ # ===================================================================
7
+ # GRADLE_PROPERTIES: gradle.properties 파일 내용 (Nexus 인증 정보 포함)
8
+ # ===================================================================
9
+
10
+ name: PROJECT-SPRING-NEXUS-PUBLISH
11
+
12
+ on:
13
+ push:
14
+ branches: ["{{MAIN_BRANCH}}"]
15
+ tags:
16
+ - 'v*.*.*'
17
+
18
+ jobs:
19
+ publish:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v5
23
+
24
+ - name: Set up JDK 17
25
+ uses: actions/setup-java@v3
26
+ with:
27
+ java-version: '17'
28
+ distribution: 'temurin'
29
+
30
+ - name: Setup Gradle
31
+ uses: gradle/gradle-build-action@v2
32
+
33
+ - name: Create gradle.properties from secret
34
+ run: |
35
+ echo -e "${{ secrets.GRADLE_PROPERTIES }}" > gradle.properties
36
+
37
+ - name: Get version
38
+ id: get_version
39
+ run: |
40
+ if [[ "${{ github.ref }}" == refs/tags/* ]]; then
41
+ # 태그에서 버전 추출
42
+ TAG_NAME=${GITHUB_REF#refs/tags/}
43
+ VERSION=${TAG_NAME#v}
44
+ echo "🏷️ 태그에서 버전 추출: $VERSION"
45
+ else
46
+ # {{MAIN_BRANCH}} 브랜치에서 version.yml로부터 버전 추출
47
+ VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
48
+ echo "📄 version.yml에서 버전 추출: $VERSION"
49
+ fi
50
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
51
+ echo "Publishing version $VERSION"
52
+
53
+ - name: Publish to Nexus
54
+ run: |
55
+ VERSION="${{ steps.get_version.outputs.version }}"
56
+ echo "🚀 Publishing version $VERSION to Nexus Repository"
57
+ ./gradlew publish -Pversion=$VERSION
58
+
@@ -0,0 +1,535 @@
1
+ # ===================================================================
2
+ # Spring Boot 무중단 배포 (Nginx Blue-Green, SSH + Docker)
3
+ # ===================================================================
4
+ #
5
+ # deploy 브랜치 푸시 시 Docker 이미지를 빌드하고
6
+ # nginx config 의 proxy_pass 포트 토글로 Blue-Green 무중단 배포합니다.
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: 프로젝트명 (컨테이너/이미지 이름)
29
+ # SERVICE_DOMAIN: 외부 도메인 (nginx server_name 과 일치)
30
+ # CONTAINER_INTERNAL_PORT: Spring Boot 내부 포트 (기본: 8080)
31
+ # BLUE_PORT: Blue 호스트 노출 포트 (기본: 8080)
32
+ # GREEN_PORT: Green 호스트 노출 포트 (기본: 8081)
33
+ #
34
+ # 🔧 Nginx:
35
+ # NGINX_RP_CONF: nginx 역방향 프록시 config 파일 경로
36
+ # NGINX_BACKUP_DIR: nginx config 백업 디렉토리
37
+ # NGINX_BACKUP_KEEP: 백업 보존 개수 (기본: 10)
38
+ #
39
+ # 📦 볼륨/스토리지:
40
+ # ENABLE_VOLUME_MOUNT: 볼륨 마운트 여부 (true/false)
41
+ # VOLUME_HOST_PATH: 호스트 볼륨 경로
42
+ # VOLUME_CONTAINER_PATH: 컨테이너 내부 마운트 경로
43
+ #
44
+ # 🔌 SSH 연결:
45
+ # SSH_PORT: SSH 포트 (기본: 2022)
46
+ # SSH_COMMAND_TIMEOUT: 명령 타임아웃 (기본: 600s)
47
+ # SSH_CONNECTION_TIMEOUT: 연결 타임아웃 (기본: 60s)
48
+ #
49
+ # ⏱️ 헬스체크:
50
+ # HEALTHCHECK_PATH: HTTP 경로 (기본: /actuator/health, fallback: /)
51
+ # HEALTHCHECK_MAX_RETRIES: 컨테이너 헬스체크 최대 횟수 (기본: 120)
52
+ # HEALTHCHECK_RETRY_INTERVAL: 재시도 간격 초 (기본: 1)
53
+ # DOMAIN_CHECK_RETRIES: nginx reload 후 도메인 검증 횟수 (기본: 10)
54
+ # DOMAIN_CHECK_INTERVAL: 도메인 검증 간격 초 (기본: 2)
55
+ # IN_FLIGHT_WAIT: old 컨테이너 제거 전 대기 초 (기본: 5)
56
+ #
57
+ # 💡 사전 조건:
58
+ # - 서버에 nginx 설치 + sites-enabled config 존재
59
+ # - 해당 config 의 server 블록에 다음 라인 존재:
60
+ # server_name ${SERVICE_DOMAIN};
61
+ # proxy_pass http://127.0.0.1:${BLUE_PORT}; # 또는 ${GREEN_PORT}
62
+ # - SSH 사용자가 sudo 로 nginx -t / reload, docker 명령 실행 가능
63
+ # ===================================================================
64
+
65
+ name: PROJECT-SPRING-NONSTOP-NGINX-CICD
66
+
67
+ # ===================================================================
68
+ # 트리거 설정
69
+ # ===================================================================
70
+ # deploy 브랜치 push 자동 트리거는 주석 처리되어 있습니다.
71
+ # 본 워크플로우로 전환하려면 아래 push.branches 주석을 해제하세요.
72
+ on:
73
+ # push:
74
+ # branches:
75
+ # - {{MAIN_BRANCH}}
76
+ workflow_dispatch:
77
+
78
+ # ===================================================================
79
+ # 환경 변수 설정
80
+ # ===================================================================
81
+ env:
82
+ # 🔧 프로젝트 설정 - 실제 프로젝트명으로 변경하세요
83
+ PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
84
+ SERVICE_DOMAIN: "__SERVICE_DOMAIN__" # @wizard ask:example.com
85
+
86
+ # ☁️ Spring Boot 설정
87
+ SPRING_PROFILE: "prod"
88
+ JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:21
89
+ GRADLE_OPTS: "-Dspring.profiles.active=prod"
90
+
91
+ # 📁 경로 설정
92
+ APPLICATION_YML_DIR: "__APPLICATION_YML_DIR__" # @wizard auto:spring-app-yml-dir
93
+ DOCKERFILE_PATH: "./Dockerfile"
94
+
95
+ # 🌐 포트 및 컨테이너 설정
96
+ CONTAINER_INTERNAL_PORT: "8080"
97
+ BLUE_PORT: "8080"
98
+ GREEN_PORT: "8081"
99
+
100
+ # 🔧 Nginx 설정
101
+ NGINX_RP_CONF: "/etc/nginx/sites-enabled/example.conf"
102
+ NGINX_BACKUP_DIR: "/volume1/project/nginx/backups"
103
+ NGINX_BACKUP_KEEP: "10"
104
+
105
+ # 📦 볼륨 마운트 설정
106
+ ENABLE_VOLUME_MOUNT: "false"
107
+ VOLUME_HOST_PATH: "/volume1/projects/__PROJECT_NAME__" # @wizard ask:/volume1/projects/__PROJECT_NAME__
108
+ VOLUME_CONTAINER_PATH: "/app"
109
+
110
+ # 🔐 SSH 인증 방식: "password"(Synology·일반 서버) | "key"(AWS EC2·.pem)
111
+ # password → SERVER_PASSWORD secret 사용
112
+ # key → SSH_KEY secret 사용 (.pem 파일 전체 내용을 secret 값으로)
113
+ SSH_AUTH_METHOD: "password" # @wizard ask:password
114
+
115
+ # 🔌 SSH 연결 설정
116
+ SSH_PORT: "2022"
117
+ SSH_COMMAND_TIMEOUT: "600s"
118
+ SSH_CONNECTION_TIMEOUT: "60s"
119
+
120
+ # ⏱️ 헬스체크 설정
121
+ HEALTHCHECK_PATH: "/actuator/health"
122
+ HEALTHCHECK_MAX_RETRIES: "120"
123
+ HEALTHCHECK_RETRY_INTERVAL: "1"
124
+ DOMAIN_CHECK_RETRIES: "10"
125
+ DOMAIN_CHECK_INTERVAL: "2"
126
+ IN_FLIGHT_WAIT: "5"
127
+
128
+ jobs:
129
+ # ===================================================================
130
+ # 빌드 작업
131
+ # ===================================================================
132
+ build:
133
+ name: Spring Boot 애플리케이션 빌드
134
+ runs-on: ubuntu-latest
135
+
136
+ steps:
137
+ - name: 코드 체크아웃
138
+ uses: actions/checkout@v5
139
+
140
+ - name: Java 설정
141
+ uses: actions/setup-java@v4
142
+ with:
143
+ java-version: ${{ env.JAVA_VERSION }}
144
+ distribution: 'temurin'
145
+ cache: 'gradle'
146
+
147
+ - name: Gradle Wrapper 실행권한 부여
148
+ run: chmod +x gradlew
149
+
150
+ - name: application-prod.yml 생성
151
+ run: |
152
+ mkdir -p ${{ env.APPLICATION_YML_DIR }}
153
+ cat << 'EOF' > ./${{ env.APPLICATION_YML_DIR }}/application-prod.yml
154
+ ${{ secrets.APPLICATION_PROD_YML }}
155
+ EOF
156
+ echo "✅ application-prod.yml 파일 생성 완료 (${{ env.APPLICATION_YML_DIR }})"
157
+
158
+ - name: Build with Gradle
159
+ run: ./gradlew clean build -x test ${{ env.GRADLE_OPTS }}
160
+
161
+ - name: Docker 빌드환경 설정
162
+ uses: docker/setup-buildx-action@v3
163
+
164
+ - name: DockerHub 로그인
165
+ uses: docker/login-action@v3
166
+ with:
167
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
168
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
169
+
170
+ - name: Cache Docker layers
171
+ uses: actions/cache@v4
172
+ with:
173
+ path: /tmp/.buildx-cache
174
+ key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }}
175
+ restore-keys: |
176
+ ${{ runner.os }}-buildx-
177
+
178
+ - name: Docker 이미지 빌드 및 푸시
179
+ uses: docker/build-push-action@v5
180
+ with:
181
+ context: .
182
+ file: ${{ env.DOCKERFILE_PATH }}
183
+ push: true
184
+ tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}:${{ github.ref_name }}
185
+ cache-from: type=local,src=/tmp/.buildx-cache
186
+ cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
187
+
188
+ - name: Move Docker cache
189
+ run: |
190
+ rm -rf /tmp/.buildx-cache
191
+ mv /tmp/.buildx-cache-new /tmp/.buildx-cache
192
+
193
+ # ===================================================================
194
+ # 배포 작업 (Blue-Green 무중단)
195
+ # ===================================================================
196
+ deploy:
197
+ name: Blue-Green 무중단 배포
198
+ needs: build
199
+ runs-on: ubuntu-latest
200
+
201
+ steps:
202
+ - name: Deploy
203
+ uses: appleboy/ssh-action@v1.0.3
204
+ env:
205
+ PROJECT_NAME: ${{ env.PROJECT_NAME }}
206
+ SERVICE_DOMAIN: ${{ env.SERVICE_DOMAIN }}
207
+ SPRING_PROFILE: ${{ env.SPRING_PROFILE }}
208
+ CONTAINER_INTERNAL_PORT: ${{ env.CONTAINER_INTERNAL_PORT }}
209
+ BLUE_PORT: ${{ env.BLUE_PORT }}
210
+ GREEN_PORT: ${{ env.GREEN_PORT }}
211
+ ENABLE_VOLUME_MOUNT: ${{ env.ENABLE_VOLUME_MOUNT }}
212
+ VOLUME_HOST_PATH: ${{ env.VOLUME_HOST_PATH }}
213
+ VOLUME_CONTAINER_PATH: ${{ env.VOLUME_CONTAINER_PATH }}
214
+ NGINX_RP_CONF: ${{ env.NGINX_RP_CONF }}
215
+ NGINX_BACKUP_DIR: ${{ env.NGINX_BACKUP_DIR }}
216
+ NGINX_BACKUP_KEEP: ${{ env.NGINX_BACKUP_KEEP }}
217
+ HEALTHCHECK_MAX_RETRIES: ${{ env.HEALTHCHECK_MAX_RETRIES }}
218
+ HEALTHCHECK_RETRY_INTERVAL: ${{ env.HEALTHCHECK_RETRY_INTERVAL }}
219
+ HEALTHCHECK_PATH: ${{ env.HEALTHCHECK_PATH }}
220
+ DOMAIN_CHECK_RETRIES: ${{ env.DOMAIN_CHECK_RETRIES }}
221
+ DOMAIN_CHECK_INTERVAL: ${{ env.DOMAIN_CHECK_INTERVAL }}
222
+ IN_FLIGHT_WAIT: ${{ env.IN_FLIGHT_WAIT }}
223
+ BRANCH_REF: ${{ github.ref_name }}
224
+ DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
225
+ SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
226
+ SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
227
+ with:
228
+ host: ${{ secrets.SERVER_HOST }}
229
+ username: ${{ secrets.SERVER_USER }}
230
+ password: ${{ secrets.SERVER_PASSWORD }}
231
+ key: ${{ secrets.SSH_KEY }}
232
+ port: ${{ env.SSH_PORT }}
233
+ command_timeout: ${{ env.SSH_COMMAND_TIMEOUT }}
234
+ timeout: ${{ env.SSH_CONNECTION_TIMEOUT }}
235
+ envs: PROJECT_NAME,SERVICE_DOMAIN,SPRING_PROFILE,CONTAINER_INTERNAL_PORT,BLUE_PORT,GREEN_PORT,ENABLE_VOLUME_MOUNT,VOLUME_HOST_PATH,VOLUME_CONTAINER_PATH,NGINX_RP_CONF,NGINX_BACKUP_DIR,NGINX_BACKUP_KEEP,HEALTHCHECK_MAX_RETRIES,HEALTHCHECK_RETRY_INTERVAL,HEALTHCHECK_PATH,DOMAIN_CHECK_RETRIES,DOMAIN_CHECK_INTERVAL,IN_FLIGHT_WAIT,BRANCH_REF,DOCKERHUB_USERNAME,SERVER_PASSWORD,SSH_AUTH_METHOD
236
+ script: |
237
+ set -e
238
+ export PATH=$PATH:/usr/local/bin
239
+ export PW="${SERVER_PASSWORD}"
240
+
241
+ # 🔐 SSH 인증 방식에 따른 sudo 추상화
242
+ SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
243
+ if [ "${SSH_AUTH_METHOD}" = "key" ]; then
244
+ SUDO() { sudo "$@"; }
245
+ else
246
+ SUDO() { echo "$PW" | sudo -S "$@"; }
247
+ fi
248
+ echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
249
+
250
+ IMAGE="${DOCKERHUB_USERNAME}/${PROJECT_NAME}:${BRANCH_REF}"
251
+ BLUE_CONTAINER_NAME="${PROJECT_NAME}-blue"
252
+ GREEN_CONTAINER_NAME="${PROJECT_NAME}-green"
253
+
254
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
255
+ echo "🚀 Nginx Blue-Green 무중단 배포 시작"
256
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
257
+ echo "📦 프로젝트: ${PROJECT_NAME}"
258
+ echo "🌐 도메인: ${SERVICE_DOMAIN}"
259
+ echo "🖼️ 이미지: ${IMAGE}"
260
+ echo "🎨 Blue=${BLUE_PORT} | Green=${GREEN_PORT}"
261
+ echo "🔧 Nginx config: ${NGINX_RP_CONF}"
262
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
263
+
264
+ # ============================================================
265
+ # 1. 최신 이미지 Pull
266
+ # ============================================================
267
+ echo "📥 이미지 Pull: ${IMAGE}"
268
+ SUDO docker pull "${IMAGE}"
269
+
270
+ # ============================================================
271
+ # 2. nginx 설정 백업
272
+ # ============================================================
273
+ echo "📁 nginx 백업 디렉토리 생성: ${NGINX_BACKUP_DIR}"
274
+ SUDO mkdir -p "${NGINX_BACKUP_DIR}"
275
+
276
+ BACKUP_FILE="${NGINX_BACKUP_DIR}/server.ReverseProxy.conf.$(date +%Y%m%d_%H%M%S).bak"
277
+ echo "💾 nginx config 백업: ${BACKUP_FILE}"
278
+ SUDO cp "${NGINX_RP_CONF}" "${BACKUP_FILE}"
279
+
280
+ # ============================================================
281
+ # 3. 현재 활성 포트 판별 (nginx config proxy_pass 파싱)
282
+ # ============================================================
283
+ echo "🔍 nginx config 의 현재 활성 포트 탐색"
284
+ ACTIVE_PORT=$(
285
+ SUDO awk -v dom="${SERVICE_DOMAIN}" '
286
+ BEGIN {
287
+ inserver=0; depth=0; entry_depth=0;
288
+ dom_esc = dom;
289
+ gsub(/[.[\]()+*?^$\\|]/, "\\\\&", dom_esc);
290
+ }
291
+ {
292
+ raw = $0;
293
+ line = raw;
294
+ sub(/#.*/, "", line);
295
+ depth += gsub(/\{/, "{", line);
296
+ depth -= gsub(/\}/, "}", line);
297
+
298
+ if (!inserver && line ~ /^[[:space:]]*server_name[[:space:]]+/) {
299
+ tmp = line;
300
+ sub(/^[[:space:]]*server_name[[:space:]]+/, "", tmp);
301
+ sub(/[[:space:]]*;.*/, "", tmp);
302
+ if (match(tmp, "(^|[[:space:]])" dom_esc "([[:space:]]|$)")) {
303
+ inserver=1; entry_depth=depth;
304
+ }
305
+ }
306
+
307
+ if (inserver && line ~ /proxy_pass[[:space:]]+https?:\/\/(localhost|127\.0\.0\.1):[0-9]{2,5}/) {
308
+ if (match(line, /https?:\/\/(localhost|127\.0\.0\.1):([0-9]{2,5})/, a)) { print a[2]; exit }
309
+ }
310
+
311
+ if (inserver && (depth < entry_depth || (depth==0 && entry_depth==0))) inserver=0;
312
+ }
313
+ ' "${NGINX_RP_CONF}" || true
314
+ )
315
+
316
+ if [ -z "${ACTIVE_PORT}" ]; then
317
+ echo "⚠️ 활성 포트를 찾지 못함 → 기본값(BLUE=${BLUE_PORT})을 활성으로 간주"
318
+ ACTIVE_PORT="${BLUE_PORT}"
319
+ fi
320
+ echo "🎨 현재 활성 포트: ${ACTIVE_PORT}"
321
+
322
+ if [ "${ACTIVE_PORT}" = "${BLUE_PORT}" ]; then
323
+ NEW_PORT="${GREEN_PORT}"
324
+ NEW_CONTAINER_NAME="${GREEN_CONTAINER_NAME}"
325
+ OLD_CONTAINER_NAME="${BLUE_CONTAINER_NAME}"
326
+ else
327
+ NEW_PORT="${BLUE_PORT}"
328
+ NEW_CONTAINER_NAME="${BLUE_CONTAINER_NAME}"
329
+ OLD_CONTAINER_NAME="${GREEN_CONTAINER_NAME}"
330
+ fi
331
+
332
+ echo "🐳 신규 배포 → 포트=${NEW_PORT} 컨테이너=${NEW_CONTAINER_NAME}"
333
+ echo "🗑️ 이전 컨테이너 (제거 예정) = ${OLD_CONTAINER_NAME}"
334
+
335
+ # ============================================================
336
+ # 4. 동일 슬롯(idle 쪽) 잔존 컨테이너 정리
337
+ # ============================================================
338
+ if SUDO docker ps -a --format '{{.Names}}' | grep -Eq "^${NEW_CONTAINER_NAME}\$"; then
339
+ echo "🗑️ idle 슬롯 정리: ${NEW_CONTAINER_NAME}"
340
+ SUDO docker rm -f "${NEW_CONTAINER_NAME}"
341
+ fi
342
+
343
+ # ============================================================
344
+ # 5. 볼륨 마운트 옵션 구성
345
+ # ============================================================
346
+ VOLUME_MOUNT_OPTION="-v /etc/localtime:/etc/localtime:ro"
347
+ if [ "${ENABLE_VOLUME_MOUNT}" = "true" ]; then
348
+ if [ ! -d "${VOLUME_HOST_PATH}" ]; then
349
+ echo "📁 볼륨 마운트 디렉토리 생성: ${VOLUME_HOST_PATH}"
350
+ SUDO mkdir -p "${VOLUME_HOST_PATH}"
351
+ fi
352
+ VOLUME_MOUNT_OPTION="${VOLUME_MOUNT_OPTION} -v ${VOLUME_HOST_PATH}:${VOLUME_CONTAINER_PATH}"
353
+ echo "📦 볼륨 마운트 활성화: ${VOLUME_HOST_PATH} → ${VOLUME_CONTAINER_PATH}"
354
+ else
355
+ echo "ℹ️ 볼륨 마운트 비활성화됨"
356
+ fi
357
+
358
+ # ============================================================
359
+ # 6. 새 컨테이너 기동
360
+ # ============================================================
361
+ echo "🐳 새 컨테이너 기동: ${NEW_CONTAINER_NAME} (포트 ${NEW_PORT})"
362
+ SUDO docker run -d \
363
+ -p ${NEW_PORT}:${CONTAINER_INTERNAL_PORT} \
364
+ --name "${NEW_CONTAINER_NAME}" \
365
+ -e TZ=Asia/Seoul \
366
+ -e "SPRING_PROFILES_ACTIVE=${SPRING_PROFILE}" \
367
+ ${VOLUME_MOUNT_OPTION} \
368
+ "${IMAGE}"
369
+
370
+ # ============================================================
371
+ # 7. 컨테이너 헬스체크 (로컬 포트 직접 호출)
372
+ # ============================================================
373
+ echo "⏳ 헬스체크 시작 (최대 ${HEALTHCHECK_MAX_RETRIES}회 × ${HEALTHCHECK_RETRY_INTERVAL}초)"
374
+ HEALTHCHECK_SUCCESS=false
375
+ for i in $(seq 1 ${HEALTHCHECK_MAX_RETRIES}); do
376
+ if curl -fsS "http://127.0.0.1:${NEW_PORT}${HEALTHCHECK_PATH}" >/dev/null 2>&1 || \
377
+ curl -fsS "http://127.0.0.1:${NEW_PORT}/" >/dev/null 2>&1 ; then
378
+ echo "✅ 헬스체크 성공 (시도 ${i})"
379
+ HEALTHCHECK_SUCCESS=true
380
+ break
381
+ fi
382
+ echo "⏳ 헬스체크 진행중... (${i}/${HEALTHCHECK_MAX_RETRIES})"
383
+ sleep ${HEALTHCHECK_RETRY_INTERVAL}
384
+ done
385
+
386
+ if [ "${HEALTHCHECK_SUCCESS}" != "true" ]; then
387
+ echo "❌ 헬스체크 실패 → 배포 중단"
388
+ SUDO docker logs --tail 200 "${NEW_CONTAINER_NAME}" || true
389
+ echo "🗑️ 신규 컨테이너 정리: ${NEW_CONTAINER_NAME}"
390
+ SUDO docker rm -f "${NEW_CONTAINER_NAME}" || true
391
+ exit 1
392
+ fi
393
+
394
+ # ============================================================
395
+ # 8. nginx config 포트 토글 (proxy_pass 포트 치환)
396
+ # ============================================================
397
+ echo "🔧 nginx config 포트 토글: ${ACTIVE_PORT} → ${NEW_PORT}"
398
+ TMP_FILE="$(mktemp)"
399
+ SUDO awk -v dom="${SERVICE_DOMAIN}" -v new_port="${NEW_PORT}" '
400
+ BEGIN {
401
+ inserver=0; depth=0; entry_depth=0;
402
+ dom_esc = dom;
403
+ gsub(/[.[\]()+*?^$\\|]/, "\\\\&", dom_esc);
404
+ }
405
+ {
406
+ if ($0 ~ "server_name[[:space:]]+" dom_esc "[[:space:]]*;") {
407
+ inserver=1;
408
+ entry_depth=depth;
409
+ }
410
+
411
+ line = $0;
412
+ sub(/#.*/, "", line);
413
+ depth += gsub(/\{/, "{", line);
414
+ depth -= gsub(/\}/, "}", line);
415
+
416
+ if (inserver && \
417
+ $0 !~ /^[[:space:]]*#/ && \
418
+ $0 ~ /proxy_pass[[:space:]]+https?:\/\/(localhost|127\.0\.0\.1):[0-9]{2,5}/) {
419
+ sub(/:[0-9]{2,5}/, ":" new_port);
420
+ }
421
+
422
+ print;
423
+
424
+ if (inserver && (depth < entry_depth || (depth == 0 && entry_depth == 0))) {
425
+ inserver=0;
426
+ }
427
+ }
428
+ ' "${NGINX_RP_CONF}" > "${TMP_FILE}"
429
+
430
+ # ============================================================
431
+ # 9. nginx 설정 검증 + reload (실패 시 백업 복구)
432
+ # ============================================================
433
+ echo "🔧 nginx 설정 적용 및 검증"
434
+ SUDO cp "${TMP_FILE}" "${NGINX_RP_CONF}"
435
+ if ! (SUDO nginx -t); then
436
+ echo "❌ nginx 설정 검증 실패 → 백업 파일로 복구"
437
+ SUDO cp "${BACKUP_FILE}" "${NGINX_RP_CONF}"
438
+ SUDO rm -f "${BACKUP_FILE}"
439
+ rm -f "${TMP_FILE}"
440
+ echo "🗑️ 신규 컨테이너 정리: ${NEW_CONTAINER_NAME}"
441
+ SUDO docker rm -f "${NEW_CONTAINER_NAME}" || true
442
+ exit 1
443
+ fi
444
+ echo "✅ nginx 설정 검증 성공"
445
+ rm -f "${TMP_FILE}"
446
+
447
+ echo "🔄 nginx reload"
448
+ if ! (SUDO systemctl reload nginx); then
449
+ echo "⚠️ reload 실패 → restart 시도"
450
+ if ! (SUDO systemctl restart nginx); then
451
+ echo "❌ nginx reload/restart 실패 → 백업 복구"
452
+ SUDO cp "${BACKUP_FILE}" "${NGINX_RP_CONF}"
453
+ SUDO systemctl restart nginx
454
+ SUDO rm -f "${BACKUP_FILE}"
455
+ echo "🗑️ 신규 컨테이너 정리: ${NEW_CONTAINER_NAME}"
456
+ SUDO docker rm -f "${NEW_CONTAINER_NAME}" || true
457
+ exit 1
458
+ fi
459
+ fi
460
+
461
+ # ============================================================
462
+ # 10. 도메인 접근 검증
463
+ # ============================================================
464
+ echo "🌐 도메인 접근 검증 (${SERVICE_DOMAIN})"
465
+ DOMAIN_OK=false
466
+ for i in $(seq 1 ${DOMAIN_CHECK_RETRIES}); do
467
+ if curl -fsS "https://${SERVICE_DOMAIN}${HEALTHCHECK_PATH}" >/dev/null 2>&1 || \
468
+ curl -fsS "https://${SERVICE_DOMAIN}/" >/dev/null 2>&1 ; then
469
+ echo "✅ 도메인 접근 검증 성공 (시도 ${i})"
470
+ DOMAIN_OK=true
471
+ break
472
+ fi
473
+ echo "⏳ 도메인 접근 검증 중... (${i}/${DOMAIN_CHECK_RETRIES})"
474
+ sleep ${DOMAIN_CHECK_INTERVAL}
475
+ done
476
+
477
+ if [ "${DOMAIN_OK}" != "true" ]; then
478
+ echo "⚠️ 도메인 접근 검증 실패: 배포 진행 (DNS 전파 지연 가능성)"
479
+ fi
480
+
481
+ # ============================================================
482
+ # 11. 이전 컨테이너 제거
483
+ # ============================================================
484
+ echo "⏱️ in-flight 요청 처리 대기 (${IN_FLIGHT_WAIT}초)"
485
+ sleep ${IN_FLIGHT_WAIT}
486
+
487
+ if SUDO docker ps -a --format '{{.Names}}' | grep -Eq "^${OLD_CONTAINER_NAME}\$"; then
488
+ echo "🗑️ old=${OLD_CONTAINER_NAME} 제거"
489
+ SUDO docker rm -f "${OLD_CONTAINER_NAME}" || true
490
+ else
491
+ echo "ℹ️ old=${OLD_CONTAINER_NAME} 없음 — 정리 생략"
492
+ fi
493
+
494
+ # ============================================================
495
+ # 12. dangling 이미지 정리 + 백업 보존
496
+ # ============================================================
497
+ echo "🧹 dangling(<none>) 이미지 정리..."
498
+ SUDO docker image prune -af
499
+
500
+ echo "🧹 백업 파일 최신 ${NGINX_BACKUP_KEEP}개 보존"
501
+ KEEP_PLUS_ONE=$((NGINX_BACKUP_KEEP + 1))
502
+ SUDO /bin/bash -c "ls -1t '${NGINX_BACKUP_DIR}'/server.ReverseProxy.conf.*.bak 2>/dev/null | tail -n +${KEEP_PLUS_ONE} | xargs -r rm -f"
503
+
504
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
505
+ echo "✅ 무중단 배포 완료. active=${NEW_CONTAINER_NAME} (포트 ${NEW_PORT})"
506
+ echo "🌐 도메인: https://${SERVICE_DOMAIN}"
507
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
508
+
509
+ # ===================================================================
510
+ # 📝 사용 방법
511
+ # ===================================================================
512
+ #
513
+ # 1) GitHub Secrets 등록:
514
+ # APPLICATION_PROD_YML, DOCKERHUB_USERNAME, DOCKERHUB_TOKEN,
515
+ # SERVER_HOST, SERVER_USER, SERVER_PASSWORD
516
+ #
517
+ # 2) 본 워크플로우의 env 블록에서 프로젝트별 설정 수정:
518
+ # - PROJECT_NAME, SERVICE_DOMAIN
519
+ # - APPLICATION_YML_DIR, CONTAINER_INTERNAL_PORT
520
+ # - BLUE_PORT, GREEN_PORT (서로 다른 호스트 포트)
521
+ # - NGINX_RP_CONF, NGINX_BACKUP_DIR
522
+ # - ENABLE_VOLUME_MOUNT, VOLUME_HOST_PATH, VOLUME_CONTAINER_PATH
523
+ #
524
+ # 3) 서버 사전 준비:
525
+ # - nginx 설치 + sites-enabled config 존재
526
+ # - 해당 config server 블록에 다음 라인 존재:
527
+ # server_name ${SERVICE_DOMAIN};
528
+ # proxy_pass http://127.0.0.1:${BLUE_PORT}; # 또는 ${GREEN_PORT}
529
+ # - SSH 사용자가 sudo 로 nginx -t / reload, docker 명령 실행 가능
530
+ #
531
+ # 4) 본 워크플로우로 전환:
532
+ # - 본 파일의 push.branches 주석 해제
533
+ # - PROJECT-SPRING-SIMPLE-CICD.yaml 의 push 트리거 주석 처리
534
+ #
535
+ # ===================================================================