project-auto-wizard 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/bin/project-auto-wizard.js +19 -0
  4. package/package.json +36 -0
  5. package/payload/coderabbit.yaml +19 -0
  6. package/payload/config/breaking-changes.json +1 -0
  7. package/payload/config/wizard-prompts.yml +62 -0
  8. package/payload/scripts/changelog_manager.py +726 -0
  9. package/payload/scripts/version_manager.py +617 -0
  10. package/payload/version.yml.template +48 -0
  11. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +303 -0
  12. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +293 -0
  13. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +289 -0
  14. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +192 -0
  15. package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +209 -0
  16. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +591 -0
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +700 -0
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +308 -0
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +992 -0
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +689 -0
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +987 -0
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +471 -0
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml +500 -0
  24. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +185 -0
  25. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +271 -0
  26. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +81 -0
  27. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +2191 -0
  28. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +386 -0
  29. package/payload/workflows/react/PROJECT-REACT-CI.yaml +194 -0
  30. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +255 -0
  31. package/payload/workflows/spring/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +84 -0
  32. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +316 -0
  33. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +58 -0
  34. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +535 -0
  35. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +437 -0
  36. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +2277 -0
  37. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +425 -0
  38. package/src/cli/args.js +95 -0
  39. package/src/cli/help.js +27 -0
  40. package/src/commands/full.js +53 -0
  41. package/src/commands/interactive.js +276 -0
  42. package/src/commands/revert.js +60 -0
  43. package/src/commands/version.js +31 -0
  44. package/src/commands/workflows.js +49 -0
  45. package/src/context.js +26 -0
  46. package/src/core/assets.js +47 -0
  47. package/src/core/branches.js +60 -0
  48. package/src/core/branding.js +15 -0
  49. package/src/core/breaking-check.js +65 -0
  50. package/src/core/breaking.js +26 -0
  51. package/src/core/copy/coderabbit.js +26 -0
  52. package/src/core/copy/gitignore.js +55 -0
  53. package/src/core/copy/readme.js +30 -0
  54. package/src/core/copy/simple.js +23 -0
  55. package/src/core/copy/workflows.js +233 -0
  56. package/src/core/detect-fs.js +106 -0
  57. package/src/core/detect.js +62 -0
  58. package/src/core/fsutil.js +46 -0
  59. package/src/core/options-ask.js +99 -0
  60. package/src/core/paths-resolve.js +261 -0
  61. package/src/core/paths.js +16 -0
  62. package/src/core/version-yml.js +190 -0
  63. package/src/core/wizard-env.js +101 -0
  64. package/src/core/wizard-labels.js +107 -0
  65. package/src/index.js +162 -0
  66. package/src/ui/ansi.js +26 -0
  67. package/src/ui/banner.js +29 -0
  68. package/src/ui/env-plan.js +207 -0
  69. package/src/ui/prompts.js +99 -0
  70. package/src/ui/readline-engine.js +257 -0
  71. package/src/ui/status-cards.js +56 -0
  72. package/src/ui/summary.js +127 -0
@@ -0,0 +1,987 @@
1
+ # ===================================================================
2
+ # Flutter iOS 테스트용 TestFlight 배포 워크플로우
3
+ # ===================================================================
4
+ #
5
+ # 이 워크플로우는 기능 브랜치에서 수동으로 실행하여
6
+ # 테스트용 iOS 빌드를 TestFlight에 배포합니다.
7
+ #
8
+ # ★ 마법사 우선 아키텍처 ★
9
+ # - 빌드에 필요한 설정 파일들은 웹 마법사가 생성합니다
10
+ # - 워크플로우는 마법사가 생성한 파일들을 그대로 사용합니다
11
+ # - 마법사 경로: .github/util/flutter/ios-testflight-setup-wizard/index.html
12
+ #
13
+ # 주요 특징:
14
+ # - workflow_dispatch로 수동 실행 또는 repository_dispatch로 트리거
15
+ # - 버전은 version.yml에서 동적으로 읽어 사용 (운영 버전과 동일, 빌드 번호로 구분)
16
+ # - 빌드 번호는 PR번호 + 카운트 또는 GITHUB_RUN_NUMBER 사용
17
+ # - 브랜치명에서 이슈 번호 자동 추출 (YYYYMMDD_#이슈번호_내용 형식)
18
+ # - GitHub API로 이슈 정보 가져와서 릴리즈 노트에 포함
19
+ #
20
+ # 빌드 파이프라인:
21
+ # 1. flutter build ios --no-codesign (Flutter 빌드)
22
+ # 2. xcodebuild archive (Xcode 아카이브 생성)
23
+ # 3. xcodebuild -exportArchive (IPA 생성, ExportOptions.plist 사용)
24
+ # 4. fastlane upload_testflight (마법사 생성 Fastfile 사용)
25
+ #
26
+ # ===================================================================
27
+ # 🔑 필수 GitHub Secrets
28
+ # ===================================================================
29
+ #
30
+ # 🔐 Apple 인증서 & 프로비저닝 프로파일:
31
+ # APPLE_CERTIFICATE_BASE64: .p12 인증서 (base64 인코딩)
32
+ # APPLE_CERTIFICATE_PASSWORD: .p12 인증서 비밀번호
33
+ # APPLE_PROVISIONING_PROFILE_BASE64: .mobileprovision 파일 (base64 인코딩)
34
+ # IOS_PROVISIONING_PROFILE_NAME: 프로비저닝 프로파일 이름
35
+ #
36
+ # 🔐 App Store Connect API:
37
+ # APP_STORE_CONNECT_API_KEY_ID: API Key ID (10자리)
38
+ # APP_STORE_CONNECT_ISSUER_ID: Issuer ID (UUID 형식)
39
+ # APP_STORE_CONNECT_API_KEY_BASE64: AuthKey_XXXXXX.p8 파일 (base64 인코딩)
40
+ #
41
+ # 📝 환경 설정 (선택):
42
+ # ENV_FILE (선택): .env 파일 내용
43
+ # SECRETS_XCCONFIG (선택): ios/Flutter/Secrets.xcconfig 내용
44
+ #
45
+ # ===================================================================
46
+
47
+ name: PROJECT-Flutter-iOS-Test-TestFlight
48
+
49
+ concurrency:
50
+ group: ${{ github.workflow }}-${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref }}
51
+ cancel-in-progress: true
52
+
53
+ on:
54
+ workflow_dispatch:
55
+ repository_dispatch:
56
+ types: [build-ios-app]
57
+
58
+ # ===================================================================
59
+ # 🔧 프로젝트별 설정 (아래 값들을 수정하세요)
60
+ # ===================================================================
61
+ env:
62
+ FLUTTER_VERSION: "3.35.5"
63
+ XCODE_VERSION: "26.0"
64
+ PROJECT_TYPE: "flutter"
65
+ ENV_FILE_PATH: ".env"
66
+
67
+ jobs:
68
+ # ===================================================================
69
+ # 즉시 댓글 생성 (ubuntu-latest로 빠르게 실행)
70
+ # ===================================================================
71
+ notify-start:
72
+ name: 진행 상황 알림
73
+ runs-on: ubuntu-latest
74
+ if: github.event_name == 'repository_dispatch'
75
+
76
+ outputs:
77
+ comment_id: ${{ steps.progress.outputs.comment_id }}
78
+ start_time: ${{ steps.progress.outputs.start_time }}
79
+ branch_valid: ${{ steps.verify_branch.outputs.valid }}
80
+
81
+ steps:
82
+ - name: 브랜치 존재 여부 사전 확인
83
+ id: verify_branch
84
+ uses: actions/github-script@v7
85
+ with:
86
+ github-token: ${{ secrets.GITHUB_TOKEN }}
87
+ script: |
88
+ const branchName = '${{ github.event.client_payload.branch_name }}';
89
+ const prNumber = '${{ github.event.client_payload.pr_number }}';
90
+ const runId = '${{ github.run_id }}';
91
+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
92
+
93
+ console.log(`🔍 브랜치 존재 여부 확인: ${branchName}`);
94
+
95
+ try {
96
+ await github.rest.repos.getBranch({
97
+ owner: context.repo.owner,
98
+ repo: context.repo.repo,
99
+ branch: branchName
100
+ });
101
+ console.log(`✅ 브랜치 존재 확인됨: ${branchName}`);
102
+ core.setOutput('valid', 'true');
103
+ } catch (error) {
104
+ if (error.status === 404) {
105
+ console.log(`❌ 브랜치를 찾을 수 없음: ${branchName}`);
106
+
107
+ const body = [
108
+ '🍎 ❌ **iOS TestFlight 빌드 실패 - 브랜치를 찾을 수 없습니다**',
109
+ '',
110
+ '| 항목 | 값 |',
111
+ '|------|-----|',
112
+ `| **요청된 브랜치** | \`${branchName}\` |`,
113
+ `| **이슈/PR** | #${prNumber} |`,
114
+ '',
115
+ '### 💡 확인 사항',
116
+ '1. 브랜치가 원격 저장소에 push되었는지 확인하세요',
117
+ '2. "Guide by SUH-LAB" 댓글의 브랜치명이 올바른지 확인하세요',
118
+ '3. 브랜치명에 오타가 없는지 확인하세요',
119
+ '',
120
+ `🔗 [워크플로우 로그](${runUrl})`
121
+ ].join('\n');
122
+
123
+ await github.rest.issues.createComment({
124
+ owner: context.repo.owner,
125
+ repo: context.repo.repo,
126
+ issue_number: parseInt(prNumber),
127
+ body: body
128
+ });
129
+
130
+ core.setFailed(`브랜치를 찾을 수 없습니다: ${branchName}`);
131
+ } else {
132
+ core.setFailed(`브랜치 확인 중 오류: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ - name: 진행 상황 댓글 생성
137
+ id: progress
138
+ if: github.event.client_payload.pr_number != ''
139
+ continue-on-error: true
140
+ uses: actions/github-script@v7
141
+ with:
142
+ github-token: ${{ secrets.GITHUB_TOKEN }}
143
+ script: |
144
+ const prNumber = parseInt('${{ github.event.client_payload.pr_number }}');
145
+ const branchName = '${{ github.event.client_payload.branch_name }}';
146
+ const buildNumber = '${{ github.event.client_payload.build_number }}';
147
+ const runId = '${{ github.run_id }}';
148
+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
149
+ const startTime = Date.now();
150
+
151
+ const body = [
152
+ '## 🍎 iOS TestFlight 빌드 진행 중...',
153
+ '',
154
+ '| 단계 | 상태 | 소요 시간 |',
155
+ '|------|------|----------|',
156
+ '| 🔧 준비 | ⏳ 진행 중... | - |',
157
+ '| 🔨 IPA 빌드 | ⏸️ 대기 | - |',
158
+ '| 📤 TestFlight 배포 | ⏸️ 대기 | - |',
159
+ '',
160
+ '| 항목 | 값 |',
161
+ '|------|-----|',
162
+ `| **빌드 번호** | \`${buildNumber}\` |`,
163
+ `| **브랜치** | \`${branchName}\` |`,
164
+ '',
165
+ `**[📋 실시간 로그 보기](${runUrl})**`,
166
+ '',
167
+ '---',
168
+ '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
169
+ ].join('\n');
170
+
171
+ const { data: comment } = await github.rest.issues.createComment({
172
+ owner: context.repo.owner,
173
+ repo: context.repo.repo,
174
+ issue_number: prNumber,
175
+ body: body
176
+ });
177
+
178
+ console.log(`✅ 진행 상황 댓글 생성 완료: #${comment.id}`);
179
+ core.setOutput('comment_id', comment.id);
180
+ core.setOutput('start_time', startTime);
181
+
182
+ # ===================================================================
183
+ # 테스트 빌드 준비 (macOS 환경 설정)
184
+ # ===================================================================
185
+ prepare-test-build:
186
+ name: 테스트 빌드 준비
187
+ runs-on: macos-26
188
+ needs: notify-start
189
+ if: |
190
+ always() &&
191
+ (needs.notify-start.result == 'skipped' || needs.notify-start.outputs.branch_valid == 'true')
192
+
193
+ outputs:
194
+ version: ${{ steps.test_version.outputs.version }}
195
+ build_number: ${{ steps.test_version.outputs.build_number }}
196
+ issue_url: ${{ steps.issue_info.outputs.issue_url }}
197
+ issue_title: ${{ steps.issue_info.outputs.issue_title }}
198
+ issue_number: ${{ steps.issue_info.outputs.issue_number }}
199
+ branch_name: ${{ steps.issue_info.outputs.branch_name }}
200
+ pr_number: ${{ steps.test_version.outputs.pr_number }}
201
+ commit_hash: ${{ steps.release_notes.outputs.commit_hash }}
202
+
203
+ steps:
204
+ - name: Checkout repository
205
+ uses: actions/checkout@v5
206
+ with:
207
+ fetch-depth: 0
208
+ ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref }}
209
+
210
+ - name: Pull latest changes
211
+ run: git pull origin ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}
212
+
213
+ - name: Select Xcode version
214
+ run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
215
+
216
+ - name: Create .env file
217
+ env:
218
+ ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
219
+ run: |
220
+ printf '%s\n' "$ENV_CONTENT" > ${{ env.ENV_FILE_PATH }}
221
+ echo "✅ ${{ env.ENV_FILE_PATH }} file created"
222
+
223
+ - name: Create Secrets.xcconfig file
224
+ env:
225
+ SECRETS_XCCONFIG_CONTENT: ${{ secrets.SECRETS_XCCONFIG }}
226
+ run: |
227
+ mkdir -p ios/Flutter
228
+ if [ -n "$SECRETS_XCCONFIG_CONTENT" ]; then
229
+ echo "$SECRETS_XCCONFIG_CONTENT" > ios/Flutter/Secrets.xcconfig
230
+ echo "✅ Secrets.xcconfig created"
231
+ else
232
+ echo "// No secrets provided" > ios/Flutter/Secrets.xcconfig
233
+ echo "ℹ️ Empty Secrets.xcconfig created (no secrets provided)"
234
+ fi
235
+
236
+ # 테스트 빌드 버전 설정 (version.yml에서 동적 읽기)
237
+ - name: 테스트 빌드 버전 설정
238
+ id: test_version
239
+ run: |
240
+ # version_manager.py로 버전 가져오기 (프로덕션 워크플로우와 동일한 방식)
241
+ VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
242
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
243
+
244
+ # repository_dispatch 이벤트인 경우 PR 번호를 빌드 번호로 사용
245
+ if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
246
+ BUILD_NUMBER="${{ github.event.client_payload.build_number }}"
247
+ PR_NUMBER="${{ github.event.client_payload.pr_number }}"
248
+ echo "📋 repository_dispatch 이벤트 감지"
249
+ echo " PR 번호: #$PR_NUMBER"
250
+ echo " 빌드 번호: $BUILD_NUMBER (PR 번호 사용)"
251
+ else
252
+ BUILD_NUMBER="${{ github.run_number }}"
253
+ PR_NUMBER=""
254
+ echo "📋 workflow_dispatch 이벤트 감지"
255
+ echo " 빌드 번호: $BUILD_NUMBER (기본값)"
256
+ fi
257
+
258
+ echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
259
+ echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
260
+ echo "📋 테스트 빌드: 버전=$VERSION, 빌드번호=$BUILD_NUMBER"
261
+
262
+ # 브랜치명에서 이슈 번호 추출 및 GitHub API로 이슈 정보 가져오기
263
+ - name: 브랜치명에서 이슈 정보 추출
264
+ id: issue_info
265
+ run: |
266
+ # repository_dispatch인 경우 client_payload에서 브랜치명 가져오기
267
+ if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
268
+ BRANCH_NAME="${{ github.event.client_payload.branch_name }}"
269
+ ISSUE_NUMBER="${{ github.event.client_payload.issue_number }}"
270
+ else
271
+ BRANCH_NAME="${{ github.ref_name }}"
272
+ # 브랜치명에서 이슈 번호 추출 (#387 형식)
273
+ ISSUE_NUMBER=$(echo "$BRANCH_NAME" | sed -n 's/.*#\([0-9]*\).*/\1/p')
274
+ fi
275
+
276
+ echo "🌿 브랜치명: $BRANCH_NAME"
277
+ echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
278
+
279
+ if [ -z "$ISSUE_NUMBER" ]; then
280
+ echo "⚠️ 브랜치명에서 이슈 번호를 찾을 수 없습니다."
281
+ echo "issue_url=" >> $GITHUB_OUTPUT
282
+ echo "issue_title=" >> $GITHUB_OUTPUT
283
+ echo "issue_number=" >> $GITHUB_OUTPUT
284
+ else
285
+ echo "✅ 추출된 이슈 번호: #$ISSUE_NUMBER"
286
+ ISSUE_URL="https://github.com/${{ github.repository }}/issues/$ISSUE_NUMBER"
287
+ echo "issue_url=$ISSUE_URL" >> $GITHUB_OUTPUT
288
+ echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
289
+
290
+ # GitHub API로 이슈 정보 가져오기
291
+ echo "🔍 GitHub API로 이슈 정보 조회 중..."
292
+ ISSUE_RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
293
+ "https://api.github.com/repos/${{ github.repository }}/issues/$ISSUE_NUMBER")
294
+
295
+ ISSUE_TITLE=$(echo "$ISSUE_RESPONSE" | jq -r '.title // "이슈 정보 없음"')
296
+ ISSUE_STATE=$(echo "$ISSUE_RESPONSE" | jq -r '.state // "unknown"')
297
+
298
+ if [ "$ISSUE_TITLE" = "null" ] || [ "$ISSUE_TITLE" = "이슈 정보 없음" ]; then
299
+ echo "⚠️ 이슈 #$ISSUE_NUMBER를 찾을 수 없습니다."
300
+ echo "issue_title=" >> $GITHUB_OUTPUT
301
+ else
302
+ echo "📋 이슈 제목: $ISSUE_TITLE"
303
+ echo "📌 이슈 상태: $ISSUE_STATE"
304
+ echo "issue_title=$ISSUE_TITLE" >> $GITHUB_OUTPUT
305
+ fi
306
+ fi
307
+
308
+ # 릴리즈 노트 생성
309
+ - name: 릴리즈 노트 생성
310
+ id: release_notes
311
+ run: |
312
+ BUILD_NUMBER="${{ steps.test_version.outputs.build_number }}"
313
+ BRANCH_NAME="${{ steps.issue_info.outputs.branch_name }}"
314
+ COMMIT_SHA="${{ github.sha }}"
315
+ COMMIT_SHORT=$(echo "$COMMIT_SHA" | cut -c1-7)
316
+ BUILD_DATE=$(date '+%Y-%m-%d %H:%M:%S')
317
+
318
+ echo "commit_hash=$COMMIT_SHORT" >> $GITHUB_OUTPUT
319
+
320
+ cat > final_release_notes.txt << EOF
321
+ 테스트 빌드 #$BUILD_NUMBER
322
+
323
+ 브랜치: $BRANCH_NAME
324
+ 커밋: $COMMIT_SHORT
325
+ 날짜: $BUILD_DATE
326
+
327
+ EOF
328
+
329
+ # 이슈 정보가 있으면 추가
330
+ if [ -n "${{ steps.issue_info.outputs.issue_number }}" ]; then
331
+ ISSUE_NUMBER="${{ steps.issue_info.outputs.issue_number }}"
332
+ ISSUE_TITLE="${{ steps.issue_info.outputs.issue_title }}"
333
+ ISSUE_URL="${{ steps.issue_info.outputs.issue_url }}"
334
+
335
+ cat >> final_release_notes.txt << EOF
336
+ 관련 이슈:
337
+ - #$ISSUE_NUMBER: $ISSUE_TITLE
338
+ - URL: $ISSUE_URL
339
+
340
+ EOF
341
+ fi
342
+
343
+ echo "📋 생성된 릴리즈 노트:"
344
+ echo "----------------------------------------"
345
+ cat final_release_notes.txt
346
+ echo "----------------------------------------"
347
+
348
+ - name: Upload release notes
349
+ uses: actions/upload-artifact@v4
350
+ with:
351
+ name: release-notes
352
+ path: final_release_notes.txt
353
+ retention-days: 1
354
+
355
+ - name: Upload project files
356
+ uses: actions/upload-artifact@v4
357
+ with:
358
+ name: project-files
359
+ path: |
360
+ ${{ env.ENV_FILE_PATH }}
361
+ ios/Flutter/Secrets.xcconfig
362
+ pubspec.yaml
363
+ lib/
364
+ assets/
365
+ retention-days: 1
366
+
367
+ # 진행 상황 업데이트 - 준비 실패
368
+ - name: 진행 상황 업데이트 - 준비 실패
369
+ if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
370
+ uses: actions/github-script@v7
371
+ with:
372
+ github-token: ${{ secrets.GITHUB_TOKEN }}
373
+ script: |
374
+ const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
375
+ const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}');
376
+ const branchName = '${{ github.event.client_payload.branch_name }}';
377
+ const buildNumber = '${{ github.event.client_payload.build_number }}';
378
+ const runId = '${{ github.run_id }}';
379
+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
380
+
381
+ const now = Date.now();
382
+ const elapsed = now - startTime;
383
+ const minutes = Math.floor(elapsed / 60000);
384
+ const seconds = Math.floor((elapsed % 60000) / 1000);
385
+ const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
386
+
387
+ const body = [
388
+ '## 🍎 ❌ iOS TestFlight 빌드 실패',
389
+ '',
390
+ '| 단계 | 상태 | 소요 시간 |',
391
+ '|------|------|----------|',
392
+ `| 🔧 준비 | ❌ 실패 | ${duration} |`,
393
+ '| 🔨 IPA 빌드 | ⏸️ 취소됨 | - |',
394
+ '| 📤 TestFlight 배포 | ⏸️ 취소됨 | - |',
395
+ '',
396
+ '| 항목 | 값 |',
397
+ '|------|-----|',
398
+ `| **빌드 번호** | \`${buildNumber}\` |`,
399
+ `| **브랜치** | \`${branchName}\` |`,
400
+ '',
401
+ '❌ **환경 준비 중 오류가 발생했습니다.**',
402
+ '',
403
+ `🔗 [워크플로우 실행 로그 확인](${runUrl})`
404
+ ].join('\n');
405
+
406
+ await github.rest.issues.updateComment({
407
+ owner: context.repo.owner,
408
+ repo: context.repo.repo,
409
+ comment_id: commentId,
410
+ body: body
411
+ });
412
+
413
+ console.log(`❌ 진행 상황 업데이트 완료: 준비 실패`);
414
+
415
+ # ===================================================================
416
+ # iOS 빌드 (xcodebuild 직접 사용)
417
+ # ===================================================================
418
+ build-ios-test:
419
+ name: iOS 테스트 빌드
420
+ runs-on: macos-26
421
+ needs: [notify-start, prepare-test-build]
422
+ if: |
423
+ !cancelled() &&
424
+ needs.prepare-test-build.result == 'success'
425
+
426
+ outputs:
427
+ step1_elapsed: ${{ steps.progress_step1.outputs.elapsed }}
428
+ step1_end_time: ${{ steps.progress_step1.outputs.end_time }}
429
+ step2_elapsed: ${{ steps.progress_step2.outputs.elapsed }}
430
+ step2_end_time: ${{ steps.progress_step2.outputs.end_time }}
431
+
432
+ steps:
433
+ # 진행 상황 업데이트 - 준비 완료 (build job 시작 시점에 업데이트해야 준비 시간이 정확히 표시됨)
434
+ - name: 진행 상황 업데이트 - 준비 완료
435
+ id: progress_step1
436
+ if: github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
437
+ uses: actions/github-script@v7
438
+ with:
439
+ github-token: ${{ secrets.GITHUB_TOKEN }}
440
+ script: |
441
+ const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
442
+ const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}');
443
+ const branchName = '${{ github.event.client_payload.branch_name }}';
444
+ const buildNumber = '${{ github.event.client_payload.build_number }}';
445
+ const version = '${{ needs.prepare-test-build.outputs.version }}';
446
+ const runId = '${{ github.run_id }}';
447
+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
448
+
449
+ const now = Date.now();
450
+ const elapsed = Math.round((now - startTime) / 1000);
451
+ const minutes = Math.floor(elapsed / 60);
452
+ const seconds = elapsed % 60;
453
+ const elapsedStr = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
454
+
455
+ const body = [
456
+ '## 🍎 iOS TestFlight 빌드 진행 중...',
457
+ '',
458
+ '| 단계 | 상태 | 소요 시간 |',
459
+ '|------|------|----------|',
460
+ `| 🔧 준비 | ✅ 완료 | ${elapsedStr} |`,
461
+ '| 🔨 IPA 빌드 | ⏳ 진행 중... | - |',
462
+ '| 📤 TestFlight 배포 | ⏸️ 대기 | - |',
463
+ '',
464
+ '| 항목 | 값 |',
465
+ '|------|-----|',
466
+ `| **앱 버전** | \`${version}(${buildNumber})\` |`,
467
+ `| **브랜치** | \`${branchName}\` |`,
468
+ '',
469
+ `**[📋 실시간 로그 보기](${runUrl})**`,
470
+ '',
471
+ '---',
472
+ '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
473
+ ].join('\n');
474
+
475
+ await github.rest.issues.updateComment({
476
+ owner: context.repo.owner,
477
+ repo: context.repo.repo,
478
+ comment_id: commentId,
479
+ body: body
480
+ });
481
+
482
+ console.log(`✅ 진행 상황 업데이트 완료: 준비 완료 (${elapsedStr})`);
483
+ core.setOutput('elapsed', elapsedStr);
484
+ core.setOutput('end_time', now.toString());
485
+
486
+ - name: Checkout repository
487
+ uses: actions/checkout@v5
488
+ with:
489
+ fetch-depth: 0
490
+ ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref }}
491
+
492
+ - name: Pull latest changes
493
+ run: git pull origin ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}
494
+
495
+ - name: Select Xcode version
496
+ run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
497
+
498
+ - name: Install iOS device platform
499
+ env:
500
+ # xcode-select만으로는 서브프로세스에 전파되지 않아 명시적으로 지정
501
+ DEVELOPER_DIR: /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
502
+ run: |
503
+ echo "=== iPhoneOS.platform check ==="
504
+ ls "$DEVELOPER_DIR/Platforms/iPhoneOS.platform/" 2>/dev/null && echo "iPhoneOS.platform EXISTS" || echo "iPhoneOS.platform NOT FOUND"
505
+ echo "=== Installing MobileDevice packages ==="
506
+ sudo installer -pkg /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Resources/Packages/MobileDevice.pkg -target /
507
+ sudo installer -pkg /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Resources/Packages/MobileDeviceDevelopment.pkg -target /
508
+ echo "=== Accepting Xcode license ==="
509
+ sudo xcodebuild -license accept
510
+ echo "=== Initializing Xcode components ==="
511
+ xcrun simctl list >/dev/null 2>&1 || true
512
+ echo "=== Downloading iOS platform ==="
513
+ xcodebuild -downloadPlatform iOS
514
+
515
+ - name: Download project files
516
+ uses: actions/download-artifact@v4
517
+ with:
518
+ name: project-files
519
+ path: .
520
+
521
+ - name: Ensure .env file exists
522
+ env:
523
+ ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
524
+ run: |
525
+ if [ ! -f "${{ env.ENV_FILE_PATH }}" ]; then
526
+ printf '%s\n' "$ENV_CONTENT" > ${{ env.ENV_FILE_PATH }}
527
+ echo "✅ ${{ env.ENV_FILE_PATH }} file created (fallback)"
528
+ fi
529
+
530
+ - name: Set up Flutter
531
+ uses: subosito/flutter-action@v2
532
+ with:
533
+ flutter-version: ${{ env.FLUTTER_VERSION }}
534
+ cache: true
535
+
536
+ - name: Cache Flutter dependencies
537
+ uses: actions/cache@v4
538
+ with:
539
+ path: ~/.pub-cache
540
+ key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }}
541
+ restore-keys: ${{ runner.os }}-flutter-pub-
542
+
543
+ - name: Install dependencies
544
+ run: flutter pub get
545
+
546
+ - name: Set up Ruby
547
+ uses: ruby/setup-ruby@v1
548
+ with:
549
+ ruby-version: "3.1"
550
+ bundler-cache: true
551
+
552
+ - name: Install CocoaPods
553
+ run: |
554
+ gem install cocoapods
555
+ cd ios && pod install
556
+
557
+ - name: Import Code-Signing Certificates
558
+ uses: Apple-Actions/import-codesign-certs@v2
559
+ with:
560
+ p12-file-base64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
561
+ p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
562
+
563
+ - name: Install Provisioning Profile
564
+ env:
565
+ PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_PROVISIONING_PROFILE_BASE64 }}
566
+ run: |
567
+ echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > profile.mobileprovision
568
+ mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
569
+ cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
570
+ uuid=$(grep -A1 -a "UUID" profile.mobileprovision | grep string | sed -e "s/<string>//" -e "s/<\/string>//" -e "s/[[:space:]]//g")
571
+ cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/$uuid.mobileprovision
572
+
573
+ - name: Verify ExportOptions.plist
574
+ run: |
575
+ cd ios
576
+ if [ ! -f "ExportOptions.plist" ]; then
577
+ echo "❌ ios/ExportOptions.plist이 없습니다!"
578
+ echo "웹 마법사를 실행하여 설정 파일을 생성하세요:"
579
+ echo " 브라우저에서 .github/util/flutter/ios-testflight-setup-wizard/index.html 열기"
580
+ exit 1
581
+ fi
582
+ echo "✅ ExportOptions.plist 확인됨"
583
+
584
+ - name: Flutter build (no codesign)
585
+ env:
586
+ # xcode-select만으로는 서브프로세스에 전파되지 않아 명시적으로 지정
587
+ DEVELOPER_DIR: /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
588
+ run: |
589
+ flutter build ios --release --no-codesign \
590
+ --build-name="${{ needs.prepare-test-build.outputs.version }}" \
591
+ --build-number="${{ needs.prepare-test-build.outputs.build_number }}"
592
+
593
+ - name: Create Archive
594
+ env:
595
+ IOS_PROVISIONING_PROFILE_NAME: ${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}
596
+ # xcode-select만으로는 서브프로세스에 전파되지 않아 명시적으로 지정
597
+ DEVELOPER_DIR: /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
598
+ run: |
599
+ cd ios
600
+ xcodebuild -workspace Runner.xcworkspace \
601
+ -scheme Runner \
602
+ -archivePath build/Runner.xcarchive \
603
+ -destination 'generic/platform=iOS' \
604
+ archive \
605
+ CODE_SIGN_STYLE=Manual \
606
+ PROVISIONING_PROFILE_SPECIFIER="$IOS_PROVISIONING_PROFILE_NAME" \
607
+ CODE_SIGN_IDENTITY="Apple Distribution"
608
+
609
+ - name: Export IPA
610
+ run: |
611
+ cd ios
612
+ xcodebuild -exportArchive \
613
+ -archivePath build/Runner.xcarchive \
614
+ -exportPath build/ipa \
615
+ -exportOptionsPlist ExportOptions.plist
616
+
617
+ - name: Create build metadata file
618
+ run: |
619
+ cat > build-metadata.json << EOF
620
+ {
621
+ "pr_number": "${{ needs.prepare-test-build.outputs.pr_number }}",
622
+ "build_number": "${{ needs.prepare-test-build.outputs.build_number }}",
623
+ "issue_number": "${{ needs.prepare-test-build.outputs.issue_number }}",
624
+ "branch_name": "${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
625
+ }
626
+ EOF
627
+ cat build-metadata.json
628
+
629
+ - name: Upload IPA artifact
630
+ uses: actions/upload-artifact@v4
631
+ with:
632
+ name: ios-ipa
633
+ path: |
634
+ ios/build/ipa/*.ipa
635
+ build-metadata.json
636
+ retention-days: 1
637
+
638
+ # 진행 상황 업데이트 - IPA 빌드 완료
639
+ - name: 진행 상황 업데이트 - IPA 빌드 완료
640
+ id: progress_step2
641
+ if: success() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
642
+ uses: actions/github-script@v7
643
+ with:
644
+ github-token: ${{ secrets.GITHUB_TOKEN }}
645
+ script: |
646
+ const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
647
+ const step1EndTime = parseInt('${{ steps.progress_step1.outputs.end_time }}');
648
+ const step1Elapsed = '${{ steps.progress_step1.outputs.elapsed }}';
649
+ const branchName = '${{ github.event.client_payload.branch_name }}';
650
+ const buildNumber = '${{ github.event.client_payload.build_number }}';
651
+ const version = '${{ needs.prepare-test-build.outputs.version }}';
652
+ const runId = '${{ github.run_id }}';
653
+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
654
+
655
+ const now = Date.now();
656
+
657
+ // 빌드 단계 소요 시간 (step1 종료 ~ 현재)
658
+ const elapsed = Math.round((now - step1EndTime) / 1000);
659
+ const buildMin = Math.floor(elapsed / 60);
660
+ const buildSec = elapsed % 60;
661
+ const elapsedStr = buildMin > 0 ? `${buildMin}분 ${buildSec}초` : `${buildSec}초`;
662
+
663
+ const body = [
664
+ '## 🍎 ✅ iOS 빌드 완료',
665
+ '',
666
+ '| 단계 | 상태 | 소요 시간 |',
667
+ '|------|------|----------|',
668
+ `| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
669
+ `| 🔨 IPA 빌드 | ✅ 완료 | ${elapsedStr} |`,
670
+ '| 📤 TestFlight 배포 | ⏳ 진행 중... | - |',
671
+ '',
672
+ '| 항목 | 값 |',
673
+ '|------|-----|',
674
+ `| **앱 버전** | \`${version}(${buildNumber})\` |`,
675
+ `| **브랜치** | \`${branchName}\` |`,
676
+ '',
677
+ `**[📋 실시간 로그 보기](${runUrl})**`,
678
+ '',
679
+ '---',
680
+ '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
681
+ ].join('\n');
682
+
683
+ await github.rest.issues.updateComment({
684
+ owner: context.repo.owner,
685
+ repo: context.repo.repo,
686
+ comment_id: commentId,
687
+ body: body
688
+ });
689
+
690
+ console.log(`✅ 진행 상황 업데이트 완료: IPA 빌드 완료 (${elapsedStr})`);
691
+ core.setOutput('elapsed', elapsedStr);
692
+ core.setOutput('end_time', now.toString());
693
+
694
+ # 빌드 실패 시 progress 댓글 업데이트
695
+ - name: 빌드 실패 시 progress 댓글 업데이트
696
+ if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
697
+ uses: actions/github-script@v7
698
+ with:
699
+ github-token: ${{ secrets.GITHUB_TOKEN }}
700
+ script: |
701
+ const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
702
+ const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}');
703
+ const step1EndTime = parseInt('${{ steps.progress_step1.outputs.end_time }}') || 0;
704
+ const step1Elapsed = '${{ steps.progress_step1.outputs.elapsed }}';
705
+ const branchName = '${{ github.event.client_payload.branch_name }}';
706
+ const buildNumber = '${{ github.event.client_payload.build_number }}';
707
+ const version = '${{ needs.prepare-test-build.outputs.version }}';
708
+ const commitHash = '${{ needs.prepare-test-build.outputs.commit_hash }}';
709
+ const runId = '${{ github.run_id }}';
710
+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
711
+
712
+ const now = Date.now();
713
+
714
+ // 빌드 단계 소요 시간 (step1 종료 ~ 현재)
715
+ let step2ElapsedStr = '-';
716
+ if (step1EndTime > 0) {
717
+ const step2_s = Math.round((now - step1EndTime) / 1000);
718
+ const s2Min = Math.floor(step2_s / 60);
719
+ const s2Sec = step2_s % 60;
720
+ step2ElapsedStr = s2Min > 0 ? `${s2Min}분 ${s2Sec}초` : `${s2Sec}초`;
721
+ }
722
+
723
+ const body = [
724
+ '## 🍎 ❌ iOS TestFlight 빌드 실패',
725
+ '',
726
+ '| 단계 | 상태 | 소요 시간 |',
727
+ '|------|------|----------|',
728
+ `| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
729
+ `| 🔨 IPA 빌드 | ❌ 실패 | ${step2ElapsedStr} |`,
730
+ '| 📤 TestFlight 배포 | ⏸️ 취소됨 | - |',
731
+ '',
732
+ '| 항목 | 값 |',
733
+ '|------|-----|',
734
+ `| **앱 버전** | \`${version}(${buildNumber})\` |`,
735
+ `| **브랜치** | \`${branchName}\` |`,
736
+ `| **커밋** | \`${commitHash}\` |`,
737
+ '',
738
+ '❌ **IPA 빌드 중 오류가 발생했습니다.**',
739
+ '',
740
+ `🔗 [워크플로우 실행 로그 확인](${runUrl})`
741
+ ].join('\n');
742
+
743
+ await github.rest.issues.updateComment({
744
+ owner: context.repo.owner,
745
+ repo: context.repo.repo,
746
+ comment_id: commentId,
747
+ body: body
748
+ });
749
+
750
+ console.log(`❌ 진행 상황 업데이트 완료: IPA 빌드 실패`);
751
+
752
+ # ===================================================================
753
+ # TestFlight 배포 (마법사 생성 Fastfile 사용)
754
+ # ===================================================================
755
+ deploy-testflight-test:
756
+ name: TestFlight 테스트 배포
757
+ runs-on: macos-26
758
+ needs: [notify-start, prepare-test-build, build-ios-test]
759
+ if: |
760
+ !cancelled() &&
761
+ needs.build-ios-test.result == 'success'
762
+
763
+ steps:
764
+ - name: Checkout repository
765
+ uses: actions/checkout@v5
766
+ with:
767
+ fetch-depth: 0
768
+ ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref }}
769
+
770
+ - name: Setup App Store Connect API Key
771
+ env:
772
+ API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
773
+ API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
774
+ run: |
775
+ mkdir -p ~/.appstoreconnect/private_keys
776
+ echo "$API_KEY_BASE64" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8
777
+
778
+ - name: Download IPA artifact
779
+ uses: actions/download-artifact@v4
780
+ with:
781
+ name: ios-ipa
782
+ path: ios/build/ipa/
783
+
784
+ - name: Download release notes
785
+ uses: actions/download-artifact@v4
786
+ with:
787
+ name: release-notes
788
+ path: .
789
+
790
+ - name: Verify Fastfile exists
791
+ run: |
792
+ if [ ! -f "ios/fastlane/Fastfile" ]; then
793
+ echo "❌ ios/fastlane/Fastfile이 없습니다!"
794
+ echo "웹 마법사를 실행하여 설정 파일을 생성하세요:"
795
+ echo " 브라우저에서 .github/util/flutter/ios-testflight-setup-wizard/index.html 열기"
796
+ exit 1
797
+ fi
798
+ echo "✅ Fastfile 확인됨: ios/fastlane/Fastfile"
799
+
800
+ - name: Install Fastlane
801
+ working-directory: ios
802
+ run: |
803
+ # multi_json: google-apis transitive 의존성이 선언 누락한 upstream 버그 회피 (gemspec 미선언 → Gem::LoadError)
804
+ printf 'source "https://rubygems.org"\ngem "fastlane"\ngem "multi_json"\n' > Gemfile
805
+ bundle install
806
+ echo "✅ Fastlane installed (Bundler)"
807
+
808
+ - name: Upload to TestFlight
809
+ env:
810
+ APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
811
+ APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
812
+ run: |
813
+ # IPA 파일 찾기
814
+ IPA_PATH=$(find $GITHUB_WORKSPACE/ios/build/ipa -name "*.ipa" | head -1)
815
+ echo "📦 Found IPA at: $IPA_PATH"
816
+
817
+ if [ ! -f "$IPA_PATH" ]; then
818
+ echo "❌ IPA 파일을 찾을 수 없습니다!"
819
+ ls -la ios/build/ipa/
820
+ exit 1
821
+ fi
822
+
823
+ # Release notes 준비
824
+ if [ -f "final_release_notes.txt" ]; then
825
+ RELEASE_NOTES=$(cat final_release_notes.txt)
826
+ echo "📝 Release Notes:"
827
+ echo "$RELEASE_NOTES"
828
+ else
829
+ RELEASE_NOTES="테스트 빌드 #${{ needs.prepare-test-build.outputs.build_number }}"
830
+ echo "📝 기본 Release Notes: $RELEASE_NOTES"
831
+ fi
832
+
833
+ # 환경변수 설정 (Fastfile에서 사용)
834
+ export API_KEY_PATH="$HOME/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8"
835
+ export IPA_PATH="$IPA_PATH"
836
+ export RELEASE_NOTES="$RELEASE_NOTES"
837
+
838
+ echo "🔧 Working directory: $(pwd)"
839
+ echo "🔧 IPA_PATH: $IPA_PATH"
840
+ echo "🔧 API_KEY_PATH: $API_KEY_PATH"
841
+
842
+ # Fastlane 실행 (마법사가 생성한 Fastfile 사용)
843
+ cd ios
844
+ bundle exec fastlane upload_testflight
845
+
846
+ - name: Notify TestFlight Upload Success
847
+ if: success()
848
+ run: |
849
+ echo "✅ TestFlight 테스트 빌드 업로드 성공!"
850
+ echo "버전: ${{ needs.prepare-test-build.outputs.version }}"
851
+ echo "빌드 번호: ${{ needs.prepare-test-build.outputs.build_number }}"
852
+ if [ -n "${{ needs.prepare-test-build.outputs.pr_number }}" ]; then
853
+ echo "PR 번호: ${{ needs.prepare-test-build.outputs.pr_number }}"
854
+ fi
855
+ echo "브랜치: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
856
+ echo "커밋: ${{ github.sha }}"
857
+ if [ -n "${{ needs.prepare-test-build.outputs.issue_url }}" ]; then
858
+ echo "관련 이슈: ${{ needs.prepare-test-build.outputs.issue_url }}"
859
+ fi
860
+
861
+ - name: Notify on Failure
862
+ if: failure()
863
+ run: |
864
+ echo "❌ TestFlight 테스트 빌드 업로드 실패!"
865
+ echo "로그를 확인해주세요."
866
+
867
+ # 진행 상황 최종 업데이트 - 성공
868
+ - name: 진행 상황 최종 업데이트 - 성공
869
+ if: success() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
870
+ uses: actions/github-script@v7
871
+ with:
872
+ github-token: ${{ secrets.GITHUB_TOKEN }}
873
+ script: |
874
+ const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
875
+ const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}');
876
+ const step2EndTime = parseInt('${{ needs.build-ios-test.outputs.step2_end_time }}');
877
+ const step1Elapsed = '${{ needs.build-ios-test.outputs.step1_elapsed }}';
878
+ const step2Elapsed = '${{ needs.build-ios-test.outputs.step2_elapsed }}';
879
+ const branchName = '${{ github.event.client_payload.branch_name }}';
880
+ const buildNumber = '${{ github.event.client_payload.build_number }}';
881
+ const version = '${{ needs.prepare-test-build.outputs.version }}';
882
+ const commitHash = '${{ needs.prepare-test-build.outputs.commit_hash }}';
883
+ const runId = '${{ github.run_id }}';
884
+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
885
+
886
+ const now = Date.now();
887
+
888
+ // 배포 단계 소요 시간 (step2 종료 ~ 현재)
889
+ const step3_s = Math.round((now - step2EndTime) / 1000);
890
+ const s3Min = Math.floor(step3_s / 60);
891
+ const s3Sec = step3_s % 60;
892
+ const step3Elapsed = s3Min > 0 ? `${s3Min}분 ${s3Sec}초` : `${s3Sec}초`;
893
+
894
+ // 전체 소요 시간
895
+ const total_s = Math.round((now - startTime) / 1000);
896
+ const tMin = Math.floor(total_s / 60);
897
+ const tSec = total_s % 60;
898
+ const totalElapsed = tMin > 0 ? `${tMin}분 ${tSec}초` : `${tSec}초`;
899
+
900
+ const body = [
901
+ '## 🍎 ✅ iOS TestFlight 배포 완료',
902
+ '',
903
+ '| 단계 | 상태 | 소요 시간 |',
904
+ '|------|------|----------|',
905
+ `| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
906
+ `| 🔨 IPA 빌드 | ✅ 완료 | ${step2Elapsed || '-'} |`,
907
+ `| 📤 TestFlight 배포 | ✅ 완료 | ${step3Elapsed} |`,
908
+ '',
909
+ '| 항목 | 값 |',
910
+ '|------|-----|',
911
+ `| **앱 버전** | \`${version}(${buildNumber})\` |`,
912
+ `| **브랜치** | \`${branchName}\` |`,
913
+ `| **커밋** | \`${commitHash}\` |`,
914
+ `| **총 소요 시간** | ${totalElapsed} |`,
915
+ '',
916
+ '📱 **TestFlight에서 테스트 앱을 다운로드할 수 있습니다.**',
917
+ '',
918
+ `🔗 [워크플로우 실행 로그](${runUrl})`
919
+ ].join('\n');
920
+
921
+ await github.rest.issues.updateComment({
922
+ owner: context.repo.owner,
923
+ repo: context.repo.repo,
924
+ comment_id: commentId,
925
+ body: body
926
+ });
927
+
928
+ console.log(`✅ 진행 상황 최종 업데이트 완료: 전체 성공 (${totalElapsed})`);
929
+
930
+ # 진행 상황 최종 업데이트 - 실패
931
+ - name: 진행 상황 최종 업데이트 - 실패
932
+ if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
933
+ uses: actions/github-script@v7
934
+ with:
935
+ github-token: ${{ secrets.GITHUB_TOKEN }}
936
+ script: |
937
+ const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
938
+ const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}');
939
+ const step2EndTime = parseInt('${{ needs.build-ios-test.outputs.step2_end_time }}') || 0;
940
+ const step1Elapsed = '${{ needs.build-ios-test.outputs.step1_elapsed }}';
941
+ const step2Elapsed = '${{ needs.build-ios-test.outputs.step2_elapsed }}';
942
+ const branchName = '${{ github.event.client_payload.branch_name }}';
943
+ const buildNumber = '${{ github.event.client_payload.build_number }}';
944
+ const version = '${{ needs.prepare-test-build.outputs.version }}';
945
+ const commitHash = '${{ needs.prepare-test-build.outputs.commit_hash }}';
946
+ const runId = '${{ github.run_id }}';
947
+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
948
+
949
+ const now = Date.now();
950
+
951
+ // 배포 단계 소요 시간 (step2 종료 ~ 현재)
952
+ let step3Elapsed = '-';
953
+ if (step2EndTime > 0) {
954
+ const step3_s = Math.round((now - step2EndTime) / 1000);
955
+ const s3Min = Math.floor(step3_s / 60);
956
+ const s3Sec = step3_s % 60;
957
+ step3Elapsed = s3Min > 0 ? `${s3Min}분 ${s3Sec}초` : `${s3Sec}초`;
958
+ }
959
+
960
+ const body = [
961
+ '## 🍎 ❌ iOS TestFlight 배포 실패',
962
+ '',
963
+ '| 단계 | 상태 | 소요 시간 |',
964
+ '|------|------|----------|',
965
+ `| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
966
+ `| 🔨 IPA 빌드 | ✅ 완료 | ${step2Elapsed || '-'} |`,
967
+ `| 📤 TestFlight 배포 | ❌ 실패 | ${step3Elapsed} |`,
968
+ '',
969
+ '| 항목 | 값 |',
970
+ '|------|-----|',
971
+ `| **앱 버전** | \`${version}(${buildNumber})\` |`,
972
+ `| **브랜치** | \`${branchName}\` |`,
973
+ `| **커밋** | \`${commitHash}\` |`,
974
+ '',
975
+ '❌ **TestFlight 배포 중 오류가 발생했습니다.**',
976
+ '',
977
+ `🔗 [워크플로우 실행 로그 확인](${runUrl})`
978
+ ].join('\n');
979
+
980
+ await github.rest.issues.updateComment({
981
+ owner: context.repo.owner,
982
+ repo: context.repo.repo,
983
+ comment_id: commentId,
984
+ body: body
985
+ });
986
+
987
+ console.log(`❌ 진행 상황 최종 업데이트 완료: TestFlight 배포 실패`);