project-auto-wizard 0.1.25 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@ npx project-auto-wizard
17
17
  [![node](https://img.shields.io/badge/node-%3E%3D20.12-brightgreen)](package.json)
18
18
 
19
19
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
20
- ## 최신 버전 : v0.1.24 (2026-08-06)
20
+ ## 최신 버전 : v0.1.30 (2026-08-08)
21
21
 
22
22
  [전체 버전 기록 보기](CHANGELOG.md)
23
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-auto-wizard",
3
- "version": "0.1.25",
3
+ "version": "0.1.32",
4
4
  "description": "One command DevOps: npx wizard that installs GitHub-native AI Release Automation into any project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -433,12 +433,9 @@ def cmd_classify_bump(commits_file: str) -> int:
433
433
  def cmd_update_from_summary() -> int:
434
434
  """pr_body.md에서 Markdown을 파싱하여 CHANGELOG.json 갱신."""
435
435
  version = os.environ.get('VERSION')
436
- project_type = os.environ.get('PROJECT_TYPE')
437
- # 멀티타입 — PROJECT_TYPES(csv) env가 있으면 배열로, 없으면 단수 키 fallback
436
+ # PROJECT_TYPES(csv)가 유일한 입력 — 단수 PROJECT_TYPE 폴백은 제거됐다 (issue #62)
438
437
  project_types_csv = os.environ.get('PROJECT_TYPES', '')
439
438
  project_types = [t.strip() for t in project_types_csv.split(',') if t.strip()]
440
- if not project_types and project_type:
441
- project_types = [project_type]
442
439
  today = os.environ.get('TODAY')
443
440
  pr_number_raw = os.environ.get('PR_NUMBER')
444
441
  timestamp = os.environ.get('TIMESTAMP')
@@ -482,8 +479,7 @@ def cmd_update_from_summary() -> int:
482
479
  # 릴리즈 데이터 생성
483
480
  new_release = {
484
481
  "version": version,
485
- "project_type": project_type, # 기존 단수 키 — 유지 (하위 호환)
486
- "project_types": project_types, # 신규 멀티타입 배열
482
+ "project_types": project_types,
487
483
  "date": today,
488
484
  "pr_number": pr_number,
489
485
  "raw_summary": raw_summary,
@@ -510,7 +506,6 @@ def cmd_update_from_summary() -> int:
510
506
  "metadata": {
511
507
  "lastUpdated": timestamp,
512
508
  "currentVersion": version,
513
- "projectType": project_type,
514
509
  "projectTypes": project_types,
515
510
  "totalReleases": 0,
516
511
  },
@@ -525,7 +520,6 @@ def cmd_update_from_summary() -> int:
525
520
 
526
521
  changelog_data["metadata"]["lastUpdated"] = timestamp
527
522
  changelog_data["metadata"]["currentVersion"] = version
528
- changelog_data["metadata"]["projectType"] = project_type
529
523
  changelog_data["metadata"]["projectTypes"] = project_types
530
524
  changelog_data["metadata"]["totalReleases"] = len(changelog_data.get("releases", [])) + 1
531
525
  changelog_data.setdefault("releases", []).insert(0, new_release)
@@ -146,21 +146,22 @@ def get_current_version():
146
146
  return read_scalar_key("version", "0.0.0")
147
147
 
148
148
 
149
- def get_project_type():
150
- return read_scalar_key("project_type", "basic")
151
-
152
-
153
149
  def get_project_types_csv():
154
150
  """Return project_types as a list. Supports both:
155
151
  project_types: ["a", "b"]
156
152
  project_types:
157
153
  - "a"
158
154
  - "b"
159
- Returns [] if key absent (legacy single-type mode)."""
155
+ Returns [] if the key is absent project_types is the single source of
156
+ truth (issue #62), so callers must treat [] as a hard error rather than
157
+ falling back to a singular key."""
160
158
  text = read_text()
161
159
 
162
- # Inline array form: project_types: ["a", "b"]
163
- m = re.search(r'^project_types:[ \t]*\[(.*?)\][ \t]*$', text, re.MULTILINE)
160
+ # Inline array form: project_types: ["a", "b"] # trailing comment allowed
161
+ # The template always appends "# first entry is primary", so anchoring at
162
+ # end-of-line made this branch never match — every install silently fell
163
+ # through to the singular key instead (issue #62).
164
+ m = re.search(r'^project_types:[ \t]*\[(.*?)\][ \t]*(?:#.*)?$', text, re.MULTILINE)
164
165
  if m:
165
166
  inner = m.group(1)
166
167
  items = re.findall(r'"([^"]*)"|\'([^\']*)\'', inner)
@@ -174,7 +175,8 @@ def get_project_types_csv():
174
175
  m = re.search(r'^project_types:[ \t]*\n((?:[ \t]+-[ \t]*.*\n?)+)', text, re.MULTILINE)
175
176
  if m:
176
177
  block = m.group(1)
177
- types = re.findall(r'-\s*["\']?([^"\'\n]+?)["\']?\s*$', block, re.MULTILINE)
178
+ # trailing comments are allowed on list items too
179
+ types = re.findall(r'-[ \t]*["\']?([^"\'#\n]+?)["\']?[ \t]*(?:#.*)?$', block, re.MULTILINE)
178
180
  return [t.strip() for t in types if t.strip()]
179
181
 
180
182
  return []
@@ -221,6 +223,13 @@ def get_version_code():
221
223
 
222
224
 
223
225
  def set_version_code(new_code):
226
+ current = read_scalar_key("version_code", None)
227
+ if current not in (None, "", "null"):
228
+ try:
229
+ if int(new_code) < int(current):
230
+ log(f"WARNING: version_code {new_code} is lower than current {current} — writing anyway (regression?)")
231
+ except ValueError:
232
+ pass
224
233
  text = read_text()
225
234
  pattern = re.compile(r'^version_code:[ \t]*.*$', re.MULTILINE)
226
235
  replacement = f'version_code: {new_code} # app build number'
@@ -423,7 +432,9 @@ def sync_for_type(project_type, new_version, version_code_getter):
423
432
  def sync_all_project_files(new_version):
424
433
  types = get_project_types_csv()
425
434
  if not types:
426
- types = [get_project_type()]
435
+ # No silent fallback: an unreadable project_types used to degrade to
436
+ # "basic" and skip every sync without a word (issue #62).
437
+ raise SystemExit("ERROR: version.yml has no readable project_types — cannot sync project files")
427
438
  for t in types:
428
439
  sync_for_type(t, new_version, get_version_code)
429
440
 
@@ -503,7 +514,10 @@ def get_project_file_version(project_type):
503
514
 
504
515
  def sync_versions():
505
516
  yml_version = get_current_version()
506
- primary_type = get_project_type()
517
+ types = get_project_types_csv()
518
+ if not types:
519
+ raise SystemExit("ERROR: version.yml has no readable project_types — cannot sync versions")
520
+ primary_type = types[0]
507
521
  project_version = get_project_file_version(primary_type)
508
522
 
509
523
  log("Version sync check")
@@ -10,7 +10,7 @@
10
10
  # Supported project types: spring, flutter, next, react, react-native,
11
11
  # react-native-expo, node, python, basic
12
12
  # Multi-type projects: list every type in the project_types array
13
- # -> project_type (singular) always mirrors project_types[0] do not edit it directly
13
+ # -> the first entry is the primary type (project_types is the single source of truth)
14
14
  # Monorepo paths: project_paths maps each type to its subdir relative to the repo root
15
15
  # (e.g., flutter: "app", react: "client"). Omitted -> repo root.
16
16
  #
@@ -27,7 +27,6 @@
27
27
  version: "{{VERSION}}"
28
28
  version_code: {{VERSION_CODE}} # app build number
29
29
  project_types: {{PROJECT_TYPES}} # first entry is primary
30
- project_type: "{{PROJECT_TYPE}}" # auto-mirror of project_types[0] — do not edit directly
31
30
  {{PROJECT_PATHS}}
32
31
  metadata:
33
32
  last_updated: "{{NOW}}"
@@ -156,13 +156,11 @@ jobs:
156
156
  run: |
157
157
  # note: `grep | sed || echo default` never fires the default (the
158
158
  # pipeline exits with sed's 0) — default via ${var:-} instead
159
- PROJECT_TYPE=$(grep "^project_type:" version.yml 2>/dev/null | sed 's/project_type: *"\([^"]*\)".*/\1/')
160
- PROJECT_TYPE=${PROJECT_TYPE:-unknown}
161
159
  PROJECT_TYPES=$(grep "^project_types:" version.yml 2>/dev/null | sed -E 's/.*\[([^]]*)\].*/\1/' | tr -d '" ')
162
- PROJECT_TYPES=${PROJECT_TYPES:-$PROJECT_TYPE}
160
+ PROJECT_TYPES=${PROJECT_TYPES:-unknown}
163
161
 
164
162
  export VERSION="${{ steps.bump.outputs.new_version }}"
165
- export PROJECT_TYPE PROJECT_TYPES
163
+ export PROJECT_TYPES
166
164
  export TODAY=$(date '+%Y-%m-%d')
167
165
  export PR_NUMBER="${{ github.event.pull_request.number }}"
168
166
  export TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%SZ')
@@ -192,13 +192,11 @@ jobs:
192
192
 
193
193
  # note: `grep | sed || echo default` never fires the default (the
194
194
  # pipeline exits with sed's 0) — default via ${var:-} instead
195
- PROJECT_TYPE=$(grep "^project_type:" version.yml 2>/dev/null | sed 's/project_type: *"\([^"]*\)".*/\1/')
196
- PROJECT_TYPE=${PROJECT_TYPE:-unknown}
197
195
  PROJECT_TYPES=$(grep "^project_types:" version.yml 2>/dev/null | sed -E 's/.*\[([^]]*)\].*/\1/' | tr -d '" ')
198
- PROJECT_TYPES=${PROJECT_TYPES:-$PROJECT_TYPE}
196
+ PROJECT_TYPES=${PROJECT_TYPES:-unknown}
199
197
 
200
198
  export VERSION="$NEW_VERSION"
201
- export PROJECT_TYPE PROJECT_TYPES
199
+ export PROJECT_TYPES
202
200
  export TODAY=$(date '+%Y-%m-%d')
203
201
  export TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%SZ')
204
202
 
@@ -114,21 +114,16 @@ jobs:
114
114
  echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
115
115
  echo "new version: $NEW_VERSION"
116
116
 
117
- - name: Read project type
117
+ - name: Read project types
118
118
  id: project_info
119
119
  if: steps.release_guard.outputs.skip != 'true'
120
120
  run: |
121
121
  if [ -f "version.yml" ]; then
122
- PROJECT_TYPE=$(grep "^project_type:" version.yml | sed 's/project_type: *"\([^"]*\)".*/\1/' || echo "unknown")
123
- PROJECT_TYPES=$(grep "^project_types:" version.yml 2>/dev/null | sed -E 's/.*\[([^]]*)\].*/\1/' | tr -d '" ' || echo "")
124
- if [ -z "$PROJECT_TYPES" ]; then
125
- PROJECT_TYPES="$PROJECT_TYPE"
126
- fi
127
- echo "project_type=$PROJECT_TYPE" >> $GITHUB_OUTPUT
122
+ PROJECT_TYPES=$(grep "^project_types:" version.yml 2>/dev/null | sed -E 's/.*\[([^]]*)\].*/\1/' | tr -d '" ')
123
+ PROJECT_TYPES=${PROJECT_TYPES:-unknown}
128
124
  echo "project_types=$PROJECT_TYPES" >> $GITHUB_OUTPUT
129
- echo "project type: $PROJECT_TYPE (types: $PROJECT_TYPES)"
125
+ echo "project types: $PROJECT_TYPES"
130
126
  else
131
- echo "project_type=unknown" >> $GITHUB_OUTPUT
132
127
  echo "project_types=unknown" >> $GITHUB_OUTPUT
133
128
  echo "version.yml not found"
134
129
  fi
@@ -112,7 +112,11 @@ jobs:
112
112
  restore-keys: ${{ runner.os }}-flutter-pub-
113
113
 
114
114
  - name: Install dependencies
115
- run: flutter pub get
115
+ run: |
116
+ flutter pub get
117
+ if grep -q "build_runner" pubspec.yaml; then
118
+ dart run build_runner build --delete-conflicting-outputs
119
+ fi
116
120
 
117
121
  # version_manager.py를 사용하여 현재 버전 정보 가져오기
118
122
  - name: 현재 버전 정보 가져오기
@@ -265,7 +269,11 @@ jobs:
265
269
  restore-keys: ${{ runner.os }}-flutter-pub-
266
270
 
267
271
  - name: Install dependencies
268
- run: flutter pub get
272
+ run: |
273
+ flutter pub get
274
+ if grep -q "build_runner" pubspec.yaml; then
275
+ dart run build_runner build --delete-conflicting-outputs
276
+ fi
269
277
 
270
278
  - name: Set up Java
271
279
  uses: actions/setup-java@v5
@@ -328,6 +336,9 @@ jobs:
328
336
  echo "🔄 Flutter clean 및 의존성 재설치..."
329
337
  flutter clean
330
338
  flutter pub get
339
+ if grep -q "build_runner" pubspec.yaml; then
340
+ dart run build_runner build --delete-conflicting-outputs
341
+ fi
331
342
 
332
343
  # local.properties에 버전 정보 추가 (Flutter 플래그가 무시되는 문제 해결)
333
344
  echo "🔧 local.properties에 버전 정보 설정..."
@@ -47,7 +47,7 @@ env:
47
47
  PROJECT_TYPE: "flutter"
48
48
  # Flutter 루트 경로(레포 루트 기준). 단일레포면 ".", 모노레포면 "app" 등.
49
49
  # 통합 시점에 integrator의 flutter-root resolver가 project_paths.flutter 값으로 1회 치환.
50
- FLUTTER_ROOT: "." # @wizard auto:flutter-root
50
+ FLUTTER_PROJECT_DIR: "." # @wizard auto:flutter-root
51
51
  # 배포 모드 (store_only | store_prepare | store_submit)
52
52
  DEPLOY_MODE: ${{ github.event.inputs.deploy_mode || vars.ANDROID_DEPLOY_MODE || 'store_only' }}
53
53
 
@@ -57,7 +57,7 @@ jobs:
57
57
  runs-on: ubuntu-latest
58
58
  defaults:
59
59
  run:
60
- working-directory: ${{ env.FLUTTER_ROOT }}
60
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
61
61
  # CHANGELOG 워크플로우가 성공한 경우에만 실행
62
62
  if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name != 'workflow_run' }}
63
63
 
@@ -97,7 +97,11 @@ jobs:
97
97
  restore-keys: ${{ runner.os }}-flutter-pub-
98
98
 
99
99
  - name: Install dependencies
100
- run: flutter pub get
100
+ run: |
101
+ flutter pub get
102
+ if grep -q "build_runner" pubspec.yaml; then
103
+ dart run build_runner build --delete-conflicting-outputs
104
+ fi
101
105
 
102
106
  # version_manager.py를 사용하여 현재 버전 정보 가져오기
103
107
  - name: 현재 버전 정보 가져오기
@@ -167,6 +171,7 @@ jobs:
167
171
  name: release-notes
168
172
  path: final_release_notes.txt
169
173
  retention-days: 1
174
+ if-no-files-found: error
170
175
 
171
176
  # 프로젝트 파일들을 아티팩트로 업로드
172
177
  # .env는 보안상 아티팩트에 포함하지 않고, 각 job에서 secrets로 재생성
@@ -179,6 +184,7 @@ jobs:
179
184
  lib/
180
185
  assets/
181
186
  retention-days: 1
187
+ if-no-files-found: error
182
188
 
183
189
  build-android:
184
190
  name: Android AAB 빌드
@@ -186,7 +192,7 @@ jobs:
186
192
  needs: prepare-build
187
193
  defaults:
188
194
  run:
189
- working-directory: ${{ env.FLUTTER_ROOT }}
195
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
190
196
 
191
197
  steps:
192
198
  - name: Checkout repository
@@ -255,7 +261,11 @@ jobs:
255
261
  restore-keys: ${{ runner.os }}-flutter-pub-
256
262
 
257
263
  - name: Install dependencies
258
- run: flutter pub get
264
+ run: |
265
+ flutter pub get
266
+ if grep -q "build_runner" pubspec.yaml; then
267
+ dart run build_runner build --delete-conflicting-outputs
268
+ fi
259
269
 
260
270
  - name: Set up Java
261
271
  uses: actions/setup-java@v5
@@ -318,6 +328,9 @@ jobs:
318
328
  echo "🔄 Flutter clean 및 의존성 재설치..."
319
329
  flutter clean
320
330
  flutter pub get
331
+ if grep -q "build_runner" pubspec.yaml; then
332
+ dart run build_runner build --delete-conflicting-outputs
333
+ fi
321
334
 
322
335
  # local.properties에 버전 정보 추가 (Flutter 플래그가 무시되는 문제 해결)
323
336
  echo "🔧 local.properties에 버전 정보 설정..."
@@ -503,8 +516,9 @@ jobs:
503
516
  uses: actions/upload-artifact@v7
504
517
  with:
505
518
  name: android-aab
506
- path: ${{ env.FLUTTER_ROOT }}/build/app/outputs/bundle/release/app-release.aab
519
+ path: ${{ env.FLUTTER_PROJECT_DIR }}/build/app/outputs/bundle/release/app-release.aab
507
520
  retention-days: 1
521
+ if-no-files-found: error
508
522
 
509
523
  deploy-playstore:
510
524
  name: Play Store 내부 테스트 배포
@@ -512,7 +526,7 @@ jobs:
512
526
  needs: [prepare-build, build-android]
513
527
  defaults:
514
528
  run:
515
- working-directory: ${{ env.FLUTTER_ROOT }}
529
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
516
530
 
517
531
  steps:
518
532
  - name: Checkout repository
@@ -533,7 +547,7 @@ jobs:
533
547
  uses: actions/download-artifact@v8
534
548
  with:
535
549
  name: android-aab
536
- path: ${{ env.FLUTTER_ROOT }}/build/app/outputs/bundle/release/
550
+ path: ${{ env.FLUTTER_PROJECT_DIR }}/build/app/outputs/bundle/release/
537
551
 
538
552
  # 릴리즈 노트 다운로드
539
553
  - name: Download release notes
@@ -549,7 +563,7 @@ jobs:
549
563
  ruby-version: "3.4.1"
550
564
 
551
565
  - name: Install Fastlane
552
- working-directory: ${{ env.FLUTTER_ROOT }}/android
566
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}/android
553
567
  run: |
554
568
  # multi_json: google-apis transitive 의존성이 선언 누락한 upstream 버그 회피 (gemspec 미선언 → Gem::LoadError)
555
569
  printf 'source "https://rubygems.org"\ngem "fastlane"\ngem "multi_json"\n' > Gemfile
@@ -650,7 +664,7 @@ jobs:
650
664
 
651
665
  if [ -f "$GITHUB_WORKSPACE/final_release_notes.txt" ]; then
652
666
  # Google Play release notes 한도(500 글자)에 맞춰 절단 (마진 두어 480자)
653
- # final_release_notes.txt·truncate 스크립트는 레포 루트 기준 — working-directory가 FLUTTER_ROOT이므로 절대경로 사용
667
+ # final_release_notes.txt·truncate 스크립트는 레포 루트 기준 — working-directory가 FLUTTER_PROJECT_DIR이므로 절대경로 사용
654
668
  bash "$GITHUB_WORKSPACE/.github/scripts/truncate_release_notes.sh" "$GITHUB_WORKSPACE/final_release_notes.txt" 480 char
655
669
  cp "$GITHUB_WORKSPACE/final_release_notes.txt" "$CHANGELOG_DIR/${VERSION_CODE}.txt"
656
670
  echo "✅ Changelog 준비 완료 (버전 코드: $VERSION_CODE)"
@@ -134,6 +134,9 @@ jobs:
134
134
  - name: Install dependencies
135
135
  run: |
136
136
  flutter pub get
137
+ if grep -q "build_runner" pubspec.yaml; then
138
+ dart run build_runner build --delete-conflicting-outputs
139
+ fi
137
140
  echo "Dependencies installed"
138
141
  ls -la
139
142
 
@@ -333,11 +333,11 @@ jobs:
333
333
  KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
334
334
  run: |
335
335
  cat > android/key.properties << EOF
336
- storeFile=keystore/key.jks
337
- storePassword=$STORE_PASSWORD
338
- keyAlias=$KEY_ALIAS
339
- keyPassword=$KEY_PASSWORD
340
- EOF
336
+ storeFile=keystore/key.jks
337
+ storePassword=$STORE_PASSWORD
338
+ keyAlias=$KEY_ALIAS
339
+ keyPassword=$KEY_PASSWORD
340
+ EOF
341
341
  echo "✅ key.properties 생성 완료"
342
342
 
343
343
  # google-services.json 생성 (선택적)
@@ -388,6 +388,9 @@ EOF
388
388
  - name: Install dependencies
389
389
  run: |
390
390
  flutter pub get
391
+ if grep -q "build_runner" pubspec.yaml; then
392
+ dart run build_runner build --delete-conflicting-outputs
393
+ fi
391
394
  echo "✅ Dependencies installed"
392
395
 
393
396
  # Gradle 셋업
@@ -634,24 +637,24 @@ EOF
634
637
  BUILD_DATE=$(date '+%Y-%m-%d %H:%M:%S')
635
638
 
636
639
  cat > build-info.txt << EOF
637
- 테스트 APK 빌드 정보
640
+ 테스트 APK 빌드 정보
638
641
 
639
- 빌드 번호: #$BUILD_NUMBER
640
- EOF
642
+ 빌드 번호: #$BUILD_NUMBER
643
+ EOF
641
644
 
642
645
  if [ -n "$PR_NUMBER" ]; then
643
646
  cat >> build-info.txt << EOF
644
- PR 번호: #$PR_NUMBER
645
- EOF
647
+ PR 번호: #$PR_NUMBER
648
+ EOF
646
649
  fi
647
650
 
648
651
  cat >> build-info.txt << EOF
649
- 브랜치: $BRANCH_NAME
650
- 커밋: $COMMIT_SHORT
651
- 전체 커밋 해시: $COMMIT_SHA
652
- 빌드 날짜: $BUILD_DATE
652
+ 브랜치: $BRANCH_NAME
653
+ 커밋: $COMMIT_SHORT
654
+ 전체 커밋 해시: $COMMIT_SHA
655
+ 빌드 날짜: $BUILD_DATE
653
656
 
654
- EOF
657
+ EOF
655
658
 
656
659
  # 이슈 정보가 있으면 추가
657
660
  if [ -n "${{ needs.prepare-test-build.outputs.issue_number }}" ]; then
@@ -660,11 +663,11 @@ EOF
660
663
  ISSUE_URL="${{ needs.prepare-test-build.outputs.issue_url }}"
661
664
 
662
665
  cat >> build-info.txt << EOF
663
- 관련 이슈:
664
- - #$ISSUE_NUMBER: $ISSUE_TITLE
665
- - URL: $ISSUE_URL
666
+ 관련 이슈:
667
+ - #$ISSUE_NUMBER: $ISSUE_TITLE
668
+ - URL: $ISSUE_URL
666
669
 
667
- EOF
670
+ EOF
668
671
  fi
669
672
 
670
673
  echo "📋 빌드 정보 파일 생성 완료:"
@@ -674,13 +677,13 @@ EOF
674
677
  - name: Create build metadata file
675
678
  run: |
676
679
  cat > build-metadata.json << EOF
677
- {
678
- "pr_number": "${{ needs.prepare-test-build.outputs.pr_number }}",
679
- "build_number": "${{ needs.prepare-test-build.outputs.build_number }}",
680
- "issue_number": "${{ needs.prepare-test-build.outputs.issue_number }}",
681
- "branch_name": "${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
682
- }
683
- EOF
680
+ {
681
+ "pr_number": "${{ needs.prepare-test-build.outputs.pr_number }}",
682
+ "build_number": "${{ needs.prepare-test-build.outputs.build_number }}",
683
+ "issue_number": "${{ needs.prepare-test-build.outputs.issue_number }}",
684
+ "branch_name": "${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
685
+ }
686
+ EOF
684
687
  cat build-metadata.json
685
688
 
686
689
  # APK와 빌드 정보를 아티팩트로 업로드 (브랜치명 포함)
@@ -278,6 +278,9 @@ jobs:
278
278
  - name: Install dependencies
279
279
  run: |
280
280
  flutter pub get
281
+ if grep -q "build_runner" pubspec.yaml; then
282
+ dart run build_runner build --delete-conflicting-outputs
283
+ fi
281
284
  echo "✅ Dependencies installed"
282
285
 
283
286
  # Flutter Analyze 실행
@@ -377,6 +380,9 @@ jobs:
377
380
  - name: Install dependencies
378
381
  run: |
379
382
  flutter pub get
383
+ if grep -q "build_runner" pubspec.yaml; then
384
+ dart run build_runner build --delete-conflicting-outputs
385
+ fi
380
386
  echo "✅ Dependencies installed"
381
387
 
382
388
  # Java 설정
@@ -398,12 +404,12 @@ jobs:
398
404
  chmod +x gradlew
399
405
  echo "✅ Gradle wrapper permissions set"
400
406
 
401
- # APK 빌드
407
+ # APK 빌드 (CI는 빌드 검증 목적이므로 서명 불필요한 debug 빌드 사용)
402
408
  - name: Build APK
403
409
  id: build
404
410
  run: |
405
- echo "📦 Flutter APK 빌드 시작..."
406
- flutter build apk --release
411
+ echo "📦 Flutter APK 빌드 시작 (debug)..."
412
+ flutter build apk --debug
407
413
  echo "✅ APK 빌드 완료"
408
414
  ls -la ./build/app/outputs/flutter-apk/ || true
409
415
 
@@ -456,6 +462,23 @@ jobs:
456
462
  - name: Select Xcode version
457
463
  run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
458
464
 
465
+ - name: Install iOS device platform
466
+ env:
467
+ # xcode-select만으로는 서브프로세스에 전파되지 않아 명시적으로 지정
468
+ DEVELOPER_DIR: /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
469
+ run: |
470
+ echo "=== iPhoneOS.platform check ==="
471
+ ls "$DEVELOPER_DIR/Platforms/iPhoneOS.platform/" 2>/dev/null && echo "iPhoneOS.platform EXISTS" || echo "iPhoneOS.platform NOT FOUND"
472
+ echo "=== Installing MobileDevice packages ==="
473
+ sudo installer -pkg /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Resources/Packages/MobileDevice.pkg -target /
474
+ sudo installer -pkg /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Resources/Packages/MobileDeviceDevelopment.pkg -target /
475
+ echo "=== Accepting Xcode license ==="
476
+ sudo xcodebuild -license accept
477
+ echo "=== Initializing Xcode components ==="
478
+ xcrun simctl list >/dev/null 2>&1 || true
479
+ echo "=== Downloading iOS platform ==="
480
+ xcodebuild -downloadPlatform iOS
481
+
459
482
  # .env 파일 생성
460
483
  - name: Create .env file
461
484
  run: |
@@ -488,6 +511,9 @@ jobs:
488
511
  - name: Install dependencies
489
512
  run: |
490
513
  flutter pub get
514
+ if grep -q "build_runner" pubspec.yaml; then
515
+ dart run build_runner build --delete-conflicting-outputs
516
+ fi
491
517
  echo "✅ Dependencies installed"
492
518
 
493
519
  # Ruby 및 CocoaPods 설정
@@ -319,13 +319,13 @@ jobs:
319
319
  echo "commit_hash=$COMMIT_SHORT" >> $GITHUB_OUTPUT
320
320
 
321
321
  cat > final_release_notes.txt << EOF
322
- 테스트 빌드 #$BUILD_NUMBER
322
+ 테스트 빌드 #$BUILD_NUMBER
323
323
 
324
- 브랜치: $BRANCH_NAME
325
- 커밋: $COMMIT_SHORT
326
- 날짜: $BUILD_DATE
324
+ 브랜치: $BRANCH_NAME
325
+ 커밋: $COMMIT_SHORT
326
+ 날짜: $BUILD_DATE
327
327
 
328
- EOF
328
+ EOF
329
329
 
330
330
  # 이슈 정보가 있으면 추가
331
331
  if [ -n "${{ steps.issue_info.outputs.issue_number }}" ]; then
@@ -334,11 +334,11 @@ EOF
334
334
  ISSUE_URL="${{ steps.issue_info.outputs.issue_url }}"
335
335
 
336
336
  cat >> final_release_notes.txt << EOF
337
- 관련 이슈:
338
- - #$ISSUE_NUMBER: $ISSUE_TITLE
339
- - URL: $ISSUE_URL
337
+ 관련 이슈:
338
+ - #$ISSUE_NUMBER: $ISSUE_TITLE
339
+ - URL: $ISSUE_URL
340
340
 
341
- EOF
341
+ EOF
342
342
  fi
343
343
 
344
344
  echo "📋 생성된 릴리즈 노트:"
@@ -542,7 +542,11 @@ EOF
542
542
  restore-keys: ${{ runner.os }}-flutter-pub-
543
543
 
544
544
  - name: Install dependencies
545
- run: flutter pub get
545
+ run: |
546
+ flutter pub get
547
+ if grep -q "build_runner" pubspec.yaml; then
548
+ dart run build_runner build --delete-conflicting-outputs
549
+ fi
546
550
 
547
551
  - name: Set up Ruby
548
552
  uses: ruby/setup-ruby@v1
@@ -618,13 +622,13 @@ EOF
618
622
  - name: Create build metadata file
619
623
  run: |
620
624
  cat > build-metadata.json << EOF
621
- {
622
- "pr_number": "${{ needs.prepare-test-build.outputs.pr_number }}",
623
- "build_number": "${{ needs.prepare-test-build.outputs.build_number }}",
624
- "issue_number": "${{ needs.prepare-test-build.outputs.issue_number }}",
625
- "branch_name": "${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
626
- }
627
- EOF
625
+ {
626
+ "pr_number": "${{ needs.prepare-test-build.outputs.pr_number }}",
627
+ "build_number": "${{ needs.prepare-test-build.outputs.build_number }}",
628
+ "issue_number": "${{ needs.prepare-test-build.outputs.issue_number }}",
629
+ "branch_name": "${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
630
+ }
631
+ EOF
628
632
  cat build-metadata.json
629
633
 
630
634
  - name: Upload IPA artifact
@@ -75,7 +75,7 @@ on:
75
75
  # 🔧 프로젝트별 설정 (아래 값들을 수정하세요)
76
76
  # ============================================
77
77
  env:
78
- FLUTTER_ROOT: "." # @wizard auto:flutter-root
78
+ FLUTTER_PROJECT_DIR: "." # @wizard auto:flutter-root
79
79
  FLUTTER_VERSION: "3.35.5"
80
80
  XCODE_VERSION: "26.0"
81
81
  PROJECT_TYPE: "flutter"
@@ -102,7 +102,7 @@ jobs:
102
102
  runs-on: macos-26
103
103
  defaults:
104
104
  run:
105
- working-directory: ${{ env.FLUTTER_ROOT }}
105
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
106
106
  if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name != 'workflow_run' }}
107
107
 
108
108
  outputs:
@@ -154,7 +154,11 @@ jobs:
154
154
  restore-keys: ${{ runner.os }}-flutter-pub-
155
155
 
156
156
  - name: Install dependencies
157
- run: flutter pub get
157
+ run: |
158
+ flutter pub get
159
+ if grep -q "build_runner" pubspec.yaml; then
160
+ dart run build_runner build --delete-conflicting-outputs
161
+ fi
158
162
 
159
163
  - name: Set up Ruby
160
164
  uses: ruby/setup-ruby@v1
@@ -207,6 +211,7 @@ jobs:
207
211
  name: release-notes
208
212
  path: final_release_notes.txt
209
213
  retention-days: 1
214
+ if-no-files-found: error
210
215
 
211
216
  - name: Upload project files
212
217
  uses: actions/upload-artifact@v7
@@ -219,6 +224,7 @@ jobs:
219
224
  lib/
220
225
  assets/
221
226
  retention-days: 1
227
+ if-no-files-found: error
222
228
 
223
229
  # ============================================
224
230
  # iOS 빌드 (xcodebuild 직접 사용)
@@ -228,7 +234,7 @@ jobs:
228
234
  runs-on: macos-26
229
235
  defaults:
230
236
  run:
231
- working-directory: ${{ env.FLUTTER_ROOT }}
237
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
232
238
  needs: prepare-build
233
239
 
234
240
  steps:
@@ -290,7 +296,11 @@ jobs:
290
296
  restore-keys: ${{ runner.os }}-flutter-pub-
291
297
 
292
298
  - name: Install dependencies
293
- run: flutter pub get
299
+ run: |
300
+ flutter pub get
301
+ if grep -q "build_runner" pubspec.yaml; then
302
+ dart run build_runner build --delete-conflicting-outputs
303
+ fi
294
304
 
295
305
  - name: Set up Ruby
296
306
  uses: ruby/setup-ruby@v1
@@ -361,8 +371,9 @@ jobs:
361
371
  uses: actions/upload-artifact@v7
362
372
  with:
363
373
  name: ios-ipa
364
- path: ${{ env.FLUTTER_ROOT }}/ios/build/ipa/*.ipa
374
+ path: ${{ env.FLUTTER_PROJECT_DIR }}/ios/build/ipa/*.ipa
365
375
  retention-days: 1
376
+ if-no-files-found: error
366
377
 
367
378
  # ============================================
368
379
  # TestFlight 배포 (마법사 생성 Fastfile 사용)
@@ -372,7 +383,7 @@ jobs:
372
383
  runs-on: macos-26
373
384
  defaults:
374
385
  run:
375
- working-directory: ${{ env.FLUTTER_ROOT }}
386
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
376
387
  needs: [prepare-build, build-ios]
377
388
 
378
389
  steps:
@@ -391,7 +402,7 @@ jobs:
391
402
  uses: actions/download-artifact@v8
392
403
  with:
393
404
  name: ios-ipa
394
- path: ${{ env.FLUTTER_ROOT }}/ios/build/ipa/
405
+ path: ${{ env.FLUTTER_PROJECT_DIR }}/ios/build/ipa/
395
406
 
396
407
  - name: Download release notes
397
408
  uses: actions/download-artifact@v8
@@ -410,7 +421,7 @@ jobs:
410
421
  echo "✅ Fastfile 확인됨: ios/fastlane/Fastfile"
411
422
 
412
423
  - name: Install Fastlane
413
- working-directory: ${{ env.FLUTTER_ROOT }}/ios
424
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}/ios
414
425
  run: |
415
426
  # multi_json: google-apis transitive 의존성이 선언 누락한 upstream 버그 회피 (gemspec 미선언 → Gem::LoadError)
416
427
  printf 'source "https://rubygems.org"\ngem "fastlane"\ngem "multi_json"\n' > Gemfile
@@ -424,14 +435,14 @@ jobs:
424
435
  SKIP_WAITING_FOR_BUILD_PROCESSING: ${{ env.SKIP_WAITING_FOR_BUILD_PROCESSING }}
425
436
  run: |
426
437
  # IPA 절대경로 탐색
427
- IPA_PATH=$(find "$GITHUB_WORKSPACE/${FLUTTER_ROOT}/ios/build/ipa" -name "*.ipa" | head -1)
438
+ IPA_PATH=$(find "$GITHUB_WORKSPACE/${FLUTTER_PROJECT_DIR}/ios/build/ipa" -name "*.ipa" | head -1)
428
439
  echo "📦 Found IPA at: $IPA_PATH"
429
440
 
430
441
  if [ ! -f "$IPA_PATH" ]; then
431
442
  echo "❌ IPA 파일을 찾을 수 없습니다!"; ls -la ios/build/ipa/ || true; exit 1
432
443
  fi
433
444
 
434
- # Release notes 준비 (final_release_notes.txt·truncate 스크립트는 레포 루트 기준 — working-directory가 FLUTTER_ROOT이므로 절대경로 사용)
445
+ # Release notes 준비 (final_release_notes.txt·truncate 스크립트는 레포 루트 기준 — working-directory가 FLUTTER_PROJECT_DIR이므로 절대경로 사용)
435
446
  if [ -f "$GITHUB_WORKSPACE/final_release_notes.txt" ]; then
436
447
  # App Store Connect "What to Test" 한도(4000 바이트)에 맞춰 절단 (마진 두어 3800byte)
437
448
  bash "$GITHUB_WORKSPACE/.github/scripts/truncate_release_notes.sh" "$GITHUB_WORKSPACE/final_release_notes.txt" 3800 byte
@@ -5,7 +5,7 @@
5
5
  import { join } from "node:path";
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "../core/assets.js";
8
- import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers } from "../core/detect-fs.js";
8
+ import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectBuildNumber } from "../core/detect-fs.js";
9
9
  import { parseExisting } from "../core/version-yml.js";
10
10
  import { runBreakingCheck } from "../core/breaking-check.js";
11
11
  import { resolveProjectPaths } from "../core/paths-resolve.js";
@@ -85,7 +85,6 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
85
85
  let version = (existing?.version) || detectVersion(cwd);
86
86
  let branch = detectDefaultBranch(cwd);
87
87
  const repoName = detectRepoName(cwd);
88
- const versionCode = existing?.versionCode ?? 1; // 기존 빌드번호 보존
89
88
  // 선택 워크플로우 초기값: version.yml 저장 옵션 (.sh read_template_options L2361 등가)
90
89
  let includeNexus = existing?.options?.nexus ?? false;
91
90
  let includeSecretBackup = existing?.options?.secretBackup ?? false;
@@ -166,6 +165,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
166
165
  }
167
166
  }
168
167
 
168
+ const versionCode = existing?.versionCode ?? detectBuildNumber(cwd, { types }) ?? 1; // 기존 빌드번호 보존, 신규 통합 시 프로젝트 파일에서 감지 (이슈 #41)
169
+
169
170
  // 신규 질문 ① — 브랜치 설정 (DESIGN-SPEC §4). full/workflows만 질문, version은 기본값 기록.
170
171
  // 저장값(version.yml metadata.template.branches)이 있으면 재질문 없이 재사용 (업데이트 모드).
171
172
  let branches = existing?.branches
@@ -250,7 +251,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
250
251
 
251
252
  // 완료 요약 (.sh print_summary L5438)
252
253
  io.summary?.({
253
- mode, types, version, branches,
254
+ mode, types, version, versionCode, branches,
254
255
  copiedFiles: result?.workflows?.copiedFiles ?? [],
255
256
  gitignoreUpdated: result?.gitignoreUpdated === true,
256
257
  });
@@ -3,7 +3,7 @@
3
3
  import { existsSync, readFileSync, readdirSync } from "node:fs";
4
4
  import { join, basename } from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
- import { detectTypesFromMarkers, detectVersionFromFiles } from "./detect.js";
6
+ import { detectTypesFromMarkers, detectVersionFromFiles, detectBuildNumberFromFiles } from "./detect.js";
7
7
  import { parseExisting } from "./version-yml.js";
8
8
 
9
9
  const hasFile = (root) => (rel) => existsSync(join(root, rel));
@@ -35,6 +35,13 @@ export function detectVersion(root, { warn = (m) => console.error(m) } = {}) {
35
35
  return detectVersionFromFiles({ read, readJson, gitTag, warn });
36
36
  }
37
37
 
38
+ // 빌드 번호 감지 — 신규 통합 시 pubspec.yaml/build.gradle/app.json에서 실제 빌드 번호를 읽는다 (이슈 #41).
39
+ export function detectBuildNumber(root, { types = [], warn = (m) => console.error(m) } = {}) {
40
+ const read = readFile(root);
41
+ const readJson = (rel) => { const c = read(rel); try { return c ? JSON.parse(c) : null; } catch { return null; } };
42
+ return detectBuildNumberFromFiles({ types, read, readJson, warn });
43
+ }
44
+
38
45
  // 기본 브랜치 감지 — symbolic-ref → remote show → main.
39
46
  export function detectDefaultBranch(root) {
40
47
  let b = gitOut(root, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
@@ -62,3 +62,42 @@ export function markerForType(type) {
62
62
  export function extraMarkers(type) {
63
63
  return { python: ["setup.py", "requirements.txt"], spring: ["build.gradle.kts", "pom.xml"] }[type] || [];
64
64
  }
65
+
66
+ // 빌드 번호 감지 (이슈 #41) — 신규 통합 시 pubspec.yaml/build.gradle/app.json에 이미 기록된
67
+ // 빌드 번호를 읽어 version_code가 항상 1로 초기화되는 걸 막는다. types 배열에서 먼저 매칭되는
68
+ // 첫 타입만 사용한다(다른 감지 로직의 types[0]=primary 관례와 동일). read(rel)=>string|null,
69
+ // readJson(rel)=>object|null 로 주입.
70
+ export function detectBuildNumberFromFiles({ types = [], read, readJson, warn }) {
71
+ const tryFlutter = () => {
72
+ const content = read("pubspec.yaml");
73
+ if (content == null) return null;
74
+ const m = content.match(/^version:\s*\d+\.\d+\.\d+\+(\d+)/m);
75
+ if (m) return parseInt(m[1], 10);
76
+ warn?.("⚠️ pubspec.yaml에 빌드 번호(+N)가 없어 version_code를 감지하지 못했습니다 — 기본값 1을 사용합니다. 실제 빌드 번호를 확인하세요.");
77
+ return null;
78
+ };
79
+ const tryReactNative = () => {
80
+ const content = read("android/app/build.gradle");
81
+ if (content == null) return null;
82
+ // 앵커 + m 플래그로 한 줄 전체가 "versionCode N"인 라인만 매칭 — 주석 처리된
83
+ // "// versionCode 2"나 다른 블록의 versionCode 참조에 오매칭되지 않도록 함.
84
+ const m = content.match(/^\s*versionCode\s+(\d+)\s*$/m);
85
+ if (m) return parseInt(m[1], 10);
86
+ warn?.("⚠️ android/app/build.gradle에 versionCode가 없어 version_code를 감지하지 못했습니다 — 기본값 1을 사용합니다. 실제 빌드 번호를 확인하세요.");
87
+ return null;
88
+ };
89
+ const tryExpo = () => {
90
+ const data = readJson?.("app.json");
91
+ if (data == null) return null;
92
+ const code = data?.expo?.android?.versionCode;
93
+ if (Number.isInteger(code)) return code;
94
+ warn?.("⚠️ app.json의 expo.android.versionCode가 없어 version_code를 감지하지 못했습니다 — 기본값 1을 사용합니다. 실제 빌드 번호를 확인하세요.");
95
+ return null;
96
+ };
97
+ for (const t of types) {
98
+ if (t === "flutter") return tryFlutter();
99
+ if (t === "react-native") return tryReactNative();
100
+ if (t === "react-native-expo") return tryExpo();
101
+ }
102
+ return null;
103
+ }
@@ -5,6 +5,9 @@ import { escapeYamlDoubleQuoted } from "./wizard-env.js";
5
5
  // 레이아웃 단일 진실 = payload/version.yml.template (호출부가 templateText로 주입).
6
6
 
7
7
  // version.yml.template이 아는 최상위 키 — 이 밖의 최상위 키는 사용자가 직접 추가한 것으로 간주한다.
8
+ // "project_type"(단수)은 더 이상 렌더하지 않는 레거시 키지만 이 집합에는 남겨둔다 (issue #62):
9
+ // 빼면 기존 파일의 단수 줄이 "사용자가 추가한 필드"로 오인돼 재생성 때 되살아난다. 아는 키로
10
+ // 둬야 재통합 시 흡수되어 사라진다.
8
11
  const KNOWN_TOP_LEVEL_KEYS = new Set([
9
12
  "version", "version_code", "project_types", "project_type", "project_paths", "metadata", "deploy",
10
13
  ]);
@@ -147,7 +150,7 @@ export function parseTemplateBranches(content) {
147
150
  }
148
151
 
149
152
  // version.yml 전체 생성 — payload/version.yml.template 렌더링.
150
- // opts: { templateText, version, types:[], primaryType?, paths:Map, pathMarkers?:Map,
153
+ // opts: { templateText, version, types:[], paths:Map, pathMarkers?:Map,
151
154
  // branch, branches?, versionCode, now, today, templateOptions?, deployValues?,
152
155
  // extraTopLevel?:string[] } ← 기존 version.yml의 알려지지 않은 최상위 필드 보존 (issue #20 M8)
153
156
  // templateText = payload/version.yml.template 원문 (readVersionYmlTemplate — 필수)
@@ -156,13 +159,12 @@ export function parseTemplateBranches(content) {
156
159
  // pathMarkers = Map<type, markerFilename> (project_paths 주석용)
157
160
  // templateOptions = { templateVersion, includeNexus, includeSecretBackup, optionsDate }
158
161
  export function buildVersionYml({
159
- templateText, version, types = [], primaryType, paths = new Map(), pathMarkers = new Map(),
162
+ templateText, version, types = [], paths = new Map(), pathMarkers = new Map(),
160
163
  branch = "main", branches = null, versionCode = 1, now, today,
161
164
  templateOptions = null, deployValues = new Map(), extraTopLevel = [],
162
165
  }) {
163
166
  if (!templateText) throw new Error("version.yml.template 원문이 필요합니다 (payload/version.yml.template 누락?)");
164
167
  const typesJson = types.length ? `[${types.map((t) => `"${t}"`).join(", ")}]` : `["basic"]`;
165
- const primary = primaryType || types[0] || "basic";
166
168
  const b = branches || { main: branch || "main", develop: "develop", mode: "pr-flow" };
167
169
  const {
168
170
  templateVersion = "unknown", includeNexus = false, includeSecretBackup = false,
@@ -197,7 +199,7 @@ export function buildVersionYml({
197
199
 
198
200
  const scalars = {
199
201
  VERSION: version, VERSION_CODE: String(versionCode),
200
- PROJECT_TYPES: typesJson, PROJECT_TYPE: primary,
202
+ PROJECT_TYPES: typesJson,
201
203
  NOW: now, TODAY: today || optionsDate, DEFAULT_BRANCH: branch,
202
204
  TEMPLATE_VERSION: templateVersion,
203
205
  MAIN_BRANCH: b.main, DEVELOP_BRANCH: b.develop, BRANCH_MODE: b.mode,
package/src/index.js CHANGED
@@ -9,7 +9,7 @@ import { parseArgs, parsePathsCsv, CliError } from "./cli/args.js";
9
9
  import { HELP_TEXT } from "./cli/help.js";
10
10
  import { createContext } from "./context.js";
11
11
  import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "./core/assets.js";
12
- import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers } from "./core/detect-fs.js";
12
+ import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectBuildNumber } from "./core/detect-fs.js";
13
13
  import { parseExisting } from "./core/version-yml.js";
14
14
  import { runBreakingCheck } from "./core/breaking-check.js";
15
15
  import { resolveProjectPaths } from "./core/paths-resolve.js";
@@ -237,7 +237,7 @@ export async function run(argv, {
237
237
  const types = opts.types.length ? opts.types : detectTypes(cwd);
238
238
  // version: 기존 version.yml 최우선(SSoT — 재실행 시 덮어쓰기 방지) → CLI 지정 → 파일 감지
239
239
  const version = (existing?.version) || opts.version || detectVersion(cwd);
240
- const versionCode = existing?.versionCode ?? 1; // 기존 빌드번호 보존 (.sh L2208~2221)
240
+ const versionCode = existing?.versionCode ?? detectBuildNumber(cwd, { types }) ?? 1; // 기존 빌드번호 보존, 신규 통합 시 프로젝트 파일에서 감지 (.sh L2208~2221, 이슈 #41)
241
241
  const branch = detectDefaultBranch(cwd);
242
242
  const repoName = detectRepoName(cwd);
243
243
  // 경로 확정 (.sh resolve_project_paths 비대화형 경로 — --paths 우선 → 저장값 → 후보 1개 자동 → 에러)
@@ -314,7 +314,7 @@ export async function run(argv, {
314
314
 
315
315
  // 완료 요약 (.sh print_summary — CLI 모드에서도 출력)
316
316
  printSummary({
317
- mode: opts.mode, types, version, branches,
317
+ mode: opts.mode, types, version, versionCode, branches,
318
318
  copiedFiles: result?.workflows?.copiedFiles ?? [],
319
319
  gitignoreUpdated: result?.gitignoreUpdated === true,
320
320
  });
package/src/ui/summary.js CHANGED
@@ -6,7 +6,7 @@ import { paint, A, colorEnabled } from "./ansi.js";
6
6
  const SEPARATOR = "────────────────────────────────────────";
7
7
 
8
8
  export function printSummary(ctx) {
9
- const { mode, types = [], version = "", copiedFiles = [], branches = null, gitignoreUpdated = false } = ctx || {};
9
+ const { mode, types = [], version = "", versionCode = null, copiedFiles = [], branches = null, gitignoreUpdated = false } = ctx || {};
10
10
  const err = (s = "") => process.stderr.write(`${s}\n`);
11
11
  // 색상은 ansi.js의 공용 가드로 통일 (NO_COLOR + stderr TTY 여부)
12
12
  const enabled = colorEnabled(process.stderr);
@@ -56,6 +56,10 @@ export function printSummary(ctx) {
56
56
  err("");
57
57
  err("추가된 파일:");
58
58
  err(` 📄 version.yml (버전: ${version}, 타입: ${types.join(",")})`);
59
+ const BUILD_NUMBER_TYPES = new Set(["flutter", "react-native", "react-native-expo"]);
60
+ if (versionCode != null && types.some((t) => BUILD_NUMBER_TYPES.has(t))) {
61
+ err(` 빌드 번호: ${versionCode}`);
62
+ }
59
63
  err(" 📝 README.md (버전 섹션 추가)");
60
64
  err("");
61
65
  err("추가된 워크플로우:");