project-auto-wizard 0.13.2 → 0.14.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 (112) hide show
  1. package/README.md +1 -1
  2. package/bin/project-auto-wizard.js +18 -4
  3. package/package.json +2 -2
  4. package/payload/config/breaking-changes.json +28 -14
  5. package/payload/config/wizard-prompts.yml +102 -46
  6. package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
  7. package/payload/flutter-app/ios/ExportOptions.plist +8 -8
  8. package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
  9. package/payload/scripts/changelog_manager.py +296 -257
  10. package/payload/scripts/issue_helper.py +54 -50
  11. package/payload/scripts/messages.py +2279 -0
  12. package/payload/scripts/truncate_release_notes.py +7 -6
  13. package/payload/scripts/version_manager.py +64 -61
  14. package/payload/version.yml.template +2 -1
  15. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +17 -12
  16. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +59 -34
  17. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
  18. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +17 -10
  19. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +54 -44
  20. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +24 -17
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
  25. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
  26. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
  27. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
  28. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
  29. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
  30. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
  31. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
  32. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
  33. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
  34. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
  35. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
  36. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
  37. package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
  38. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
  39. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
  40. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
  41. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
  42. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
  43. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
  44. package/src/cli/args.js +98 -52
  45. package/src/cli/help.js +8 -41
  46. package/src/commands/doctor.js +85 -89
  47. package/src/commands/dry-run.js +63 -43
  48. package/src/commands/full.js +79 -77
  49. package/src/commands/install-settings.js +16 -15
  50. package/src/commands/interactive-flutter.js +20 -19
  51. package/src/commands/interactive.js +135 -132
  52. package/src/commands/purge.js +47 -42
  53. package/src/commands/status.js +36 -36
  54. package/src/commands/uninstall.js +43 -41
  55. package/src/context.js +12 -11
  56. package/src/core/assets.js +12 -11
  57. package/src/core/baseline.js +27 -27
  58. package/src/core/branches.js +29 -27
  59. package/src/core/branding.js +7 -6
  60. package/src/core/breaking-check.js +22 -21
  61. package/src/core/breaking.js +15 -8
  62. package/src/core/copy/app-files.js +5 -4
  63. package/src/core/copy/flutter-app.js +8 -8
  64. package/src/core/copy/gitignore.js +58 -50
  65. package/src/core/copy/readme.js +45 -35
  66. package/src/core/copy/simple.js +16 -15
  67. package/src/core/copy/workflows.js +131 -124
  68. package/src/core/deploy-style.js +51 -48
  69. package/src/core/detect-fs.js +57 -57
  70. package/src/core/detect.js +67 -65
  71. package/src/core/errors.js +2 -2
  72. package/src/core/flutter-doctor.js +17 -16
  73. package/src/core/flutter-hooks.js +33 -32
  74. package/src/core/flutter-options.js +34 -33
  75. package/src/core/fsutil.js +12 -12
  76. package/src/core/installed-stores.js +4 -4
  77. package/src/core/logger.js +42 -40
  78. package/src/core/paths-resolve.js +88 -86
  79. package/src/core/paths.js +10 -10
  80. package/src/core/release-options.js +8 -8
  81. package/src/core/removal-exec.js +14 -13
  82. package/src/core/removal-plan.js +37 -37
  83. package/src/core/types.js +47 -47
  84. package/src/core/verify.js +44 -38
  85. package/src/core/version-yml.js +76 -68
  86. package/src/core/wizard-env.js +46 -46
  87. package/src/core/wizard-labels.js +63 -41
  88. package/src/i18n/catalog/en/cli.js +50 -0
  89. package/src/i18n/catalog/en/commands.js +187 -0
  90. package/src/i18n/catalog/en/copy.js +19 -0
  91. package/src/i18n/catalog/en/core.js +74 -0
  92. package/src/i18n/catalog/en/core2.js +15 -0
  93. package/src/i18n/catalog/en/ui.js +221 -0
  94. package/src/i18n/catalog/en.js +12 -0
  95. package/src/i18n/catalog/index.js +5 -0
  96. package/src/i18n/catalog/ko/cli.js +50 -0
  97. package/src/i18n/catalog/ko/commands.js +187 -0
  98. package/src/i18n/catalog/ko/copy.js +19 -0
  99. package/src/i18n/catalog/ko/core.js +74 -0
  100. package/src/i18n/catalog/ko/core2.js +15 -0
  101. package/src/i18n/catalog/ko/ui.js +221 -0
  102. package/src/i18n/catalog/ko.js +10 -0
  103. package/src/i18n/index.js +44 -0
  104. package/src/i18n/languages.js +9 -0
  105. package/src/index.js +128 -94
  106. package/src/ui/ansi.js +7 -7
  107. package/src/ui/banner.js +7 -6
  108. package/src/ui/env-plan.js +82 -81
  109. package/src/ui/prompts.js +97 -122
  110. package/src/ui/readline-engine.js +59 -58
  111. package/src/ui/status-cards.js +37 -31
  112. package/src/ui/summary.js +89 -88
@@ -31,6 +31,8 @@ Contract:
31
31
  import argparse
32
32
  import sys
33
33
 
34
+ from messages import t, use_utf8_output
35
+
34
36
 
35
37
  def log(message):
36
38
  print(message, file=sys.stderr)
@@ -47,19 +49,19 @@ def run(path, limit, mode):
47
49
  with open(path, "r", encoding="utf-8", newline="", errors="replace") as f:
48
50
  text = f.read()
49
51
  except FileNotFoundError:
50
- log(f"릴리즈 노트 파일 없음, 건너뜀: {path}")
52
+ log(t("truncate.file_missing", path=path))
51
53
  return 0
52
54
 
53
55
  length = len(text) if mode == "char" else len(text.encode("utf-8"))
54
56
  if length <= limit:
55
- log(f"한도 이내 ({length}/{limit} {mode}), 자르지 않음: {path}")
57
+ log(t("truncate.within_limit", length=length, limit=limit, mode=mode, path=path))
56
58
  return 0
57
59
 
58
60
  truncated = truncate_text(text, limit, mode)
59
61
  with open(path, "w", encoding="utf-8", newline="") as f:
60
62
  f.write(truncated)
61
63
 
62
- log(f"한도 초과 ({length} -> {limit} {mode} 이하로 절단): {path}")
64
+ log(t("truncate.truncated", length=length, limit=limit, mode=mode, path=path))
63
65
  return 0
64
66
 
65
67
 
@@ -72,14 +74,13 @@ def build_parser():
72
74
 
73
75
 
74
76
  def main(argv=None):
75
- sys.stdout.reconfigure(errors="replace")
76
- sys.stderr.reconfigure(errors="replace")
77
+ use_utf8_output()
77
78
 
78
79
  parser = build_parser()
79
80
  args = parser.parse_args(argv)
80
81
 
81
82
  if args.limit <= 0:
82
- log(f"ERROR: limit은 양수여야 함: {args.limit}")
83
+ log(t("truncate.limit_positive", limit=args.limit))
83
84
  return 1
84
85
 
85
86
  return run(args.file, args.limit, args.mode)
@@ -38,26 +38,28 @@ import sys
38
38
  from pathlib import Path
39
39
  from typing import Callable, NamedTuple
40
40
 
41
+ from messages import t, use_utf8_output
42
+
41
43
  VERSION_YML = "version.yml"
42
44
 
43
45
 
44
46
  class VersionSyncError(Exception):
45
- """version.yml·프로젝트 파일에 버전을 쓰지 못했다 — exit 1로 알린다."""
47
+ """The version could not be written to version.yml or a project file — reported via exit 1."""
46
48
  SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
47
49
 
48
- # 프로젝트 파일 버전 해석 규칙 — 설치 시 감지(src/core/detect.js coreVersion)와 같아야 한다.
49
- # version.yml은 x.y.z만 저장·증가시키므로 파일의 1.2.3-rc.1·1.2.3+4·1.2.0-SNAPSHOT은 코어 x.y.z로 읽는다.
50
- # 다르게 읽으면 설치 직후 version.yml 값과 첫 릴리스 때 읽은 값이 어긋난다.
50
+ # How project-file versions are interpreted — must match the install-time detection (src/core/detect.js coreVersion).
51
+ # version.yml only stores and bumps x.y.z, so 1.2.3-rc.1, 1.2.3+4 and 1.2.0-SNAPSHOT in a file are read as the core x.y.z.
52
+ # Reading them differently would make the value right after install disagree with the one read at the first release.
51
53
  _CORE_VERSION_RE = re.compile(r"^v?(\d+\.\d+\.\d+)(?:[-+][0-9A-Za-z.+-]*)?$")
52
54
 
53
- # Gradle·Maven의 -SNAPSHOT은 특정 버전의 프리릴리스가 아니라 "개발 중 빌드" 표식이다(배포 저장소 선택,
54
- # 산출물 이름에 쓰인다). 그래서 버전을 올려도 떼지 않고 새 버전 뒤에 그대로 붙인다.
55
- # rc.1·beta.1 같은 프리릴리스는 그 버전 전용 표식이라 새 x.y.z로 릴리스할 때 버린다.
55
+ # Gradle/Maven -SNAPSHOT is not a pre-release of a specific version but an "in-development build" marker (it selects the
56
+ # publishing repository and appears in artifact names). So it is kept and re-appended after a version bump.
57
+ # Pre-releases such as rc.1 or beta.1 belong to that version only, so they are dropped when releasing a new x.y.z.
56
58
  SNAPSHOT_SUFFIX = "-SNAPSHOT"
57
59
 
58
60
 
59
61
  def core_version(value):
60
- """x.y.z[-pre][+build] → x.y.z. 버전 형식이 아니면 None."""
62
+ """x.y.z[-pre][+build] -> x.y.z. None if the value is not a version."""
61
63
  m = _CORE_VERSION_RE.match(str(value or "").strip())
62
64
  return m.group(1) if m else None
63
65
 
@@ -287,7 +289,7 @@ def increment_patch(version):
287
289
 
288
290
 
289
291
  def increment_version(version, bump="patch"):
290
- """bump: 'major'|'minor'|'patch'. 생략하면 기존과 동일하게 patch(increment_patch)로 동작."""
292
+ """bump: 'major'|'minor'|'patch'. Defaults to patch when omitted."""
291
293
  if bump == "major":
292
294
  major, _minor, _patch = version.split(".")
293
295
  return f"{int(major) + 1}.0.0"
@@ -315,7 +317,7 @@ def get_higher_version(v1, v2):
315
317
 
316
318
  def update_version_yml(new_version):
317
319
  if not write_scalar_key("version", new_version):
318
- # 키가 없으면 아무것도 쓰지 않은 채 성공처럼 끝나 매번 같은 버전이 나왔다.
320
+ # Without the key nothing was written yet the call looked successful, so the same version came out every time.
319
321
  log("WARNING: version field missing in version.yml, adding it")
320
322
  text = read_text()
321
323
  line = f'version: "{new_version}"\n'
@@ -365,16 +367,16 @@ _XML_TOKEN_RE = re.compile(
365
367
 
366
368
 
367
369
  def _pom_text_span(text, path):
368
- """루트(<project>) 기준 경로의 요소 텍스트 구간 (start, end). 없으면 None.
369
- <parent>·<dependencies> 안의 <version>은 다른 아티팩트 버전이라 깊이로 구분한다."""
370
+ """Text span (start, end) of the element at `path` under the root (<project>). None if absent.
371
+ A <version> inside <parent>/<dependencies> is another artifact's version, so depth tells them apart."""
370
372
  stack = []
371
373
  start = None
372
374
  target = list(path)
373
375
  for m in _XML_TOKEN_RE.finditer(text):
374
376
  name = m.group(2)
375
377
  if not name:
376
- continue # 주석·CDATA·선언
377
- if m.group(1): # 닫는 태그
378
+ continue # comment / CDATA / declaration
379
+ if m.group(1): # closing tag
378
380
  if start is not None and stack[1:] == target:
379
381
  return start, m.start()
380
382
  if stack:
@@ -401,8 +403,8 @@ def _pom_replace(text, path, value):
401
403
 
402
404
 
403
405
  def sync_maven(path_dir, new_version):
404
- """pom.xml의 프로젝트 자신의 <version>만 바꾼다. 하위 모듈은 루트를 가리키는
405
- <parent><version>(과 루트와 같던 자기 <version>)만 따라 올린다."""
406
+ """Only change the project's own <version> in pom.xml. A child module follows only the
407
+ <parent><version> that points at the root (and its own <version> if it equaled the root's)."""
406
408
  root_pom = Path(path_dir) / "pom.xml"
407
409
  if not root_pom.is_file():
408
410
  return False
@@ -412,7 +414,7 @@ def sync_maven(path_dir, new_version):
412
414
  log(f"WARNING: spring: {root_pom} has no project <version> (inherited from parent?) — skipping")
413
415
  return True
414
416
  if "${" in old_version:
415
- # ${revision} 같은 CI-friendly 버전은 프로퍼티 쪽에서 관리하므로 건드리지 않는다.
417
+ # CI-friendly versions such as ${revision} are managed in a property, so leave them alone.
416
418
  log(f"WARNING: spring: {root_pom} <version> is a property ({old_version}) — skipping")
417
419
  return True
418
420
  new_full = _keep_snapshot(old_version, new_version)
@@ -440,12 +442,12 @@ _GRADLE_SHARED_BLOCKS = ("allprojects", "subprojects")
440
442
 
441
443
 
442
444
  def _gradle_code(line, quote=""):
443
- """한 줄에서 주석을 떼고 따옴표 안 글자는 공백으로 바꾼다. url 'https://…'의 `//`나
444
- 문자열 속 중괄호가 블록 깊이 계산을 흐트러뜨리지 않게 한다. detect.js와 같은 규칙.
445
+ """Strip the comment from a line and blank out quoted text, so the `//` in a url 'https://...' or
446
+ a brace inside a string cannot disturb block-depth counting. Same rule as detect.js.
445
447
 
446
- quote는 이 줄이 시작될 때 열려 있던 따옴표다. 삼중따옴표(\"\"\" ''')는 여러 줄에 걸치므로
447
- (코드, 줄 끝에서 열려 있는 따옴표)를 돌려줘 다음 줄이 이어받게 한다. 한 줄짜리 따옴표는
448
- 줄 끝에서 닫힌 것으로 본다."""
448
+ `quote` is the quote that was open when this line started. Triple quotes (\"\"\" ''') span lines, so
449
+ return (code, quote still open at end of line) for the next line to continue. A single-line quote
450
+ is treated as closed at end of line."""
449
451
  out, i = [], 0
450
452
  while i < len(line):
451
453
  ch = line[i]
@@ -468,14 +470,14 @@ def _gradle_code(line, quote=""):
468
470
 
469
471
 
470
472
  def _gradle_version_matches(text):
471
- """프로젝트 버전 줄의 match 목록. 읽기와 동기화가 이 한 곳을 공유해 같은 줄만 다룬다.
473
+ """Matches of the project version lines. Reading and syncing share this one place so they touch the same lines.
472
474
 
473
- 들여쓰지 않은 `version =`이 있으면 그것만 쓴다. 없을 때만 allprojects/subprojects 블록 안의
474
- 들여쓴 줄을 인정한다 — `node { version = '20.11.0' }` 같은 플러그인 설정 블록을
475
- 프로젝트 버전으로 오인하면 버전이 뛰고 빌드 설정이 깨진다."""
475
+ If an unindented `version =` exists, only that is used. Only when there is none are indented lines
476
+ inside allprojects/subprojects blocks accepted — mistaking a plugin config block such as `node { version = '20.11.0' }`
477
+ for the project version would make the version jump and break the build config."""
476
478
  top, shared = [], []
477
- stack = [] # 열린 블록 이름. 줄 끝이 `{`로 끝나는 줄의 마지막 단어
478
- quote = "" # 여러 줄 문자열 안이면 그 따옴표. 문자열 속 줄은 코드가 아니다
479
+ stack = [] # open block names: the last word of each line ending in `{`
480
+ quote = "" # the open quote while inside a multi-line string; lines inside a string are not code
479
481
  pos = 0
480
482
  for line in text.splitlines(keepends=True):
481
483
  m = None if quote else _GRADLE_VERSION_RE.match(line)
@@ -511,11 +513,11 @@ def sync_spring(path_dir, new_version):
511
513
  return
512
514
  for gradle_file in candidates:
513
515
  text = read_file(gradle_file)
514
- # 줄 시작의 `version =`만 프로젝트 버전이다. 앵커가 없으면 kotlin_version 같은
515
- # 의존성 버전 변수까지 함께 바뀌어 빌드가 깨진다.
516
+ # Only a `version =` at the start of a line is the project version. Without the anchor,
517
+ # dependency version variables such as kotlin_version would change too and break the build.
516
518
  matches = _gradle_version_matches(text)
517
519
  new_text = text
518
- # 뒤에서부터 바꿔 앞쪽 span이 밀리지 않게 한다.
520
+ # Replace from the end so earlier spans do not shift.
519
521
  for m in reversed(matches):
520
522
  replaced = f"{m.group(1)}{m.group(2)}{_keep_snapshot(m.group(3), new_version)}{m.group(2)}"
521
523
  new_text = new_text[:m.start()] + replaced + new_text[m.end():]
@@ -527,7 +529,7 @@ def sync_spring(path_dir, new_version):
527
529
 
528
530
 
529
531
  def _pubspec_build_number(path_dir):
530
- """pubspec.yaml `version: x.y.z+N`의 N. 없으면 None."""
532
+ """N in pubspec.yaml `version: x.y.z+N`. None if absent."""
531
533
  target = Path(path_dir) / "pubspec.yaml"
532
534
  if not target.is_file():
533
535
  return None
@@ -536,9 +538,9 @@ def _pubspec_build_number(path_dir):
536
538
 
537
539
 
538
540
  def get_reconciled_version_code():
539
- """version.yml의 version_code와 pubspec.yaml의 +N 중 큰 값.
540
- 로컬 수동 배포 등으로 pubspec 쪽이 앞서 있으면 스토어는 더 작은 build number를
541
- 거부하므로, 그 값을 version.yml에 반영해 역행을 막는다."""
541
+ """The larger of version.yml version_code and the +N in pubspec.yaml.
542
+ If pubspec is ahead (e.g. after a manual local release), the store rejects a lower build number,
543
+ so that value is adopted into version.yml to prevent a regression."""
542
544
  code = int(get_version_code())
543
545
  types = get_project_types_csv()
544
546
  pubspec_codes = [
@@ -586,7 +588,7 @@ def sync_json_version(target, new_version, key_path):
586
588
  try:
587
589
  data = json.loads(raw)
588
590
  except json.JSONDecodeError as e:
589
- # 건너뛰고 성공으로 끝내면 태그·version.yml과 패키지 버전이 조용히 어긋난다.
591
+ # Skipping and reporting success would silently leave the tag/version.yml and the package version out of step.
590
592
  raise VersionSyncError(f"{target} is not valid JSON ({e}) — cannot write version")
591
593
  node = data
592
594
  for k in key_path[:-1]:
@@ -601,8 +603,8 @@ _TOML_VERSION_RE = re.compile(r'^[ \t]*version[ \t]*=[ \t]*([\'"])([^\'"\n]*)\1'
601
603
 
602
604
 
603
605
  def _pyproject_version_span(text):
604
- """[project] 또는 [tool.poetry] 섹션의 version 값 구간. [tool.*] 등 다른 섹션의
605
- `version =`은 도구 설정이라 패키지 버전으로 쓰지 않는다."""
606
+ """Value span of `version` in the [project] or [tool.poetry] section. A `version =` in other sections
607
+ such as [tool.*] is tool config, not the package version."""
606
608
  headers = list(_TOML_HEADER_RE.finditer(text))
607
609
  for i, h in enumerate(headers):
608
610
  if h.group(1) not in ("project", "tool.poetry"):
@@ -614,13 +616,13 @@ def _pyproject_version_span(text):
614
616
  return None
615
617
 
616
618
 
617
- # setup(version="x.y.z"). python_requires·python_version 같은 다른 키는 앞 글자로 거른다.
619
+ # setup(version="x.y.z"). Other keys such as python_requires / python_version are filtered out by the preceding character.
618
620
  _SETUP_PY_VERSION_RE = re.compile(r"""(?<![\w.])version\s*=\s*(['"])([^'"]+)\1""")
619
621
 
620
622
 
621
623
  def _python_version_spans(path_dir):
622
- """버전을 읽고 쓰는 파일과 값 구간 목록. 읽을 때는 앞쪽(pyproject.toml → setup.py)이 우선이다.
623
- setup.py만 쓰는 프로젝트도 설치 때 버전을 읽으므로, 릴리스 때도 같은 파일을 읽고 써야 한다."""
624
+ """Files and value spans for reading/writing the version. When reading, earlier entries win (pyproject.toml -> setup.py).
625
+ Projects using only setup.py also have their version read at install time, so releases must read and write the same file."""
624
626
  spans = []
625
627
  pyproject = Path(path_dir) / "pyproject.toml"
626
628
  if pyproject.is_file():
@@ -638,7 +640,7 @@ def _python_version_spans(path_dir):
638
640
  def sync_python(path_dir, new_version):
639
641
  spans = _python_version_spans(path_dir)
640
642
  if not spans:
641
- # dynamic = ["version"] 등 버전을 파일에 두지 않는 구성은 쓸 곳이 없다.
643
+ # Setups that keep the version out of the file (e.g. dynamic = ["version"]) have nowhere to write it.
642
644
  log(f"WARNING: python: no version in pyproject.toml [project]/[tool.poetry] or setup.py under {path_dir} — skipping")
643
645
  return
644
646
  for target, (start, end) in spans:
@@ -651,8 +653,8 @@ _PLIST_VERSION_RE = re.compile(r'(<key>CFBundleShortVersionString</key>\s*<strin
651
653
 
652
654
 
653
655
  def _rn_app_plists(ios_dir):
654
- """앱 타깃의 Info.plist만 이름순으로. Pods·빌드 산출물·테스트 타깃은 우리 버전이 아니다.
655
- 읽기와 동기화가 같은 파일 집합을 보도록 한 곳에서 고른다."""
656
+ """Only the app target Info.plist files, sorted by name. Pods, build output and test targets do not carry our version.
657
+ Chosen in one place so reading and syncing see the same set of files."""
656
658
  skip = {"Pods", "build"}
657
659
  return [
658
660
  p for p in sorted(ios_dir.glob("*/Info.plist"))
@@ -669,7 +671,7 @@ def sync_react_native(path_dir, new_version):
669
671
  m = _PLIST_VERSION_RE.search(text)
670
672
  if not m:
671
673
  continue
672
- # $(MARKETING_VERSION) 같은 빌드 변수 참조는 Xcode 설정이 원본이라 덮어쓰지 않는다.
674
+ # A build-variable reference such as $(MARKETING_VERSION) is owned by the Xcode settings, so do not overwrite it.
673
675
  if m.group(2).strip().startswith("$("):
674
676
  log(f"skipped: {plist_file} — CFBundleShortVersionString references a build variable")
675
677
  continue
@@ -710,7 +712,7 @@ def sync_all_project_files(new_version):
710
712
  raise SystemExit("ERROR: version.yml has no readable project_types — cannot sync project files")
711
713
  errors = []
712
714
  for t in types:
713
- # 한 타입이 실패해도 나머지는 맞춰 두고, 실패는 끝에서 모아 종료 코드로 알린다.
715
+ # Keep syncing the other types when one fails; collect failures and report them via the exit code at the end.
714
716
  try:
715
717
  sync_for_type(t, new_version, get_reconciled_version_code)
716
718
  except VersionSyncError as e:
@@ -725,11 +727,11 @@ README_VERSION_LINE_RE = re.compile(
725
727
 
726
728
 
727
729
  def update_readme_version(new_version, path="README.md"):
728
- """AUTO-VERSION-SECTION 마커 바로 아래 버전 줄만 새 버전으로 바꾼다.
730
+ """Rewrite only the version line right below the AUTO-VERSION-SECTION marker.
729
731
 
730
- README 갱신 워크플로우는 태그를 만든 뒤에 돌기 때문에, 버전 확정 커밋에 README가 함께
731
- 들어가지 않으면 태그 시점 README가 한 버전 전을 가리킨다. 마커·줄 삽입 같은 나머지 일은
732
- 그 워크플로우에 맡기고, 여기서는 이미 있는 줄만 고친다(날짜 표기 여부도 그대로 둔다)."""
732
+ The README update workflow runs after the tag is created, so unless the README is part of the
733
+ version-confirm commit, the README at the tag points one version behind. Everything else, such as
734
+ inserting the marker or line, is left to that workflow; here only an existing line is fixed (date presence is kept)."""
733
735
  p = Path(path)
734
736
  if not p.is_file():
735
737
  return False
@@ -763,7 +765,7 @@ def update_all_versions(new_version):
763
765
  # ===================================================================
764
766
 
765
767
  def _read_spring(path_dir):
766
- # build.gradle에 버전이 없으면(allprojects 설정만 있는 경우 등) build.gradle.kts → pom.xml 순으로 이어 본다.
768
+ # If build.gradle has no version (e.g. only an allprojects config), try build.gradle.kts, then pom.xml.
767
769
  for name in ("build.gradle", "build.gradle.kts"):
768
770
  p = Path(path_dir) / name
769
771
  if p.is_file():
@@ -796,9 +798,9 @@ def _read_package_json(path_dir):
796
798
 
797
799
 
798
800
  def _read_react_native(path_dir):
799
- """릴리스 때 쓰는 파일(ios/<앱>/Info.plist, android/app/build.gradle)에서 읽는다.
800
- package.json version은 동기화하지 않으므로 기준으로 삼으면 매 릴리스마다 어긋난다.
801
- $(MARKETING_VERSION) 참조나 템플릿 기본값 "1.0"처럼 x.y.z가 아닌 값은 건너뛴다."""
801
+ """Read from the files used at release time (ios/<app>/Info.plist, android/app/build.gradle).
802
+ package.json version is not synced, so using it as the reference would drift on every release.
803
+ Values that are not x.y.z, such as a $(MARKETING_VERSION) reference or the template default "1.0", are skipped."""
802
804
  ios_dir = Path(path_dir) / "ios"
803
805
  if ios_dir.is_dir():
804
806
  for plist in _rn_app_plists(ios_dir):
@@ -839,8 +841,8 @@ def _sync_none(path_dir, new_version, version_code_getter):
839
841
 
840
842
 
841
843
  class TypeHandler(NamedTuple):
842
- """타입별 버전 파일 처리. read(path_dir)는 파일의 버전(없으면 None),
843
- sync(path_dir, new_version, version_code_getter)는 새 버전을 파일에 쓴다."""
844
+ """Per-type version file handling. read(path_dir) returns the version in the file (None if absent),
845
+ sync(path_dir, new_version, version_code_getter) writes the new version to the file."""
844
846
  read: Callable
845
847
  sync: Callable
846
848
 
@@ -850,11 +852,11 @@ _PACKAGE_JSON = TypeHandler(
850
852
  sync=lambda d, v, _code: sync_json_version(Path(d) / "package.json", v, ["version"]),
851
853
  )
852
854
 
853
- # 새 타입은 여기 한 줄만 추가하면 읽기(sync 비교)와 쓰기가 함께 따라온다.
854
- # 버전 파일이 없는 타입(basic·go)은 version.yml만 쓰도록 빈 핸들러를 둔다.
855
+ # Adding one line here is enough for a new type: both reading (sync comparison) and writing follow.
856
+ # Types without a version file (basic, go) get an empty handler so only version.yml is written.
855
857
  TYPE_HANDLERS = {
856
858
  "spring": TypeHandler(read=_read_spring, sync=lambda d, v, _code: sync_spring(d, v)),
857
- # build number가 필요한 타입만 version_code를 계산한다 (pubspec 조정 부수효과가 있다).
859
+ # Only types that need a build number compute version_code (this has a pubspec-adjusting side effect).
858
860
  "flutter": TypeHandler(read=_read_flutter, sync=lambda d, v, code: sync_flutter(d, v, code())),
859
861
  "react": _PACKAGE_JSON,
860
862
  "next": _PACKAGE_JSON,
@@ -954,7 +956,7 @@ def cmd_increment(args):
954
956
  return 1
955
957
  bump = getattr(args, "bump", None) or "patch"
956
958
  new_version = increment_version(current_version, bump)
957
- # build number를 먼저 올려야 이어지는 pubspec 동기화에 새 값이 함께 기록된다.
959
+ # Bump the build number first so the following pubspec sync writes the new value too.
958
960
  set_version_code(get_reconciled_version_code() + 1)
959
961
  update_all_versions(new_version)
960
962
 
@@ -988,7 +990,7 @@ def build_parser():
988
990
  sub.add_parser("get-code")
989
991
  p_increment = sub.add_parser("increment")
990
992
  p_increment.add_argument("--bump", choices=["major", "minor", "patch"], default="patch",
991
- help="승격 폭 (기본 patch — 지정 안 하면 기존 동작과 동일)")
993
+ help=t("version.bump_help"))
992
994
  sub.add_parser("increment-code")
993
995
  sub.add_parser("sync")
994
996
 
@@ -999,6 +1001,7 @@ def build_parser():
999
1001
 
1000
1002
 
1001
1003
  def main(argv=None):
1004
+ use_utf8_output()
1002
1005
  parser = build_parser()
1003
1006
  args = parser.parse_args(argv)
1004
1007
 
@@ -34,6 +34,7 @@
34
34
  version: "{{VERSION}}"
35
35
  version_code: {{VERSION_CODE}} # app build number
36
36
  project_types: {{PROJECT_TYPES}} # first entry is primary
37
+ language: "{{LANGUAGE}}" # message language: en | ko
37
38
  {{PROJECT_PATHS}}
38
39
  metadata:
39
40
  last_updated: "{{NOW}}"
@@ -52,6 +53,6 @@ metadata:
52
53
  options:
53
54
  semver_auto: {{OPT_SEMVER_AUTO}}
54
55
  copilot_ai: {{OPT_COPILOT_AI}}
55
- deploy_style: "{{OPT_DEPLOY_STYLE}}" # simple | nginx | traefik | none (서버 배포 워크플로우)
56
+ deploy_style: "{{OPT_DEPLOY_STYLE}}" # simple | nginx | traefik | none (server deploy workflows)
56
57
  {{TYPE_OPTIONS}}
57
58
  {{DEPLOY}}
@@ -78,8 +78,9 @@ jobs:
78
78
 
79
79
  git fetch origin {{MAIN_BRANCH}}
80
80
  git log --pretty=%s "origin/{{MAIN_BRANCH}}..HEAD" > commits.txt
81
- # 본문 푸터 BREAKING CHANGE:는 제목 목록 뒤에 한 줄씩 덧붙인다 — 스크립트는 한 줄을
82
- # 한 항목으로 읽으므로 본문 전체(%B)를 넣으면 설명 줄까지 변경 항목으로 잡힌다.
81
+ # Body-footer BREAKING CHANGE: lines are appended after the subject list, one per
82
+ # line. The script reads each line as one entry, so passing the whole body (%B)
83
+ # would turn description lines into change entries.
83
84
  git log --pretty=%b "origin/{{MAIN_BRANCH}}..HEAD" | grep -E '^BREAKING[ -]CHANGE[[:space:]]*:' >> commits.txt || true
84
85
  git diff --stat "origin/{{MAIN_BRANCH}}...HEAD" > _full_diff_stat.txt
85
86
  if [ "$(wc -l < _full_diff_stat.txt)" -gt 49 ]; then
@@ -89,10 +90,12 @@ jobs:
89
90
  fi
90
91
  rm -f _full_diff_stat.txt
91
92
 
92
- # 헤더에는 이미 발행된 현재 버전이 아니라 이 PR이 병합되면 붙을 예상 버전을 보인다.
93
- # 병합 뒤 버전을 올리는 쪽과 같은 규칙: trunk-based는 RELEASE-PUBLISH가 semver_auto면
94
- # 커밋으로 승격 폭을 정하고, pr-flow에서 {{MAIN_BRANCH}}로 바로 들어가는 PR은
95
- # VERSION-CONTROL 안전망이 patch를 올린다. 예상치라 AI 보조 판정 없이 규칙만 쓴다.
93
+ # The header shows the version this PR will get once merged, not the version
94
+ # that is already published. Same rules as whatever bumps the version after the
95
+ # merge: in trunk-based mode RELEASE-PUBLISH picks the bump size from the commits
96
+ # when semver_auto is on, and in pr-flow a PR going straight into {{MAIN_BRANCH}}
97
+ # gets a patch bump from the VERSION-CONTROL safety net. It is only an estimate,
98
+ # so rules alone are used, without any AI-assisted classification.
96
99
  CURRENT_VERSION=$(python3 .github/scripts/version_manager.py get 2>/dev/null | tail -n 1)
97
100
  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")
98
101
  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")
@@ -111,12 +114,12 @@ jobs:
111
114
  --pr-title "$PR_TITLE" \
112
115
  --diff-stat-file diff_stat.txt | tail -n 1)
113
116
  ENGINE=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("engine") or "unknown")' "$SUMMARY_RESULT" 2>/dev/null || echo "unknown")
114
- # fallback이면 사유(스크립트가 ::warning::으로도 남김)를 engine 줄에 붙인다 — 잡 로그를 열지 않아도 원인이 보이게
117
+ # 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
115
118
  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 "")
116
119
  ENGINE_LINE="$ENGINE${FALLBACK_REASON:+ ($FALLBACK_REASON)}"
117
120
  echo "engine: $ENGINE_LINE" >> "$GITHUB_STEP_SUMMARY"
118
121
 
119
- # push마다 댓글이 쌓이지 않도록 이 봇이 남긴 요약 댓글을 표식으로 찾아 최신 내용으로 교체한다
122
+ # Find this bot's earlier summary comment by its marker and replace it with the latest content so pushes do not pile up comments
120
123
  MARKER="<!-- project-auto-wizard:pr-summary -->"
121
124
  {
122
125
  echo "$MARKER"
@@ -143,15 +146,17 @@ jobs:
143
146
  continue-on-error: true
144
147
  env:
145
148
  GITHUB_TOKEN: ${{ github.token }}
146
- # 브랜치명을 run: 스크립트에 직접 ${{ }} 보간하지 않고 env로 전달한다 —
147
- # git ref 이름은 $, 백틱, 세미콜론 등을 포함할 수 있어 직접 보간하면
148
- # 셸 인젝션 경로가 된다(AUTO-CHANGELOG-CONTROL의 PR_TITLE과 동일한 이유).
149
+ # Pass the branch name through env instead of interpolating ${{ }} into the run:
150
+ # script - git ref names can contain $, backticks, semicolons and so on, so direct
151
+ # interpolation would be a shell-injection path (same reason as PR_TITLE in
152
+ # AUTO-CHANGELOG-CONTROL).
149
153
  HEAD_REF: ${{ github.event.pull_request.head.ref }}
150
154
  run: |
155
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
151
156
  ISSUE_NUM=$(python3 .github/scripts/issue_helper.py extract-branch-issue "$HEAD_REF")
152
157
 
153
158
  if [ -z "$ISSUE_NUM" ]; then
154
- echo "브랜치명에서 이슈 번호를 찾지 못함 — 건너뜀: $HEAD_REF"
159
+ echo "$(m wf_aisum.no_issue_in_branch ref="$HEAD_REF")"
155
160
  exit 0
156
161
  fi
157
162