project-auto-wizard 0.16.0 → 0.16.2

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 (47) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +2 -2
  4. package/README.zh-CN.md +1 -1
  5. package/package.json +1 -1
  6. package/payload/scripts/messages.py +48 -6
  7. package/payload/scripts/version_manager.py +76 -3
  8. package/payload/version.yml.template +3 -2
  9. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +7 -7
  10. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +111 -46
  11. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +10 -1
  12. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +13 -3
  13. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +10 -1
  14. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +2 -2
  15. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +10 -10
  16. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +10 -10
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +14 -12
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +35 -18
  19. package/payload/workflows/go/PROJECT-GO-CI.yaml +3 -1
  20. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +81 -61
  21. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +3 -1
  22. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +81 -61
  23. package/payload/workflows/react/PROJECT-REACT-CI.yaml +3 -2
  24. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +4 -4
  25. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +86 -68
  26. package/src/commands/doctor.js +17 -23
  27. package/src/commands/dry-run.js +4 -3
  28. package/src/commands/full.js +6 -6
  29. package/src/commands/interactive.js +5 -1
  30. package/src/commands/status.js +9 -2
  31. package/src/core/copy/gitignore.js +3 -3
  32. package/src/core/copy/readme.js +11 -10
  33. package/src/core/detect-fs.js +9 -4
  34. package/src/core/options.js +36 -0
  35. package/src/core/version-yml.js +37 -13
  36. package/src/i18n/catalog/en/cli.js +1 -0
  37. package/src/i18n/catalog/en/commands.js +4 -1
  38. package/src/i18n/catalog/en/core.js +1 -0
  39. package/src/i18n/catalog/en/core2.js +2 -0
  40. package/src/i18n/catalog/en/ui.js +3 -0
  41. package/src/i18n/catalog/ko/cli.js +1 -0
  42. package/src/i18n/catalog/ko/commands.js +4 -1
  43. package/src/i18n/catalog/ko/core.js +1 -0
  44. package/src/i18n/catalog/ko/core2.js +2 -0
  45. package/src/i18n/catalog/ko/ui.js +3 -0
  46. package/src/index.js +19 -7
  47. package/src/ui/summary.js +15 -6
package/README.ja.md CHANGED
@@ -49,7 +49,7 @@ npx project-auto-wizard --mode full --force --type node --dry-run # preview on
49
49
 
50
50
  生成されたファイルをコミットして push してください。`npx project-auto-wizard --mode doctor` は、ワークフローが依存するリポジトリ設定を確認します。
51
51
 
52
- メッセージの言語は英語または韓国語(`en` | `ko`)です。`--lang`、環境変数 `PROJECT_AUTO_WIZARD_LANG`、`version.yml` の `language` の順に優先されます。
52
+ メッセージの言語は英語または韓国語(`en` | `ko`)です。`--lang`、環境変数 `PROJECT_AUTO_WIZARD_LANG`、`version.yml` の `language` の順に優先されます。環境変数はその実行にのみ適用され、有効な保存済み `language` は上書きしません(有効な値がない場合のみ保存)。上書きするには `--lang` を使います。
53
53
 
54
54
  ## インストールされるもの
55
55
 
package/README.ko.md CHANGED
@@ -49,7 +49,7 @@ npx project-auto-wizard --mode full --force --type node --dry-run # preview on
49
49
 
50
50
  생성된 파일을 커밋하고 push하세요. `npx project-auto-wizard --mode doctor`는 워크플로우가 의존하는 레포 설정을 점검합니다.
51
51
 
52
- 메시지 언어는 영어 또는 한국어(`en` | `ko`)입니다. `--lang`, 환경변수 `PROJECT_AUTO_WIZARD_LANG`, `version.yml`의 `language` 순서로 우선 적용됩니다.
52
+ 메시지 언어는 영어 또는 한국어(`en` | `ko`)입니다. `--lang`, 환경변수 `PROJECT_AUTO_WIZARD_LANG`, `version.yml`의 `language` 순서로 우선 적용됩니다. 환경변수는 그 실행에만 적용되며 저장된 유효한 `language`를 덮어쓰지 않습니다(유효한 값이 없을 때만 저장). 덮어쓰려면 `--lang`을 쓰세요.
53
53
 
54
54
  ## 설치되는 것
55
55
 
package/README.md CHANGED
@@ -21,7 +21,7 @@ Everything it installs is plain GitHub Actions in your own repository. No API ke
21
21
  </div>
22
22
 
23
23
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
24
- ## Latest Version : v0.16.0 (2026-10-01)
24
+ ## Latest Version : v0.16.2 (2026-10-01)
25
25
 
26
26
  [Full release history](CHANGELOG.md)
27
27
 
@@ -50,7 +50,7 @@ npx project-auto-wizard --mode full --force --type node --dry-run # preview on
50
50
 
51
51
  Commit the generated files and push. `npx project-auto-wizard --mode doctor` checks the repository settings the workflows depend on.
52
52
 
53
- Messages are in English or Korean (`en` | `ko`). Pick one with `--lang`, the `PROJECT_AUTO_WIZARD_LANG` environment variable, or `language` in `version.yml`, in that order of precedence.
53
+ Messages are in English or Korean (`en` | `ko`). Pick one with `--lang`, the `PROJECT_AUTO_WIZARD_LANG` environment variable, or `language` in `version.yml`, in that order of precedence. The environment variable applies to that run only and does not overwrite a valid saved `language` (it is saved only when there is none); `--lang` does.
54
54
 
55
55
  ## What you get
56
56
 
package/README.zh-CN.md CHANGED
@@ -49,7 +49,7 @@ npx project-auto-wizard --mode full --force --type node --dry-run # preview on
49
49
 
50
50
  提交生成的文件并 push。`npx project-auto-wizard --mode doctor` 会检查工作流所依赖的仓库设置。
51
51
 
52
- 消息语言为英语或韩语(`en` | `ko`)。可通过 `--lang`、环境变量 `PROJECT_AUTO_WIZARD_LANG` 或 `version.yml` 中的 `language` 指定,优先级依次降低。
52
+ 消息语言为英语或韩语(`en` | `ko`)。可通过 `--lang`、环境变量 `PROJECT_AUTO_WIZARD_LANG` 或 `version.yml` 中的 `language` 指定,优先级依次降低。环境变量仅对该次运行生效,不会覆盖已保存的有效 `language`(仅在没有有效值时才保存);如需覆盖请使用 `--lang`。
53
53
 
54
54
  ## 安装内容
55
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-auto-wizard",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "One command DevOps: npx wizard that installs GitHub-native Release Automation (optional GitHub Copilot AI summaries) into any project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -674,6 +674,7 @@ EN = {
674
674
  "flutter_a.ci_apk_start": "📦 Starting the Flutter APK build (debug)...",
675
675
  "flutter_a.ci_apk_done": "✅ APK build complete",
676
676
  "flutter_a.ci_podfile_missing": "ℹ️ ios/Podfile not found, skipping pod install (no CocoaPods plugins)",
677
+ "flutter_a.ci_pods_installed": "✅ CocoaPods installed",
677
678
  "flutter_a.ci_ios_start": "📦 Starting the Flutter iOS build...",
678
679
  "flutter_a.ci_ios_done": "✅ iOS build complete",
679
680
  "flutter_a.ci_gate_results": "needs results: {results}",
@@ -751,7 +752,8 @@ EN = {
751
752
  "ios_test_tf.st_waiting": "⏸️ Waiting",
752
753
  "ios_test_tf.st_done": "✅ Done",
753
754
  "ios_test_tf.st_failed": "❌ Failed",
754
- "ios_test_tf.st_canceled": "⏸️ Canceled",
755
+ "ios_test_tf.st_not_run": "⏸️ Not run (an earlier step failed)",
756
+ "ios_test_tf.st_precheck_blocked": "⏸️ Not run (pre-check failed: {missing})",
755
757
  "ios_test_tf.row_build_number": "| **Build number** | `{build}` |",
756
758
  "ios_test_tf.row_app_version": "| **App version** | `{version}({build})` |",
757
759
  "ios_test_tf.row_branch": "| **Branch** | `{branch}` |",
@@ -789,6 +791,7 @@ EN = {
789
791
  "ios_test_tf.notes_generated": "📋 Generated release notes:",
790
792
  "ios_test_tf.title_prepare_failed": "## 🍎 ❌ iOS TestFlight build failed",
791
793
  "ios_test_tf.prepare_error": "❌ **An error occurred while preparing the environment.**",
794
+ "ios_test_tf.prepare_precheck_error": "❌ **The pre-check failed, so the build did not start.** Missing or invalid: {missing}",
792
795
  "ios_test_tf.run_log_check_link": "🔗 [Check the workflow run log]({url})",
793
796
  "ios_test_tf.log_prepare_failed": "❌ Progress update complete: prepare failed",
794
797
  "ios_test_tf.log_prepare_done": "✅ Progress update complete: prepare done ({elapsed})",
@@ -882,6 +885,7 @@ EN = {
882
885
  "truncate.limit_positive": "ERROR: limit must be a positive integer: {limit}",
883
886
  # --- version ---
884
887
  "version.bump_help": "bump level (default patch — same as before when not given)",
888
+ "version.option_default_help": "value to print when the key is not in version.yml (default false)",
885
889
  # --- wf_aisum ---
886
890
  "wf_aisum.no_issue_in_branch": "No issue number found in the branch name — skipping: {ref}",
887
891
  # --- wf_changelog ---
@@ -1047,7 +1051,8 @@ EN = {
1047
1051
  "wf_preview.status_done": "✅ Done",
1048
1052
  "wf_preview.status_failed": "❌ Failed",
1049
1053
  "wf_preview.status_init_failed": "⚠️ Initialization failed",
1050
- "wf_preview.status_waiting": "⏸️ Waiting",
1054
+ "wf_preview.status_not_run": "⏸️ Not run (an earlier step failed)",
1055
+ "wf_preview.status_precheck_blocked": "⏸️ Not run (pre-check failed: {missing})",
1051
1056
  "wf_preview.tbl_commands": "| Command | Description |",
1052
1057
  "wf_preview.tbl_items": "| Item | Value |",
1053
1058
  "wf_preview.tbl_steps": "| Step | Status | Duration |",
@@ -1265,6 +1270,7 @@ EN = {
1265
1270
  "version_manager.err_invalid_version": "ERROR: invalid version format: {version}",
1266
1271
  "version_manager.err_invalid_version_xyz": "ERROR: invalid version format: {version} (must be x.y.z)",
1267
1272
  "version_manager.err_generic": "ERROR: {error}",
1273
+ "version_manager.option_invalid": "::warning::version.yml option {option} has an unrecognized value \"{value}\"; using false. Use true or false.",
1268
1274
  "version_manager.err_no_types_sync": "ERROR: version.yml has no readable project_types — cannot sync project files",
1269
1275
  "version_manager.err_no_types_check": "ERROR: version.yml has no readable project_types — cannot sync versions",
1270
1276
  "version_manager.err_write_failed": "failed to write version {version} to version.yml",
@@ -1290,11 +1296,15 @@ EN = {
1290
1296
  "wf_common.rebase_ok": "rebase ok, retrying push...",
1291
1297
  "wf_common.rebase_failed": "rebase failed, manual conflict resolution required",
1292
1298
  "wf_common.push_gave_up": "push failed after {max_retries} attempts",
1299
+ "wf_common.push_protected": "Push to {branch} was rejected by a branch protection rule, not by a race, so retrying cannot help. The bot commit has to be pushed to {branch} directly: allow the pushing account (the WORKFLOW_PAT owner, or the GitHub Actions bot when no PAT is set) to bypass the rule.",
1300
+ "wf_release.push_protected_trunk": "trunk-based mode commits every release (version bump and CHANGELOG) straight to {branch}, so its protection rule must allow that push. If it cannot, switch to the develop -> main release flow, where the bot only commits to the release PR branch.",
1293
1301
  "wf_common.readme_conflict_resolved": "README.md conflicted with a concurrent update - kept the remote copy and reapplied the version line",
1294
1302
  # --- wf_aisum ---
1295
1303
  "wf_aisum.expected_version": "expected next version: {version} (current: {current_version}, mode: {mode}, bump: {bump})",
1296
1304
  "wf_aisum.comment_update_failed": "comment update failed — continuing (this workflow never blocks the PR)",
1297
1305
  "wf_aisum.comment_post_failed": "comment post failed — continuing (this workflow never blocks the PR)",
1306
+ "wf_aisum.summary_heading": "📋 **PR Summary (project-auto-wizard)**",
1307
+ "wf_aisum.engine_label": "engine: {engine}",
1298
1308
  # --- wf_changelog ---
1299
1309
  "wf_changelog.pr_not_open": "PR #{pr_number} is already {state} — skipping the release pipeline",
1300
1310
  "wf_changelog.pr_state": "PR #{pr_number} state: {state}",
@@ -1314,10 +1324,20 @@ EN = {
1314
1324
  "wf_changelog.automerge_enabled": "automerge enabled: {subject}",
1315
1325
  "wf_changelog.automerge_unavailable": "automerge unavailable — merged directly: {subject}",
1316
1326
  "wf_changelog.automerge_off": "release_automerge is off — PR renamed to \"{subject}\"; merge it yourself",
1327
+ "wf_changelog.automerge_off_list_failed": "Could not read the comments of PR #{pr_number}; skipped the merge guidance comment to avoid a duplicate",
1328
+ "wf_changelog.automerge_off_rename_hint": "The PR title could not be updated automatically. Before merging, set the merge commit message (or the PR title) to `{subject}` so the release is published.",
1317
1329
  "wf_changelog.automerge_rename_failed": "Could not update PR #{pr_number} (title or guidance comment); the release still needs \"chore(release):\" in the merge commit message",
1318
- "wf_changelog.automerge_off_comment": "**Automatic merge is off** (`release_automerge: false` in `version.yml`). Merge this PR yourself when you are ready to release, using **Create a merge commit**. Do not use *Squash and merge* or *Rebase and merge*: the release gate reads `chore(release):` from the resulting commit message, and squashing or rebasing also rewrites the history that the next release notes are built from.",
1330
+ "wf_changelog.automerge_off_comment": "**Automatic merge is off** (`release_automerge: false` in `version.yml`). Wait until this workflow run has finished (it confirms the version and renames this PR to `chore(release): ...`), then merge it yourself with **Create a merge commit** (recommended).\n\nOther merge methods may not publish the release by themselves. *Rebase and merge* leaves the version-confirm commit (`[skip ci]`) as the last commit, and *Squash and merge* with the default message (commit list included) puts that `[skip ci]` into the merge commit body; in both cases GitHub skips the push workflows and no tag or Release is created. If the repository's squash message default is the PR title only, a squash merge is published. If a release was skipped, run **PROJECT-RELEASE-PUBLISH** manually on the release branch (Actions > Run workflow) to publish. Both methods also rewrite the commit history the next release notes are built from, so earlier changes can be listed again (after a rebase merge only once develop has merged the release branch back). Merging before this run finishes has a similar effect: the safety-net workflow (PROJECT-VERSION-CONTROL) then publishes an extra patch version.",
1319
1331
  "wf_changelog.merge_retry": "merge attempt {attempt}/{max_attempts} failed — retrying in {wait}s",
1332
+ "wf_changelog.head_updated": "head branch updated from base",
1320
1333
  "wf_changelog.merge_gave_up": "PR merge failed after {max_attempts} attempts (allow_merge_commit={allow_merge_commit}) — check branch protection rules, required checks, and token permissions",
1334
+ "wf_changelog.merge_blocked_review": "The release PR cannot be merged automatically: branch protection on {prod} requires an approving review. Approve it and merge it yourself with a merge commit, or change the rule so the merging account can merge without the review. To stop this attempt, set release_automerge: false in version.yml.",
1335
+ "wf_changelog.merge_blocked_policy": "The release PR cannot be merged automatically: branch protection on {prod} blocks the merge (required checks, reviews or another rule). Merge it yourself with a merge commit once the requirements are met, or change the rule so the merging account is allowed. To stop this attempt, set release_automerge: false in version.yml.",
1336
+ "wf_changelog.automerge_blocked_renamed": "automerge was blocked — PR renamed to \"{subject}\"; merge it yourself once the requirements are met",
1337
+ "wf_changelog.merge_blocked_reason_review": "the release branch requires an approving review.",
1338
+ "wf_changelog.merge_blocked_reason_policy": "branch protection on the release branch blocks the merge (required checks, reviews or another rule).",
1339
+ "wf_changelog.merge_blocked_reason_other": "GitHub rejected the merge.",
1340
+ "wf_changelog.merge_blocked_comment": "**The release PR was not merged automatically** — {reason}\n\nThis PR was renamed to `chore(release): ...`, so a merge you make yourself still publishes the release. Once the requirements are met, merge it with **Create a merge commit** (recommended).\n\n- To let the workflow merge it next time, change the protection rule on the release branch so the account that merges (the `WORKFLOW_PAT` owner, or the GitHub Actions bot when no PAT is set) is allowed to merge.\n- To always merge by hand without this run failing, set `release_automerge: false` in `version.yml`.\n\nSquash or rebase merges may not publish the release by themselves. If a release was skipped, run **PROJECT-RELEASE-PUBLISH** manually on the release branch (Actions > Run workflow).",
1321
1341
  # --- wf_readme ---
1322
1342
  "wf_readme.not_found": "README.md not found — nothing to update, skipping",
1323
1343
  "wf_readme.read_version_failed": "failed to read the current version",
@@ -1438,6 +1458,7 @@ EN = {
1438
1458
  "wf_flutter.release_notes_heading": "📝 Release Notes:",
1439
1459
  "wf_flutter.working_dir": "🔧 Working directory: {dir}",
1440
1460
  "wf_flutter.run_number": "🔍 GitHub Run Number: {number}",
1461
+ "wf_flutter.github_sha": "🔍 GitHub SHA: {sha}",
1441
1462
  "wf_flutter.ci_analyze_only": " Analyze Only: {value}",
1442
1463
  }
1443
1464
 
@@ -1947,6 +1968,7 @@ KO = {
1947
1968
  "flutter_a.ci_apk_start": "📦 Flutter APK 빌드 시작 (debug)...",
1948
1969
  "flutter_a.ci_apk_done": "✅ APK 빌드 완료",
1949
1970
  "flutter_a.ci_podfile_missing": "ℹ️ ios/Podfile이 없어 pod install을 건너뜁니다 (CocoaPods 플러그인 없음)",
1971
+ "flutter_a.ci_pods_installed": "✅ CocoaPods 설치 완료",
1950
1972
  "flutter_a.ci_ios_start": "📦 Flutter iOS 빌드 시작...",
1951
1973
  "flutter_a.ci_ios_done": "✅ iOS 빌드 완료",
1952
1974
  "flutter_a.ci_gate_results": "needs 결과: {results}",
@@ -2024,7 +2046,8 @@ KO = {
2024
2046
  "ios_test_tf.st_waiting": "⏸️ 대기",
2025
2047
  "ios_test_tf.st_done": "✅ 완료",
2026
2048
  "ios_test_tf.st_failed": "❌ 실패",
2027
- "ios_test_tf.st_canceled": "⏸️ 취소됨",
2049
+ "ios_test_tf.st_not_run": "⏸️ 실행 안 됨 (이전 단계 실패)",
2050
+ "ios_test_tf.st_precheck_blocked": "⏸️ 실행 안 됨 (사전 점검 실패: {missing})",
2028
2051
  "ios_test_tf.row_build_number": "| **빌드 번호** | `{build}` |",
2029
2052
  "ios_test_tf.row_app_version": "| **앱 버전** | `{version}({build})` |",
2030
2053
  "ios_test_tf.row_branch": "| **브랜치** | `{branch}` |",
@@ -2062,6 +2085,7 @@ KO = {
2062
2085
  "ios_test_tf.notes_generated": "📋 생성된 릴리즈 노트:",
2063
2086
  "ios_test_tf.title_prepare_failed": "## 🍎 ❌ iOS TestFlight 빌드 실패",
2064
2087
  "ios_test_tf.prepare_error": "❌ **환경 준비 중 오류가 발생했습니다.**",
2088
+ "ios_test_tf.prepare_precheck_error": "❌ **사전 점검에 실패해 빌드를 시작하지 않았습니다.** 누락되었거나 올바르지 않은 항목: {missing}",
2065
2089
  "ios_test_tf.run_log_check_link": "🔗 [워크플로우 실행 로그 확인]({url})",
2066
2090
  "ios_test_tf.log_prepare_failed": "❌ 진행 상황 업데이트 완료: 준비 실패",
2067
2091
  "ios_test_tf.log_prepare_done": "✅ 진행 상황 업데이트 완료: 준비 완료 ({elapsed})",
@@ -2155,6 +2179,7 @@ KO = {
2155
2179
  "truncate.limit_positive": "ERROR: limit은 양수여야 함: {limit}",
2156
2180
  # --- version ---
2157
2181
  "version.bump_help": "승격 폭 (기본 patch — 지정 안 하면 기존 동작과 동일)",
2182
+ "version.option_default_help": "version.yml에 키가 없을 때 출력할 값 (기본 false)",
2158
2183
  # --- wf_aisum ---
2159
2184
  "wf_aisum.no_issue_in_branch": "브랜치명에서 이슈 번호를 찾지 못함 — 건너뜀: {ref}",
2160
2185
  # --- wf_changelog ---
@@ -2320,7 +2345,8 @@ KO = {
2320
2345
  "wf_preview.status_done": "✅ 완료",
2321
2346
  "wf_preview.status_failed": "❌ 실패",
2322
2347
  "wf_preview.status_init_failed": "⚠️ 초기화 실패",
2323
- "wf_preview.status_waiting": "⏸️ 대기",
2348
+ "wf_preview.status_not_run": "⏸️ 실행 안 됨 (이전 단계 실패)",
2349
+ "wf_preview.status_precheck_blocked": "⏸️ 실행 안 됨 (사전 점검 실패: {missing})",
2324
2350
  "wf_preview.tbl_commands": "| 명령어 | 설명 |",
2325
2351
  "wf_preview.tbl_items": "| 항목 | 값 |",
2326
2352
  "wf_preview.tbl_steps": "| 단계 | 상태 | 소요 시간 |",
@@ -2538,6 +2564,7 @@ KO = {
2538
2564
  "version_manager.err_invalid_version": "오류: 올바르지 않은 버전 형식: {version}",
2539
2565
  "version_manager.err_invalid_version_xyz": "오류: 올바르지 않은 버전 형식: {version} (x.y.z 형식이어야 합니다)",
2540
2566
  "version_manager.err_generic": "오류: {error}",
2567
+ "version_manager.option_invalid": "::warning::version.yml의 {option} 값 \"{value}\"을(를) 인식하지 못해 false로 처리합니다. true 또는 false를 쓰세요.",
2541
2568
  "version_manager.err_no_types_sync": "오류: version.yml에서 project_types를 읽을 수 없어 프로젝트 파일을 동기화할 수 없습니다",
2542
2569
  "version_manager.err_no_types_check": "오류: version.yml에서 project_types를 읽을 수 없어 버전을 동기화할 수 없습니다",
2543
2570
  "version_manager.err_write_failed": "version.yml에 버전 {version}을(를) 기록하지 못했습니다",
@@ -2563,11 +2590,15 @@ KO = {
2563
2590
  "wf_common.rebase_ok": "rebase 성공, push를 다시 시도합니다...",
2564
2591
  "wf_common.rebase_failed": "rebase 실패, 충돌을 직접 해결해야 합니다",
2565
2592
  "wf_common.push_gave_up": "push가 {max_retries}회 시도 후에도 실패했습니다",
2593
+ "wf_common.push_protected": "{branch} 브랜치 보호 규칙이 push를 거부했습니다. 경합이 아니므로 재시도해도 소용없습니다. 봇 커밋은 {branch}에 직접 push되어야 합니다: push하는 계정(WORKFLOW_PAT 소유자, PAT가 없으면 GitHub Actions 봇)이 규칙을 우회할 수 있게 허용하세요.",
2594
+ "wf_release.push_protected_trunk": "trunk-based 모드는 모든 릴리스(버전 증가와 CHANGELOG)를 {branch}에 직접 커밋하므로 보호 규칙이 그 push를 허용해야 합니다. 허용할 수 없다면 봇이 릴리스 PR 브랜치에만 커밋하는 develop -> main 흐름으로 바꾸세요.",
2566
2595
  "wf_common.readme_conflict_resolved": "동시에 수정된 README.md 충돌을 원격 내용을 유지하고 버전 줄을 다시 반영해 해결했습니다",
2567
2596
  # --- wf_aisum ---
2568
2597
  "wf_aisum.expected_version": "예상 다음 버전: {version} (현재: {current_version}, 모드: {mode}, 승격: {bump})",
2569
2598
  "wf_aisum.comment_update_failed": "댓글 수정 실패 — 계속 진행합니다 (이 워크플로우는 PR을 막지 않습니다)",
2570
2599
  "wf_aisum.comment_post_failed": "댓글 등록 실패 — 계속 진행합니다 (이 워크플로우는 PR을 막지 않습니다)",
2600
+ "wf_aisum.summary_heading": "📋 **PR 요약 (project-auto-wizard)**",
2601
+ "wf_aisum.engine_label": "엔진: {engine}",
2571
2602
  # --- wf_changelog ---
2572
2603
  "wf_changelog.pr_not_open": "PR #{pr_number}은(는) 이미 {state} 상태입니다 — 릴리스 파이프라인을 건너뜁니다",
2573
2604
  "wf_changelog.pr_state": "PR #{pr_number} 상태: {state}",
@@ -2587,10 +2618,20 @@ KO = {
2587
2618
  "wf_changelog.automerge_enabled": "automerge 활성화: {subject}",
2588
2619
  "wf_changelog.automerge_unavailable": "automerge를 쓸 수 없어 직접 병합했습니다: {subject}",
2589
2620
  "wf_changelog.automerge_off": "release_automerge가 꺼져 있어 PR 제목을 \"{subject}\"로 바꿨습니다. 직접 머지하세요",
2621
+ "wf_changelog.automerge_off_list_failed": "PR #{pr_number}의 댓글을 읽지 못해 중복을 피하려고 머지 안내 댓글을 건너뛰었습니다",
2622
+ "wf_changelog.automerge_off_rename_hint": "PR 제목을 자동으로 바꾸지 못했습니다. 릴리스가 발행되도록 머지하기 전에 머지 커밋 메시지(또는 PR 제목)를 `{subject}`로 지정하세요.",
2590
2623
  "wf_changelog.automerge_rename_failed": "PR #{pr_number}의 제목 또는 안내 댓글을 갱신하지 못했습니다. 릴리스가 되려면 머지 커밋 메시지에 \"chore(release):\"가 있어야 합니다",
2591
- "wf_changelog.automerge_off_comment": "**자동 머지가 꺼져 있습니다** (`version.yml`의 `release_automerge: false`). 릴리스할 준비가 되면 **Create a merge commit**으로 직접 머지하세요. *Squash and merge*와 *Rebase and merge*는 쓰지 마세요. 릴리스 게이트가 결과 커밋 메시지의 `chore(release):`를 읽고, squash나 rebase는 다음 릴리스 노트가 기대는 커밋 이력까지 바꿉니다.",
2624
+ "wf_changelog.automerge_off_comment": "**자동 머지가 꺼져 있습니다** (`version.yml`의 `release_automerge: false`). 이 워크플로우 실행이 끝난 뒤(버전 확정과 PR 제목이 `chore(release): ...`로 바뀐 뒤) 릴리스할 준비가 되면 **Create a merge commit**으로 직접 머지하는 것을 권장합니다.\n\n다른 머지 방식은 릴리스가 저절로 발행되지 않을 수 있습니다. *Rebase and merge*는 버전 확정 커밋(`[skip ci]`)이 마지막 커밋으로 남고, 기본 메시지(커밋 목록 포함)의 *Squash and merge*는 그 `[skip ci]`가 머지 커밋 본문에 들어가므로, 두 경우 모두 GitHub가 push 워크플로우를 건너뛰어 태그와 Release가 만들어지지 않습니다. 레포의 squash 메시지 기본값을 PR 제목으로 바꿨다면 squash는 발행됩니다. 건너뛰었다면 릴리스 브랜치에서 **PROJECT-RELEASE-PUBLISH**를 수동으로 실행(Actions > Run workflow)하면 발행됩니다. 두 방식은 다음 릴리스 노트가 기대는 커밋 이력도 바꾸어 이전 변경이 다시 기재될 수 있습니다(rebase는 develop이 릴리스 브랜치를 역머지하기 전까지는 제외됩니다). 이 실행이 끝나기 전에 머지해도 비슷한 문제가 생깁니다. 안전망 워크플로우(PROJECT-VERSION-CONTROL)가 패치 버전을 하나 더 발행합니다.",
2592
2625
  "wf_changelog.merge_retry": "병합 시도 {attempt}/{max_attempts} 실패 — {wait}초 후 다시 시도합니다",
2626
+ "wf_changelog.head_updated": "기준 브랜치의 변경을 head 브랜치에 반영했습니다",
2593
2627
  "wf_changelog.merge_gave_up": "PR 병합이 {max_attempts}회 시도 후에도 실패했습니다 (allow_merge_commit={allow_merge_commit}) — 브랜치 보호 규칙, 필수 체크, 토큰 권한을 확인하세요",
2628
+ "wf_changelog.merge_blocked_review": "릴리스 PR을 자동으로 머지할 수 없습니다: {prod}의 브랜치 보호 규칙이 승인(approving review)을 요구합니다. 승인한 뒤 merge commit으로 직접 머지하거나, 머지하는 계정이 승인 없이 머지할 수 있도록 규칙을 바꾸세요. 이 시도를 끄려면 version.yml에 release_automerge: false를 설정하세요.",
2629
+ "wf_changelog.merge_blocked_policy": "릴리스 PR을 자동으로 머지할 수 없습니다: {prod}의 브랜치 보호 규칙이 머지를 막고 있습니다 (필수 체크, 리뷰 또는 다른 규칙). 조건이 충족되면 merge commit으로 직접 머지하거나, 머지하는 계정이 허용되도록 규칙을 바꾸세요. 이 시도를 끄려면 version.yml에 release_automerge: false를 설정하세요.",
2630
+ "wf_changelog.automerge_blocked_renamed": "automerge가 막혀 PR 제목을 \"{subject}\"로 바꿨습니다. 조건이 충족되면 직접 머지하세요",
2631
+ "wf_changelog.merge_blocked_reason_review": "릴리스 브랜치가 승인(approving review)을 요구합니다.",
2632
+ "wf_changelog.merge_blocked_reason_policy": "릴리스 브랜치의 보호 규칙이 머지를 막고 있습니다 (필수 체크, 리뷰 또는 다른 규칙).",
2633
+ "wf_changelog.merge_blocked_reason_other": "GitHub이 머지를 거부했습니다.",
2634
+ "wf_changelog.merge_blocked_comment": "**릴리스 PR이 자동으로 머지되지 않았습니다** — {reason}\n\n이 PR의 제목을 `chore(release): ...`로 바꿔 두었으므로, 직접 머지해도 릴리스가 발행됩니다. 조건이 충족되면 **Create a merge commit**(권장)으로 머지하세요.\n\n- 다음부터 워크플로우가 머지하게 하려면, 릴리스 브랜치의 보호 규칙에서 머지하는 계정(`WORKFLOW_PAT` 소유자, PAT가 없으면 GitHub Actions 봇)이 머지할 수 있도록 바꾸세요.\n- 항상 직접 머지하고 이 실행이 실패로 끝나지 않게 하려면 `version.yml`에 `release_automerge: false`를 설정하세요.\n\nSquash나 rebase 머지는 릴리스가 자동으로 발행되지 않을 수 있습니다. 릴리스가 건너뛰어졌다면 릴리스 브랜치에서 **PROJECT-RELEASE-PUBLISH**를 수동 실행하세요 (Actions > Run workflow).",
2594
2635
  # --- wf_readme ---
2595
2636
  "wf_readme.not_found": "README.md가 없습니다 — 수정할 것이 없어 건너뜁니다",
2596
2637
  "wf_readme.read_version_failed": "현재 버전을 읽지 못했습니다",
@@ -2711,6 +2752,7 @@ KO = {
2711
2752
  "wf_flutter.release_notes_heading": "📝 릴리스 노트:",
2712
2753
  "wf_flutter.working_dir": "🔧 작업 디렉터리: {dir}",
2713
2754
  "wf_flutter.run_number": "🔍 GitHub 실행 번호: {number}",
2755
+ "wf_flutter.github_sha": "🔍 커밋 SHA: {sha}",
2714
2756
  "wf_flutter.ci_analyze_only": " 분석 전용: {value}",
2715
2757
  }
2716
2758
 
@@ -22,6 +22,7 @@ Usage:
22
22
  version_manager.py increment-code # version_code+1 only
23
23
  version_manager.py set X.Y.Z # set version explicitly, sync
24
24
  version_manager.py sync # sync version.yml <-> project files
25
+ version_manager.py option KEY [--default true|false] # boolean option from metadata.template.options
25
26
 
26
27
  Contract:
27
28
  - The LAST line printed to stdout is always the value (callers do `| tail -n 1`).
@@ -189,6 +190,58 @@ def key_exists(key):
189
190
  return re.search(r'^' + re.escape(key) + r':', text, re.MULTILINE) is not None
190
191
 
191
192
 
193
+ def parse_option_value(raw):
194
+ """One boolean option value -> True / False / None (None = not recognized).
195
+
196
+ Same rule as parseOptionValue() in src/core/options.js (tests/fixtures/option-value-cases.json runs both):
197
+ an inline comment (whitespace + #) is dropped, then one pair of matching quotes, then case is ignored.
198
+ Only true / false are recognized; yes, off, 0, maybe and an empty value are not."""
199
+ v = re.sub(r'(^|\s)#.*$', '', str(raw if raw is not None else '')).strip()
200
+ q = re.match(r'^(["\'])(.*)\1$', v)
201
+ if q:
202
+ v = q.group(2).strip()
203
+ v = v.lower()
204
+ if v == 'true':
205
+ return True
206
+ if v == 'false':
207
+ return False
208
+ return None
209
+
210
+
211
+ def read_option(key, default, text=None):
212
+ """(value, raw) for a boolean under metadata.template.options in version.yml.
213
+
214
+ - key missing: (default, None)
215
+ - key written with a recognized value: (that value, None)
216
+ - key written with an unrecognized value: (False, raw) - the conservative side, reported by the caller;
217
+ a present-but-unreadable value is not a missing key, so the default must not be guessed for it
218
+ Mirrors the section walk of parseTemplateOptions() in src/core/version-yml.js; the first occurrence wins."""
219
+ if text is None:
220
+ text = read_text()
221
+ line_re = re.compile(r'^\s+' + re.escape(key) + r':\s*(.*)$')
222
+ in_template = in_options = False
223
+ for line in text.split("\n"):
224
+ if re.match(r'^\s*template:', line):
225
+ in_template = True
226
+ continue
227
+ if in_template and re.match(r'^\s+options:', line):
228
+ in_options = True
229
+ continue
230
+ if in_template and in_options:
231
+ m = line_re.match(line)
232
+ if m:
233
+ raw = m.group(1).strip()
234
+ parsed = parse_option_value(raw)
235
+ return (False, raw) if parsed is None else (parsed, None)
236
+ # another key indented 0-4 spaces ends the options section
237
+ if re.match(r'^\s{0,4}[a-z_]+:', line):
238
+ in_options = in_template = False
239
+ # a top-level key ends the template section
240
+ if in_template and re.match(r'^[a-z_]+:', line):
241
+ in_template = in_options = False
242
+ return default, None
243
+
244
+
192
245
  def get_current_version():
193
246
  return read_scalar_key("version", "0.0.0")
194
247
 
@@ -278,13 +331,13 @@ def get_version_code():
278
331
  if re.search(r'^version:', text, re.MULTILINE):
279
332
  new_text = re.sub(
280
333
  r'^(version:[^\n]*\n)',
281
- r'\1version_code: 1 # app build number\n',
334
+ r'\1version_code: 1 # app build number\n',
282
335
  text,
283
336
  count=1,
284
337
  flags=re.MULTILINE,
285
338
  )
286
339
  else:
287
- new_text = text.rstrip("\n") + '\nversion_code: 1 # app build number\n'
340
+ new_text = text.rstrip("\n") + '\nversion_code: 1 # app build number\n'
288
341
  write_text(new_text)
289
342
  return "1"
290
343
  return code.strip()
@@ -300,7 +353,7 @@ def set_version_code(new_code):
300
353
  pass
301
354
  text = read_text()
302
355
  pattern = re.compile(r'^version_code:[ \t]*.*$', re.MULTILINE)
303
- replacement = f'version_code: {new_code} # app build number'
356
+ replacement = f'version_code: {new_code} # app build number'
304
357
  if pattern.search(text):
305
358
  new_text = pattern.sub(replacement, text, count=1)
306
359
  else:
@@ -1011,6 +1064,20 @@ def cmd_sync(args):
1011
1064
  return 0
1012
1065
 
1013
1066
 
1067
+ def cmd_option(args):
1068
+ """Print `true` / `false` for one boolean option; a missing version.yml or key prints the default.
1069
+ An unrecognized value prints false and a warning on stderr (stdout stays the value contract)."""
1070
+ default = args.default == "true"
1071
+ if not _version_yml_path().is_file():
1072
+ print("true" if default else "false")
1073
+ return 0
1074
+ value, raw = read_option(args.key, default)
1075
+ if raw is not None:
1076
+ log(t("version_manager.option_invalid", option=args.key, value=raw))
1077
+ print("true" if value else "false")
1078
+ return 0
1079
+
1080
+
1014
1081
  def build_parser():
1015
1082
  parser = argparse.ArgumentParser(prog="version_manager.py")
1016
1083
  sub = parser.add_subparsers(dest="command", required=True)
@@ -1023,6 +1090,11 @@ def build_parser():
1023
1090
  sub.add_parser("increment-code")
1024
1091
  sub.add_parser("sync")
1025
1092
 
1093
+ p_option = sub.add_parser("option")
1094
+ p_option.add_argument("key")
1095
+ p_option.add_argument("--default", choices=["true", "false"], default="false",
1096
+ help=t("version.option_default_help"))
1097
+
1026
1098
  p_set = sub.add_parser("set")
1027
1099
  p_set.add_argument("version")
1028
1100
 
@@ -1041,6 +1113,7 @@ def main(argv=None):
1041
1113
  "increment-code": cmd_increment_code,
1042
1114
  "set": cmd_set,
1043
1115
  "sync": cmd_sync,
1116
+ "option": cmd_option,
1044
1117
  }
1045
1118
  handler = handlers[args.command]
1046
1119
  try:
@@ -11,7 +11,8 @@
11
11
  # Falls back to rule-based summaries whenever Copilot is off or unavailable.
12
12
  # - release_automerge: default true — false stops the release PR (develop -> main)
13
13
  # from being merged automatically. Merge it yourself with a merge commit
14
- # (not squash or rebase); the workflow renames the PR so the release still publishes.
14
+ # (recommended); squash and rebase merges skip the release workflow, so run
15
+ # PROJECT-RELEASE-PUBLISH manually if you used one.
15
16
  # ===
16
17
  # Supported project types: spring, flutter, react, react-native,
17
18
  # react-native-expo, node, python, basic, go
@@ -41,7 +42,7 @@ language: "{{LANGUAGE}}" # message language: en | ko
41
42
  {{PROJECT_PATHS}}
42
43
  metadata:
43
44
  last_updated: "{{NOW}}"
44
- last_updated_by: "project-auto-wizard"
45
+ last_updated_by: "{{LAST_UPDATED_BY}}"
45
46
  default_branch: "{{DEFAULT_BRANCH}}"
46
47
  integration_date: "{{TODAY}}"
47
48
  template:
@@ -57,7 +57,7 @@ jobs:
57
57
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
58
58
  # scoped to the metadata: -> template: -> options: chain so an
59
59
  # unrelated "copilot_ai:" key can never hijack the value
60
- COPILOT_AI=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?copilot_ai:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
60
+ COPILOT_AI=$(python3 "$GITHUB_WORKSPACE/.github/scripts/version_manager.py" option copilot_ai --default false || echo "false")
61
61
  echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
62
62
  echo "$(m wf_common.copilot_ai_option copilot_ai="$COPILOT_AI")"
63
63
 
@@ -81,11 +81,11 @@ jobs:
81
81
  PR_NUMBER=${{ github.event.pull_request.number }}
82
82
 
83
83
  git fetch origin {{MAIN_BRANCH}}
84
- git log --pretty=%s "origin/{{MAIN_BRANCH}}..HEAD" > commits.txt
84
+ git log --cherry-pick --right-only --pretty=%s "origin/{{MAIN_BRANCH}}...HEAD" > commits.txt
85
85
  # Body-footer BREAKING CHANGE: lines are appended after the subject list, one per
86
86
  # line. The script reads each line as one entry, so passing the whole body (%B)
87
87
  # would turn description lines into change entries.
88
- git log --pretty=%b "origin/{{MAIN_BRANCH}}..HEAD" | grep -E '^BREAKING[ -]CHANGE[[:space:]]*:' >> commits.txt || true
88
+ git log --cherry-pick --right-only --pretty=%b "origin/{{MAIN_BRANCH}}...HEAD" | grep -E '^BREAKING[ -]CHANGE[[:space:]]*:' >> commits.txt || true
89
89
  git diff --stat "origin/{{MAIN_BRANCH}}...HEAD" > _full_diff_stat.txt
90
90
  if [ "$(wc -l < _full_diff_stat.txt)" -gt 49 ]; then
91
91
  { head -49 _full_diff_stat.txt; tail -1 _full_diff_stat.txt; } > diff_stat.txt
@@ -102,7 +102,7 @@ jobs:
102
102
  # so rules alone are used, without any AI-assisted classification.
103
103
  CURRENT_VERSION=$(python3 .github/scripts/version_manager.py get 2>/dev/null | tail -n 1)
104
104
  MODE=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?branches:.*?mode:\s*\"?([A-Za-z-]+)", t, re.S); print(m.group(1) if m else "pr-flow")' 2>/dev/null || echo "pr-flow")
105
- SEMVER_AUTO=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?semver_auto:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
105
+ SEMVER_AUTO=$(python3 .github/scripts/version_manager.py option semver_auto --default false || echo "false")
106
106
  BUMP="patch"
107
107
  if [ "$MODE" = "trunk-based" ] && [ "$SEMVER_AUTO" = "true" ]; then
108
108
  BUMP=$(AI_API_KEY="" COPILOT_AI="false" python3 .github/scripts/changelog_manager.py classify-bump --commits-file commits.txt 2>/dev/null | tail -n 1)
@@ -121,17 +121,17 @@ jobs:
121
121
  # On fallback, append the reason (the script also emits it as ::warning::) to the engine line so the cause is visible without opening the job log
122
122
  FALLBACK_REASON=$(python3 -c 'import html,json,sys; print(html.escape((json.loads(sys.argv[1]).get("fallback_reason") or "")[:200]))' "$SUMMARY_RESULT" 2>/dev/null || echo "")
123
123
  ENGINE_LINE="$ENGINE${FALLBACK_REASON:+ ($FALLBACK_REASON)}"
124
- echo "engine: $ENGINE_LINE" >> "$GITHUB_STEP_SUMMARY"
124
+ m wf_aisum.engine_label engine="$ENGINE_LINE" >> "$GITHUB_STEP_SUMMARY"
125
125
 
126
126
  # Find this bot's earlier summary comment by its marker and replace it with the latest content so pushes do not pile up comments
127
127
  MARKER="<!-- project-auto-wizard:pr-summary -->"
128
128
  {
129
129
  echo "$MARKER"
130
- echo "📋 **PR Summary (project-auto-wizard)**"
130
+ m wf_aisum.summary_heading
131
131
  echo ""
132
132
  cat summary.md
133
133
  echo ""
134
- echo "<sub>engine: ${ENGINE_LINE}</sub>"
134
+ echo "<sub>$(m wf_aisum.engine_label engine="$ENGINE_LINE")</sub>"
135
135
  } > comment_body.md
136
136
 
137
137
  python3 -c 'import json; print(json.dumps({"body": open("comment_body.md", encoding="utf-8").read()}))' > comment_payload.json