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,500 @@
1
+ # ===================================================================
2
+ # Flutter 테스트 앱 빌드 트리거 워크플로우 (SUH-LAB)
3
+ # ===================================================================
4
+ #
5
+ # 이 워크플로우는 PR 또는 이슈에 댓글로 빌드 명령어를 작성하면
6
+ # Android와 iOS 테스트 앱 빌드를 자동으로 트리거합니다.
7
+ #
8
+ # 주요 특징:
9
+ # - PR/이슈 댓글 감지: "@suh-lab app build / apk build / ios build" 키워드 감지 (어순 무관: build app 도 동작)
10
+ # - repository_dispatch 이벤트로 빌드 워크플로우 트리거
11
+ # - PR/이슈 번호 + 빌드 횟수로 고유한 빌드 번호 생성 (예: 38700, 38701...)
12
+ # - 브랜치명에서 이슈 번호 자동 추출 (YYYYMMDD_#이슈번호_내용 형식)
13
+ # - 테스트 버전은 version.yml에서 동적으로 읽어 사용 (빌드 번호로 운영/테스트 구분)
14
+ #
15
+ # 사용 방법:
16
+ # - PR에 댓글 작성: "@suh-lab app build" (Android + iOS 모두 빌드)
17
+ # - PR에 댓글 작성: "@suh-lab apk build" (Android만 빌드)
18
+ # - PR에 댓글 작성: "@suh-lab ios build" (iOS만 빌드)
19
+ # - 이슈에 댓글 작성: 위 명령어 동일 (Guide by SUH-LAB 댓글 필요)
20
+ # - 어순은 무관: "build app" / "build apk" / "build ios"로 적어도 동일하게 동작
21
+ # - 특정 브랜치 지정: "@suh-lab app build <브랜치명>" (명령어 뒤에 브랜치명을 적으면 해당 브랜치를 빌드)
22
+ # 예: "@suh-lab apk build 20260609_#349_feature" → 해당 브랜치로 Android 빌드
23
+ # 브랜치명을 생략하면 PR head 또는 이슈의 Guide by SUH-LAB 댓글 브랜치를 사용
24
+ #
25
+ # 트리거되는 워크플로우:
26
+ # - PROJECT-FLUTTER-ANDROID-TEST-APK.yaml (event_type: build-android-app)
27
+ # - PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml (event_type: build-ios-app)
28
+ #
29
+ # ===================================================================
30
+
31
+ name: PROJECT-Flutter-SUH-LAB-App-Build-Trigger
32
+
33
+ on:
34
+ issue_comment:
35
+ types: [created]
36
+
37
+ permissions:
38
+ contents: write
39
+ pull-requests: write
40
+ issues: write
41
+
42
+ jobs:
43
+ trigger-builds:
44
+ name: 빌드 워크플로우 트리거
45
+ # @suh-lab app build / @suh-lab apk build / @suh-lab ios build 키워드 감지 (어순 무관)
46
+ if: |
47
+ contains(github.event.comment.body, '@suh-lab') &&
48
+ (
49
+ (contains(github.event.comment.body, 'build') && contains(github.event.comment.body, 'app')) ||
50
+ (contains(github.event.comment.body, 'apk') && contains(github.event.comment.body, 'build')) ||
51
+ (contains(github.event.comment.body, 'ios') && contains(github.event.comment.body, 'build'))
52
+ )
53
+ runs-on: ubuntu-latest
54
+
55
+ steps:
56
+ - name: 댓글에 👀 리액션 추가
57
+ uses: actions/github-script@v7
58
+ with:
59
+ github-token: ${{ secrets.GITHUB_TOKEN }}
60
+ script: |
61
+ const commentId = context.payload.comment.id;
62
+
63
+ await github.rest.reactions.createForIssueComment({
64
+ owner: context.repo.owner,
65
+ repo: context.repo.repo,
66
+ comment_id: commentId,
67
+ content: 'eyes'
68
+ });
69
+
70
+ console.log('👀 댓글에 확인 리액션 추가 완료');
71
+
72
+ - name: 빌드 타입 판별
73
+ id: build_type
74
+ uses: actions/github-script@v7
75
+ with:
76
+ github-token: ${{ secrets.GITHUB_TOKEN }}
77
+ script: |
78
+ // 키워드 판별은 소문자로(대소문자 무관 인식), 브랜치명 캡처는 원본 본문에서(대소문자 보존)
79
+ const comment = context.payload.comment.body.toLowerCase();
80
+ const rawComment = context.payload.comment.body;
81
+
82
+ let buildAndroid = false;
83
+ let buildIos = false;
84
+ let buildType = '';
85
+ let customBranch = '';
86
+ let m;
87
+
88
+ // 명령어 키워드 뒤의 같은 줄 토큰을 옵셔널 브랜치 인자로 캡처
89
+ // (줄바꿈 너머 단어가 브랜치로 오인되지 않도록 [^\S\r\n]+ 로 같은 줄 공백만 허용)
90
+ // 브랜치명은 rawComment에서 i 플래그로 다시 추출 → Feature/MyBranch 같은 대문자 브랜치 보존
91
+ //
92
+ // 어순은 양방향 지원: "{타입} build"(app/apk/ios build)와 "build {타입}"(build app 등) 모두 허용.
93
+ // apk가 app으로 오인되지 않도록 app 토큰은 \b 단어경계로 정확히 끊는다(apk·application 제외).
94
+ // apk/ios를 app보다 먼저 검사해, "apk build"가 app 분기로 새지 않게 한다.
95
+ if (comment.match(/@suh-lab[^\S\r\n]+apk[^\S\r\n]+build/) || comment.match(/@suh-lab[^\S\r\n]+build[^\S\r\n]+apk/)) {
96
+ // @suh-lab apk build [브랜치] (또는 build apk) → Android만
97
+ buildAndroid = true;
98
+ buildType = 'apk';
99
+ m = rawComment.match(/@suh-lab[^\S\r\n]+apk[^\S\r\n]+build(?:[^\S\r\n]+(\S+))?/i)
100
+ || rawComment.match(/@suh-lab[^\S\r\n]+build[^\S\r\n]+apk(?:[^\S\r\n]+(\S+))?/i);
101
+ customBranch = (m && m[1]) || '';
102
+ } else if (comment.match(/@suh-lab[^\S\r\n]+ios[^\S\r\n]+build/) || comment.match(/@suh-lab[^\S\r\n]+build[^\S\r\n]+ios/)) {
103
+ // @suh-lab ios build [브랜치] (또는 build ios) → iOS만
104
+ buildIos = true;
105
+ buildType = 'ios';
106
+ m = rawComment.match(/@suh-lab[^\S\r\n]+ios[^\S\r\n]+build(?:[^\S\r\n]+(\S+))?/i)
107
+ || rawComment.match(/@suh-lab[^\S\r\n]+build[^\S\r\n]+ios(?:[^\S\r\n]+(\S+))?/i);
108
+ customBranch = (m && m[1]) || '';
109
+ } else if (comment.match(/@suh-lab[^\S\r\n]+app\b[^\S\r\n]+build/) || comment.match(/@suh-lab[^\S\r\n]+build[^\S\r\n]+app\b/)) {
110
+ // @suh-lab app build [브랜치] (또는 build app) → Android + iOS 모두
111
+ buildAndroid = true;
112
+ buildIos = true;
113
+ buildType = 'app';
114
+ m = rawComment.match(/@suh-lab[^\S\r\n]+app\b[^\S\r\n]+build(?:[^\S\r\n]+(\S+))?/i)
115
+ || rawComment.match(/@suh-lab[^\S\r\n]+build[^\S\r\n]+app\b(?:[^\S\r\n]+(\S+))?/i);
116
+ customBranch = (m && m[1]) || '';
117
+ }
118
+
119
+ core.setOutput('buildAndroid', buildAndroid.toString());
120
+ core.setOutput('buildIos', buildIos.toString());
121
+ core.setOutput('buildType', buildType);
122
+ core.setOutput('customBranch', customBranch);
123
+
124
+ console.log(`📱 빌드 타입: ${buildType} (Android=${buildAndroid}, iOS=${buildIos})`);
125
+ if (customBranch) {
126
+ console.log(`🌿 댓글에서 지정된 브랜치: ${customBranch}`);
127
+ }
128
+
129
+ - name: PR/이슈 정보 확인 및 추출
130
+ id: source_info
131
+ uses: actions/github-script@v7
132
+ env:
133
+ CUSTOM_BRANCH: ${{ steps.build_type.outputs.customBranch }}
134
+ with:
135
+ github-token: ${{ secrets.GITHUB_TOKEN }}
136
+ script: |
137
+ const customBranch = (process.env.CUSTOM_BRANCH || '').trim();
138
+ const issueNumber = context.payload.issue.number;
139
+ let sourceType = '';
140
+ let sourceNumber = '';
141
+ let branchName = '';
142
+ let headSha = '';
143
+ let relatedIssueNumber = '';
144
+
145
+ // 1. 먼저 PR인지 확인
146
+ try {
147
+ const pr = await github.rest.pulls.get({
148
+ owner: context.repo.owner,
149
+ repo: context.repo.repo,
150
+ pull_number: issueNumber
151
+ });
152
+
153
+ sourceType = 'PR';
154
+ sourceNumber = pr.data.number.toString();
155
+ branchName = customBranch || pr.data.head.ref;
156
+ headSha = pr.data.head.sha;
157
+
158
+ if (customBranch) {
159
+ console.log(`🌿 댓글 지정 브랜치 우선 적용: ${customBranch} (PR head=${pr.data.head.ref})`);
160
+ }
161
+
162
+ console.log(`✅ PR 확인: #${sourceNumber}`);
163
+ console.log(`🌿 브랜치명: ${branchName}`);
164
+ console.log(`📝 커밋 해시: ${headSha}`);
165
+
166
+ // 브랜치명에서 이슈 번호 추출 (#280 형식)
167
+ const issueMatch = branchName.match(/#(\d+)/);
168
+ relatedIssueNumber = issueMatch ? issueMatch[1] : '';
169
+
170
+ if (relatedIssueNumber) {
171
+ console.log(`📌 추출된 이슈 번호: #${relatedIssueNumber}`);
172
+ }
173
+
174
+ } catch (error) {
175
+ console.log('ℹ️ PR이 아닙니다. 이슈에서 브랜치 정보를 찾습니다...');
176
+
177
+ // 2. PR이 아니면 이슈에서 "Guide by SUH-LAB" 댓글 찾기
178
+ sourceType = 'ISSUE';
179
+ sourceNumber = issueNumber.toString();
180
+ relatedIssueNumber = issueNumber.toString();
181
+
182
+ // 댓글에서 브랜치를 직접 지정한 경우 Guide 댓글 탐색을 건너뛴다
183
+ if (customBranch) {
184
+ branchName = customBranch;
185
+ console.log(`🌿 댓글 지정 브랜치 사용 (Guide 댓글 건너뜀): ${branchName}`);
186
+ console.log(`📌 관련 이슈 번호: #${relatedIssueNumber}`);
187
+
188
+ core.setOutput('found', 'true');
189
+ core.setOutput('sourceType', sourceType);
190
+ core.setOutput('sourceNumber', sourceNumber);
191
+ core.setOutput('branchName', branchName);
192
+ core.setOutput('headSha', headSha);
193
+ core.setOutput('relatedIssueNumber', relatedIssueNumber);
194
+ return;
195
+ }
196
+
197
+ try {
198
+ const comments = await github.rest.issues.listComments({
199
+ owner: context.repo.owner,
200
+ repo: context.repo.repo,
201
+ issue_number: issueNumber,
202
+ per_page: 100
203
+ });
204
+
205
+ // "Guide by SUH-LAB" 댓글 찾기
206
+ const guideComment = comments.data.find(c =>
207
+ c.body.includes('Guide by SUH-LAB')
208
+ );
209
+
210
+ if (!guideComment) {
211
+ console.log('❌ "Guide by SUH-LAB" 댓글을 찾을 수 없습니다.');
212
+ core.setOutput('found', 'false');
213
+ core.setOutput('errorMessage', '이슈에서 "Guide by SUH-LAB" 댓글을 찾을 수 없습니다. 브랜치 정보가 포함된 댓글이 필요합니다.');
214
+ return;
215
+ }
216
+
217
+ console.log('✅ "Guide by SUH-LAB" 댓글 발견');
218
+
219
+ // 브랜치명 추출 (### 브랜치 다음 ``` 블록 내용)
220
+ const branchMatch = guideComment.body.match(/### 브랜치\s*```\s*([\s\S]*?)\s*```/);
221
+
222
+ if (!branchMatch) {
223
+ console.log('❌ 브랜치 정보를 파싱할 수 없습니다.');
224
+ core.setOutput('found', 'false');
225
+ core.setOutput('errorMessage', '"Guide by SUH-LAB" 댓글에서 브랜치 정보를 파싱할 수 없습니다. 댓글 형식을 확인해주세요.');
226
+ return;
227
+ }
228
+
229
+ branchName = branchMatch[1].trim();
230
+ console.log(`🌿 추출된 브랜치명: ${branchName}`);
231
+ console.log(`✅ 이슈 #${sourceNumber}에서 빌드 정보 추출 완료`);
232
+
233
+ } catch (commentError) {
234
+ console.log('❌ 이슈 댓글 조회 중 오류 발생:', commentError.message);
235
+ core.setOutput('found', 'false');
236
+ core.setOutput('errorMessage', `이슈 댓글 조회 중 오류가 발생했습니다: ${commentError.message}`);
237
+ return;
238
+ }
239
+ }
240
+
241
+ // 결과 출력
242
+ core.setOutput('found', 'true');
243
+ core.setOutput('sourceType', sourceType);
244
+ core.setOutput('sourceNumber', sourceNumber);
245
+ core.setOutput('branchName', branchName);
246
+ core.setOutput('headSha', headSha);
247
+ core.setOutput('relatedIssueNumber', relatedIssueNumber);
248
+
249
+ - name: 브랜치 정보를 찾을 수 없는 경우 에러 댓글 작성
250
+ if: steps.source_info.outputs.found != 'true'
251
+ uses: actions/github-script@v7
252
+ with:
253
+ github-token: ${{ secrets.GITHUB_TOKEN }}
254
+ script: |
255
+ const issueNumber = context.payload.issue.number;
256
+ const errorMessage = '${{ steps.source_info.outputs.errorMessage }}';
257
+
258
+ const body = `❌ **앱 빌드 트리거 실패**
259
+
260
+ ${errorMessage}
261
+
262
+ **해결 방법:**
263
+ - PR에서 빌드하는 경우: PR 페이지에서 댓글을 작성해주세요.
264
+ - 이슈에서 빌드하는 경우: "Guide by SUH-LAB" 댓글이 있어야 합니다.
265
+ - 댓글에 \`### 브랜치\` 섹션과 브랜치명이 포함되어 있어야 합니다.
266
+ - 또는 댓글에 브랜치명을 직접 지정할 수 있습니다: \`@suh-lab app build <브랜치명>\``;
267
+
268
+ await github.rest.issues.createComment({
269
+ owner: context.repo.owner,
270
+ repo: context.repo.repo,
271
+ issue_number: issueNumber,
272
+ body: body
273
+ });
274
+
275
+ core.setFailed(errorMessage);
276
+
277
+ - name: 브랜치 존재 여부 확인
278
+ id: check_branch
279
+ if: steps.source_info.outputs.found == 'true'
280
+ uses: actions/github-script@v7
281
+ with:
282
+ github-token: ${{ secrets.GITHUB_TOKEN }}
283
+ script: |
284
+ const branchName = '${{ steps.source_info.outputs.branchName }}';
285
+ const runId = '${{ github.run_id }}';
286
+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
287
+
288
+ console.log(`🔍 브랜치 존재 여부 확인: ${branchName}`);
289
+
290
+ try {
291
+ await github.rest.repos.getBranch({
292
+ owner: context.repo.owner,
293
+ repo: context.repo.repo,
294
+ branch: branchName
295
+ });
296
+ console.log(`✅ 브랜치 존재 확인됨: ${branchName}`);
297
+ core.setOutput('exists', 'true');
298
+ } catch (error) {
299
+ if (error.status === 404) {
300
+ console.log(`❌ 브랜치를 찾을 수 없음: ${branchName}`);
301
+ core.setOutput('exists', 'false');
302
+ core.setOutput('errorMessage', `브랜치 \`${branchName}\`를 찾을 수 없습니다.`);
303
+ } else {
304
+ console.log(`❌ 브랜치 확인 중 오류: ${error.message}`);
305
+ core.setOutput('exists', 'false');
306
+ core.setOutput('errorMessage', `브랜치 확인 중 오류가 발생했습니다: ${error.message}`);
307
+ }
308
+ }
309
+
310
+ - name: 브랜치가 존재하지 않는 경우 에러 댓글 작성
311
+ if: steps.source_info.outputs.found == 'true' && steps.check_branch.outputs.exists == 'false'
312
+ uses: actions/github-script@v7
313
+ with:
314
+ github-token: ${{ secrets.GITHUB_TOKEN }}
315
+ script: |
316
+ const issueNumber = context.payload.issue.number;
317
+ const branchName = '${{ steps.source_info.outputs.branchName }}';
318
+ const runId = '${{ github.run_id }}';
319
+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
320
+
321
+ const body = [
322
+ '❌ **앱 빌드 트리거 실패 - 브랜치를 찾을 수 없습니다**',
323
+ '',
324
+ '| 항목 | 값 |',
325
+ '|------|-----|',
326
+ `| **요청된 브랜치** | \`${branchName}\` |`,
327
+ `| **이슈/PR** | #${issueNumber} |`,
328
+ '',
329
+ '### 💡 확인 사항',
330
+ '1. 브랜치가 원격 저장소에 push되었는지 확인하세요',
331
+ '2. "Guide by SUH-LAB" 댓글의 브랜치명이 올바른지 확인하세요',
332
+ '3. 브랜치명에 오타가 없는지 확인하세요',
333
+ '',
334
+ `🔗 [워크플로우 로그](${runUrl})`
335
+ ].join('\n');
336
+
337
+ await github.rest.issues.createComment({
338
+ owner: context.repo.owner,
339
+ repo: context.repo.repo,
340
+ issue_number: issueNumber,
341
+ body: body
342
+ });
343
+
344
+ core.setFailed(`브랜치를 찾을 수 없습니다: ${branchName}`);
345
+
346
+ - name: 빌드 횟수 조회 및 빌드 번호 생성
347
+ id: build_number_calc
348
+ if: steps.source_info.outputs.found == 'true' && steps.check_branch.outputs.exists == 'true'
349
+ uses: actions/github-script@v7
350
+ with:
351
+ github-token: ${{ secrets.GITHUB_TOKEN }}
352
+ script: |
353
+ const sourceNumber = parseInt('${{ steps.source_info.outputs.sourceNumber }}');
354
+ const sourceType = '${{ steps.source_info.outputs.sourceType }}';
355
+ const buildType = '${{ steps.build_type.outputs.buildType }}';
356
+
357
+ console.log(`📊 ${sourceType} #${sourceNumber}의 빌드 횟수 조회 중...`);
358
+ console.log(`📱 빌드 타입: ${buildType}`);
359
+
360
+ // PR/이슈의 모든 댓글 조회
361
+ const comments = await github.rest.issues.listComments({
362
+ owner: context.repo.owner,
363
+ repo: context.repo.repo,
364
+ issue_number: sourceNumber,
365
+ per_page: 100
366
+ });
367
+
368
+ // 빌드 타입에 따라 해당 플랫폼의 빌드 횟수 카운트
369
+ // 어순 무관 카운트: includes('build') && includes('{타입}') 로 "ios build"·"build ios" 모두 잡는다.
370
+ // iOS: ios 빌드 + app(공용) 빌드 통합 (App Store Connect는 앱 전체에서 빌드 번호 unique)
371
+ // Android: apk 빌드 + app(공용) 빌드 통합 (Play Store도 동일)
372
+ // App: 모든 빌드 명령어 통합 카운트
373
+ let buildCount = 0;
374
+
375
+ if (buildType === 'ios') {
376
+ // iOS 빌드: ios 빌드 + app 공용 빌드 둘 다 카운트 (어순 무관)
377
+ buildCount = comments.data.filter(c => {
378
+ const body = c.body.toLowerCase();
379
+ return body.includes('@suh-lab') && body.includes('build') && (
380
+ body.includes('ios') || body.includes('app')
381
+ );
382
+ }).length;
383
+ console.log(`📊 iOS 빌드 카운트: ios + app(공용) 통합`);
384
+
385
+ } else if (buildType === 'apk') {
386
+ // Android 빌드: apk 빌드 + app 공용 빌드 둘 다 카운트 (어순 무관)
387
+ buildCount = comments.data.filter(c => {
388
+ const body = c.body.toLowerCase();
389
+ return body.includes('@suh-lab') && body.includes('build') && (
390
+ body.includes('apk') || body.includes('app')
391
+ );
392
+ }).length;
393
+ console.log(`📊 Android 빌드 카운트: apk + app(공용) 통합`);
394
+
395
+ } else if (buildType === 'app') {
396
+ // 전체 앱 빌드: 모든 빌드 명령어 카운트 (어순 무관)
397
+ buildCount = comments.data.filter(c => {
398
+ const body = c.body.toLowerCase();
399
+ return body.includes('@suh-lab') && body.includes('build') && (
400
+ body.includes('ios') ||
401
+ body.includes('apk') ||
402
+ body.includes('app')
403
+ );
404
+ }).length;
405
+ console.log(`📊 전체 앱 빌드 카운트: 모든 빌드 명령어 통합`);
406
+ }
407
+
408
+ // 빌드 번호: 소스번호 + 2자리 카운트 (38800, 38801, 38802...)
409
+ const buildNumber = `${sourceNumber}${String(buildCount).padStart(2, '0')}`;
410
+
411
+ console.log(`📊 ${sourceType} #${sourceNumber}: ${buildType} ${buildCount}번째 빌드`);
412
+ console.log(`📦 생성된 빌드 번호: ${buildNumber}`);
413
+
414
+ core.setOutput('buildCount', buildCount.toString());
415
+ core.setOutput('buildNumber', buildNumber);
416
+
417
+ - name: Android 빌드 워크플로우 트리거
418
+ if: |
419
+ steps.source_info.outputs.found == 'true' &&
420
+ steps.check_branch.outputs.exists == 'true' &&
421
+ steps.build_type.outputs.buildAndroid == 'true'
422
+ uses: actions/github-script@v7
423
+ with:
424
+ github-token: ${{ secrets.GITHUB_TOKEN }}
425
+ script: |
426
+ const sourceType = '${{ steps.source_info.outputs.sourceType }}';
427
+ const sourceNumber = '${{ steps.source_info.outputs.sourceNumber }}';
428
+ const branchName = '${{ steps.source_info.outputs.branchName }}';
429
+ const relatedIssueNumber = '${{ steps.source_info.outputs.relatedIssueNumber }}';
430
+ const buildNumber = '${{ steps.build_number_calc.outputs.buildNumber }}';
431
+
432
+ console.log(`🚀 Android 빌드 워크플로우 트리거 시작...`);
433
+ console.log(` ${sourceType} 번호: #${sourceNumber}`);
434
+ console.log(` 빌드 번호: ${buildNumber}`);
435
+ console.log(` 브랜치: ${branchName}`);
436
+ console.log(` 관련 이슈 번호: ${relatedIssueNumber || '없음'}`);
437
+
438
+ await github.rest.repos.createDispatchEvent({
439
+ owner: context.repo.owner,
440
+ repo: context.repo.repo,
441
+ event_type: 'build-android-app',
442
+ client_payload: {
443
+ pr_number: sourceNumber,
444
+ build_number: buildNumber,
445
+ branch_name: branchName,
446
+ issue_number: relatedIssueNumber || '',
447
+ triggered_by: `${sourceType.toLowerCase()}-comment`,
448
+ comment_id: context.payload.comment.id.toString(),
449
+ source_type: sourceType
450
+ }
451
+ });
452
+
453
+ console.log('✅ Android 빌드 워크플로우 트리거 완료');
454
+
455
+ - name: iOS 빌드 워크플로우 트리거
456
+ if: |
457
+ steps.source_info.outputs.found == 'true' &&
458
+ steps.check_branch.outputs.exists == 'true' &&
459
+ steps.build_type.outputs.buildIos == 'true'
460
+ uses: actions/github-script@v7
461
+ with:
462
+ github-token: ${{ secrets.GITHUB_TOKEN }}
463
+ script: |
464
+ const sourceType = '${{ steps.source_info.outputs.sourceType }}';
465
+ const sourceNumber = '${{ steps.source_info.outputs.sourceNumber }}';
466
+ const branchName = '${{ steps.source_info.outputs.branchName }}';
467
+ const relatedIssueNumber = '${{ steps.source_info.outputs.relatedIssueNumber }}';
468
+ const buildNumber = '${{ steps.build_number_calc.outputs.buildNumber }}';
469
+
470
+ console.log(`🚀 iOS 빌드 워크플로우 트리거 시작...`);
471
+ console.log(` ${sourceType} 번호: #${sourceNumber}`);
472
+ console.log(` 빌드 번호: ${buildNumber}`);
473
+ console.log(` 브랜치: ${branchName}`);
474
+ console.log(` 관련 이슈 번호: ${relatedIssueNumber || '없음'}`);
475
+
476
+ await github.rest.repos.createDispatchEvent({
477
+ owner: context.repo.owner,
478
+ repo: context.repo.repo,
479
+ event_type: 'build-ios-app',
480
+ client_payload: {
481
+ pr_number: sourceNumber,
482
+ build_number: buildNumber,
483
+ branch_name: branchName,
484
+ issue_number: relatedIssueNumber || '',
485
+ triggered_by: `${sourceType.toLowerCase()}-comment`,
486
+ comment_id: context.payload.comment.id.toString(),
487
+ source_type: sourceType
488
+ }
489
+ });
490
+
491
+ console.log('✅ iOS 빌드 워크플로우 트리거 완료');
492
+
493
+ # 트리거 완료 댓글은 제거됨 - Android/iOS 각각의 진행상황 댓글이 이 역할을 대체
494
+ - name: 트리거 완료 로그
495
+ if: steps.source_info.outputs.found == 'true' && steps.check_branch.outputs.exists == 'true'
496
+ run: |
497
+ echo "✅ Android 및 iOS 빌드 워크플로우 트리거 완료"
498
+ echo "📦 빌드 번호: ${{ steps.build_number_calc.outputs.buildNumber }}"
499
+ echo "🌿 브랜치: ${{ steps.source_info.outputs.branchName }}"
500
+ echo "📋 각 플랫폼별 진행상황 댓글이 자동으로 생성됩니다."
@@ -0,0 +1,185 @@
1
+ # ===================================================================
2
+ # Next.js CI (코드 분석 및 빌드 검증)
3
+ # ===================================================================
4
+ #
5
+ # {{MAIN_BRANCH}} 브랜치 푸시 시 Next.js 애플리케이션의 빌드를 검증합니다.
6
+ #
7
+ # 주요 특징:
8
+ # - npm ci로 의존성 설치
9
+ # - npm run build로 빌드 검증
10
+ # - Next.js 증분 빌드 캐싱 (.next/cache)
11
+ # - node_modules 캐싱 지원
12
+ #
13
+ # ===================================================================
14
+ # 🔑 필수 GitHub Secrets
15
+ # ===================================================================
16
+ # ENV_FILE (선택): .env 파일 내용
17
+ #
18
+ # ※ CI는 빌드 검증 목적이므로 배포 관련 Secrets 불필요
19
+ # ===================================================================
20
+ #
21
+ # 🔧 프로젝트별 설정 (env 섹션)
22
+ # ===================================================================
23
+ # PROJECT_NAME : 프로젝트 이름
24
+ # NODE_VERSION : Node.js 버전 (기본: 20.15.0)
25
+ # ENV_FILE : 환경변수 파일명 (기본: .env)
26
+ # ===================================================================
27
+
28
+ name: PROJECT-NEXT-CI
29
+
30
+ on:
31
+ push:
32
+ branches: ["{{DEVELOP_BRANCH}}"]
33
+ workflow_dispatch:
34
+
35
+ env:
36
+ PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
37
+ NODE_VERSION: "20.15.0"
38
+ ENV_FILE: ".env"
39
+
40
+ permissions:
41
+ contents: read
42
+ pull-requests: write
43
+ issues: write
44
+
45
+ jobs:
46
+ build:
47
+ name: Next.js 애플리케이션 빌드
48
+ runs-on: ubuntu-latest
49
+
50
+ steps:
51
+ # 1. 소스코드 체크아웃
52
+ - name: 코드 체크아웃
53
+ uses: actions/checkout@v5
54
+
55
+ # 2. 환경변수 파일 생성
56
+ - name: 환경변수 파일 생성 (${{ env.ENV_FILE }})
57
+ run: |
58
+ echo -e "${{ secrets.ENV_FILE || secrets.ENV }}" > ${{ env.ENV_FILE }}
59
+ echo "✅ ${{ env.ENV_FILE }} 파일 생성 완료"
60
+
61
+ # 3. Node.js 설정
62
+ - name: Node.js 설정
63
+ uses: actions/setup-node@v4
64
+ with:
65
+ node-version: ${{ env.NODE_VERSION }}
66
+ cache: "npm"
67
+
68
+ # 4. npm 의존성 캐싱
69
+ - name: npm 의존성 캐싱
70
+ uses: actions/cache@v4
71
+ with:
72
+ path: ~/.npm
73
+ key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
74
+ restore-keys: |
75
+ ${{ runner.os }}-npm-
76
+
77
+ # 5. node_modules 캐싱
78
+ - name: node_modules 캐싱
79
+ id: node-modules-cache
80
+ uses: actions/cache@v4
81
+ with:
82
+ path: "**/node_modules"
83
+ key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
84
+ restore-keys: |
85
+ ${{ runner.os }}-node-modules-
86
+
87
+ # 6. 의존성 설치
88
+ - name: 의존성 설치
89
+ if: steps.node-modules-cache.outputs.cache-hit != 'true'
90
+ run: |
91
+ echo "📦 의존성 설치 중..."
92
+ npm ci
93
+ echo "✅ 의존성 설치 완료"
94
+
95
+ # 7. Next.js 빌드 캐싱 (.next/cache)
96
+ # 증분 빌드를 위해 .next/cache 폴더를 캐싱합니다
97
+ - name: Next.js 빌드 캐싱
98
+ uses: actions/cache@v4
99
+ with:
100
+ path: .next/cache
101
+ key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
102
+ restore-keys: |
103
+ ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
104
+ ${{ runner.os }}-nextjs-
105
+
106
+ # 8. 프로젝트 빌드 (SSR/SSG)
107
+ - name: 프로젝트 빌드
108
+ id: build
109
+ run: |
110
+ echo "🔨 Next.js 빌드 시작..."
111
+ echo "프로젝트: ${{ env.PROJECT_NAME }}"
112
+ echo "Node.js: ${{ env.NODE_VERSION }}"
113
+ echo ""
114
+
115
+ # 빌드 실행 및 로그 저장
116
+ npm run build > build_log.txt 2>&1 || {
117
+ echo "build_failed=true" >> $GITHUB_OUTPUT
118
+ echo "status=failed" >> $GITHUB_OUTPUT
119
+ echo ""
120
+ echo "❌ 빌드 실패"
121
+ exit 0
122
+ }
123
+
124
+ echo "build_failed=false" >> $GITHUB_OUTPUT
125
+ echo "status=success" >> $GITHUB_OUTPUT
126
+ echo "✅ 빌드 완료"
127
+
128
+ # 빌드 결과 확인
129
+ if [ -d ".next" ]; then
130
+ echo ""
131
+ echo "📁 빌드 결과물:"
132
+ echo " - .next/ 폴더 생성됨"
133
+ du -sh .next/ || true
134
+ fi
135
+
136
+ # 9. 빌드 실패 시 에러 로그 출력
137
+ - name: 빌드 에러 로그 출력
138
+ if: steps.build.outputs.build_failed == 'true'
139
+ run: |
140
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
141
+ echo "❌ 빌드 실패 - 에러 로그"
142
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
143
+ echo ""
144
+
145
+ if [ -f "build_log.txt" ]; then
146
+ tail -n 50 build_log.txt
147
+ else
148
+ echo "에러 로그 파일을 찾을 수 없습니다."
149
+ fi
150
+
151
+ echo ""
152
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
153
+ exit 1
154
+
155
+ # 10. 빌드 로그 아티팩트 업로드
156
+ - name: 빌드 로그 업로드
157
+ if: always()
158
+ uses: actions/upload-artifact@v4
159
+ with:
160
+ name: build-log
161
+ path: build_log.txt
162
+ retention-days: 7
163
+
164
+ # 11. 빌드 결과 요약
165
+ - name: 빌드 결과 요약
166
+ if: always()
167
+ run: |
168
+ echo ""
169
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
170
+ if [ "${{ steps.build.outputs.status }}" = "success" ]; then
171
+ echo "🎉 Next.js 빌드 완료!"
172
+ else
173
+ echo "❌ Next.js 빌드 실패"
174
+ fi
175
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
176
+ echo ""
177
+ echo "📊 빌드 결과:"
178
+ echo " 📦 프로젝트: ${{ env.PROJECT_NAME }}"
179
+ echo " 📌 Node.js 버전: ${{ env.NODE_VERSION }}"
180
+ echo " 🆙 빌드 상태: ${{ steps.build.outputs.status }}"
181
+ echo " 🌿 브랜치: ${{ github.ref_name }}"
182
+ echo " 📝 커밋: ${{ github.sha }}"
183
+ echo " 🕐 시간: $(date '+%Y-%m-%d %H:%M:%S UTC')"
184
+ echo ""
185
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"