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.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/bin/project-auto-wizard.js +19 -0
- package/package.json +36 -0
- package/payload/coderabbit.yaml +19 -0
- package/payload/config/breaking-changes.json +1 -0
- package/payload/config/wizard-prompts.yml +62 -0
- package/payload/scripts/changelog_manager.py +726 -0
- package/payload/scripts/version_manager.py +617 -0
- package/payload/version.yml.template +48 -0
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +303 -0
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +293 -0
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +289 -0
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +192 -0
- package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +209 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +591 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +700 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +308 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +992 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +689 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +987 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +471 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml +500 -0
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +185 -0
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +271 -0
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +81 -0
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +2191 -0
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +386 -0
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +194 -0
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +255 -0
- package/payload/workflows/spring/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +84 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +316 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +58 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +535 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +437 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +2277 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +425 -0
- package/src/cli/args.js +95 -0
- package/src/cli/help.js +27 -0
- package/src/commands/full.js +53 -0
- package/src/commands/interactive.js +276 -0
- package/src/commands/revert.js +60 -0
- package/src/commands/version.js +31 -0
- package/src/commands/workflows.js +49 -0
- package/src/context.js +26 -0
- package/src/core/assets.js +47 -0
- package/src/core/branches.js +60 -0
- package/src/core/branding.js +15 -0
- package/src/core/breaking-check.js +65 -0
- package/src/core/breaking.js +26 -0
- package/src/core/copy/coderabbit.js +26 -0
- package/src/core/copy/gitignore.js +55 -0
- package/src/core/copy/readme.js +30 -0
- package/src/core/copy/simple.js +23 -0
- package/src/core/copy/workflows.js +233 -0
- package/src/core/detect-fs.js +106 -0
- package/src/core/detect.js +62 -0
- package/src/core/fsutil.js +46 -0
- package/src/core/options-ask.js +99 -0
- package/src/core/paths-resolve.js +261 -0
- package/src/core/paths.js +16 -0
- package/src/core/version-yml.js +190 -0
- package/src/core/wizard-env.js +101 -0
- package/src/core/wizard-labels.js +107 -0
- package/src/index.js +162 -0
- package/src/ui/ansi.js +26 -0
- package/src/ui/banner.js +29 -0
- package/src/ui/env-plan.js +207 -0
- package/src/ui/prompts.js +99 -0
- package/src/ui/readline-engine.js +257 -0
- package/src/ui/status-cards.js +56 -0
- 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
|
+
# ===================================================================
|