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,2191 @@
|
|
|
1
|
+
# ===================================================================
|
|
2
|
+
# Python/FastAPI PR Preview (SSH + Docker + Traefik)
|
|
3
|
+
# ===================================================================
|
|
4
|
+
#
|
|
5
|
+
# PR 또는 Issue 코멘트 명령어를 통해 Preview 환경을 자동으로 관리합니다.
|
|
6
|
+
# Traefik 리버스 프록시를 통해 동적 라우팅됩니다.
|
|
7
|
+
#
|
|
8
|
+
# ===================================================================
|
|
9
|
+
# 🔑 필수 GitHub Secrets
|
|
10
|
+
# ===================================================================
|
|
11
|
+
# ENV_FILE: .env 파일 전체 내용
|
|
12
|
+
# DOCKERHUB_USERNAME: Docker Hub 사용자명
|
|
13
|
+
# DOCKERHUB_TOKEN: Docker Hub 액세스 토큰
|
|
14
|
+
# SERVER_HOST: 서버 호스트(SSH 접속 주소)
|
|
15
|
+
# SERVER_USER: SSH 사용자명
|
|
16
|
+
# SERVER_PASSWORD: SSH 비밀번호 (SSH_AUTH_METHOD=password일 때)
|
|
17
|
+
# SSH_KEY: SSH 개인키 .pem 내용 (SSH_AUTH_METHOD=key일 때, AWS EC2 등)
|
|
18
|
+
# ===================================================================
|
|
19
|
+
#
|
|
20
|
+
# 🚀 사용법:
|
|
21
|
+
# @suh-lab server build - Preview 빌드 및 배포
|
|
22
|
+
# @suh-lab server destroy - Preview 환경 삭제
|
|
23
|
+
# @suh-lab server status - 현재 상태 확인
|
|
24
|
+
#
|
|
25
|
+
# 📊 리소스 네이밍:
|
|
26
|
+
# - 컨테이너: {PROJECT_NAME}-pr-{번호}
|
|
27
|
+
# - 도메인: {PROJECT_NAME}-pr-{번호}.pr.suhsaechan.kr
|
|
28
|
+
#
|
|
29
|
+
# 📋 사전 요구사항:
|
|
30
|
+
# - Traefik 컨테이너 실행 중 (traefik-network)
|
|
31
|
+
# - 와일드카드 DNS 설정: *.pr.suhsaechan.kr → 서버
|
|
32
|
+
# ===================================================================
|
|
33
|
+
|
|
34
|
+
name: PROJECT-PYTHON-PR-PREVIEW
|
|
35
|
+
|
|
36
|
+
# ===================================================================
|
|
37
|
+
# ⚠️ [영역 1] 프로젝트별 설정 - 다른 프로젝트에서 사용 시 이 섹션만 수정하세요
|
|
38
|
+
# ===================================================================
|
|
39
|
+
env:
|
|
40
|
+
# 프로젝트 고유 식별자 (컨테이너명, 이미지명, 도메인에 사용)
|
|
41
|
+
PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
|
|
42
|
+
|
|
43
|
+
# Docker 설정
|
|
44
|
+
DOCKERFILE_PATH: './Dockerfile'
|
|
45
|
+
INTERNAL_PORT: '8000'
|
|
46
|
+
|
|
47
|
+
# 🗂️ 볼륨 마운트 설정 (프로젝트별 수정 필요, 빈값이면 마운트 안함)
|
|
48
|
+
PROJECT_TARGET_DIR: "/volume1/projects/프로젝트명/ai" # 서버의 데이터 디렉토리
|
|
49
|
+
PROJECT_MNT_DIR: "/mnt/프로젝트명" # 컨테이너 내부 마운트 경로
|
|
50
|
+
|
|
51
|
+
# Traefik & Preview 도메인 설정 (환경 구축 후 수정 금지)
|
|
52
|
+
TRAEFIK_NETWORK: traefik-network
|
|
53
|
+
PREVIEW_DOMAIN_SUFFIX: pr.suhsaechan.kr
|
|
54
|
+
PREVIEW_PORT: '8079'
|
|
55
|
+
|
|
56
|
+
# 🔐 SSH 인증 방식: "password"(Synology·일반 서버) | "key"(AWS EC2·.pem)
|
|
57
|
+
# password → SERVER_PASSWORD secret 사용
|
|
58
|
+
# key → SSH_KEY secret 사용 (.pem 파일 전체 내용을 secret 값으로)
|
|
59
|
+
SSH_AUTH_METHOD: "password" # @wizard ask:password
|
|
60
|
+
|
|
61
|
+
# SSH 포트 (서버 포트 환경 구축 후 수정 금지)
|
|
62
|
+
SSH_PORT: '2022'
|
|
63
|
+
|
|
64
|
+
# Issue Helper 댓글 마커 (프로젝트별 수정 필요)
|
|
65
|
+
# Issue 댓글에서 브랜치명을 추출할 때 사용
|
|
66
|
+
ISSUE_HELPER_MARKER: 'Guide by SUH-LAB'
|
|
67
|
+
|
|
68
|
+
# Health Check 설정 (프로젝트별 수정 필요)
|
|
69
|
+
# - HEALTH_CHECK_PATH: HTTP Health Check 경로 (빈값이면 HTTP 체크 건너뜀)
|
|
70
|
+
# - HEALTH_CHECK_LOG_PATTERN: 로그 패턴 매칭 (HTTP 실패 시 폴백)
|
|
71
|
+
# - API_DOCS_PATH: API 문서 URL (빈값이면 배포 코멘트에 미표시)
|
|
72
|
+
#
|
|
73
|
+
# 프레임워크별 기본값 예시:
|
|
74
|
+
# FastAPI: '/docs', 'Uvicorn running on|Application startup complete'
|
|
75
|
+
# Spring Boot: '/actuator/health', 'Started.*Application|Tomcat started on port'
|
|
76
|
+
# Express: '/health', 'Server listening on port'
|
|
77
|
+
HEALTH_CHECK_PATH: '/docs'
|
|
78
|
+
HEALTH_CHECK_LOG_PATTERN: 'Uvicorn running on|Application startup complete'
|
|
79
|
+
API_DOCS_PATH: '/docs'
|
|
80
|
+
|
|
81
|
+
# ===================================================================
|
|
82
|
+
# 트리거 설정
|
|
83
|
+
# ===================================================================
|
|
84
|
+
on:
|
|
85
|
+
issue_comment:
|
|
86
|
+
types: [created] # PR 댓글 + Issue 댓글
|
|
87
|
+
issues:
|
|
88
|
+
types: [closed] # Issue 닫힘 시 destroy
|
|
89
|
+
pull_request:
|
|
90
|
+
types: [closed] # PR 닫힘 시 destroy
|
|
91
|
+
|
|
92
|
+
permissions:
|
|
93
|
+
contents: read
|
|
94
|
+
pull-requests: write
|
|
95
|
+
issues: write
|
|
96
|
+
|
|
97
|
+
# ===================================================================
|
|
98
|
+
# Jobs
|
|
99
|
+
# ===================================================================
|
|
100
|
+
jobs:
|
|
101
|
+
# -----------------------------------------------------------------
|
|
102
|
+
# Job 1: 명령어 파싱 (PR 댓글 + Issue 댓글 모두 지원)
|
|
103
|
+
# -----------------------------------------------------------------
|
|
104
|
+
check-command:
|
|
105
|
+
name: 명령어 확인
|
|
106
|
+
if: github.event_name == 'issue_comment'
|
|
107
|
+
runs-on: ubuntu-latest
|
|
108
|
+
outputs:
|
|
109
|
+
command: ${{ steps.parse.outputs.command }}
|
|
110
|
+
is_valid: ${{ steps.parse.outputs.is_valid }}
|
|
111
|
+
is_pr: ${{ steps.parse.outputs.is_pr }}
|
|
112
|
+
custom_branch: ${{ steps.parse.outputs.custom_branch }}
|
|
113
|
+
is_custom_branch: ${{ steps.parse.outputs.is_custom_branch }}
|
|
114
|
+
steps:
|
|
115
|
+
- name: 댓글에 👀 리액션 추가
|
|
116
|
+
if: contains(github.event.comment.body, '@suh-lab') && contains(github.event.comment.body, 'server')
|
|
117
|
+
uses: actions/github-script@v7
|
|
118
|
+
with:
|
|
119
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
120
|
+
script: |
|
|
121
|
+
await github.rest.reactions.createForIssueComment({
|
|
122
|
+
owner: context.repo.owner,
|
|
123
|
+
repo: context.repo.repo,
|
|
124
|
+
comment_id: context.payload.comment.id,
|
|
125
|
+
content: 'eyes'
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
- name: 커맨드 파싱
|
|
129
|
+
id: parse
|
|
130
|
+
env:
|
|
131
|
+
COMMENT: ${{ github.event.comment.body }}
|
|
132
|
+
IS_PR: ${{ github.event.issue.pull_request != null }}
|
|
133
|
+
run: |
|
|
134
|
+
# PR인지 Issue인지 확인
|
|
135
|
+
if [[ "$IS_PR" == "true" ]]; then
|
|
136
|
+
echo "is_pr=true" >> $GITHUB_OUTPUT
|
|
137
|
+
echo "ℹ️ PR 댓글에서 명령어 감지"
|
|
138
|
+
else
|
|
139
|
+
echo "is_pr=false" >> $GITHUB_OUTPUT
|
|
140
|
+
echo "ℹ️ Issue 댓글에서 명령어 감지"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# 명령어 파싱 (브랜치 파라미터 지원)
|
|
144
|
+
# 형식: @suh-lab server <command> [브랜치명]
|
|
145
|
+
if [[ "$COMMENT" =~ @suh-lab[[:space:]]+server[[:space:]]+build([[:space:]]+([^[:space:]]+))? ]]; then
|
|
146
|
+
echo "command=build" >> $GITHUB_OUTPUT
|
|
147
|
+
echo "is_valid=true" >> $GITHUB_OUTPUT
|
|
148
|
+
if [[ -n "${BASH_REMATCH[2]}" ]]; then
|
|
149
|
+
echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
|
|
150
|
+
echo "is_custom_branch=true" >> $GITHUB_OUTPUT
|
|
151
|
+
echo "✅ 명령어 감지: build (브랜치: ${BASH_REMATCH[2]})"
|
|
152
|
+
else
|
|
153
|
+
echo "is_custom_branch=false" >> $GITHUB_OUTPUT
|
|
154
|
+
echo "✅ 명령어 감지: build"
|
|
155
|
+
fi
|
|
156
|
+
elif [[ "$COMMENT" =~ @suh-lab[[:space:]]+server[[:space:]]+destroy([[:space:]]+([^[:space:]]+))? ]]; then
|
|
157
|
+
echo "command=destroy" >> $GITHUB_OUTPUT
|
|
158
|
+
echo "is_valid=true" >> $GITHUB_OUTPUT
|
|
159
|
+
if [[ -n "${BASH_REMATCH[2]}" ]]; then
|
|
160
|
+
echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
|
|
161
|
+
echo "is_custom_branch=true" >> $GITHUB_OUTPUT
|
|
162
|
+
echo "✅ 명령어 감지: destroy (브랜치: ${BASH_REMATCH[2]})"
|
|
163
|
+
else
|
|
164
|
+
echo "is_custom_branch=false" >> $GITHUB_OUTPUT
|
|
165
|
+
echo "✅ 명령어 감지: destroy"
|
|
166
|
+
fi
|
|
167
|
+
elif [[ "$COMMENT" =~ @suh-lab[[:space:]]+server[[:space:]]+status([[:space:]]+([^[:space:]]+))? ]]; then
|
|
168
|
+
echo "command=status" >> $GITHUB_OUTPUT
|
|
169
|
+
echo "is_valid=true" >> $GITHUB_OUTPUT
|
|
170
|
+
if [[ -n "${BASH_REMATCH[2]}" ]]; then
|
|
171
|
+
echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
|
|
172
|
+
echo "is_custom_branch=true" >> $GITHUB_OUTPUT
|
|
173
|
+
echo "✅ 명령어 감지: status (브랜치: ${BASH_REMATCH[2]})"
|
|
174
|
+
else
|
|
175
|
+
echo "is_custom_branch=false" >> $GITHUB_OUTPUT
|
|
176
|
+
echo "✅ 명령어 감지: status"
|
|
177
|
+
fi
|
|
178
|
+
else
|
|
179
|
+
echo "is_valid=false" >> $GITHUB_OUTPUT
|
|
180
|
+
echo "ℹ️ @suh-lab server 명령어가 아님"
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# -----------------------------------------------------------------
|
|
184
|
+
# Job 2-1: 빌드 & 배포 (PR 댓글에서 실행)
|
|
185
|
+
# -----------------------------------------------------------------
|
|
186
|
+
build-preview-pr:
|
|
187
|
+
name: Preview 빌드 & 배포 (PR)
|
|
188
|
+
needs: check-command
|
|
189
|
+
if: |
|
|
190
|
+
needs.check-command.outputs.is_valid == 'true' &&
|
|
191
|
+
needs.check-command.outputs.command == 'build' &&
|
|
192
|
+
needs.check-command.outputs.is_pr == 'true' &&
|
|
193
|
+
needs.check-command.outputs.is_custom_branch != 'true'
|
|
194
|
+
runs-on: ubuntu-latest
|
|
195
|
+
steps:
|
|
196
|
+
- name: PR 정보 가져오기
|
|
197
|
+
id: pr
|
|
198
|
+
uses: actions/github-script@v7
|
|
199
|
+
with:
|
|
200
|
+
script: |
|
|
201
|
+
const pr = await github.rest.pulls.get({
|
|
202
|
+
owner: context.repo.owner,
|
|
203
|
+
repo: context.repo.repo,
|
|
204
|
+
pull_number: context.issue.number
|
|
205
|
+
});
|
|
206
|
+
core.setOutput('ref', pr.data.head.ref);
|
|
207
|
+
core.setOutput('sha', pr.data.head.sha.substring(0, 7));
|
|
208
|
+
return pr.data.number;
|
|
209
|
+
|
|
210
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
211
|
+
# 진행 상황 댓글 시스템
|
|
212
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
213
|
+
- name: 진행 상황 댓글 생성
|
|
214
|
+
id: progress
|
|
215
|
+
uses: actions/github-script@v7
|
|
216
|
+
with:
|
|
217
|
+
script: |
|
|
218
|
+
const prNumber = context.issue.number;
|
|
219
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
220
|
+
const startTime = Date.now();
|
|
221
|
+
|
|
222
|
+
const body = [
|
|
223
|
+
'## 🚀 PR Preview 빌드 중...',
|
|
224
|
+
'',
|
|
225
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
226
|
+
'|------|------|----------|',
|
|
227
|
+
'| 🐳 Docker 이미지 빌드 & Push | ⏳ 진행 중... | - |',
|
|
228
|
+
'| 🚀 서버 배포 & Health Check | ⏸️ 대기 | - |',
|
|
229
|
+
'',
|
|
230
|
+
`**[📋 실시간 로그 보기](${runUrl})**`,
|
|
231
|
+
'',
|
|
232
|
+
'---',
|
|
233
|
+
'*🤖 이 댓글은 자동으로 업데이트됩니다.*'
|
|
234
|
+
].join('\n');
|
|
235
|
+
|
|
236
|
+
const { data: comment } = await github.rest.issues.createComment({
|
|
237
|
+
owner: context.repo.owner,
|
|
238
|
+
repo: context.repo.repo,
|
|
239
|
+
issue_number: prNumber,
|
|
240
|
+
body: body
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
core.setOutput('comment_id', comment.id);
|
|
244
|
+
core.setOutput('start_time', startTime);
|
|
245
|
+
core.setOutput('docker_start', startTime);
|
|
246
|
+
|
|
247
|
+
- name: 코드 체크아웃
|
|
248
|
+
uses: actions/checkout@v5
|
|
249
|
+
with:
|
|
250
|
+
ref: ${{ steps.pr.outputs.ref }}
|
|
251
|
+
|
|
252
|
+
# =================================================================
|
|
253
|
+
# ⚠️ [영역 2] 환경변수 파일 생성
|
|
254
|
+
# =================================================================
|
|
255
|
+
- name: "[필수] .env 파일 생성"
|
|
256
|
+
run: |
|
|
257
|
+
cat << 'EOF' > .env
|
|
258
|
+
${{ secrets.ENV_FILE }}
|
|
259
|
+
EOF
|
|
260
|
+
# =================================================================
|
|
261
|
+
# ⚠️ [영역 2 끝] 환경변수 파일 생성 끝
|
|
262
|
+
# =================================================================
|
|
263
|
+
|
|
264
|
+
- name: Docker 로그인
|
|
265
|
+
uses: docker/login-action@v3
|
|
266
|
+
with:
|
|
267
|
+
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
268
|
+
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
269
|
+
|
|
270
|
+
- name: Docker Buildx 설정
|
|
271
|
+
uses: docker/setup-buildx-action@v3
|
|
272
|
+
|
|
273
|
+
- name: Docker 이미지 빌드 & Push
|
|
274
|
+
uses: docker/build-push-action@v5
|
|
275
|
+
with:
|
|
276
|
+
context: .
|
|
277
|
+
file: ${{ env.DOCKERFILE_PATH }}
|
|
278
|
+
push: true
|
|
279
|
+
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}:pr-${{ github.event.issue.number }}
|
|
280
|
+
cache-from: type=gha
|
|
281
|
+
cache-to: type=gha,mode=max
|
|
282
|
+
|
|
283
|
+
- name: 진행 상황 - Docker 완료
|
|
284
|
+
id: docker_progress
|
|
285
|
+
uses: actions/github-script@v7
|
|
286
|
+
with:
|
|
287
|
+
script: |
|
|
288
|
+
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
289
|
+
const dockerStart = ${{ steps.progress.outputs.docker_start }};
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
const dockerElapsed = now - dockerStart;
|
|
292
|
+
const minutes = Math.floor(dockerElapsed / 60000);
|
|
293
|
+
const seconds = Math.floor((dockerElapsed % 60000) / 1000);
|
|
294
|
+
const dockerDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
|
|
295
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
296
|
+
|
|
297
|
+
const body = [
|
|
298
|
+
'## 🚀 PR Preview 빌드 중...',
|
|
299
|
+
'',
|
|
300
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
301
|
+
'|------|------|----------|',
|
|
302
|
+
`| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
|
|
303
|
+
'| 🚀 서버 배포 & Health Check | ⏳ 진행 중... | - |',
|
|
304
|
+
'',
|
|
305
|
+
`**[📋 실시간 로그 보기](${runUrl})**`,
|
|
306
|
+
'',
|
|
307
|
+
'---',
|
|
308
|
+
'*🤖 이 댓글은 자동으로 업데이트됩니다.*'
|
|
309
|
+
].join('\n');
|
|
310
|
+
|
|
311
|
+
await github.rest.issues.updateComment({
|
|
312
|
+
owner: context.repo.owner,
|
|
313
|
+
repo: context.repo.repo,
|
|
314
|
+
comment_id: commentId,
|
|
315
|
+
body: body
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
core.setOutput('docker_duration', dockerDuration);
|
|
319
|
+
core.setOutput('deploy_start', now);
|
|
320
|
+
|
|
321
|
+
- name: 서버에 배포
|
|
322
|
+
uses: appleboy/ssh-action@v1.0.3
|
|
323
|
+
env:
|
|
324
|
+
SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
|
|
325
|
+
with:
|
|
326
|
+
host: ${{ secrets.SERVER_HOST }}
|
|
327
|
+
username: ${{ secrets.SERVER_USER }}
|
|
328
|
+
password: ${{ secrets.SERVER_PASSWORD }}
|
|
329
|
+
key: ${{ secrets.SSH_KEY }}
|
|
330
|
+
port: ${{ env.SSH_PORT }}
|
|
331
|
+
envs: SSH_AUTH_METHOD
|
|
332
|
+
script: |
|
|
333
|
+
set -e
|
|
334
|
+
|
|
335
|
+
# 환경 변수 설정 (배포 서버용)
|
|
336
|
+
export PATH=$PATH:/usr/local/bin
|
|
337
|
+
export PW="${{ secrets.SERVER_PASSWORD }}"
|
|
338
|
+
|
|
339
|
+
# 🔐 SSH 인증 방식에 따른 sudo 추상화
|
|
340
|
+
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
341
|
+
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
342
|
+
SUDO() { sudo "$@"; }
|
|
343
|
+
else
|
|
344
|
+
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
345
|
+
fi
|
|
346
|
+
echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
|
|
347
|
+
|
|
348
|
+
# 변수 설정
|
|
349
|
+
PR_NUMBER=${{ github.event.issue.number }}
|
|
350
|
+
PROJECT_NAME="${{ env.PROJECT_NAME }}"
|
|
351
|
+
CONTAINER_NAME="${PROJECT_NAME}-pr-${PR_NUMBER}"
|
|
352
|
+
IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:pr-${PR_NUMBER}"
|
|
353
|
+
DOMAIN="${PROJECT_NAME}-pr-${PR_NUMBER}.${{ env.PREVIEW_DOMAIN_SUFFIX }}"
|
|
354
|
+
INTERNAL_PORT="${{ env.INTERNAL_PORT }}"
|
|
355
|
+
TRAEFIK_NETWORK="${{ env.TRAEFIK_NETWORK }}"
|
|
356
|
+
|
|
357
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
358
|
+
echo "🚀 PR Preview 배포 시작"
|
|
359
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
360
|
+
echo "📦 프로젝트: ${PROJECT_NAME}"
|
|
361
|
+
echo "🔢 PR 번호: #${PR_NUMBER}"
|
|
362
|
+
echo "📛 컨테이너: ${CONTAINER_NAME}"
|
|
363
|
+
echo "🌐 도메인: ${DOMAIN}"
|
|
364
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
365
|
+
|
|
366
|
+
# 이미지 Pull
|
|
367
|
+
echo "📥 Docker 이미지 Pull 중..."
|
|
368
|
+
SUDO docker pull "${IMAGE}"
|
|
369
|
+
|
|
370
|
+
# 기존 컨테이너 삭제
|
|
371
|
+
echo "🗑️ 기존 컨테이너 정리 중..."
|
|
372
|
+
SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
|
373
|
+
|
|
374
|
+
# 볼륨 마운트 옵션 구성
|
|
375
|
+
VOLUME_OPTS="-v /etc/localtime:/etc/localtime:ro"
|
|
376
|
+
if [ -n "${{ env.PROJECT_TARGET_DIR }}" ] && [ -n "${{ env.PROJECT_MNT_DIR }}" ]; then
|
|
377
|
+
VOLUME_OPTS="$VOLUME_OPTS -v ${{ env.PROJECT_TARGET_DIR }}:${{ env.PROJECT_MNT_DIR }}"
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# 새 컨테이너 실행
|
|
381
|
+
echo "🐳 새 컨테이너 실행 중..."
|
|
382
|
+
SUDO docker run -d \
|
|
383
|
+
--name "${CONTAINER_NAME}" \
|
|
384
|
+
--network "${TRAEFIK_NETWORK}" \
|
|
385
|
+
--label "traefik.enable=true" \
|
|
386
|
+
--label "traefik.http.routers.${CONTAINER_NAME}.rule=Host(\`${DOMAIN}\`)" \
|
|
387
|
+
--label "traefik.http.routers.${CONTAINER_NAME}.entrypoints=web" \
|
|
388
|
+
--label "traefik.http.services.${CONTAINER_NAME}.loadbalancer.server.port=${INTERNAL_PORT}" \
|
|
389
|
+
-e TZ=Asia/Seoul \
|
|
390
|
+
-e ENVIRONMENT=prod \
|
|
391
|
+
$VOLUME_OPTS \
|
|
392
|
+
"${IMAGE}"
|
|
393
|
+
|
|
394
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
395
|
+
# Health Check (최대 120초 대기) - 하이브리드 방식
|
|
396
|
+
# 1. HEALTH_CHECK_PATH가 설정되어 있으면 HTTP 체크 시도
|
|
397
|
+
# 2. 폴백: 로그 패턴 매칭 (HEALTH_CHECK_LOG_PATTERN)
|
|
398
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
399
|
+
echo ""
|
|
400
|
+
echo "⏳ Health Check 시작 (최대 120초 대기)..."
|
|
401
|
+
HEALTH_PATH="${{ env.HEALTH_CHECK_PATH }}"
|
|
402
|
+
LOG_PATTERN="${{ env.HEALTH_CHECK_LOG_PATTERN }}"
|
|
403
|
+
MAX_RETRIES=24
|
|
404
|
+
RETRY_COUNT=0
|
|
405
|
+
HEALTH_CHECK_PASSED=false
|
|
406
|
+
HEALTH_CHECK_METHOD=""
|
|
407
|
+
|
|
408
|
+
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
|
409
|
+
sleep 5
|
|
410
|
+
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
411
|
+
|
|
412
|
+
# 1. 컨테이너 상태 확인
|
|
413
|
+
STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")
|
|
414
|
+
|
|
415
|
+
if [ "$STATUS" = "exited" ]; then
|
|
416
|
+
echo "❌ 컨테이너 비정상 종료!"
|
|
417
|
+
echo ""
|
|
418
|
+
echo "📋 컨테이너 로그 (최근 100줄):"
|
|
419
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
420
|
+
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
421
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
422
|
+
exit 1
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
if [ "$STATUS" = "running" ]; then
|
|
426
|
+
# 2. HTTP Health Check 시도 (HEALTH_CHECK_PATH가 설정된 경우에만)
|
|
427
|
+
if [ -n "$HEALTH_PATH" ]; then
|
|
428
|
+
HEALTH=$(SUDO docker exec "${CONTAINER_NAME}" curl -sf "http://localhost:${INTERNAL_PORT}${HEALTH_PATH}" 2>/dev/null || echo "")
|
|
429
|
+
|
|
430
|
+
if [ -n "$HEALTH" ]; then
|
|
431
|
+
echo "✅ 정상 기동 확인! (HTTP 응답: ${HEALTH_PATH})"
|
|
432
|
+
HEALTH_CHECK_PASSED=true
|
|
433
|
+
HEALTH_CHECK_METHOD="HTTP"
|
|
434
|
+
break
|
|
435
|
+
fi
|
|
436
|
+
fi
|
|
437
|
+
|
|
438
|
+
# 3. HTTP 응답 없으면 로그 패턴 매칭으로 폴백
|
|
439
|
+
if [ -n "$LOG_PATTERN" ]; then
|
|
440
|
+
STARTED=$(SUDO docker logs --tail 50 "${CONTAINER_NAME}" 2>&1 | grep -E "$LOG_PATTERN" || echo "")
|
|
441
|
+
|
|
442
|
+
if [ -n "$STARTED" ]; then
|
|
443
|
+
echo "✅ 정상 기동 확인! (로그 패턴)"
|
|
444
|
+
echo " $STARTED"
|
|
445
|
+
HEALTH_CHECK_PASSED=true
|
|
446
|
+
HEALTH_CHECK_METHOD="Log"
|
|
447
|
+
break
|
|
448
|
+
fi
|
|
449
|
+
fi
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
echo "⏳ 대기 중... ($RETRY_COUNT/$MAX_RETRIES) - 상태: $STATUS"
|
|
453
|
+
done
|
|
454
|
+
|
|
455
|
+
if [ "$HEALTH_CHECK_PASSED" = "false" ]; then
|
|
456
|
+
echo ""
|
|
457
|
+
echo "❌ Health Check 타임아웃 (120초)"
|
|
458
|
+
echo ""
|
|
459
|
+
echo "📋 컨테이너 로그 (최근 100줄):"
|
|
460
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
461
|
+
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
462
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
463
|
+
exit 1
|
|
464
|
+
fi
|
|
465
|
+
|
|
466
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
467
|
+
echo "✅ 배포 및 Health Check 완료! (방식: ${HEALTH_CHECK_METHOD})"
|
|
468
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
469
|
+
|
|
470
|
+
- name: 배포 완료 코멘트
|
|
471
|
+
uses: actions/github-script@v7
|
|
472
|
+
with:
|
|
473
|
+
script: |
|
|
474
|
+
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
475
|
+
const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
|
|
476
|
+
const deployStart = ${{ steps.docker_progress.outputs.deploy_start }};
|
|
477
|
+
const now = Date.now();
|
|
478
|
+
const deployElapsed = now - deployStart;
|
|
479
|
+
const minutes = Math.floor(deployElapsed / 60000);
|
|
480
|
+
const seconds = Math.floor((deployElapsed % 60000) / 1000);
|
|
481
|
+
const deployDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
|
|
482
|
+
|
|
483
|
+
const projectName = '${{ env.PROJECT_NAME }}';
|
|
484
|
+
const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
|
|
485
|
+
const previewPort = '${{ env.PREVIEW_PORT }}';
|
|
486
|
+
const apiDocsPath = '${{ env.API_DOCS_PATH }}';
|
|
487
|
+
const prNumber = context.issue.number;
|
|
488
|
+
const domain = `${projectName}-pr-${prNumber}.${domainSuffix}`;
|
|
489
|
+
const previewUrl = `http://${domain}:${previewPort}`;
|
|
490
|
+
const sha = '${{ steps.pr.outputs.sha }}';
|
|
491
|
+
|
|
492
|
+
// Preview 환경 테이블 구성 (API_DOCS_PATH가 있을 때만 API Docs 행 추가)
|
|
493
|
+
const envRows = [
|
|
494
|
+
`| **Preview URL** | ${previewUrl} |`,
|
|
495
|
+
];
|
|
496
|
+
if (apiDocsPath) {
|
|
497
|
+
envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
|
|
498
|
+
}
|
|
499
|
+
envRows.push(`| **컨테이너** | \`${projectName}-pr-${prNumber}\` |`);
|
|
500
|
+
envRows.push(`| **커밋** | \`${sha}\` |`);
|
|
501
|
+
|
|
502
|
+
const branchName = '${{ steps.pr.outputs.ref }}';
|
|
503
|
+
|
|
504
|
+
const body = [
|
|
505
|
+
'## ✅ PR Preview 배포 완료!',
|
|
506
|
+
'',
|
|
507
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
508
|
+
'|------|------|----------|',
|
|
509
|
+
`| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
|
|
510
|
+
`| 🚀 서버 배포 & Health Check | ✅ 완료 | ${deployDuration} |`,
|
|
511
|
+
'',
|
|
512
|
+
'### 🌐 Preview 환경',
|
|
513
|
+
'| 항목 | 값 |',
|
|
514
|
+
'|------|-----|',
|
|
515
|
+
...envRows,
|
|
516
|
+
'',
|
|
517
|
+
'### 📋 명령어',
|
|
518
|
+
'| 명령어 | 설명 |',
|
|
519
|
+
'|--------|------|',
|
|
520
|
+
'| `@suh-lab server build` | 최신 커밋으로 재배포 |',
|
|
521
|
+
'| `@suh-lab server destroy` | Preview 환경 삭제 |',
|
|
522
|
+
'| `@suh-lab server status` | 현재 상태 확인 |',
|
|
523
|
+
'',
|
|
524
|
+
'<details>',
|
|
525
|
+
'<summary>🔧 고급 명령어 (다른 Issue/PR에서 제어)</summary>',
|
|
526
|
+
'',
|
|
527
|
+
'```',
|
|
528
|
+
`@suh-lab server build ${branchName}`,
|
|
529
|
+
`@suh-lab server destroy ${branchName}`,
|
|
530
|
+
`@suh-lab server status ${branchName}`,
|
|
531
|
+
'```',
|
|
532
|
+
'</details>',
|
|
533
|
+
'',
|
|
534
|
+
'---',
|
|
535
|
+
'*🤖 이 댓글은 PR Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
536
|
+
].join('\n');
|
|
537
|
+
|
|
538
|
+
await github.rest.issues.updateComment({
|
|
539
|
+
owner: context.repo.owner,
|
|
540
|
+
repo: context.repo.repo,
|
|
541
|
+
comment_id: commentId,
|
|
542
|
+
body: body
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
- name: 빌드/배포 실패 시 에러 코멘트
|
|
546
|
+
if: failure()
|
|
547
|
+
uses: actions/github-script@v7
|
|
548
|
+
with:
|
|
549
|
+
script: |
|
|
550
|
+
const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
|
|
551
|
+
const projectName = '${{ env.PROJECT_NAME }}';
|
|
552
|
+
const prNumber = context.issue.number;
|
|
553
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
554
|
+
|
|
555
|
+
// 진행 상황에 따라 어디서 실패했는지 표시
|
|
556
|
+
const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
|
|
557
|
+
|
|
558
|
+
// 실패 지점 판단 - 초기 단계 실패도 구분
|
|
559
|
+
let dockerStatus, deployStatus;
|
|
560
|
+
let dockerTime = '-';
|
|
561
|
+
|
|
562
|
+
if (!commentId) {
|
|
563
|
+
// 진행 상황 댓글 생성 전 실패 (초기화 단계)
|
|
564
|
+
dockerStatus = '⚠️ 초기화 실패';
|
|
565
|
+
deployStatus = '-';
|
|
566
|
+
} else if (!dockerDuration || dockerDuration === '') {
|
|
567
|
+
// Docker 빌드 단계에서 실패
|
|
568
|
+
dockerStatus = '❌ 실패';
|
|
569
|
+
deployStatus = '⏸️ 대기';
|
|
570
|
+
} else {
|
|
571
|
+
// 배포 단계에서 실패
|
|
572
|
+
dockerStatus = '✅ 완료';
|
|
573
|
+
deployStatus = '❌ 실패';
|
|
574
|
+
dockerTime = dockerDuration;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const body = [
|
|
578
|
+
'## ❌ PR Preview 배포 실패!',
|
|
579
|
+
'',
|
|
580
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
581
|
+
'|------|------|----------|',
|
|
582
|
+
`| 🐳 Docker 이미지 빌드 & Push | ${dockerStatus} | ${dockerTime} |`,
|
|
583
|
+
`| 🚀 서버 배포 & Health Check | ${deployStatus} | - |`,
|
|
584
|
+
'',
|
|
585
|
+
`**[📋 빌드/배포 로그 확인](${runUrl})**`,
|
|
586
|
+
'',
|
|
587
|
+
'### 🔍 가능한 원인',
|
|
588
|
+
'- Docker 이미지 빌드 실패 (Python 의존성 문제)',
|
|
589
|
+
'- 컨테이너 시작 실패 (FastAPI/Uvicorn 기동 오류)',
|
|
590
|
+
'- Health Check 타임아웃 (120초 내 기동 완료 안됨)',
|
|
591
|
+
'- 환경변수 누락 (.env 파일 설정 확인)',
|
|
592
|
+
'',
|
|
593
|
+
'### 💡 다음 단계',
|
|
594
|
+
'1. 위 링크에서 빌드/배포 로그를 확인하세요',
|
|
595
|
+
'2. 문제를 수정한 후 다시 시도하세요: `@suh-lab server build`',
|
|
596
|
+
'',
|
|
597
|
+
'---',
|
|
598
|
+
'*🤖 이 댓글은 PR Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
599
|
+
].join('\n');
|
|
600
|
+
|
|
601
|
+
// 진행 상황 댓글이 있으면 업데이트, 없으면 새로 생성
|
|
602
|
+
if (commentId) {
|
|
603
|
+
await github.rest.issues.updateComment({
|
|
604
|
+
owner: context.repo.owner,
|
|
605
|
+
repo: context.repo.repo,
|
|
606
|
+
comment_id: commentId,
|
|
607
|
+
body: body
|
|
608
|
+
});
|
|
609
|
+
} else {
|
|
610
|
+
await github.rest.issues.createComment({
|
|
611
|
+
owner: context.repo.owner,
|
|
612
|
+
repo: context.repo.repo,
|
|
613
|
+
issue_number: prNumber,
|
|
614
|
+
body: body
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
# -----------------------------------------------------------------
|
|
619
|
+
# Job 2-2: Issue에서 브랜치명 추출
|
|
620
|
+
# -----------------------------------------------------------------
|
|
621
|
+
get-branch-from-issue:
|
|
622
|
+
name: Issue에서 브랜치 추출
|
|
623
|
+
needs: check-command
|
|
624
|
+
if: |
|
|
625
|
+
needs.check-command.outputs.is_valid == 'true' &&
|
|
626
|
+
needs.check-command.outputs.is_pr == 'false' &&
|
|
627
|
+
needs.check-command.outputs.is_custom_branch != 'true'
|
|
628
|
+
runs-on: ubuntu-latest
|
|
629
|
+
outputs:
|
|
630
|
+
branch_name: ${{ steps.extract.outputs.branch }}
|
|
631
|
+
found: ${{ steps.extract.outputs.found }}
|
|
632
|
+
issue_number: ${{ steps.extract.outputs.issue_number }}
|
|
633
|
+
steps:
|
|
634
|
+
- name: Issue 댓글에서 브랜치명 추출
|
|
635
|
+
id: extract
|
|
636
|
+
uses: actions/github-script@v7
|
|
637
|
+
with:
|
|
638
|
+
script: |
|
|
639
|
+
const issueNumber = context.issue.number;
|
|
640
|
+
const marker = '${{ env.ISSUE_HELPER_MARKER }}';
|
|
641
|
+
|
|
642
|
+
console.log(`🔍 Issue #${issueNumber}에서 브랜치 검색 중...`);
|
|
643
|
+
console.log(`📝 마커: ${marker}`);
|
|
644
|
+
|
|
645
|
+
// Issue의 모든 댓글 조회
|
|
646
|
+
const comments = await github.rest.issues.listComments({
|
|
647
|
+
owner: context.repo.owner,
|
|
648
|
+
repo: context.repo.repo,
|
|
649
|
+
issue_number: issueNumber
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// 브랜치 추출 패턴 (마커와 무관하게 동일한 형식)
|
|
653
|
+
// "### 브랜치" 또는 "### 브랜치명" 둘 다 매칭
|
|
654
|
+
const branchRegex = /###\s*브랜치(?:명)?\s*\n```\s*\n([^\n]+)\s*\n```/;
|
|
655
|
+
|
|
656
|
+
for (const comment of comments.data) {
|
|
657
|
+
// 마커가 포함된 댓글 찾기
|
|
658
|
+
if (comment.body.includes(marker)) {
|
|
659
|
+
const match = comment.body.match(branchRegex);
|
|
660
|
+
if (match) {
|
|
661
|
+
const branchName = match[1].trim();
|
|
662
|
+
core.setOutput('branch', branchName);
|
|
663
|
+
core.setOutput('found', 'true');
|
|
664
|
+
core.setOutput('issue_number', issueNumber);
|
|
665
|
+
console.log(`✅ 브랜치 발견: ${branchName}`);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// 브랜치를 찾지 못한 경우 - 에러가 아님, graceful하게 처리
|
|
672
|
+
core.setOutput('found', 'false');
|
|
673
|
+
core.setOutput('issue_number', issueNumber);
|
|
674
|
+
console.log('⚠️ Issue Helper 댓글에서 브랜치를 찾을 수 없습니다.');
|
|
675
|
+
|
|
676
|
+
- name: 브랜치 없음 알림
|
|
677
|
+
if: steps.extract.outputs.found == 'false'
|
|
678
|
+
uses: actions/github-script@v7
|
|
679
|
+
with:
|
|
680
|
+
script: |
|
|
681
|
+
const issueNumber = context.issue.number;
|
|
682
|
+
const marker = '${{ env.ISSUE_HELPER_MARKER }}';
|
|
683
|
+
|
|
684
|
+
const body = [
|
|
685
|
+
'## ⚠️ 브랜치를 찾을 수 없습니다',
|
|
686
|
+
'',
|
|
687
|
+
'| 항목 | 값 |',
|
|
688
|
+
'|------|-----|',
|
|
689
|
+
`| **Issue** | #${issueNumber} |`,
|
|
690
|
+
`| **마커** | \`${marker}\` |`,
|
|
691
|
+
'',
|
|
692
|
+
'### 💡 확인 사항',
|
|
693
|
+
'1. Issue Helper 댓글이 존재하는지 확인하세요',
|
|
694
|
+
'2. 댓글에 `### 브랜치` 섹션이 있는지 확인하세요',
|
|
695
|
+
'3. `ISSUE_HELPER_MARKER` 환경변수가 올바른지 확인하세요',
|
|
696
|
+
'',
|
|
697
|
+
'---',
|
|
698
|
+
'*🤖 이 댓글은 Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
699
|
+
].join('\n');
|
|
700
|
+
|
|
701
|
+
await github.rest.issues.createComment({
|
|
702
|
+
owner: context.repo.owner,
|
|
703
|
+
repo: context.repo.repo,
|
|
704
|
+
issue_number: issueNumber,
|
|
705
|
+
body: body
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
# -----------------------------------------------------------------
|
|
709
|
+
# Job 2-3: 빌드 & 배포 (Issue 댓글에서 실행)
|
|
710
|
+
# -----------------------------------------------------------------
|
|
711
|
+
build-preview-issue:
|
|
712
|
+
name: Preview 빌드 & 배포 (Issue)
|
|
713
|
+
needs: [check-command, get-branch-from-issue]
|
|
714
|
+
if: |
|
|
715
|
+
needs.check-command.outputs.is_valid == 'true' &&
|
|
716
|
+
needs.check-command.outputs.command == 'build' &&
|
|
717
|
+
needs.check-command.outputs.is_pr == 'false' &&
|
|
718
|
+
needs.check-command.outputs.is_custom_branch != 'true' &&
|
|
719
|
+
needs.get-branch-from-issue.outputs.found == 'true'
|
|
720
|
+
runs-on: ubuntu-latest
|
|
721
|
+
steps:
|
|
722
|
+
- name: 브랜치 존재 확인
|
|
723
|
+
id: check_branch
|
|
724
|
+
uses: actions/github-script@v7
|
|
725
|
+
with:
|
|
726
|
+
script: |
|
|
727
|
+
const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
|
|
728
|
+
const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
|
|
729
|
+
|
|
730
|
+
console.log(`🔍 브랜치 존재 확인: ${branchName}`);
|
|
731
|
+
|
|
732
|
+
try {
|
|
733
|
+
await github.rest.repos.getBranch({
|
|
734
|
+
owner: context.repo.owner,
|
|
735
|
+
repo: context.repo.repo,
|
|
736
|
+
branch: branchName
|
|
737
|
+
});
|
|
738
|
+
core.setOutput('exists', 'true');
|
|
739
|
+
console.log(`✅ 브랜치 존재: ${branchName}`);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
if (error.status === 404) {
|
|
742
|
+
core.setOutput('exists', 'false');
|
|
743
|
+
console.log(`❌ 브랜치 없음: ${branchName}`);
|
|
744
|
+
|
|
745
|
+
// 브랜치 없음 알림 댓글
|
|
746
|
+
const body = [
|
|
747
|
+
'## ❌ 브랜치를 찾을 수 없습니다',
|
|
748
|
+
'',
|
|
749
|
+
'| 항목 | 값 |',
|
|
750
|
+
'|------|-----|',
|
|
751
|
+
`| **Issue** | #${issueNumber} |`,
|
|
752
|
+
`| **브랜치** | \`${branchName}\` |`,
|
|
753
|
+
'',
|
|
754
|
+
'### 💡 확인 사항',
|
|
755
|
+
'1. 브랜치가 push되었는지 확인하세요',
|
|
756
|
+
'2. 브랜치명이 정확한지 확인하세요',
|
|
757
|
+
'3. 브랜치가 삭제되지 않았는지 확인하세요',
|
|
758
|
+
'',
|
|
759
|
+
'---',
|
|
760
|
+
'*🤖 이 댓글은 Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
761
|
+
].join('\n');
|
|
762
|
+
|
|
763
|
+
await github.rest.issues.createComment({
|
|
764
|
+
owner: context.repo.owner,
|
|
765
|
+
repo: context.repo.repo,
|
|
766
|
+
issue_number: issueNumber,
|
|
767
|
+
body: body
|
|
768
|
+
});
|
|
769
|
+
} else {
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
- name: 브랜치 없으면 중단
|
|
775
|
+
if: steps.check_branch.outputs.exists == 'false'
|
|
776
|
+
run: |
|
|
777
|
+
echo "⚠️ 브랜치가 존재하지 않아 빌드를 건너뜁니다."
|
|
778
|
+
exit 0
|
|
779
|
+
|
|
780
|
+
- name: Issue 정보 설정
|
|
781
|
+
if: steps.check_branch.outputs.exists == 'true'
|
|
782
|
+
id: issue
|
|
783
|
+
run: |
|
|
784
|
+
echo "ref=${{ needs.get-branch-from-issue.outputs.branch_name }}" >> $GITHUB_OUTPUT
|
|
785
|
+
echo "issue_number=${{ needs.get-branch-from-issue.outputs.issue_number }}" >> $GITHUB_OUTPUT
|
|
786
|
+
# 브랜치의 최신 커밋 SHA 가져오기
|
|
787
|
+
echo "sha=$(git ls-remote https://github.com/${{ github.repository }}.git refs/heads/${{ needs.get-branch-from-issue.outputs.branch_name }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
|
788
|
+
|
|
789
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
790
|
+
# 진행 상황 댓글 시스템
|
|
791
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
792
|
+
- name: 진행 상황 댓글 생성
|
|
793
|
+
if: steps.check_branch.outputs.exists == 'true'
|
|
794
|
+
id: progress
|
|
795
|
+
uses: actions/github-script@v7
|
|
796
|
+
with:
|
|
797
|
+
script: |
|
|
798
|
+
const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
|
|
799
|
+
const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
|
|
800
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
801
|
+
const startTime = Date.now();
|
|
802
|
+
|
|
803
|
+
const body = [
|
|
804
|
+
'## 🚀 Issue Preview 빌드 중...',
|
|
805
|
+
'',
|
|
806
|
+
`**브랜치**: \`${branchName}\``,
|
|
807
|
+
'',
|
|
808
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
809
|
+
'|------|------|----------|',
|
|
810
|
+
'| 🐳 Docker 이미지 빌드 & Push | ⏳ 진행 중... | - |',
|
|
811
|
+
'| 🚀 서버 배포 & Health Check | ⏸️ 대기 | - |',
|
|
812
|
+
'',
|
|
813
|
+
`**[📋 실시간 로그 보기](${runUrl})**`,
|
|
814
|
+
'',
|
|
815
|
+
'---',
|
|
816
|
+
'*🤖 이 댓글은 자동으로 업데이트됩니다.*'
|
|
817
|
+
].join('\n');
|
|
818
|
+
|
|
819
|
+
const { data: comment } = await github.rest.issues.createComment({
|
|
820
|
+
owner: context.repo.owner,
|
|
821
|
+
repo: context.repo.repo,
|
|
822
|
+
issue_number: issueNumber,
|
|
823
|
+
body: body
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
core.setOutput('comment_id', comment.id);
|
|
827
|
+
core.setOutput('start_time', startTime);
|
|
828
|
+
core.setOutput('docker_start', startTime);
|
|
829
|
+
|
|
830
|
+
- name: 코드 체크아웃
|
|
831
|
+
if: steps.check_branch.outputs.exists == 'true'
|
|
832
|
+
uses: actions/checkout@v5
|
|
833
|
+
with:
|
|
834
|
+
ref: ${{ needs.get-branch-from-issue.outputs.branch_name }}
|
|
835
|
+
|
|
836
|
+
# =================================================================
|
|
837
|
+
# ⚠️ [영역 2] 환경변수 파일 생성
|
|
838
|
+
# =================================================================
|
|
839
|
+
- name: "[필수] .env 파일 생성"
|
|
840
|
+
if: steps.check_branch.outputs.exists == 'true'
|
|
841
|
+
run: |
|
|
842
|
+
cat << 'EOF' > .env
|
|
843
|
+
${{ secrets.ENV_FILE }}
|
|
844
|
+
EOF
|
|
845
|
+
# =================================================================
|
|
846
|
+
# ⚠️ [영역 2 끝] 환경변수 파일 생성 끝
|
|
847
|
+
# =================================================================
|
|
848
|
+
|
|
849
|
+
- name: Docker 로그인
|
|
850
|
+
if: steps.check_branch.outputs.exists == 'true'
|
|
851
|
+
uses: docker/login-action@v3
|
|
852
|
+
with:
|
|
853
|
+
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
854
|
+
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
855
|
+
|
|
856
|
+
- name: Docker Buildx 설정
|
|
857
|
+
if: steps.check_branch.outputs.exists == 'true'
|
|
858
|
+
uses: docker/setup-buildx-action@v3
|
|
859
|
+
|
|
860
|
+
- name: Docker 이미지 빌드 & Push
|
|
861
|
+
if: steps.check_branch.outputs.exists == 'true'
|
|
862
|
+
uses: docker/build-push-action@v5
|
|
863
|
+
with:
|
|
864
|
+
context: .
|
|
865
|
+
file: ${{ env.DOCKERFILE_PATH }}
|
|
866
|
+
push: true
|
|
867
|
+
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}:pr-${{ needs.get-branch-from-issue.outputs.issue_number }}
|
|
868
|
+
cache-from: type=gha
|
|
869
|
+
cache-to: type=gha,mode=max
|
|
870
|
+
|
|
871
|
+
- name: 진행 상황 - Docker 완료
|
|
872
|
+
if: steps.check_branch.outputs.exists == 'true'
|
|
873
|
+
id: docker_progress
|
|
874
|
+
uses: actions/github-script@v7
|
|
875
|
+
with:
|
|
876
|
+
script: |
|
|
877
|
+
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
878
|
+
const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
|
|
879
|
+
const dockerStart = ${{ steps.progress.outputs.docker_start }};
|
|
880
|
+
const now = Date.now();
|
|
881
|
+
const dockerElapsed = now - dockerStart;
|
|
882
|
+
const minutes = Math.floor(dockerElapsed / 60000);
|
|
883
|
+
const seconds = Math.floor((dockerElapsed % 60000) / 1000);
|
|
884
|
+
const dockerDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
|
|
885
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
886
|
+
|
|
887
|
+
const body = [
|
|
888
|
+
'## 🚀 Issue Preview 빌드 중...',
|
|
889
|
+
'',
|
|
890
|
+
`**브랜치**: \`${branchName}\``,
|
|
891
|
+
'',
|
|
892
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
893
|
+
'|------|------|----------|',
|
|
894
|
+
`| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
|
|
895
|
+
'| 🚀 서버 배포 & Health Check | ⏳ 진행 중... | - |',
|
|
896
|
+
'',
|
|
897
|
+
`**[📋 실시간 로그 보기](${runUrl})**`,
|
|
898
|
+
'',
|
|
899
|
+
'---',
|
|
900
|
+
'*🤖 이 댓글은 자동으로 업데이트됩니다.*'
|
|
901
|
+
].join('\n');
|
|
902
|
+
|
|
903
|
+
await github.rest.issues.updateComment({
|
|
904
|
+
owner: context.repo.owner,
|
|
905
|
+
repo: context.repo.repo,
|
|
906
|
+
comment_id: commentId,
|
|
907
|
+
body: body
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
core.setOutput('docker_duration', dockerDuration);
|
|
911
|
+
core.setOutput('deploy_start', now);
|
|
912
|
+
|
|
913
|
+
- name: 서버에 배포
|
|
914
|
+
if: steps.check_branch.outputs.exists == 'true'
|
|
915
|
+
uses: appleboy/ssh-action@v1.0.3
|
|
916
|
+
env:
|
|
917
|
+
SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
|
|
918
|
+
with:
|
|
919
|
+
host: ${{ secrets.SERVER_HOST }}
|
|
920
|
+
username: ${{ secrets.SERVER_USER }}
|
|
921
|
+
password: ${{ secrets.SERVER_PASSWORD }}
|
|
922
|
+
key: ${{ secrets.SSH_KEY }}
|
|
923
|
+
port: ${{ env.SSH_PORT }}
|
|
924
|
+
envs: SSH_AUTH_METHOD
|
|
925
|
+
script: |
|
|
926
|
+
set -e
|
|
927
|
+
|
|
928
|
+
# 환경 변수 설정 (배포 서버용)
|
|
929
|
+
export PATH=$PATH:/usr/local/bin
|
|
930
|
+
export PW="${{ secrets.SERVER_PASSWORD }}"
|
|
931
|
+
|
|
932
|
+
# 🔐 SSH 인증 방식에 따른 sudo 추상화
|
|
933
|
+
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
934
|
+
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
935
|
+
SUDO() { sudo "$@"; }
|
|
936
|
+
else
|
|
937
|
+
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
938
|
+
fi
|
|
939
|
+
echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
|
|
940
|
+
|
|
941
|
+
# 변수 설정
|
|
942
|
+
ISSUE_NUMBER=${{ needs.get-branch-from-issue.outputs.issue_number }}
|
|
943
|
+
PROJECT_NAME="${{ env.PROJECT_NAME }}"
|
|
944
|
+
CONTAINER_NAME="${PROJECT_NAME}-pr-${ISSUE_NUMBER}"
|
|
945
|
+
IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:pr-${ISSUE_NUMBER}"
|
|
946
|
+
DOMAIN="${PROJECT_NAME}-pr-${ISSUE_NUMBER}.${{ env.PREVIEW_DOMAIN_SUFFIX }}"
|
|
947
|
+
INTERNAL_PORT="${{ env.INTERNAL_PORT }}"
|
|
948
|
+
TRAEFIK_NETWORK="${{ env.TRAEFIK_NETWORK }}"
|
|
949
|
+
|
|
950
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
951
|
+
echo "🚀 Issue Preview 배포 시작"
|
|
952
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
953
|
+
echo "📦 프로젝트: ${PROJECT_NAME}"
|
|
954
|
+
echo "🔢 Issue 번호: #${ISSUE_NUMBER}"
|
|
955
|
+
echo "📛 컨테이너: ${CONTAINER_NAME}"
|
|
956
|
+
echo "🌐 도메인: ${DOMAIN}"
|
|
957
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
958
|
+
|
|
959
|
+
# 이미지 Pull
|
|
960
|
+
echo "📥 Docker 이미지 Pull 중..."
|
|
961
|
+
SUDO docker pull "${IMAGE}"
|
|
962
|
+
|
|
963
|
+
# 기존 컨테이너 삭제
|
|
964
|
+
echo "🗑️ 기존 컨테이너 정리 중..."
|
|
965
|
+
SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
|
966
|
+
|
|
967
|
+
# 볼륨 마운트 옵션 구성
|
|
968
|
+
VOLUME_OPTS="-v /etc/localtime:/etc/localtime:ro"
|
|
969
|
+
if [ -n "${{ env.PROJECT_TARGET_DIR }}" ] && [ -n "${{ env.PROJECT_MNT_DIR }}" ]; then
|
|
970
|
+
VOLUME_OPTS="$VOLUME_OPTS -v ${{ env.PROJECT_TARGET_DIR }}:${{ env.PROJECT_MNT_DIR }}"
|
|
971
|
+
fi
|
|
972
|
+
|
|
973
|
+
# 새 컨테이너 실행
|
|
974
|
+
echo "🐳 새 컨테이너 실행 중..."
|
|
975
|
+
SUDO docker run -d \
|
|
976
|
+
--name "${CONTAINER_NAME}" \
|
|
977
|
+
--network "${TRAEFIK_NETWORK}" \
|
|
978
|
+
--label "traefik.enable=true" \
|
|
979
|
+
--label "traefik.http.routers.${CONTAINER_NAME}.rule=Host(\`${DOMAIN}\`)" \
|
|
980
|
+
--label "traefik.http.routers.${CONTAINER_NAME}.entrypoints=web" \
|
|
981
|
+
--label "traefik.http.services.${CONTAINER_NAME}.loadbalancer.server.port=${INTERNAL_PORT}" \
|
|
982
|
+
-e TZ=Asia/Seoul \
|
|
983
|
+
-e ENVIRONMENT=prod \
|
|
984
|
+
$VOLUME_OPTS \
|
|
985
|
+
"${IMAGE}"
|
|
986
|
+
|
|
987
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
988
|
+
# Health Check (최대 120초 대기) - 하이브리드 방식
|
|
989
|
+
# 1. HEALTH_CHECK_PATH가 설정되어 있으면 HTTP 체크 시도
|
|
990
|
+
# 2. 폴백: 로그 패턴 매칭 (HEALTH_CHECK_LOG_PATTERN)
|
|
991
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
992
|
+
echo ""
|
|
993
|
+
echo "⏳ Health Check 시작 (최대 120초 대기)..."
|
|
994
|
+
HEALTH_PATH="${{ env.HEALTH_CHECK_PATH }}"
|
|
995
|
+
LOG_PATTERN="${{ env.HEALTH_CHECK_LOG_PATTERN }}"
|
|
996
|
+
MAX_RETRIES=24
|
|
997
|
+
RETRY_COUNT=0
|
|
998
|
+
HEALTH_CHECK_PASSED=false
|
|
999
|
+
HEALTH_CHECK_METHOD=""
|
|
1000
|
+
|
|
1001
|
+
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
|
1002
|
+
sleep 5
|
|
1003
|
+
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
1004
|
+
|
|
1005
|
+
# 1. 컨테이너 상태 확인
|
|
1006
|
+
STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")
|
|
1007
|
+
|
|
1008
|
+
if [ "$STATUS" = "exited" ]; then
|
|
1009
|
+
echo "❌ 컨테이너 비정상 종료!"
|
|
1010
|
+
echo ""
|
|
1011
|
+
echo "📋 컨테이너 로그 (최근 100줄):"
|
|
1012
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1013
|
+
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
1014
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1015
|
+
exit 1
|
|
1016
|
+
fi
|
|
1017
|
+
|
|
1018
|
+
if [ "$STATUS" = "running" ]; then
|
|
1019
|
+
# 2. HTTP Health Check 시도 (HEALTH_CHECK_PATH가 설정된 경우에만)
|
|
1020
|
+
if [ -n "$HEALTH_PATH" ]; then
|
|
1021
|
+
HEALTH=$(SUDO docker exec "${CONTAINER_NAME}" curl -sf "http://localhost:${INTERNAL_PORT}${HEALTH_PATH}" 2>/dev/null || echo "")
|
|
1022
|
+
|
|
1023
|
+
if [ -n "$HEALTH" ]; then
|
|
1024
|
+
echo "✅ 정상 기동 확인! (HTTP 응답: ${HEALTH_PATH})"
|
|
1025
|
+
HEALTH_CHECK_PASSED=true
|
|
1026
|
+
HEALTH_CHECK_METHOD="HTTP"
|
|
1027
|
+
break
|
|
1028
|
+
fi
|
|
1029
|
+
fi
|
|
1030
|
+
|
|
1031
|
+
# 3. HTTP 응답 없으면 로그 패턴 매칭으로 폴백
|
|
1032
|
+
if [ -n "$LOG_PATTERN" ]; then
|
|
1033
|
+
STARTED=$(SUDO docker logs --tail 50 "${CONTAINER_NAME}" 2>&1 | grep -E "$LOG_PATTERN" || echo "")
|
|
1034
|
+
|
|
1035
|
+
if [ -n "$STARTED" ]; then
|
|
1036
|
+
echo "✅ 정상 기동 확인! (로그 패턴)"
|
|
1037
|
+
echo " $STARTED"
|
|
1038
|
+
HEALTH_CHECK_PASSED=true
|
|
1039
|
+
HEALTH_CHECK_METHOD="Log"
|
|
1040
|
+
break
|
|
1041
|
+
fi
|
|
1042
|
+
fi
|
|
1043
|
+
fi
|
|
1044
|
+
|
|
1045
|
+
echo "⏳ 대기 중... ($RETRY_COUNT/$MAX_RETRIES) - 상태: $STATUS"
|
|
1046
|
+
done
|
|
1047
|
+
|
|
1048
|
+
if [ "$HEALTH_CHECK_PASSED" = "false" ]; then
|
|
1049
|
+
echo ""
|
|
1050
|
+
echo "❌ Health Check 타임아웃 (120초)"
|
|
1051
|
+
echo ""
|
|
1052
|
+
echo "📋 컨테이너 로그 (최근 100줄):"
|
|
1053
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1054
|
+
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
1055
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1056
|
+
exit 1
|
|
1057
|
+
fi
|
|
1058
|
+
|
|
1059
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1060
|
+
echo "✅ 배포 및 Health Check 완료! (방식: ${HEALTH_CHECK_METHOD})"
|
|
1061
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1062
|
+
|
|
1063
|
+
- name: 배포 완료 코멘트
|
|
1064
|
+
if: steps.check_branch.outputs.exists == 'true'
|
|
1065
|
+
uses: actions/github-script@v7
|
|
1066
|
+
with:
|
|
1067
|
+
script: |
|
|
1068
|
+
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
1069
|
+
const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
|
|
1070
|
+
const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
|
|
1071
|
+
const deployStart = ${{ steps.docker_progress.outputs.deploy_start }};
|
|
1072
|
+
const now = Date.now();
|
|
1073
|
+
const deployElapsed = now - deployStart;
|
|
1074
|
+
const minutes = Math.floor(deployElapsed / 60000);
|
|
1075
|
+
const seconds = Math.floor((deployElapsed % 60000) / 1000);
|
|
1076
|
+
const deployDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
|
|
1077
|
+
|
|
1078
|
+
const projectName = '${{ env.PROJECT_NAME }}';
|
|
1079
|
+
const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
|
|
1080
|
+
const previewPort = '${{ env.PREVIEW_PORT }}';
|
|
1081
|
+
const apiDocsPath = '${{ env.API_DOCS_PATH }}';
|
|
1082
|
+
const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
|
|
1083
|
+
const domain = `${projectName}-pr-${issueNumber}.${domainSuffix}`;
|
|
1084
|
+
const previewUrl = `http://${domain}:${previewPort}`;
|
|
1085
|
+
const sha = '${{ steps.issue.outputs.sha }}';
|
|
1086
|
+
|
|
1087
|
+
// Preview 환경 테이블 구성 (API_DOCS_PATH가 있을 때만 API Docs 행 추가)
|
|
1088
|
+
const envRows = [
|
|
1089
|
+
`| **Preview URL** | ${previewUrl} |`,
|
|
1090
|
+
];
|
|
1091
|
+
if (apiDocsPath) {
|
|
1092
|
+
envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
|
|
1093
|
+
}
|
|
1094
|
+
envRows.push(`| **컨테이너** | \`${projectName}-pr-${issueNumber}\` |`);
|
|
1095
|
+
envRows.push(`| **브랜치** | \`${branchName}\` |`);
|
|
1096
|
+
envRows.push(`| **커밋** | \`${sha}\` |`);
|
|
1097
|
+
|
|
1098
|
+
const body = [
|
|
1099
|
+
'## ✅ Issue Preview 배포 완료!',
|
|
1100
|
+
'',
|
|
1101
|
+
`**브랜치**: \`${branchName}\``,
|
|
1102
|
+
'',
|
|
1103
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
1104
|
+
'|------|------|----------|',
|
|
1105
|
+
`| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
|
|
1106
|
+
`| 🚀 서버 배포 & Health Check | ✅ 완료 | ${deployDuration} |`,
|
|
1107
|
+
'',
|
|
1108
|
+
'### 🌐 Preview 환경',
|
|
1109
|
+
'| 항목 | 값 |',
|
|
1110
|
+
'|------|-----|',
|
|
1111
|
+
...envRows,
|
|
1112
|
+
'',
|
|
1113
|
+
'### 📋 명령어',
|
|
1114
|
+
'| 명령어 | 설명 |',
|
|
1115
|
+
'|--------|------|',
|
|
1116
|
+
'| `@suh-lab server build` | 최신 커밋으로 재배포 |',
|
|
1117
|
+
'| `@suh-lab server destroy` | Preview 환경 삭제 |',
|
|
1118
|
+
'| `@suh-lab server status` | 현재 상태 확인 |',
|
|
1119
|
+
'',
|
|
1120
|
+
'<details>',
|
|
1121
|
+
'<summary>🔧 고급 명령어 (다른 Issue/PR에서 제어)</summary>',
|
|
1122
|
+
'',
|
|
1123
|
+
'```',
|
|
1124
|
+
`@suh-lab server build ${branchName}`,
|
|
1125
|
+
`@suh-lab server destroy ${branchName}`,
|
|
1126
|
+
`@suh-lab server status ${branchName}`,
|
|
1127
|
+
'```',
|
|
1128
|
+
'</details>',
|
|
1129
|
+
'',
|
|
1130
|
+
'---',
|
|
1131
|
+
'*🤖 이 댓글은 Issue Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
1132
|
+
].join('\n');
|
|
1133
|
+
|
|
1134
|
+
await github.rest.issues.updateComment({
|
|
1135
|
+
owner: context.repo.owner,
|
|
1136
|
+
repo: context.repo.repo,
|
|
1137
|
+
comment_id: commentId,
|
|
1138
|
+
body: body
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
- name: 빌드/배포 실패 시 에러 코멘트
|
|
1142
|
+
if: failure() && steps.check_branch.outputs.exists == 'true'
|
|
1143
|
+
uses: actions/github-script@v7
|
|
1144
|
+
with:
|
|
1145
|
+
script: |
|
|
1146
|
+
const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
|
|
1147
|
+
const projectName = '${{ env.PROJECT_NAME }}';
|
|
1148
|
+
const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
|
|
1149
|
+
const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
|
|
1150
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
1151
|
+
|
|
1152
|
+
const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
|
|
1153
|
+
|
|
1154
|
+
let dockerStatus, deployStatus;
|
|
1155
|
+
let dockerTime = '-';
|
|
1156
|
+
|
|
1157
|
+
if (!commentId) {
|
|
1158
|
+
dockerStatus = '⚠️ 초기화 실패';
|
|
1159
|
+
deployStatus = '-';
|
|
1160
|
+
} else if (!dockerDuration || dockerDuration === '') {
|
|
1161
|
+
dockerStatus = '❌ 실패';
|
|
1162
|
+
deployStatus = '⏸️ 대기';
|
|
1163
|
+
} else {
|
|
1164
|
+
dockerStatus = '✅ 완료';
|
|
1165
|
+
deployStatus = '❌ 실패';
|
|
1166
|
+
dockerTime = dockerDuration;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const body = [
|
|
1170
|
+
'## ❌ Issue Preview 배포 실패!',
|
|
1171
|
+
'',
|
|
1172
|
+
`**브랜치**: \`${branchName}\``,
|
|
1173
|
+
'',
|
|
1174
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
1175
|
+
'|------|------|----------|',
|
|
1176
|
+
`| 🐳 Docker 이미지 빌드 & Push | ${dockerStatus} | ${dockerTime} |`,
|
|
1177
|
+
`| 🚀 서버 배포 & Health Check | ${deployStatus} | - |`,
|
|
1178
|
+
'',
|
|
1179
|
+
`**[📋 빌드/배포 로그 확인](${runUrl})**`,
|
|
1180
|
+
'',
|
|
1181
|
+
'### 🔍 가능한 원인',
|
|
1182
|
+
'- Docker 이미지 빌드 실패 (Python 의존성 문제)',
|
|
1183
|
+
'- 컨테이너 시작 실패 (FastAPI/Uvicorn 기동 오류)',
|
|
1184
|
+
'- Health Check 타임아웃 (120초 내 기동 완료 안됨)',
|
|
1185
|
+
'- 환경변수 누락 (.env 파일 설정 확인)',
|
|
1186
|
+
'',
|
|
1187
|
+
'### 💡 다음 단계',
|
|
1188
|
+
'1. 위 링크에서 빌드/배포 로그를 확인하세요',
|
|
1189
|
+
'2. 문제를 수정한 후 다시 시도하세요: `@suh-lab server build`',
|
|
1190
|
+
'',
|
|
1191
|
+
'---',
|
|
1192
|
+
'*🤖 이 댓글은 Issue Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
1193
|
+
].join('\n');
|
|
1194
|
+
|
|
1195
|
+
if (commentId) {
|
|
1196
|
+
await github.rest.issues.updateComment({
|
|
1197
|
+
owner: context.repo.owner,
|
|
1198
|
+
repo: context.repo.repo,
|
|
1199
|
+
comment_id: commentId,
|
|
1200
|
+
body: body
|
|
1201
|
+
});
|
|
1202
|
+
} else {
|
|
1203
|
+
await github.rest.issues.createComment({
|
|
1204
|
+
owner: context.repo.owner,
|
|
1205
|
+
repo: context.repo.repo,
|
|
1206
|
+
issue_number: issueNumber,
|
|
1207
|
+
body: body
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
# -----------------------------------------------------------------
|
|
1212
|
+
# Job 3: Preview 삭제 (PR/Issue 닫힘 또는 destroy 명령)
|
|
1213
|
+
# -----------------------------------------------------------------
|
|
1214
|
+
destroy-preview:
|
|
1215
|
+
name: Preview 삭제
|
|
1216
|
+
# needs 의존성 제거: PR/Issue 닫힘 이벤트에서도 독립적으로 실행되도록 함
|
|
1217
|
+
if: |
|
|
1218
|
+
(github.event_name == 'pull_request' && github.event.action == 'closed') ||
|
|
1219
|
+
(github.event_name == 'issues' && github.event.action == 'closed') ||
|
|
1220
|
+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@suh-lab') && contains(github.event.comment.body, 'server destroy'))
|
|
1221
|
+
runs-on: ubuntu-latest
|
|
1222
|
+
steps:
|
|
1223
|
+
- name: PR/Issue 번호 가져오기
|
|
1224
|
+
id: pr_number
|
|
1225
|
+
run: |
|
|
1226
|
+
if [[ "${{ github.event_name }}" == "issue_comment" ]]; then
|
|
1227
|
+
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
|
|
1228
|
+
echo "type=comment" >> $GITHUB_OUTPUT
|
|
1229
|
+
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
|
1230
|
+
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
|
1231
|
+
echo "type=pr_closed" >> $GITHUB_OUTPUT
|
|
1232
|
+
else
|
|
1233
|
+
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
|
|
1234
|
+
echo "type=issue_closed" >> $GITHUB_OUTPUT
|
|
1235
|
+
fi
|
|
1236
|
+
|
|
1237
|
+
- name: 컨테이너 & 이미지 삭제
|
|
1238
|
+
uses: appleboy/ssh-action@v1.0.3
|
|
1239
|
+
env:
|
|
1240
|
+
SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
|
|
1241
|
+
with:
|
|
1242
|
+
host: ${{ secrets.SERVER_HOST }}
|
|
1243
|
+
username: ${{ secrets.SERVER_USER }}
|
|
1244
|
+
password: ${{ secrets.SERVER_PASSWORD }}
|
|
1245
|
+
key: ${{ secrets.SSH_KEY }}
|
|
1246
|
+
port: ${{ env.SSH_PORT }}
|
|
1247
|
+
envs: SSH_AUTH_METHOD
|
|
1248
|
+
script: |
|
|
1249
|
+
# 환경 변수 설정 (배포 서버용)
|
|
1250
|
+
export PATH=$PATH:/usr/local/bin
|
|
1251
|
+
export PW="${{ secrets.SERVER_PASSWORD }}"
|
|
1252
|
+
|
|
1253
|
+
# 🔐 SSH 인증 방식에 따른 sudo 추상화
|
|
1254
|
+
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
1255
|
+
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
1256
|
+
SUDO() { sudo "$@"; }
|
|
1257
|
+
else
|
|
1258
|
+
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
1259
|
+
fi
|
|
1260
|
+
echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
|
|
1261
|
+
|
|
1262
|
+
PR_NUMBER=${{ steps.pr_number.outputs.number }}
|
|
1263
|
+
PROJECT_NAME="${{ env.PROJECT_NAME }}"
|
|
1264
|
+
CONTAINER_NAME="${PROJECT_NAME}-pr-${PR_NUMBER}"
|
|
1265
|
+
IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:pr-${PR_NUMBER}"
|
|
1266
|
+
|
|
1267
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1268
|
+
echo "🗑️ PR Preview 삭제"
|
|
1269
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1270
|
+
echo "📦 프로젝트: ${PROJECT_NAME}"
|
|
1271
|
+
echo "🔢 PR 번호: #${PR_NUMBER}"
|
|
1272
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1273
|
+
|
|
1274
|
+
# 컨테이너 삭제
|
|
1275
|
+
echo "🐳 컨테이너 삭제 중..."
|
|
1276
|
+
SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
|
1277
|
+
|
|
1278
|
+
# 이미지 삭제
|
|
1279
|
+
echo "🖼️ 이미지 삭제 중..."
|
|
1280
|
+
SUDO docker rmi "${IMAGE}" 2>/dev/null || true
|
|
1281
|
+
|
|
1282
|
+
echo "✅ 삭제 완료!"
|
|
1283
|
+
|
|
1284
|
+
- name: 삭제 완료 코멘트
|
|
1285
|
+
if: |
|
|
1286
|
+
github.event_name == 'issue_comment' ||
|
|
1287
|
+
(github.event_name == 'pull_request' && github.event.action == 'closed') ||
|
|
1288
|
+
(github.event_name == 'issues' && github.event.action == 'closed')
|
|
1289
|
+
uses: actions/github-script@v7
|
|
1290
|
+
with:
|
|
1291
|
+
script: |
|
|
1292
|
+
const number = ${{ steps.pr_number.outputs.number }};
|
|
1293
|
+
const projectName = '${{ env.PROJECT_NAME }}';
|
|
1294
|
+
|
|
1295
|
+
const body = [
|
|
1296
|
+
'## 🗑️ Preview 환경 삭제 완료!',
|
|
1297
|
+
'',
|
|
1298
|
+
'| 항목 | 값 |',
|
|
1299
|
+
'|------|-----|',
|
|
1300
|
+
`| **컨테이너** | \`${projectName}-pr-${number}\` |`,
|
|
1301
|
+
'| **상태** | 삭제됨 |',
|
|
1302
|
+
'',
|
|
1303
|
+
'다시 배포하려면: `@suh-lab server build`',
|
|
1304
|
+
'',
|
|
1305
|
+
'---',
|
|
1306
|
+
'*🤖 이 댓글은 PR/Issue Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
1307
|
+
].join('\n');
|
|
1308
|
+
|
|
1309
|
+
await github.rest.issues.createComment({
|
|
1310
|
+
owner: context.repo.owner,
|
|
1311
|
+
repo: context.repo.repo,
|
|
1312
|
+
issue_number: number,
|
|
1313
|
+
body: body
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
# -----------------------------------------------------------------
|
|
1317
|
+
# Job 4: 상태 확인 (PR/Issue 모두 지원)
|
|
1318
|
+
# -----------------------------------------------------------------
|
|
1319
|
+
check-status:
|
|
1320
|
+
name: Preview 상태 확인
|
|
1321
|
+
needs: check-command
|
|
1322
|
+
if: needs.check-command.outputs.is_valid == 'true' && needs.check-command.outputs.command == 'status'
|
|
1323
|
+
runs-on: ubuntu-latest
|
|
1324
|
+
steps:
|
|
1325
|
+
- name: 컨테이너 상태 확인
|
|
1326
|
+
id: status
|
|
1327
|
+
uses: appleboy/ssh-action@v1.0.3
|
|
1328
|
+
with:
|
|
1329
|
+
host: ${{ secrets.SERVER_HOST }}
|
|
1330
|
+
username: ${{ secrets.SERVER_USER }}
|
|
1331
|
+
password: ${{ secrets.SERVER_PASSWORD }}
|
|
1332
|
+
key: ${{ secrets.SSH_KEY }}
|
|
1333
|
+
port: ${{ env.SSH_PORT }}
|
|
1334
|
+
script: |
|
|
1335
|
+
# 환경 변수 설정 (배포 서버용)
|
|
1336
|
+
export PATH=$PATH:/usr/local/bin
|
|
1337
|
+
|
|
1338
|
+
NUMBER=${{ github.event.issue.number }}
|
|
1339
|
+
PROJECT_NAME="${{ env.PROJECT_NAME }}"
|
|
1340
|
+
CONTAINER_NAME="${PROJECT_NAME}-pr-${NUMBER}"
|
|
1341
|
+
|
|
1342
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1343
|
+
echo "🔍 Preview 상태 확인"
|
|
1344
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1345
|
+
|
|
1346
|
+
if docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"; then
|
|
1347
|
+
echo "STATUS=running"
|
|
1348
|
+
echo "✅ 컨테이너 실행 중"
|
|
1349
|
+
docker ps --filter "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.RunningFor}}"
|
|
1350
|
+
else
|
|
1351
|
+
echo "STATUS=not_found"
|
|
1352
|
+
echo "❌ 컨테이너 없음"
|
|
1353
|
+
fi
|
|
1354
|
+
|
|
1355
|
+
- name: 상태 코멘트 (실행 중)
|
|
1356
|
+
uses: appleboy/ssh-action@v1.0.3
|
|
1357
|
+
id: check_running
|
|
1358
|
+
continue-on-error: true
|
|
1359
|
+
with:
|
|
1360
|
+
host: ${{ secrets.SERVER_HOST }}
|
|
1361
|
+
username: ${{ secrets.SERVER_USER }}
|
|
1362
|
+
password: ${{ secrets.SERVER_PASSWORD }}
|
|
1363
|
+
key: ${{ secrets.SSH_KEY }}
|
|
1364
|
+
port: ${{ env.SSH_PORT }}
|
|
1365
|
+
script: |
|
|
1366
|
+
# 환경 변수 설정 (배포 서버용)
|
|
1367
|
+
export PATH=$PATH:/usr/local/bin
|
|
1368
|
+
|
|
1369
|
+
NUMBER=${{ github.event.issue.number }}
|
|
1370
|
+
PROJECT_NAME="${{ env.PROJECT_NAME }}"
|
|
1371
|
+
CONTAINER_NAME="${PROJECT_NAME}-pr-${NUMBER}"
|
|
1372
|
+
docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"
|
|
1373
|
+
|
|
1374
|
+
- name: 상태 코멘트 작성
|
|
1375
|
+
uses: actions/github-script@v7
|
|
1376
|
+
with:
|
|
1377
|
+
script: |
|
|
1378
|
+
const number = context.issue.number;
|
|
1379
|
+
const projectName = '${{ env.PROJECT_NAME }}';
|
|
1380
|
+
const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
|
|
1381
|
+
const previewPort = '${{ env.PREVIEW_PORT }}';
|
|
1382
|
+
const domain = `${projectName}-pr-${number}.${domainSuffix}`;
|
|
1383
|
+
const previewUrl = `http://${domain}:${previewPort}`;
|
|
1384
|
+
const isRunning = '${{ steps.check_running.outcome }}' === 'success';
|
|
1385
|
+
const isPr = '${{ needs.check-command.outputs.is_pr }}' === 'true';
|
|
1386
|
+
const contextType = isPr ? 'PR' : 'Issue';
|
|
1387
|
+
|
|
1388
|
+
let body;
|
|
1389
|
+
if (isRunning) {
|
|
1390
|
+
body = [
|
|
1391
|
+
'## ✅ Preview 환경 실행 중',
|
|
1392
|
+
'',
|
|
1393
|
+
'| 항목 | 값 |',
|
|
1394
|
+
'|------|-----|',
|
|
1395
|
+
`| **Preview URL** | ${previewUrl} |`,
|
|
1396
|
+
`| **컨테이너** | \`${projectName}-pr-${number}\` |`,
|
|
1397
|
+
'| **상태** | 🟢 Running |',
|
|
1398
|
+
'',
|
|
1399
|
+
'### 📋 명령어',
|
|
1400
|
+
'| 명령어 | 설명 |',
|
|
1401
|
+
'|--------|------|',
|
|
1402
|
+
'| `@suh-lab server build` | 최신 커밋으로 재배포 |',
|
|
1403
|
+
'| `@suh-lab server destroy` | Preview 환경 삭제 |',
|
|
1404
|
+
'',
|
|
1405
|
+
'---',
|
|
1406
|
+
`*🤖 이 댓글은 ${contextType} Preview 시스템에 의해 자동 생성되었습니다.*`
|
|
1407
|
+
].join('\n');
|
|
1408
|
+
} else {
|
|
1409
|
+
body = [
|
|
1410
|
+
'## ❌ Preview 환경 없음',
|
|
1411
|
+
'',
|
|
1412
|
+
'| 항목 | 값 |',
|
|
1413
|
+
'|------|-----|',
|
|
1414
|
+
`| **컨테이너** | \`${projectName}-pr-${number}\` |`,
|
|
1415
|
+
'| **상태** | 🔴 Not Found |',
|
|
1416
|
+
'',
|
|
1417
|
+
'배포하려면: `@suh-lab server build`',
|
|
1418
|
+
'',
|
|
1419
|
+
'---',
|
|
1420
|
+
`*🤖 이 댓글은 ${contextType} Preview 시스템에 의해 자동 생성되었습니다.*`
|
|
1421
|
+
].join('\n');
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
await github.rest.issues.createComment({
|
|
1425
|
+
owner: context.repo.owner,
|
|
1426
|
+
repo: context.repo.repo,
|
|
1427
|
+
issue_number: number,
|
|
1428
|
+
body: body
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
# -----------------------------------------------------------------
|
|
1432
|
+
# Job 5: 커스텀 브랜치 빌드 & 배포
|
|
1433
|
+
# -----------------------------------------------------------------
|
|
1434
|
+
build-preview-custom-branch:
|
|
1435
|
+
name: Preview 빌드 & 배포 (Custom Branch)
|
|
1436
|
+
needs: check-command
|
|
1437
|
+
if: |
|
|
1438
|
+
needs.check-command.outputs.is_valid == 'true' &&
|
|
1439
|
+
needs.check-command.outputs.command == 'build' &&
|
|
1440
|
+
needs.check-command.outputs.is_custom_branch == 'true'
|
|
1441
|
+
runs-on: ubuntu-latest
|
|
1442
|
+
steps:
|
|
1443
|
+
- name: 브랜치 존재 확인 및 정보 추출
|
|
1444
|
+
id: branch_info
|
|
1445
|
+
uses: actions/github-script@v7
|
|
1446
|
+
with:
|
|
1447
|
+
script: |
|
|
1448
|
+
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
1449
|
+
const issueNumber = context.issue.number;
|
|
1450
|
+
|
|
1451
|
+
console.log(`🔍 브랜치 확인: ${branchName}`);
|
|
1452
|
+
|
|
1453
|
+
// 브랜치 존재 여부 확인
|
|
1454
|
+
try {
|
|
1455
|
+
await github.rest.repos.getBranch({
|
|
1456
|
+
owner: context.repo.owner,
|
|
1457
|
+
repo: context.repo.repo,
|
|
1458
|
+
branch: branchName
|
|
1459
|
+
});
|
|
1460
|
+
console.log(`✅ 브랜치 존재: ${branchName}`);
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
if (error.status === 404) {
|
|
1463
|
+
console.log(`❌ 브랜치 없음: ${branchName}`);
|
|
1464
|
+
|
|
1465
|
+
// 에러 댓글 작성
|
|
1466
|
+
const body = [
|
|
1467
|
+
'## ❌ 브랜치를 찾을 수 없습니다',
|
|
1468
|
+
'',
|
|
1469
|
+
'| 항목 | 값 |',
|
|
1470
|
+
'|------|-----|',
|
|
1471
|
+
`| **브랜치** | \`${branchName}\` |`,
|
|
1472
|
+
'',
|
|
1473
|
+
'### 💡 확인 사항',
|
|
1474
|
+
'1. 브랜치명이 정확한지 확인하세요',
|
|
1475
|
+
'2. 브랜치가 push되었는지 확인하세요',
|
|
1476
|
+
'3. 브랜치가 삭제되지 않았는지 확인하세요',
|
|
1477
|
+
'',
|
|
1478
|
+
'---',
|
|
1479
|
+
'*🤖 이 댓글은 Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
1480
|
+
].join('\n');
|
|
1481
|
+
|
|
1482
|
+
await github.rest.issues.createComment({
|
|
1483
|
+
owner: context.repo.owner,
|
|
1484
|
+
repo: context.repo.repo,
|
|
1485
|
+
issue_number: issueNumber,
|
|
1486
|
+
body: body
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
core.setFailed('브랜치를 찾을 수 없습니다');
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
throw error;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// 브랜치명에서 Issue 번호 추출 (#번호 패턴)
|
|
1496
|
+
const issueMatch = branchName.match(/#(\d+)/);
|
|
1497
|
+
|
|
1498
|
+
if (issueMatch) {
|
|
1499
|
+
// 번호 추출 성공
|
|
1500
|
+
core.setOutput('container_number', issueMatch[1]);
|
|
1501
|
+
core.setOutput('use_hash', 'false');
|
|
1502
|
+
console.log(`✅ Issue 번호 추출: #${issueMatch[1]}`);
|
|
1503
|
+
} else {
|
|
1504
|
+
// 해시 생성
|
|
1505
|
+
let hash = 0;
|
|
1506
|
+
for (let i = 0; i < branchName.length; i++) {
|
|
1507
|
+
const char = branchName.charCodeAt(i);
|
|
1508
|
+
hash = ((hash << 5) - hash) + char;
|
|
1509
|
+
hash = hash & hash;
|
|
1510
|
+
}
|
|
1511
|
+
const hashStr = Math.abs(hash).toString(16).slice(-6).padStart(6, '0');
|
|
1512
|
+
core.setOutput('container_suffix', `custom-${hashStr}`);
|
|
1513
|
+
core.setOutput('use_hash', 'true');
|
|
1514
|
+
console.log(`✅ 해시 생성: custom-${hashStr}`);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
core.setOutput('branch_name', branchName);
|
|
1518
|
+
core.setOutput('branch_exists', 'true');
|
|
1519
|
+
|
|
1520
|
+
- name: 브랜치의 최신 커밋 SHA 가져오기
|
|
1521
|
+
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1522
|
+
id: get_sha
|
|
1523
|
+
run: |
|
|
1524
|
+
SHA=$(git ls-remote https://github.com/${{ github.repository }}.git refs/heads/${{ needs.check-command.outputs.custom_branch }} | cut -c1-7)
|
|
1525
|
+
echo "sha=$SHA" >> $GITHUB_OUTPUT
|
|
1526
|
+
|
|
1527
|
+
# 진행 상황 댓글 생성
|
|
1528
|
+
- name: 진행 상황 댓글 생성
|
|
1529
|
+
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1530
|
+
id: progress
|
|
1531
|
+
uses: actions/github-script@v7
|
|
1532
|
+
with:
|
|
1533
|
+
script: |
|
|
1534
|
+
const issueNumber = context.issue.number;
|
|
1535
|
+
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
1536
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
1537
|
+
const startTime = Date.now();
|
|
1538
|
+
|
|
1539
|
+
const body = [
|
|
1540
|
+
'## 🚀 Custom Branch Preview 빌드 중...',
|
|
1541
|
+
'',
|
|
1542
|
+
`**브랜치**: \`${branchName}\``,
|
|
1543
|
+
'',
|
|
1544
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
1545
|
+
'|------|------|----------|',
|
|
1546
|
+
'| 🐳 Docker 이미지 빌드 & Push | ⏳ 진행 중... | - |',
|
|
1547
|
+
'| 🚀 서버 배포 & Health Check | ⏸️ 대기 | - |',
|
|
1548
|
+
'',
|
|
1549
|
+
`**[📋 실시간 로그 보기](${runUrl})**`,
|
|
1550
|
+
'',
|
|
1551
|
+
'---',
|
|
1552
|
+
'*🤖 이 댓글은 자동으로 업데이트됩니다.*'
|
|
1553
|
+
].join('\n');
|
|
1554
|
+
|
|
1555
|
+
const { data: comment } = await github.rest.issues.createComment({
|
|
1556
|
+
owner: context.repo.owner,
|
|
1557
|
+
repo: context.repo.repo,
|
|
1558
|
+
issue_number: issueNumber,
|
|
1559
|
+
body: body
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
core.setOutput('comment_id', comment.id);
|
|
1563
|
+
core.setOutput('start_time', startTime);
|
|
1564
|
+
core.setOutput('docker_start', startTime);
|
|
1565
|
+
|
|
1566
|
+
- name: 코드 체크아웃
|
|
1567
|
+
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1568
|
+
uses: actions/checkout@v5
|
|
1569
|
+
with:
|
|
1570
|
+
ref: ${{ needs.check-command.outputs.custom_branch }}
|
|
1571
|
+
|
|
1572
|
+
# 환경변수 파일 생성
|
|
1573
|
+
- name: "[필수] .env 파일 생성"
|
|
1574
|
+
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1575
|
+
run: |
|
|
1576
|
+
cat << 'EOF' > .env
|
|
1577
|
+
${{ secrets.ENV_FILE }}
|
|
1578
|
+
EOF
|
|
1579
|
+
|
|
1580
|
+
- name: Docker 로그인
|
|
1581
|
+
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1582
|
+
uses: docker/login-action@v3
|
|
1583
|
+
with:
|
|
1584
|
+
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
1585
|
+
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
1586
|
+
|
|
1587
|
+
- name: Docker Buildx 설정
|
|
1588
|
+
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1589
|
+
uses: docker/setup-buildx-action@v3
|
|
1590
|
+
|
|
1591
|
+
- name: 컨테이너 이름 결정
|
|
1592
|
+
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1593
|
+
id: container
|
|
1594
|
+
run: |
|
|
1595
|
+
if [[ "${{ steps.branch_info.outputs.use_hash }}" == "true" ]]; then
|
|
1596
|
+
SUFFIX="${{ steps.branch_info.outputs.container_suffix }}"
|
|
1597
|
+
else
|
|
1598
|
+
SUFFIX="pr-${{ steps.branch_info.outputs.container_number }}"
|
|
1599
|
+
fi
|
|
1600
|
+
echo "suffix=$SUFFIX" >> $GITHUB_OUTPUT
|
|
1601
|
+
echo "name=${{ env.PROJECT_NAME }}-${SUFFIX}" >> $GITHUB_OUTPUT
|
|
1602
|
+
|
|
1603
|
+
- name: Docker 이미지 빌드 & Push
|
|
1604
|
+
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1605
|
+
uses: docker/build-push-action@v5
|
|
1606
|
+
with:
|
|
1607
|
+
context: .
|
|
1608
|
+
file: ${{ env.DOCKERFILE_PATH }}
|
|
1609
|
+
push: true
|
|
1610
|
+
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}:${{ steps.container.outputs.suffix }}
|
|
1611
|
+
cache-from: type=gha
|
|
1612
|
+
cache-to: type=gha,mode=max
|
|
1613
|
+
|
|
1614
|
+
- name: 진행 상황 - Docker 완료
|
|
1615
|
+
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1616
|
+
id: docker_progress
|
|
1617
|
+
uses: actions/github-script@v7
|
|
1618
|
+
with:
|
|
1619
|
+
script: |
|
|
1620
|
+
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
1621
|
+
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
1622
|
+
const dockerStart = ${{ steps.progress.outputs.docker_start }};
|
|
1623
|
+
const now = Date.now();
|
|
1624
|
+
const dockerElapsed = now - dockerStart;
|
|
1625
|
+
const minutes = Math.floor(dockerElapsed / 60000);
|
|
1626
|
+
const seconds = Math.floor((dockerElapsed % 60000) / 1000);
|
|
1627
|
+
const dockerDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
|
|
1628
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
1629
|
+
|
|
1630
|
+
const body = [
|
|
1631
|
+
'## 🚀 Custom Branch Preview 빌드 중...',
|
|
1632
|
+
'',
|
|
1633
|
+
`**브랜치**: \`${branchName}\``,
|
|
1634
|
+
'',
|
|
1635
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
1636
|
+
'|------|------|----------|',
|
|
1637
|
+
`| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
|
|
1638
|
+
'| 🚀 서버 배포 & Health Check | ⏳ 진행 중... | - |',
|
|
1639
|
+
'',
|
|
1640
|
+
`**[📋 실시간 로그 보기](${runUrl})**`,
|
|
1641
|
+
'',
|
|
1642
|
+
'---',
|
|
1643
|
+
'*🤖 이 댓글은 자동으로 업데이트됩니다.*'
|
|
1644
|
+
].join('\n');
|
|
1645
|
+
|
|
1646
|
+
await github.rest.issues.updateComment({
|
|
1647
|
+
owner: context.repo.owner,
|
|
1648
|
+
repo: context.repo.repo,
|
|
1649
|
+
comment_id: commentId,
|
|
1650
|
+
body: body
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
core.setOutput('docker_duration', dockerDuration);
|
|
1654
|
+
core.setOutput('deploy_start', now);
|
|
1655
|
+
|
|
1656
|
+
- name: 서버에 배포
|
|
1657
|
+
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1658
|
+
uses: appleboy/ssh-action@v1.0.3
|
|
1659
|
+
env:
|
|
1660
|
+
SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
|
|
1661
|
+
with:
|
|
1662
|
+
host: ${{ secrets.SERVER_HOST }}
|
|
1663
|
+
username: ${{ secrets.SERVER_USER }}
|
|
1664
|
+
password: ${{ secrets.SERVER_PASSWORD }}
|
|
1665
|
+
key: ${{ secrets.SSH_KEY }}
|
|
1666
|
+
port: ${{ env.SSH_PORT }}
|
|
1667
|
+
envs: SSH_AUTH_METHOD
|
|
1668
|
+
script: |
|
|
1669
|
+
set -e
|
|
1670
|
+
|
|
1671
|
+
# 환경 변수 설정 (배포 서버용)
|
|
1672
|
+
export PATH=$PATH:/usr/local/bin
|
|
1673
|
+
export PW="${{ secrets.SERVER_PASSWORD }}"
|
|
1674
|
+
|
|
1675
|
+
# 🔐 SSH 인증 방식에 따른 sudo 추상화
|
|
1676
|
+
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
1677
|
+
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
1678
|
+
SUDO() { sudo "$@"; }
|
|
1679
|
+
else
|
|
1680
|
+
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
1681
|
+
fi
|
|
1682
|
+
echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
|
|
1683
|
+
|
|
1684
|
+
# 변수 설정
|
|
1685
|
+
PROJECT_NAME="${{ env.PROJECT_NAME }}"
|
|
1686
|
+
CONTAINER_NAME="${{ steps.container.outputs.name }}"
|
|
1687
|
+
CONTAINER_SUFFIX="${{ steps.container.outputs.suffix }}"
|
|
1688
|
+
IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:${CONTAINER_SUFFIX}"
|
|
1689
|
+
DOMAIN="${CONTAINER_NAME}.${{ env.PREVIEW_DOMAIN_SUFFIX }}"
|
|
1690
|
+
INTERNAL_PORT="${{ env.INTERNAL_PORT }}"
|
|
1691
|
+
TRAEFIK_NETWORK="${{ env.TRAEFIK_NETWORK }}"
|
|
1692
|
+
|
|
1693
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1694
|
+
echo "🚀 Custom Branch Preview 배포 시작"
|
|
1695
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1696
|
+
echo "📦 프로젝트: ${PROJECT_NAME}"
|
|
1697
|
+
echo "📛 컨테이너: ${CONTAINER_NAME}"
|
|
1698
|
+
echo "🌐 도메인: ${DOMAIN}"
|
|
1699
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1700
|
+
|
|
1701
|
+
# 이미지 Pull
|
|
1702
|
+
echo "📥 Docker 이미지 Pull 중..."
|
|
1703
|
+
SUDO docker pull "${IMAGE}"
|
|
1704
|
+
|
|
1705
|
+
# 기존 컨테이너 삭제
|
|
1706
|
+
echo "🗑️ 기존 컨테이너 정리 중..."
|
|
1707
|
+
SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
|
1708
|
+
|
|
1709
|
+
# 볼륨 마운트 옵션 구성
|
|
1710
|
+
VOLUME_OPTS="-v /etc/localtime:/etc/localtime:ro"
|
|
1711
|
+
if [ -n "${{ env.PROJECT_TARGET_DIR }}" ] && [ -n "${{ env.PROJECT_MNT_DIR }}" ]; then
|
|
1712
|
+
VOLUME_OPTS="$VOLUME_OPTS -v ${{ env.PROJECT_TARGET_DIR }}:${{ env.PROJECT_MNT_DIR }}"
|
|
1713
|
+
fi
|
|
1714
|
+
|
|
1715
|
+
# 새 컨테이너 실행
|
|
1716
|
+
echo "🐳 새 컨테이너 실행 중..."
|
|
1717
|
+
SUDO docker run -d \
|
|
1718
|
+
--name "${CONTAINER_NAME}" \
|
|
1719
|
+
--network "${TRAEFIK_NETWORK}" \
|
|
1720
|
+
--label "traefik.enable=true" \
|
|
1721
|
+
--label "traefik.http.routers.${CONTAINER_NAME}.rule=Host(\`${DOMAIN}\`)" \
|
|
1722
|
+
--label "traefik.http.routers.${CONTAINER_NAME}.entrypoints=web" \
|
|
1723
|
+
--label "traefik.http.services.${CONTAINER_NAME}.loadbalancer.server.port=${INTERNAL_PORT}" \
|
|
1724
|
+
-e TZ=Asia/Seoul \
|
|
1725
|
+
-e ENVIRONMENT=prod \
|
|
1726
|
+
$VOLUME_OPTS \
|
|
1727
|
+
"${IMAGE}"
|
|
1728
|
+
|
|
1729
|
+
# Health Check
|
|
1730
|
+
echo ""
|
|
1731
|
+
echo "⏳ Health Check 시작 (최대 120초 대기)..."
|
|
1732
|
+
HEALTH_PATH="${{ env.HEALTH_CHECK_PATH }}"
|
|
1733
|
+
LOG_PATTERN="${{ env.HEALTH_CHECK_LOG_PATTERN }}"
|
|
1734
|
+
MAX_RETRIES=24
|
|
1735
|
+
RETRY_COUNT=0
|
|
1736
|
+
HEALTH_CHECK_PASSED=false
|
|
1737
|
+
HEALTH_CHECK_METHOD=""
|
|
1738
|
+
|
|
1739
|
+
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
|
1740
|
+
sleep 5
|
|
1741
|
+
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
1742
|
+
|
|
1743
|
+
STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")
|
|
1744
|
+
|
|
1745
|
+
if [ "$STATUS" = "exited" ]; then
|
|
1746
|
+
echo "❌ 컨테이너 비정상 종료!"
|
|
1747
|
+
echo ""
|
|
1748
|
+
echo "📋 컨테이너 로그 (최근 100줄):"
|
|
1749
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1750
|
+
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
1751
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1752
|
+
exit 1
|
|
1753
|
+
fi
|
|
1754
|
+
|
|
1755
|
+
if [ "$STATUS" = "running" ]; then
|
|
1756
|
+
if [ -n "$HEALTH_PATH" ]; then
|
|
1757
|
+
HEALTH=$(SUDO docker exec "${CONTAINER_NAME}" curl -sf "http://localhost:${INTERNAL_PORT}${HEALTH_PATH}" 2>/dev/null || echo "")
|
|
1758
|
+
|
|
1759
|
+
if [ -n "$HEALTH" ]; then
|
|
1760
|
+
echo "✅ 정상 기동 확인! (HTTP 응답: ${HEALTH_PATH})"
|
|
1761
|
+
HEALTH_CHECK_PASSED=true
|
|
1762
|
+
HEALTH_CHECK_METHOD="HTTP"
|
|
1763
|
+
break
|
|
1764
|
+
fi
|
|
1765
|
+
fi
|
|
1766
|
+
|
|
1767
|
+
if [ -n "$LOG_PATTERN" ]; then
|
|
1768
|
+
STARTED=$(SUDO docker logs --tail 50 "${CONTAINER_NAME}" 2>&1 | grep -E "$LOG_PATTERN" || echo "")
|
|
1769
|
+
|
|
1770
|
+
if [ -n "$STARTED" ]; then
|
|
1771
|
+
echo "✅ 정상 기동 확인! (로그 패턴)"
|
|
1772
|
+
echo " $STARTED"
|
|
1773
|
+
HEALTH_CHECK_PASSED=true
|
|
1774
|
+
HEALTH_CHECK_METHOD="Log"
|
|
1775
|
+
break
|
|
1776
|
+
fi
|
|
1777
|
+
fi
|
|
1778
|
+
fi
|
|
1779
|
+
|
|
1780
|
+
echo "⏳ 대기 중... ($RETRY_COUNT/$MAX_RETRIES) - 상태: $STATUS"
|
|
1781
|
+
done
|
|
1782
|
+
|
|
1783
|
+
if [ "$HEALTH_CHECK_PASSED" = "false" ]; then
|
|
1784
|
+
echo ""
|
|
1785
|
+
echo "❌ Health Check 타임아웃 (120초)"
|
|
1786
|
+
echo ""
|
|
1787
|
+
echo "📋 컨테이너 로그 (최근 100줄):"
|
|
1788
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1789
|
+
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
1790
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1791
|
+
exit 1
|
|
1792
|
+
fi
|
|
1793
|
+
|
|
1794
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1795
|
+
echo "✅ 배포 및 Health Check 완료! (방식: ${HEALTH_CHECK_METHOD})"
|
|
1796
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1797
|
+
|
|
1798
|
+
- name: 배포 완료 코멘트
|
|
1799
|
+
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1800
|
+
uses: actions/github-script@v7
|
|
1801
|
+
with:
|
|
1802
|
+
script: |
|
|
1803
|
+
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
1804
|
+
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
1805
|
+
const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
|
|
1806
|
+
const deployStart = ${{ steps.docker_progress.outputs.deploy_start }};
|
|
1807
|
+
const now = Date.now();
|
|
1808
|
+
const deployElapsed = now - deployStart;
|
|
1809
|
+
const minutes = Math.floor(deployElapsed / 60000);
|
|
1810
|
+
const seconds = Math.floor((deployElapsed % 60000) / 1000);
|
|
1811
|
+
const deployDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
|
|
1812
|
+
|
|
1813
|
+
const projectName = '${{ env.PROJECT_NAME }}';
|
|
1814
|
+
const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
|
|
1815
|
+
const previewPort = '${{ env.PREVIEW_PORT }}';
|
|
1816
|
+
const apiDocsPath = '${{ env.API_DOCS_PATH }}';
|
|
1817
|
+
const containerName = '${{ steps.container.outputs.name }}';
|
|
1818
|
+
const domain = `${containerName}.${domainSuffix}`;
|
|
1819
|
+
const previewUrl = `http://${domain}:${previewPort}`;
|
|
1820
|
+
const sha = '${{ steps.get_sha.outputs.sha }}';
|
|
1821
|
+
|
|
1822
|
+
// Preview 환경 테이블 구성
|
|
1823
|
+
const envRows = [
|
|
1824
|
+
`| **Preview URL** | ${previewUrl} |`,
|
|
1825
|
+
];
|
|
1826
|
+
if (apiDocsPath) {
|
|
1827
|
+
envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
|
|
1828
|
+
}
|
|
1829
|
+
envRows.push(`| **컨테이너** | \`${containerName}\` |`);
|
|
1830
|
+
envRows.push(`| **브랜치** | \`${branchName}\` |`);
|
|
1831
|
+
envRows.push(`| **커밋** | \`${sha}\` |`);
|
|
1832
|
+
|
|
1833
|
+
const body = [
|
|
1834
|
+
'## ✅ Custom Branch Preview 배포 완료!',
|
|
1835
|
+
'',
|
|
1836
|
+
`**브랜치**: \`${branchName}\``,
|
|
1837
|
+
'',
|
|
1838
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
1839
|
+
'|------|------|----------|',
|
|
1840
|
+
`| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
|
|
1841
|
+
`| 🚀 서버 배포 & Health Check | ✅ 완료 | ${deployDuration} |`,
|
|
1842
|
+
'',
|
|
1843
|
+
'### 🌐 Preview 환경',
|
|
1844
|
+
'| 항목 | 값 |',
|
|
1845
|
+
'|------|-----|',
|
|
1846
|
+
...envRows,
|
|
1847
|
+
'',
|
|
1848
|
+
'### 📋 명령어',
|
|
1849
|
+
'| 명령어 | 설명 |',
|
|
1850
|
+
'|--------|------|',
|
|
1851
|
+
'| `@suh-lab server build` | 최신 커밋으로 재배포 |',
|
|
1852
|
+
'| `@suh-lab server destroy` | Preview 환경 삭제 |',
|
|
1853
|
+
'| `@suh-lab server status` | 현재 상태 확인 |',
|
|
1854
|
+
'',
|
|
1855
|
+
'<details>',
|
|
1856
|
+
'<summary>🔧 고급 명령어 (다른 Issue/PR에서 제어)</summary>',
|
|
1857
|
+
'',
|
|
1858
|
+
'```',
|
|
1859
|
+
`@suh-lab server build ${branchName}`,
|
|
1860
|
+
`@suh-lab server destroy ${branchName}`,
|
|
1861
|
+
`@suh-lab server status ${branchName}`,
|
|
1862
|
+
'```',
|
|
1863
|
+
'</details>',
|
|
1864
|
+
'',
|
|
1865
|
+
'---',
|
|
1866
|
+
'*🤖 이 댓글은 Custom Branch Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
1867
|
+
].join('\n');
|
|
1868
|
+
|
|
1869
|
+
await github.rest.issues.updateComment({
|
|
1870
|
+
owner: context.repo.owner,
|
|
1871
|
+
repo: context.repo.repo,
|
|
1872
|
+
comment_id: commentId,
|
|
1873
|
+
body: body
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
- name: 빌드/배포 실패 시 에러 코멘트
|
|
1877
|
+
if: failure() && steps.branch_info.outputs.branch_exists == 'true'
|
|
1878
|
+
uses: actions/github-script@v7
|
|
1879
|
+
with:
|
|
1880
|
+
script: |
|
|
1881
|
+
const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
|
|
1882
|
+
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
1883
|
+
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
1884
|
+
|
|
1885
|
+
const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
|
|
1886
|
+
|
|
1887
|
+
let dockerStatus, deployStatus;
|
|
1888
|
+
let dockerTime = '-';
|
|
1889
|
+
|
|
1890
|
+
if (!commentId) {
|
|
1891
|
+
dockerStatus = '⚠️ 초기화 실패';
|
|
1892
|
+
deployStatus = '-';
|
|
1893
|
+
} else if (!dockerDuration || dockerDuration === '') {
|
|
1894
|
+
dockerStatus = '❌ 실패';
|
|
1895
|
+
deployStatus = '⏸️ 대기';
|
|
1896
|
+
} else {
|
|
1897
|
+
dockerStatus = '✅ 완료';
|
|
1898
|
+
deployStatus = '❌ 실패';
|
|
1899
|
+
dockerTime = dockerDuration;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
const body = [
|
|
1903
|
+
'## ❌ Custom Branch Preview 배포 실패!',
|
|
1904
|
+
'',
|
|
1905
|
+
`**브랜치**: \`${branchName}\``,
|
|
1906
|
+
'',
|
|
1907
|
+
'| 단계 | 상태 | 소요 시간 |',
|
|
1908
|
+
'|------|------|----------|',
|
|
1909
|
+
`| 🐳 Docker 이미지 빌드 & Push | ${dockerStatus} | ${dockerTime} |`,
|
|
1910
|
+
`| 🚀 서버 배포 & Health Check | ${deployStatus} | - |`,
|
|
1911
|
+
'',
|
|
1912
|
+
`**[📋 빌드/배포 로그 확인](${runUrl})**`,
|
|
1913
|
+
'',
|
|
1914
|
+
'### 🔍 가능한 원인',
|
|
1915
|
+
'- Docker 이미지 빌드 실패 (Python 의존성 문제)',
|
|
1916
|
+
'- 컨테이너 시작 실패 (FastAPI/Uvicorn 기동 오류)',
|
|
1917
|
+
'- Health Check 타임아웃 (120초 내 기동 완료 안됨)',
|
|
1918
|
+
'- 환경변수 누락 (.env 파일 설정 확인)',
|
|
1919
|
+
'',
|
|
1920
|
+
'### 💡 다음 단계',
|
|
1921
|
+
'1. 위 링크에서 빌드/배포 로그를 확인하세요',
|
|
1922
|
+
`2. 문제를 수정한 후 다시 시도하세요: \`@suh-lab server build ${branchName}\``,
|
|
1923
|
+
'',
|
|
1924
|
+
'---',
|
|
1925
|
+
'*🤖 이 댓글은 Custom Branch Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
1926
|
+
].join('\n');
|
|
1927
|
+
|
|
1928
|
+
if (commentId) {
|
|
1929
|
+
await github.rest.issues.updateComment({
|
|
1930
|
+
owner: context.repo.owner,
|
|
1931
|
+
repo: context.repo.repo,
|
|
1932
|
+
comment_id: commentId,
|
|
1933
|
+
body: body
|
|
1934
|
+
});
|
|
1935
|
+
} else {
|
|
1936
|
+
await github.rest.issues.createComment({
|
|
1937
|
+
owner: context.repo.owner,
|
|
1938
|
+
repo: context.repo.repo,
|
|
1939
|
+
issue_number: context.issue.number,
|
|
1940
|
+
body: body
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
# -----------------------------------------------------------------
|
|
1945
|
+
# Job 6: 커스텀 브랜치 Preview 삭제
|
|
1946
|
+
# -----------------------------------------------------------------
|
|
1947
|
+
destroy-preview-custom-branch:
|
|
1948
|
+
name: Preview 삭제 (Custom Branch)
|
|
1949
|
+
needs: check-command
|
|
1950
|
+
if: |
|
|
1951
|
+
needs.check-command.outputs.is_valid == 'true' &&
|
|
1952
|
+
needs.check-command.outputs.command == 'destroy' &&
|
|
1953
|
+
needs.check-command.outputs.is_custom_branch == 'true'
|
|
1954
|
+
runs-on: ubuntu-latest
|
|
1955
|
+
steps:
|
|
1956
|
+
- name: 브랜치명에서 컨테이너 정보 추출
|
|
1957
|
+
id: container_info
|
|
1958
|
+
uses: actions/github-script@v7
|
|
1959
|
+
with:
|
|
1960
|
+
script: |
|
|
1961
|
+
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
1962
|
+
const projectName = '${{ env.PROJECT_NAME }}';
|
|
1963
|
+
|
|
1964
|
+
// 브랜치명에서 Issue 번호 추출 (#번호 패턴)
|
|
1965
|
+
const issueMatch = branchName.match(/#(\d+)/);
|
|
1966
|
+
|
|
1967
|
+
let containerName;
|
|
1968
|
+
if (issueMatch) {
|
|
1969
|
+
containerName = `${projectName}-pr-${issueMatch[1]}`;
|
|
1970
|
+
console.log(`✅ Issue 번호 추출: #${issueMatch[1]}`);
|
|
1971
|
+
} else {
|
|
1972
|
+
// 해시 생성
|
|
1973
|
+
let hash = 0;
|
|
1974
|
+
for (let i = 0; i < branchName.length; i++) {
|
|
1975
|
+
const char = branchName.charCodeAt(i);
|
|
1976
|
+
hash = ((hash << 5) - hash) + char;
|
|
1977
|
+
hash = hash & hash;
|
|
1978
|
+
}
|
|
1979
|
+
const hashStr = Math.abs(hash).toString(16).slice(-6).padStart(6, '0');
|
|
1980
|
+
containerName = `${projectName}-custom-${hashStr}`;
|
|
1981
|
+
console.log(`✅ 해시 생성: custom-${hashStr}`);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
core.setOutput('container_name', containerName);
|
|
1985
|
+
core.setOutput('branch_name', branchName);
|
|
1986
|
+
|
|
1987
|
+
- name: 컨테이너 & 이미지 삭제
|
|
1988
|
+
uses: appleboy/ssh-action@v1.0.3
|
|
1989
|
+
env:
|
|
1990
|
+
SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
|
|
1991
|
+
with:
|
|
1992
|
+
host: ${{ secrets.SERVER_HOST }}
|
|
1993
|
+
username: ${{ secrets.SERVER_USER }}
|
|
1994
|
+
password: ${{ secrets.SERVER_PASSWORD }}
|
|
1995
|
+
key: ${{ secrets.SSH_KEY }}
|
|
1996
|
+
port: ${{ env.SSH_PORT }}
|
|
1997
|
+
envs: SSH_AUTH_METHOD
|
|
1998
|
+
script: |
|
|
1999
|
+
# 환경 변수 설정 (배포 서버용)
|
|
2000
|
+
export PATH=$PATH:/usr/local/bin
|
|
2001
|
+
export PW="${{ secrets.SERVER_PASSWORD }}"
|
|
2002
|
+
|
|
2003
|
+
# 🔐 SSH 인증 방식에 따른 sudo 추상화
|
|
2004
|
+
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
2005
|
+
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
2006
|
+
SUDO() { sudo "$@"; }
|
|
2007
|
+
else
|
|
2008
|
+
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
2009
|
+
fi
|
|
2010
|
+
echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
|
|
2011
|
+
|
|
2012
|
+
CONTAINER_NAME="${{ steps.container_info.outputs.container_name }}"
|
|
2013
|
+
PROJECT_NAME="${{ env.PROJECT_NAME }}"
|
|
2014
|
+
|
|
2015
|
+
# 컨테이너명에서 suffix 추출
|
|
2016
|
+
SUFFIX="${CONTAINER_NAME#${PROJECT_NAME}-}"
|
|
2017
|
+
IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:${SUFFIX}"
|
|
2018
|
+
|
|
2019
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2020
|
+
echo "🗑️ Custom Branch Preview 삭제"
|
|
2021
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2022
|
+
echo "📦 프로젝트: ${PROJECT_NAME}"
|
|
2023
|
+
echo "📛 컨테이너: ${CONTAINER_NAME}"
|
|
2024
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2025
|
+
|
|
2026
|
+
# 컨테이너 삭제
|
|
2027
|
+
echo "🐳 컨테이너 삭제 중..."
|
|
2028
|
+
SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
|
2029
|
+
|
|
2030
|
+
# 이미지 삭제
|
|
2031
|
+
echo "🖼️ 이미지 삭제 중..."
|
|
2032
|
+
SUDO docker rmi "${IMAGE}" 2>/dev/null || true
|
|
2033
|
+
|
|
2034
|
+
echo "✅ 삭제 완료!"
|
|
2035
|
+
|
|
2036
|
+
- name: 삭제 완료 코멘트
|
|
2037
|
+
uses: actions/github-script@v7
|
|
2038
|
+
with:
|
|
2039
|
+
script: |
|
|
2040
|
+
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
2041
|
+
const containerName = '${{ steps.container_info.outputs.container_name }}';
|
|
2042
|
+
|
|
2043
|
+
const body = [
|
|
2044
|
+
'## 🗑️ Custom Branch Preview 환경 삭제 완료!',
|
|
2045
|
+
'',
|
|
2046
|
+
'| 항목 | 값 |',
|
|
2047
|
+
'|------|-----|',
|
|
2048
|
+
`| **브랜치** | \`${branchName}\` |`,
|
|
2049
|
+
`| **컨테이너** | \`${containerName}\` |`,
|
|
2050
|
+
'| **상태** | 삭제됨 |',
|
|
2051
|
+
'',
|
|
2052
|
+
`다시 배포하려면: \`@suh-lab server build ${branchName}\``,
|
|
2053
|
+
'',
|
|
2054
|
+
'---',
|
|
2055
|
+
'*🤖 이 댓글은 Custom Branch Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
2056
|
+
].join('\n');
|
|
2057
|
+
|
|
2058
|
+
await github.rest.issues.createComment({
|
|
2059
|
+
owner: context.repo.owner,
|
|
2060
|
+
repo: context.repo.repo,
|
|
2061
|
+
issue_number: context.issue.number,
|
|
2062
|
+
body: body
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
# -----------------------------------------------------------------
|
|
2066
|
+
# Job 7: 커스텀 브랜치 Preview 상태 확인
|
|
2067
|
+
# -----------------------------------------------------------------
|
|
2068
|
+
check-status-custom-branch:
|
|
2069
|
+
name: Preview 상태 확인 (Custom Branch)
|
|
2070
|
+
needs: check-command
|
|
2071
|
+
if: |
|
|
2072
|
+
needs.check-command.outputs.is_valid == 'true' &&
|
|
2073
|
+
needs.check-command.outputs.command == 'status' &&
|
|
2074
|
+
needs.check-command.outputs.is_custom_branch == 'true'
|
|
2075
|
+
runs-on: ubuntu-latest
|
|
2076
|
+
steps:
|
|
2077
|
+
- name: 브랜치명에서 컨테이너 정보 추출
|
|
2078
|
+
id: container_info
|
|
2079
|
+
uses: actions/github-script@v7
|
|
2080
|
+
with:
|
|
2081
|
+
script: |
|
|
2082
|
+
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
2083
|
+
const projectName = '${{ env.PROJECT_NAME }}';
|
|
2084
|
+
|
|
2085
|
+
// 브랜치명에서 Issue 번호 추출 (#번호 패턴)
|
|
2086
|
+
const issueMatch = branchName.match(/#(\d+)/);
|
|
2087
|
+
|
|
2088
|
+
let containerName;
|
|
2089
|
+
if (issueMatch) {
|
|
2090
|
+
containerName = `${projectName}-pr-${issueMatch[1]}`;
|
|
2091
|
+
} else {
|
|
2092
|
+
// 해시 생성
|
|
2093
|
+
let hash = 0;
|
|
2094
|
+
for (let i = 0; i < branchName.length; i++) {
|
|
2095
|
+
const char = branchName.charCodeAt(i);
|
|
2096
|
+
hash = ((hash << 5) - hash) + char;
|
|
2097
|
+
hash = hash & hash;
|
|
2098
|
+
}
|
|
2099
|
+
const hashStr = Math.abs(hash).toString(16).slice(-6).padStart(6, '0');
|
|
2100
|
+
containerName = `${projectName}-custom-${hashStr}`;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
core.setOutput('container_name', containerName);
|
|
2104
|
+
core.setOutput('branch_name', branchName);
|
|
2105
|
+
|
|
2106
|
+
- name: 컨테이너 상태 확인
|
|
2107
|
+
uses: appleboy/ssh-action@v1.0.3
|
|
2108
|
+
id: check_running
|
|
2109
|
+
continue-on-error: true
|
|
2110
|
+
with:
|
|
2111
|
+
host: ${{ secrets.SERVER_HOST }}
|
|
2112
|
+
username: ${{ secrets.SERVER_USER }}
|
|
2113
|
+
password: ${{ secrets.SERVER_PASSWORD }}
|
|
2114
|
+
key: ${{ secrets.SSH_KEY }}
|
|
2115
|
+
port: ${{ env.SSH_PORT }}
|
|
2116
|
+
script: |
|
|
2117
|
+
export PATH=$PATH:/usr/local/bin
|
|
2118
|
+
|
|
2119
|
+
CONTAINER_NAME="${{ steps.container_info.outputs.container_name }}"
|
|
2120
|
+
|
|
2121
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2122
|
+
echo "🔍 Custom Branch Preview 상태 확인"
|
|
2123
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2124
|
+
|
|
2125
|
+
if docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"; then
|
|
2126
|
+
echo "STATUS=running"
|
|
2127
|
+
echo "✅ 컨테이너 실행 중"
|
|
2128
|
+
docker ps --filter "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.RunningFor}}"
|
|
2129
|
+
else
|
|
2130
|
+
echo "STATUS=not_found"
|
|
2131
|
+
echo "❌ 컨테이너 없음"
|
|
2132
|
+
exit 1
|
|
2133
|
+
fi
|
|
2134
|
+
|
|
2135
|
+
- name: 상태 코멘트 작성
|
|
2136
|
+
uses: actions/github-script@v7
|
|
2137
|
+
with:
|
|
2138
|
+
script: |
|
|
2139
|
+
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
2140
|
+
const containerName = '${{ steps.container_info.outputs.container_name }}';
|
|
2141
|
+
const projectName = '${{ env.PROJECT_NAME }}';
|
|
2142
|
+
const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
|
|
2143
|
+
const previewPort = '${{ env.PREVIEW_PORT }}';
|
|
2144
|
+
const domain = `${containerName}.${domainSuffix}`;
|
|
2145
|
+
const previewUrl = `http://${domain}:${previewPort}`;
|
|
2146
|
+
const isRunning = '${{ steps.check_running.outcome }}' === 'success';
|
|
2147
|
+
|
|
2148
|
+
let body;
|
|
2149
|
+
if (isRunning) {
|
|
2150
|
+
body = [
|
|
2151
|
+
'## ✅ Custom Branch Preview 환경 실행 중',
|
|
2152
|
+
'',
|
|
2153
|
+
'| 항목 | 값 |',
|
|
2154
|
+
'|------|-----|',
|
|
2155
|
+
`| **브랜치** | \`${branchName}\` |`,
|
|
2156
|
+
`| **Preview URL** | ${previewUrl} |`,
|
|
2157
|
+
`| **컨테이너** | \`${containerName}\` |`,
|
|
2158
|
+
'| **상태** | 🟢 Running |',
|
|
2159
|
+
'',
|
|
2160
|
+
'### 📋 명령어',
|
|
2161
|
+
'| 명령어 | 설명 |',
|
|
2162
|
+
'|--------|------|',
|
|
2163
|
+
`| \`@suh-lab server build ${branchName}\` | 최신 커밋으로 재배포 |`,
|
|
2164
|
+
`| \`@suh-lab server destroy ${branchName}\` | Preview 환경 삭제 |`,
|
|
2165
|
+
'',
|
|
2166
|
+
'---',
|
|
2167
|
+
'*🤖 이 댓글은 Custom Branch Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
2168
|
+
].join('\n');
|
|
2169
|
+
} else {
|
|
2170
|
+
body = [
|
|
2171
|
+
'## ❌ Custom Branch Preview 환경 없음',
|
|
2172
|
+
'',
|
|
2173
|
+
'| 항목 | 값 |',
|
|
2174
|
+
'|------|-----|',
|
|
2175
|
+
`| **브랜치** | \`${branchName}\` |`,
|
|
2176
|
+
`| **컨테이너** | \`${containerName}\` |`,
|
|
2177
|
+
'| **상태** | 🔴 Not Found |',
|
|
2178
|
+
'',
|
|
2179
|
+
`배포하려면: \`@suh-lab server build ${branchName}\``,
|
|
2180
|
+
'',
|
|
2181
|
+
'---',
|
|
2182
|
+
'*🤖 이 댓글은 Custom Branch Preview 시스템에 의해 자동 생성되었습니다.*'
|
|
2183
|
+
].join('\n');
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
await github.rest.issues.createComment({
|
|
2187
|
+
owner: context.repo.owner,
|
|
2188
|
+
repo: context.repo.repo,
|
|
2189
|
+
issue_number: context.issue.number,
|
|
2190
|
+
body: body
|
|
2191
|
+
});
|