project-auto-wizard 0.9.1 → 0.11.0

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 (54) hide show
  1. package/README.md +83 -4
  2. package/package.json +1 -1
  3. package/payload/config/breaking-changes.json +24 -1
  4. package/payload/flutter-app/android/fastlane/Fastfile.playstore +86 -0
  5. package/payload/flutter-app/ios/ExportOptions.plist +31 -0
  6. package/payload/flutter-app/ios/fastlane/Fastfile +117 -0
  7. package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
  8. package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
  9. package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
  10. package/payload/scripts/changelog_manager.py +2 -2
  11. package/payload/scripts/version_manager.py +2 -2
  12. package/payload/version.yml.template +1 -0
  13. package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +12 -12
  14. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +48 -20
  15. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +44 -19
  16. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +28 -36
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +33 -46
  18. package/payload/workflows/flutter/{PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml → PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml} +38 -38
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +123 -32
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +57 -42
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +61 -40
  22. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -0
  23. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +40 -40
  24. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +47 -0
  25. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +47 -0
  26. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +40 -40
  27. package/payload/workflows/react/PROJECT-REACT-CI.yaml +47 -0
  28. package/payload/workflows/spring/nexus/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +1 -0
  29. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +47 -0
  30. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +1 -0
  31. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +39 -39
  32. package/src/cli/args.js +37 -0
  33. package/src/cli/help.js +4 -0
  34. package/src/commands/doctor.js +55 -2
  35. package/src/commands/dry-run.js +10 -0
  36. package/src/commands/full.js +28 -3
  37. package/src/commands/interactive-flutter.js +95 -0
  38. package/src/commands/interactive.js +51 -5
  39. package/src/commands/status.js +20 -2
  40. package/src/context.js +6 -0
  41. package/src/core/breaking.js +7 -3
  42. package/src/core/copy/flutter-app.js +42 -0
  43. package/src/core/copy/workflows.js +21 -9
  44. package/src/core/detect-fs.js +9 -1
  45. package/src/core/flutter-options.js +118 -0
  46. package/src/core/installed-stores.js +12 -0
  47. package/src/core/verify.js +2 -2
  48. package/src/core/version-yml.js +40 -5
  49. package/src/core/wizard-env.js +26 -4
  50. package/src/index.js +32 -1
  51. package/src/ui/env-plan.js +8 -5
  52. package/src/ui/prompts.js +88 -3
  53. package/src/ui/status-cards.js +13 -1
  54. package/src/ui/summary.js +24 -5
@@ -1,27 +1,27 @@
1
1
  # project-auto-wizard:managed-workflow
2
2
  # ===================================================================
3
- # Flutter 테스트 앱 빌드 트리거 워크플로우 (SUH-LAB)
3
+ # Flutter 테스트 앱 빌드 트리거 워크플로우
4
4
  # ===================================================================
5
5
  #
6
6
  # 이 워크플로우는 PR 또는 이슈에 댓글로 빌드 명령어를 작성하면
7
7
  # Android와 iOS 테스트 앱 빌드를 자동으로 트리거합니다.
8
8
  #
9
9
  # 주요 특징:
10
- # - PR/이슈 댓글 감지: "@suh-lab app build / apk build / ios build" 키워드 감지 (어순 무관: build app 도 동작)
10
+ # - PR/이슈 댓글 감지: "/wizard app build / apk build / ios build" 키워드 감지 (어순 무관: build app 도 동작)
11
11
  # - repository_dispatch 이벤트로 빌드 워크플로우 트리거
12
12
  # - PR/이슈 번호 + 빌드 횟수로 고유한 빌드 번호 생성 (예: 38700, 38701...)
13
13
  # - 브랜치명에서 이슈 번호 자동 추출 (YYYYMMDD_#이슈번호_내용 형식)
14
14
  # - 테스트 버전은 version.yml에서 동적으로 읽어 사용 (빌드 번호로 운영/테스트 구분)
15
15
  #
16
16
  # 사용 방법:
17
- # - PR에 댓글 작성: "@suh-lab app build" (Android + iOS 모두 빌드)
18
- # - PR에 댓글 작성: "@suh-lab apk build" (Android만 빌드)
19
- # - PR에 댓글 작성: "@suh-lab ios build" (iOS만 빌드)
20
- # - 이슈에 댓글 작성: 위 명령어 동일 (Guide by SUH-LAB 댓글 필요)
17
+ # - PR에 댓글 작성: "/wizard app build" (Android + iOS 모두 빌드)
18
+ # - PR에 댓글 작성: "/wizard apk build" (Android만 빌드)
19
+ # - PR에 댓글 작성: "/wizard ios build" (iOS만 빌드)
20
+ # - 이슈에 댓글 작성: 위 명령어 동일 (Issue Helper 댓글 필요)
21
21
  # - 어순은 무관: "build app" / "build apk" / "build ios"로 적어도 동일하게 동작
22
- # - 특정 브랜치 지정: "@suh-lab app build <브랜치명>" (명령어 뒤에 브랜치명을 적으면 해당 브랜치를 빌드)
23
- # 예: "@suh-lab apk build 20260609_#349_feature" → 해당 브랜치로 Android 빌드
24
- # 브랜치명을 생략하면 PR head 또는 이슈의 Guide by SUH-LAB 댓글 브랜치를 사용
22
+ # - 특정 브랜치 지정: "/wizard app build <브랜치명>" (명령어 뒤에 브랜치명을 적으면 해당 브랜치를 빌드)
23
+ # 예: "/wizard apk build 20260609_#349_feature" → 해당 브랜치로 Android 빌드
24
+ # 브랜치명을 생략하면 PR head 또는 이슈의 Issue Helper 댓글 브랜치를 사용
25
25
  #
26
26
  # 트리거되는 워크플로우:
27
27
  # - PROJECT-FLUTTER-ANDROID-TEST-APK.yaml (event_type: build-android-app)
@@ -29,7 +29,7 @@
29
29
  #
30
30
  # ===================================================================
31
31
 
32
- name: PROJECT-Flutter-SUH-LAB-App-Build-Trigger
32
+ name: PROJECT-Flutter-App-Build-Trigger
33
33
 
34
34
  on:
35
35
  issue_comment:
@@ -43,9 +43,9 @@ permissions:
43
43
  jobs:
44
44
  trigger-builds:
45
45
  name: 빌드 워크플로우 트리거
46
- # @suh-lab app build / @suh-lab apk build / @suh-lab ios build 키워드 감지 (어순 무관)
46
+ # /wizard app build / /wizard apk build / /wizard ios build 키워드 감지 (어순 무관)
47
47
  if: |
48
- contains(github.event.comment.body, '@suh-lab') &&
48
+ contains(github.event.comment.body, '/wizard') &&
49
49
  (
50
50
  (contains(github.event.comment.body, 'build') && contains(github.event.comment.body, 'app')) ||
51
51
  (contains(github.event.comment.body, 'apk') && contains(github.event.comment.body, 'build')) ||
@@ -93,27 +93,27 @@ jobs:
93
93
  // 어순은 양방향 지원: "{타입} build"(app/apk/ios build)와 "build {타입}"(build app 등) 모두 허용.
94
94
  // apk가 app으로 오인되지 않도록 app 토큰은 \b 단어경계로 정확히 끊는다(apk·application 제외).
95
95
  // apk/ios를 app보다 먼저 검사해, "apk build"가 app 분기로 새지 않게 한다.
96
- 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/)) {
97
- // @suh-lab apk build [브랜치] (또는 build apk) → Android만
96
+ if (comment.match(/\/wizard[^\S\r\n]+apk[^\S\r\n]+build/) || comment.match(/\/wizard[^\S\r\n]+build[^\S\r\n]+apk/)) {
97
+ // /wizard apk build [브랜치] (또는 build apk) → Android만
98
98
  buildAndroid = true;
99
99
  buildType = 'apk';
100
- m = rawComment.match(/@suh-lab[^\S\r\n]+apk[^\S\r\n]+build(?:[^\S\r\n]+(\S+))?/i)
101
- || rawComment.match(/@suh-lab[^\S\r\n]+build[^\S\r\n]+apk(?:[^\S\r\n]+(\S+))?/i);
100
+ m = rawComment.match(/\/wizard[^\S\r\n]+apk[^\S\r\n]+build(?:[^\S\r\n]+(\S+))?/i)
101
+ || rawComment.match(/\/wizard[^\S\r\n]+build[^\S\r\n]+apk(?:[^\S\r\n]+(\S+))?/i);
102
102
  customBranch = (m && m[1]) || '';
103
- } 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/)) {
104
- // @suh-lab ios build [브랜치] (또는 build ios) → iOS만
103
+ } else if (comment.match(/\/wizard[^\S\r\n]+ios[^\S\r\n]+build/) || comment.match(/\/wizard[^\S\r\n]+build[^\S\r\n]+ios/)) {
104
+ // /wizard ios build [브랜치] (또는 build ios) → iOS만
105
105
  buildIos = true;
106
106
  buildType = 'ios';
107
- m = rawComment.match(/@suh-lab[^\S\r\n]+ios[^\S\r\n]+build(?:[^\S\r\n]+(\S+))?/i)
108
- || rawComment.match(/@suh-lab[^\S\r\n]+build[^\S\r\n]+ios(?:[^\S\r\n]+(\S+))?/i);
107
+ m = rawComment.match(/\/wizard[^\S\r\n]+ios[^\S\r\n]+build(?:[^\S\r\n]+(\S+))?/i)
108
+ || rawComment.match(/\/wizard[^\S\r\n]+build[^\S\r\n]+ios(?:[^\S\r\n]+(\S+))?/i);
109
109
  customBranch = (m && m[1]) || '';
110
- } 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/)) {
111
- // @suh-lab app build [브랜치] (또는 build app) → Android + iOS 모두
110
+ } else if (comment.match(/\/wizard[^\S\r\n]+app\b[^\S\r\n]+build/) || comment.match(/\/wizard[^\S\r\n]+build[^\S\r\n]+app\b/)) {
111
+ // /wizard app build [브랜치] (또는 build app) → Android + iOS 모두
112
112
  buildAndroid = true;
113
113
  buildIos = true;
114
114
  buildType = 'app';
115
- m = rawComment.match(/@suh-lab[^\S\r\n]+app\b[^\S\r\n]+build(?:[^\S\r\n]+(\S+))?/i)
116
- || rawComment.match(/@suh-lab[^\S\r\n]+build[^\S\r\n]+app\b(?:[^\S\r\n]+(\S+))?/i);
115
+ m = rawComment.match(/\/wizard[^\S\r\n]+app\b[^\S\r\n]+build(?:[^\S\r\n]+(\S+))?/i)
116
+ || rawComment.match(/\/wizard[^\S\r\n]+build[^\S\r\n]+app\b(?:[^\S\r\n]+(\S+))?/i);
117
117
  customBranch = (m && m[1]) || '';
118
118
  }
119
119
 
@@ -175,7 +175,7 @@ jobs:
175
175
  } catch (error) {
176
176
  console.log('ℹ️ PR이 아닙니다. 이슈에서 브랜치 정보를 찾습니다...');
177
177
 
178
- // 2. PR이 아니면 이슈에서 "Guide by SUH-LAB" 댓글 찾기
178
+ // 2. PR이 아니면 이슈에서 "Issue Helper" 댓글 찾기
179
179
  sourceType = 'ISSUE';
180
180
  sourceNumber = issueNumber.toString();
181
181
  relatedIssueNumber = issueNumber.toString();
@@ -203,27 +203,27 @@ jobs:
203
203
  per_page: 100
204
204
  });
205
205
 
206
- // "Guide by SUH-LAB" 댓글 찾기
206
+ // "Issue Helper" 댓글 찾기
207
207
  const guideComment = comments.data.find(c =>
208
- c.body.includes('Guide by SUH-LAB')
208
+ c.body.includes('<!-- project-auto-wizard issue helper -->')
209
209
  );
210
210
 
211
211
  if (!guideComment) {
212
- console.log('❌ "Guide by SUH-LAB" 댓글을 찾을 수 없습니다.');
212
+ console.log('❌ "Issue Helper" 댓글을 찾을 수 없습니다.');
213
213
  core.setOutput('found', 'false');
214
- core.setOutput('errorMessage', '이슈에서 "Guide by SUH-LAB" 댓글을 찾을 수 없습니다. 브랜치 정보가 포함된 댓글이 필요합니다.');
214
+ core.setOutput('errorMessage', '이슈에서 "Issue Helper" 댓글을 찾을 수 없습니다. 브랜치 정보가 포함된 댓글이 필요합니다.');
215
215
  return;
216
216
  }
217
217
 
218
- console.log('✅ "Guide by SUH-LAB" 댓글 발견');
218
+ console.log('✅ "Issue Helper" 댓글 발견');
219
219
 
220
220
  // 브랜치명 추출 (### 브랜치 다음 ``` 블록 내용)
221
- const branchMatch = guideComment.body.match(/### 브랜치\s*```\s*([\s\S]*?)\s*```/);
221
+ const branchMatch = guideComment.body.match(/### 브랜치(?:명)?\s*```\s*([\s\S]*?)\s*```/);
222
222
 
223
223
  if (!branchMatch) {
224
224
  console.log('❌ 브랜치 정보를 파싱할 수 없습니다.');
225
225
  core.setOutput('found', 'false');
226
- core.setOutput('errorMessage', '"Guide by SUH-LAB" 댓글에서 브랜치 정보를 파싱할 수 없습니다. 댓글 형식을 확인해주세요.');
226
+ core.setOutput('errorMessage', '"Issue Helper" 댓글에서 브랜치 정보를 파싱할 수 없습니다. 댓글 형식을 확인해주세요.');
227
227
  return;
228
228
  }
229
229
 
@@ -262,9 +262,9 @@ jobs:
262
262
 
263
263
  **해결 방법:**
264
264
  - PR에서 빌드하는 경우: PR 페이지에서 댓글을 작성해주세요.
265
- - 이슈에서 빌드하는 경우: "Guide by SUH-LAB" 댓글이 있어야 합니다.
265
+ - 이슈에서 빌드하는 경우: "Issue Helper" 댓글이 있어야 합니다.
266
266
  - 댓글에 \`### 브랜치\` 섹션과 브랜치명이 포함되어 있어야 합니다.
267
- - 또는 댓글에 브랜치명을 직접 지정할 수 있습니다: \`@suh-lab app build <브랜치명>\``;
267
+ - 또는 댓글에 브랜치명을 직접 지정할 수 있습니다: \`/wizard app build <브랜치명>\``;
268
268
 
269
269
  await github.rest.issues.createComment({
270
270
  owner: context.repo.owner,
@@ -329,7 +329,7 @@ jobs:
329
329
  '',
330
330
  '### 💡 확인 사항',
331
331
  '1. 브랜치가 원격 저장소에 push되었는지 확인하세요',
332
- '2. "Guide by SUH-LAB" 댓글의 브랜치명이 올바른지 확인하세요',
332
+ '2. "Issue Helper" 댓글의 브랜치명이 올바른지 확인하세요',
333
333
  '3. 브랜치명에 오타가 없는지 확인하세요',
334
334
  '',
335
335
  `🔗 [워크플로우 로그](${runUrl})`
@@ -377,7 +377,7 @@ jobs:
377
377
  // iOS 빌드: ios 빌드 + app 공용 빌드 둘 다 카운트 (어순 무관)
378
378
  buildCount = comments.data.filter(c => {
379
379
  const body = c.body.toLowerCase();
380
- return body.includes('@suh-lab') && body.includes('build') && (
380
+ return body.includes('/wizard') && body.includes('build') && (
381
381
  body.includes('ios') || body.includes('app')
382
382
  );
383
383
  }).length;
@@ -387,7 +387,7 @@ jobs:
387
387
  // Android 빌드: apk 빌드 + app 공용 빌드 둘 다 카운트 (어순 무관)
388
388
  buildCount = comments.data.filter(c => {
389
389
  const body = c.body.toLowerCase();
390
- return body.includes('@suh-lab') && body.includes('build') && (
390
+ return body.includes('/wizard') && body.includes('build') && (
391
391
  body.includes('apk') || body.includes('app')
392
392
  );
393
393
  }).length;
@@ -397,7 +397,7 @@ jobs:
397
397
  // 전체 앱 빌드: 모든 빌드 명령어 카운트 (어순 무관)
398
398
  buildCount = comments.data.filter(c => {
399
399
  const body = c.body.toLowerCase();
400
- return body.includes('@suh-lab') && body.includes('build') && (
400
+ return body.includes('/wizard') && body.includes('build') && (
401
401
  body.includes('ios') ||
402
402
  body.includes('apk') ||
403
403
  body.includes('app')
@@ -32,7 +32,10 @@
32
32
  # FLUTTER_VERSION : Flutter SDK 버전
33
33
  # JAVA_VERSION : Java 버전 (Android 빌드용)
34
34
  # XCODE_VERSION : Xcode 버전 (iOS 빌드용)
35
- # ENV_FILE_PATH : .env 파일 경로 (기본값: 루트의 .env)
35
+ # ENV_FILE_PATH : dotenv 모드에서 .env를 만들 경로 (Flutter 루트 기준, 기본값: .env)
36
+ # ENV_MODE : 환경변수 전달 방식 — dart-define(--dart-define-from-file) | dotenv(.env 파일)
37
+ # FLUTTER_PROJECT_DIR : Flutter 루트 경로 (설치 시 자동 설정, 모노레포는 app 등)
38
+ # PROJECT_PATH : 변경 감지 기준 경로 (설치 시 자동 설정, "."이면 항상 실행)
36
39
  #
37
40
  # ==========================================================================================
38
41
  # 🔑 필수 GitHub Secrets
@@ -40,6 +43,8 @@
40
43
  # ENV_FILE (선택): .env 파일 내용
41
44
  #
42
45
  # ※ CI는 빌드 검증 목적이므로 서명/배포 관련 Secrets 불필요
46
+ # ※ 모노레포에서 이 CI를 required check로 쓰려면 개별 job이 아니라 `ci-gate` 하나만 등록하세요.
47
+ # (경로가 바뀌지 않아 건너뛴 job은 Success로 집계되고, ci-gate가 최종 결과를 판정합니다)
43
48
  # ==========================================================================================
44
49
 
45
50
  name: PROJECT-Flutter-CI
@@ -73,6 +78,13 @@ permissions:
73
78
  # 🔧 프로젝트별 설정 (아래 값들을 수정하세요)
74
79
  # ==========================================================================================
75
80
  env:
81
+ # 🎯 모노레포 변경 감지 경로 (설치 시 자동 설정, 단일 레포는 ".")
82
+ PROJECT_PATH: "." # @wizard auto:project-path
83
+ # Flutter 루트 경로(레포 루트 기준). 단일레포면 ".", 모노레포면 "app" 등 — 설치 시 자동 설정
84
+ FLUTTER_PROJECT_DIR: "." # @wizard auto:flutter-root
85
+ # 환경변수 전달 방식 (dart-define | dotenv) — 설치 시 선택값으로 자동 설정
86
+ ENV_MODE: "dart-define" # @wizard auto:flutter-env-mode
87
+
76
88
  # 🎯 CI 모드 설정
77
89
  ANALYZE_ONLY: "false" # true: analyze만 실행 (빌드 스킵), false: analyze + 빌드
78
90
 
@@ -84,18 +96,44 @@ env:
84
96
  FLUTTER_VERSION: "3.35.5"
85
97
  JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:17
86
98
  XCODE_VERSION: "26.0"
87
- ENV_FILE_PATH: ".env" # 환경변수 파일 경로 (커스터마이징 가능)
99
+ ENV_FILE_PATH: ".env" # dotenv 모드에서 .env를 만들 경로 (Flutter 루트 기준, 커스터마이징 가능)
88
100
 
89
101
  concurrency:
90
102
  group: flutter-ci-${{ github.ref }}
91
103
  cancel-in-progress: true
92
104
 
93
105
  jobs:
106
+ # ===================================================================
107
+ # Job 0: 변경 감지 (PROJECT_PATH 아래가 바뀌었을 때만 이후 job 실행)
108
+ # ===================================================================
109
+ changes:
110
+ name: 변경 감지
111
+ runs-on: ubuntu-latest
112
+ permissions:
113
+ contents: read
114
+ pull-requests: read
115
+ outputs:
116
+ project: ${{ steps.filter.outputs.project }}
117
+ steps:
118
+ - name: Checkout repository
119
+ uses: actions/checkout@v7
120
+
121
+ - name: 프로젝트 경로 변경 여부 판별
122
+ id: filter
123
+ if: ${{ github.event_name != 'workflow_dispatch' }}
124
+ uses: dorny/paths-filter@v4
125
+ with:
126
+ filters: |
127
+ project:
128
+ - '${{ env.PROJECT_PATH == '.' && '**' || format('{0}/**', env.PROJECT_PATH) }}'
129
+
94
130
  # ===================================================================
95
131
  # Job 1: 준비 단계
96
132
  # ===================================================================
97
133
  prepare:
98
134
  name: CI 준비
135
+ needs: changes
136
+ if: ${{ (github.event_name == 'workflow_dispatch' || needs.changes.outputs.project == 'true') }}
99
137
  runs-on: ubuntu-latest
100
138
 
101
139
  outputs:
@@ -229,7 +267,11 @@ jobs:
229
267
  analyze:
230
268
  name: 코드 분석
231
269
  runs-on: ubuntu-latest
232
- needs: prepare
270
+ needs: [changes, prepare]
271
+ if: ${{ (github.event_name == 'workflow_dispatch' || needs.changes.outputs.project == 'true') }}
272
+ defaults:
273
+ run:
274
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
233
275
 
234
276
  outputs:
235
277
  status: ${{ steps.result.outputs.status }}
@@ -246,13 +288,20 @@ jobs:
246
288
  - name: Checkout repository
247
289
  uses: actions/checkout@v7
248
290
 
249
- # .env 파일 생성
250
- - name: Create .env file
291
+ # 환경변수 준비 — ENV_MODE=dotenv면 .env 파일, dart-define이면 프로젝트 밖 임시 파일(--dart-define-from-file)
292
+ - name: Prepare env file
293
+ env:
294
+ ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
251
295
  run: |
252
- cat << 'EOF' > ${{ env.ENV_FILE_PATH }}
253
- ${{ secrets.ENV_FILE || secrets.ENV }}
254
- EOF
255
- echo "✅ ${{ env.ENV_FILE_PATH }} file created"
296
+ if [ "$ENV_MODE" = "dotenv" ]; then
297
+ printf '%s\n' "$ENV_CONTENT" > "$ENV_FILE_PATH"
298
+ echo "✅ $ENV_FILE_PATH file created"
299
+ elif [ -n "$ENV_CONTENT" ]; then
300
+ DART_DEFINE_FILE="$RUNNER_TEMP/dart-define.env"
301
+ printf '%s\n' "$ENV_CONTENT" > "$DART_DEFINE_FILE"
302
+ echo "DART_DEFINE_FILE=$DART_DEFINE_FILE" >> "$GITHUB_ENV"
303
+ echo "dart-define file prepared"
304
+ fi
256
305
 
257
306
  # Flutter 설정
258
307
  - name: Set up Flutter
@@ -271,7 +320,7 @@ jobs:
271
320
  uses: actions/cache@v6
272
321
  with:
273
322
  path: ~/.pub-cache
274
- key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }}
323
+ key: ${{ runner.os }}-flutter-pub-${{ hashFiles(format('{0}/**/pubspec.lock', env.FLUTTER_PROJECT_DIR)) }}
275
324
  restore-keys: ${{ runner.os }}-flutter-pub-
276
325
 
277
326
  # 프로젝트 의존성 설치
@@ -317,10 +366,14 @@ jobs:
317
366
  build-android:
318
367
  name: Android 빌드
319
368
  if: |
369
+ (github.event_name == 'workflow_dispatch' || needs.changes.outputs.project == 'true') &&
320
370
  needs.prepare.outputs.analyze_only != 'true' &&
321
371
  needs.prepare.outputs.enable_android == 'true'
322
372
  runs-on: ubuntu-latest
323
- needs: prepare
373
+ needs: [changes, prepare]
374
+ defaults:
375
+ run:
376
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
324
377
 
325
378
  outputs:
326
379
  status: ${{ steps.result.outputs.status }}
@@ -337,13 +390,20 @@ jobs:
337
390
  - name: Checkout repository
338
391
  uses: actions/checkout@v7
339
392
 
340
- # .env 파일 생성
341
- - name: Create .env file
393
+ # 환경변수 준비 — ENV_MODE=dotenv면 .env 파일, dart-define이면 프로젝트 밖 임시 파일(--dart-define-from-file)
394
+ - name: Prepare env file
395
+ env:
396
+ ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
342
397
  run: |
343
- cat << 'EOF' > ${{ env.ENV_FILE_PATH }}
344
- ${{ secrets.ENV_FILE || secrets.ENV }}
345
- EOF
346
- echo "✅ ${{ env.ENV_FILE_PATH }} file created"
398
+ if [ "$ENV_MODE" = "dotenv" ]; then
399
+ printf '%s\n' "$ENV_CONTENT" > "$ENV_FILE_PATH"
400
+ echo "✅ $ENV_FILE_PATH file created"
401
+ elif [ -n "$ENV_CONTENT" ]; then
402
+ DART_DEFINE_FILE="$RUNNER_TEMP/dart-define.env"
403
+ printf '%s\n' "$ENV_CONTENT" > "$DART_DEFINE_FILE"
404
+ echo "DART_DEFINE_FILE=$DART_DEFINE_FILE" >> "$GITHUB_ENV"
405
+ echo "dart-define file prepared"
406
+ fi
347
407
 
348
408
  # Flutter 설정
349
409
  - name: Set up Flutter
@@ -362,7 +422,7 @@ jobs:
362
422
  uses: actions/cache@v6
363
423
  with:
364
424
  path: ~/.pub-cache
365
- key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }}
425
+ key: ${{ runner.os }}-flutter-pub-${{ hashFiles(format('{0}/**/pubspec.lock', env.FLUTTER_PROJECT_DIR)) }}
366
426
  restore-keys: ${{ runner.os }}-flutter-pub-
367
427
 
368
428
  # Gradle 캐시
@@ -372,7 +432,7 @@ jobs:
372
432
  path: |
373
433
  ~/.gradle/caches
374
434
  ~/.gradle/wrapper
375
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle', '**/gradle-wrapper.properties') }}
435
+ key: ${{ runner.os }}-gradle-${{ hashFiles(format('{0}/android/**/build.gradle', env.FLUTTER_PROJECT_DIR), format('{0}/android/**/gradle-wrapper.properties', env.FLUTTER_PROJECT_DIR)) }}
376
436
  restore-keys: |
377
437
  ${{ runner.os }}-gradle-
378
438
 
@@ -399,7 +459,7 @@ jobs:
399
459
 
400
460
  # Gradle 셋업
401
461
  - name: Setup Gradle
402
- working-directory: android
462
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}/android
403
463
  run: |
404
464
  chmod +x gradlew
405
465
  echo "✅ Gradle wrapper permissions set"
@@ -409,7 +469,7 @@ jobs:
409
469
  id: build
410
470
  run: |
411
471
  echo "📦 Flutter APK 빌드 시작 (debug)..."
412
- flutter build apk --debug
472
+ flutter build apk --debug ${DART_DEFINE_FILE:+--dart-define-from-file="$DART_DEFINE_FILE"}
413
473
  echo "✅ APK 빌드 완료"
414
474
  ls -la ./build/app/outputs/flutter-apk/ || true
415
475
 
@@ -439,10 +499,14 @@ jobs:
439
499
  build-ios:
440
500
  name: iOS 빌드
441
501
  if: |
502
+ (github.event_name == 'workflow_dispatch' || needs.changes.outputs.project == 'true') &&
442
503
  needs.prepare.outputs.analyze_only != 'true' &&
443
504
  needs.prepare.outputs.enable_ios == 'true'
444
505
  runs-on: macos-26
445
- needs: prepare
506
+ needs: [changes, prepare]
507
+ defaults:
508
+ run:
509
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
446
510
 
447
511
  outputs:
448
512
  status: ${{ steps.result.outputs.status }}
@@ -479,13 +543,20 @@ jobs:
479
543
  echo "=== Downloading iOS platform ==="
480
544
  xcodebuild -downloadPlatform iOS
481
545
 
482
- # .env 파일 생성
483
- - name: Create .env file
546
+ # 환경변수 준비 — ENV_MODE=dotenv면 .env 파일, dart-define이면 프로젝트 밖 임시 파일(--dart-define-from-file)
547
+ - name: Prepare env file
548
+ env:
549
+ ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
484
550
  run: |
485
- cat << 'EOF' > ${{ env.ENV_FILE_PATH }}
486
- ${{ secrets.ENV_FILE || secrets.ENV }}
487
- EOF
488
- echo "✅ ${{ env.ENV_FILE_PATH }} file created"
551
+ if [ "$ENV_MODE" = "dotenv" ]; then
552
+ printf '%s\n' "$ENV_CONTENT" > "$ENV_FILE_PATH"
553
+ echo "✅ $ENV_FILE_PATH file created"
554
+ elif [ -n "$ENV_CONTENT" ]; then
555
+ DART_DEFINE_FILE="$RUNNER_TEMP/dart-define.env"
556
+ printf '%s\n' "$ENV_CONTENT" > "$DART_DEFINE_FILE"
557
+ echo "DART_DEFINE_FILE=$DART_DEFINE_FILE" >> "$GITHUB_ENV"
558
+ echo "dart-define file prepared"
559
+ fi
489
560
 
490
561
  # Flutter 설정
491
562
  - name: Set up Flutter
@@ -504,7 +575,7 @@ jobs:
504
575
  uses: actions/cache@v6
505
576
  with:
506
577
  path: ~/.pub-cache
507
- key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }}
578
+ key: ${{ runner.os }}-flutter-pub-${{ hashFiles(format('{0}/**/pubspec.lock', env.FLUTTER_PROJECT_DIR)) }}
508
579
  restore-keys: ${{ runner.os }}-flutter-pub-
509
580
 
510
581
  # 프로젝트 의존성 설치
@@ -534,7 +605,7 @@ jobs:
534
605
  id: build
535
606
  run: |
536
607
  echo "📦 Flutter iOS 빌드 시작..."
537
- flutter build ios --release --no-codesign
608
+ flutter build ios --release --no-codesign ${DART_DEFINE_FILE:+--dart-define-from-file="$DART_DEFINE_FILE"}
538
609
  echo "✅ iOS 빌드 완료"
539
610
 
540
611
  # 빌드 결과 기록
@@ -562,9 +633,9 @@ jobs:
562
633
  # ===================================================================
563
634
  report:
564
635
  name: 결과 보고
565
- if: always() && github.event_name == 'pull_request'
636
+ if: always() && github.event_name == 'pull_request' && needs.changes.outputs.project == 'true'
566
637
  runs-on: ubuntu-latest
567
- needs: [prepare, analyze, build-android, build-ios]
638
+ needs: [changes, prepare, analyze, build-android, build-ios]
568
639
 
569
640
  steps:
570
641
  - name: 최종 결과 댓글 업데이트
@@ -714,3 +785,23 @@ jobs:
714
785
  } else {
715
786
  console.log('⚠️ 댓글 ID가 없어 업데이트를 건너뜁니다.');
716
787
  }
788
+
789
+ # ===================================================================
790
+ # Job 6: CI Gate (required check로 등록할 단일 진입점)
791
+ # ===================================================================
792
+ # 건너뛴(skipped) job은 통과로 보고 failure/cancelled가 하나라도 있으면 실패한다.
793
+ ci-gate:
794
+ name: CI Gate
795
+ if: ${{ always() }}
796
+ needs: [changes, prepare, analyze, build-android, build-ios, report]
797
+ runs-on: ubuntu-latest
798
+ steps:
799
+ - name: 결과 집계
800
+ env:
801
+ RESULTS: ${{ toJSON(needs.*.result) }}
802
+ run: |
803
+ echo "needs 결과: $RESULTS"
804
+ if echo "$RESULTS" | grep -Eq '"(failure|cancelled)"'; then
805
+ echo "실패하거나 취소된 job이 있습니다."
806
+ exit 1
807
+ fi