project-auto-wizard 0.14.0 → 0.15.1

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 (64) hide show
  1. package/README.ja.md +3 -1
  2. package/README.ko.md +3 -1
  3. package/README.md +4 -2
  4. package/README.zh-CN.md +3 -1
  5. package/package.json +1 -1
  6. package/payload/config/breaking-changes.json +31 -5
  7. package/payload/config/wizard-prompts.yml +0 -4
  8. package/payload/scripts/changelog_manager.py +8 -8
  9. package/payload/scripts/messages.py +460 -24
  10. package/payload/scripts/version_manager.py +102 -73
  11. package/payload/version.yml.template +2 -2
  12. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +9 -4
  13. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +62 -30
  14. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +96 -44
  15. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +79 -35
  16. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +71 -35
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +24 -8
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +29 -10
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +57 -23
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +155 -72
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +49 -20
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +162 -62
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +184 -87
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +38 -15
  25. package/payload/workflows/go/PROJECT-GO-CI.yaml +14 -5
  26. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +375 -120
  27. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +9 -4
  28. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +12 -4
  29. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +375 -120
  30. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +9 -4
  31. package/payload/workflows/react/PROJECT-REACT-CI.yaml +38 -9
  32. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +19 -5
  33. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +71 -35
  34. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +9 -6
  35. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +7 -4
  36. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +425 -144
  37. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +7 -4
  38. package/src/cli/args.js +17 -7
  39. package/src/commands/doctor.js +16 -0
  40. package/src/commands/dry-run.js +10 -1
  41. package/src/commands/full.js +5 -2
  42. package/src/commands/interactive.js +4 -1
  43. package/src/commands/status.js +14 -1
  44. package/src/core/copy/readme.js +58 -2
  45. package/src/core/copy/simple.js +31 -0
  46. package/src/core/copy/workflows.js +2 -2
  47. package/src/core/deploy-style.js +1 -1
  48. package/src/core/detect.js +3 -2
  49. package/src/core/types.js +15 -8
  50. package/src/core/version-yml.js +52 -6
  51. package/src/i18n/catalog/en/cli.js +1 -0
  52. package/src/i18n/catalog/en/commands.js +9 -0
  53. package/src/i18n/catalog/en/copy.js +1 -0
  54. package/src/i18n/catalog/en/core2.js +3 -0
  55. package/src/i18n/catalog/ko/cli.js +1 -0
  56. package/src/i18n/catalog/ko/commands.js +9 -0
  57. package/src/i18n/catalog/ko/copy.js +1 -0
  58. package/src/i18n/catalog/ko/core2.js +3 -0
  59. package/src/index.js +8 -4
  60. package/src/ui/env-plan.js +1 -1
  61. package/src/ui/prompts.js +7 -3
  62. package/src/ui/summary.js +1 -1
  63. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +0 -241
  64. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +0 -330
@@ -95,16 +95,33 @@ def read_file(path):
95
95
 
96
96
  def write_file(path, text):
97
97
  """Write text preserving the dominant line ending of the existing file
98
- on disk (LF stays LF, CRLF stays CRLF — never platform-dependent)."""
98
+ on disk (LF stays LF, CRLF stays CRLF — never platform-dependent).
99
+ Returns False (and leaves the file untouched) when the content is already identical."""
99
100
  p = Path(path)
100
101
  eol = "\n"
102
+ previous = None
101
103
  if p.is_file():
102
104
  with open(p, "r", encoding="utf-8", newline="") as f:
103
- eol = _detect_eol(f.read())
105
+ previous = f.read()
106
+ eol = _detect_eol(previous)
104
107
  if eol != "\n":
105
108
  text = text.replace("\n", eol)
109
+ if previous == text:
110
+ return False
106
111
  with open(p, "w", encoding="utf-8", newline="") as f:
107
112
  f.write(text)
113
+ return True
114
+
115
+
116
+ # Files whose content actually changed in this process. Lets the sync check tell "reconciled" from "already in sync".
117
+ _CHANGED_FILES = []
118
+
119
+
120
+ def write_synced(path, text):
121
+ """write_file() for version files: logs `updated:` only when the content really changed."""
122
+ if write_file(path, text):
123
+ _CHANGED_FILES.append(str(path))
124
+ log(t("version_manager.updated", path=path))
108
125
 
109
126
 
110
127
  # ===================================================================
@@ -117,7 +134,7 @@ def _version_yml_path():
117
134
 
118
135
  def require_version_yml():
119
136
  if not _version_yml_path().is_file():
120
- log("ERROR: version.yml not found")
137
+ log(t("version_manager.err_yml_missing"))
121
138
  sys.exit(1)
122
139
 
123
140
 
@@ -176,6 +193,21 @@ def get_current_version():
176
193
  return read_scalar_key("version", "0.0.0")
177
194
 
178
195
 
196
+ # Names that used to be separate types. A version.yml written before the merge may still carry them,
197
+ # and the release workflow can run before the wizard rewrites that file, so they are read as the new name.
198
+ TYPE_ALIASES = {"next": "react"}
199
+
200
+
201
+ def _canonical_types(types):
202
+ """Map aliased type names to their current name, dropping duplicates while keeping order."""
203
+ out = []
204
+ for t in types:
205
+ t = TYPE_ALIASES.get(t, t)
206
+ if t not in out:
207
+ out.append(t)
208
+ return out
209
+
210
+
179
211
  def get_project_types_csv():
180
212
  """Return project_types as a list. Supports both:
181
213
  project_types: ["a", "b"]
@@ -196,7 +228,7 @@ def get_project_types_csv():
196
228
  inner = m.group(1)
197
229
  items = re.findall(r'"([^"]*)"|\'([^\']*)\'', inner)
198
230
  types = [a or b for a, b in items]
199
- return [t for t in types if t]
231
+ return _canonical_types([t for t in types if t])
200
232
 
201
233
  # Block list form:
202
234
  # project_types:
@@ -207,7 +239,7 @@ def get_project_types_csv():
207
239
  block = m.group(1)
208
240
  # trailing comments are allowed on list items too
209
241
  types = re.findall(r'-[ \t]*["\']?([^"\'#\n]+?)["\']?[ \t]*(?:#.*)?$', block, re.MULTILINE)
210
- return [t.strip() for t in types if t.strip()]
242
+ return _canonical_types([t.strip() for t in types if t.strip()])
211
243
 
212
244
  return []
213
245
 
@@ -221,16 +253,19 @@ def get_type_path(project_type, project_types_list=None):
221
253
  if not m:
222
254
  return "."
223
255
  block = m.group(1)
224
- km = re.search(
225
- r'^[ \t]+["\']?' + re.escape(project_type)
226
- + r'["\']?:[ \t]*["\']?([^"\'#\n]+?)["\']?[ \t]*(?:#.*)?$',
227
- block,
228
- re.MULTILINE,
229
- )
230
- if km:
231
- val = km.group(1).strip()
232
- if val and val != "null":
233
- return val
256
+ # The current name first, then any older name that maps to it (e.g. next -> react).
257
+ names = [project_type] + [old for old, new in TYPE_ALIASES.items() if new == project_type]
258
+ for name in names:
259
+ km = re.search(
260
+ r'^[ \t]+["\']?' + re.escape(name)
261
+ + r'["\']?:[ \t]*["\']?([^"\'#\n]+?)["\']?[ \t]*(?:#.*)?$',
262
+ block,
263
+ re.MULTILINE,
264
+ )
265
+ if km:
266
+ val = km.group(1).strip()
267
+ if val and val != "null":
268
+ return val
234
269
  return "."
235
270
 
236
271
 
@@ -238,7 +273,7 @@ def get_version_code():
238
273
  require_version_yml()
239
274
  code = read_scalar_key("version_code", None)
240
275
  if code is None or code == "" or code == "null":
241
- log("WARNING: version_code field missing, adding default value 1")
276
+ log(t("version_manager.warn_code_missing"))
242
277
  text = read_text()
243
278
  if re.search(r'^version:', text, re.MULTILINE):
244
279
  new_text = re.sub(
@@ -260,7 +295,7 @@ def set_version_code(new_code):
260
295
  if current not in (None, "", "null"):
261
296
  try:
262
297
  if int(new_code) < int(current):
263
- log(f"WARNING: version_code {new_code} is lower than current {current} — writing anyway (regression?)")
298
+ log(t("version_manager.warn_code_regress", new_code=new_code, current=current))
264
299
  except ValueError:
265
300
  pass
266
301
  text = read_text()
@@ -318,7 +353,7 @@ def get_higher_version(v1, v2):
318
353
  def update_version_yml(new_version):
319
354
  if not write_scalar_key("version", new_version):
320
355
  # Without the key nothing was written yet the call looked successful, so the same version came out every time.
321
- log("WARNING: version field missing in version.yml, adding it")
356
+ log(t("version_manager.warn_version_missing"))
322
357
  text = read_text()
323
358
  line = f'version: "{new_version}"\n'
324
359
  if re.search(r'^version_code:', text, re.MULTILINE):
@@ -327,7 +362,7 @@ def update_version_yml(new_version):
327
362
  new_text = text.rstrip("\n") + "\n" + line
328
363
  write_text(new_text)
329
364
  if read_scalar_key("version") != new_version:
330
- raise VersionSyncError(f"failed to write version {new_version} to version.yml")
365
+ raise VersionSyncError(t("version_manager.err_write_failed", version=new_version))
331
366
  today = datetime.date.today().isoformat()
332
367
  user = os.environ.get("GITHUB_ACTOR", "")
333
368
  if not user:
@@ -411,16 +446,15 @@ def sync_maven(path_dir, new_version):
411
446
  text = read_file(root_pom)
412
447
  old_version = _pom_text(text, ["version"])
413
448
  if old_version is None:
414
- log(f"WARNING: spring: {root_pom} has no project <version> (inherited from parent?) — skipping")
449
+ log(t("version_manager.warn_spring_no_version", pom=root_pom))
415
450
  return True
416
451
  if "${" in old_version:
417
452
  # CI-friendly versions such as ${revision} are managed in a property, so leave them alone.
418
- log(f"WARNING: spring: {root_pom} <version> is a property ({old_version}) — skipping")
453
+ log(t("version_manager.warn_spring_property", pom=root_pom, version=old_version))
419
454
  return True
420
455
  new_full = _keep_snapshot(old_version, new_version)
421
456
  new_text, _ = _pom_replace(text, ["version"], new_full)
422
- write_file(root_pom, new_text)
423
- log(f"updated: {root_pom}")
457
+ write_synced(root_pom, new_text)
424
458
 
425
459
  root_artifact = _pom_text(text, ["artifactId"])
426
460
  for child in sorted(Path(path_dir).glob("*/pom.xml")):
@@ -432,8 +466,7 @@ def sync_maven(path_dir, new_version):
432
466
  ctext, _ = _pom_replace(ctext, ["parent", "version"], new_full)
433
467
  if _pom_text(ctext, ["version"]) == old_version:
434
468
  ctext, _ = _pom_replace(ctext, ["version"], new_full)
435
- write_file(child, ctext)
436
- log(f"updated: {child}")
469
+ write_synced(child, ctext)
437
470
  return True
438
471
 
439
472
 
@@ -509,7 +542,7 @@ def sync_spring(path_dir, new_version):
509
542
  has_pom = sync_maven(path_dir, new_version)
510
543
  if not candidates:
511
544
  if not has_pom:
512
- log(f"WARNING: spring: no build.gradle(.kts) or pom.xml found under {path_dir} — skipping")
545
+ log(t("version_manager.warn_spring_no_files", dir=path_dir))
513
546
  return
514
547
  for gradle_file in candidates:
515
548
  text = read_file(gradle_file)
@@ -522,10 +555,9 @@ def sync_spring(path_dir, new_version):
522
555
  replaced = f"{m.group(1)}{m.group(2)}{_keep_snapshot(m.group(3), new_version)}{m.group(2)}"
523
556
  new_text = new_text[:m.start()] + replaced + new_text[m.end():]
524
557
  if not matches:
525
- log(f"WARNING: spring: no `version = '...'` line in {gradle_file} — skipping")
558
+ log(t("version_manager.warn_spring_no_gradle_line", file=gradle_file))
526
559
  continue
527
- write_file(gradle_file, new_text)
528
- log(f"updated: {gradle_file}")
560
+ write_synced(gradle_file, new_text)
529
561
 
530
562
 
531
563
  def _pubspec_build_number(path_dir):
@@ -548,7 +580,7 @@ def get_reconciled_version_code():
548
580
  if n is not None
549
581
  ]
550
582
  if pubspec_codes and max(pubspec_codes) > code:
551
- log(f"pubspec.yaml build number {max(pubspec_codes)} is ahead of version_code {code} — adopting it")
583
+ log(t("version_manager.pubspec_ahead", build=max(pubspec_codes), code=code))
552
584
  code = max(pubspec_codes)
553
585
  set_version_code(code)
554
586
  return code
@@ -557,7 +589,7 @@ def get_reconciled_version_code():
557
589
  def sync_flutter(path_dir, new_version, version_code):
558
590
  target = Path(path_dir) / "pubspec.yaml"
559
591
  if not target.is_file():
560
- log(f"WARNING: flutter: {target} not found — skipping")
592
+ log(t("version_manager.warn_flutter_missing", target=target))
561
593
  return
562
594
  text = read_file(target)
563
595
  full_version = f"{new_version}+{version_code}"
@@ -566,8 +598,7 @@ def sync_flutter(path_dir, new_version, version_code):
566
598
  new_text = pattern.sub(r'\1 ' + full_version, text, count=1)
567
599
  else:
568
600
  new_text = text.rstrip("\n") + f"\nversion: {full_version}\n"
569
- write_file(target, new_text)
570
- log(f"updated: {target}")
601
+ write_synced(target, new_text)
571
602
 
572
603
 
573
604
  def _json_indent(text):
@@ -582,20 +613,19 @@ def _json_indent(text):
582
613
 
583
614
  def sync_json_version(target, new_version, key_path):
584
615
  if not target.is_file():
585
- log(f"WARNING: {target} not found — skipping")
616
+ log(t("version_manager.warn_target_missing", target=target))
586
617
  return
587
618
  raw = read_file(target)
588
619
  try:
589
620
  data = json.loads(raw)
590
621
  except json.JSONDecodeError as e:
591
622
  # Skipping and reporting success would silently leave the tag/version.yml and the package version out of step.
592
- raise VersionSyncError(f"{target} is not valid JSON ({e}) — cannot write version")
623
+ raise VersionSyncError(t("version_manager.err_not_json", target=target, error=e))
593
624
  node = data
594
625
  for k in key_path[:-1]:
595
626
  node = node.setdefault(k, {})
596
627
  node[key_path[-1]] = new_version
597
- write_file(target, json.dumps(data, indent=_json_indent(raw), ensure_ascii=False) + "\n")
598
- log(f"updated: {target}")
628
+ write_synced(target, json.dumps(data, indent=_json_indent(raw), ensure_ascii=False) + "\n")
599
629
 
600
630
 
601
631
  _TOML_HEADER_RE = re.compile(r'^[ \t]*\[+[ \t]*([^\]\n]+?)[ \t]*\]+[ \t]*(?:#.*)?$', re.MULTILINE)
@@ -641,12 +671,11 @@ def sync_python(path_dir, new_version):
641
671
  spans = _python_version_spans(path_dir)
642
672
  if not spans:
643
673
  # Setups that keep the version out of the file (e.g. dynamic = ["version"]) have nowhere to write it.
644
- log(f"WARNING: python: no version in pyproject.toml [project]/[tool.poetry] or setup.py under {path_dir} — skipping")
674
+ log(t("version_manager.warn_python_no_version", dir=path_dir))
645
675
  return
646
676
  for target, (start, end) in spans:
647
677
  text = read_file(target)
648
- write_file(target, text[:start] + new_version + text[end:])
649
- log(f"updated: {target}")
678
+ write_synced(target, text[:start] + new_version + text[end:])
650
679
 
651
680
 
652
681
  _PLIST_VERSION_RE = re.compile(r'(<key>CFBundleShortVersionString</key>\s*<string>)([^<]*)(</string>)')
@@ -673,33 +702,31 @@ def sync_react_native(path_dir, new_version):
673
702
  continue
674
703
  # A build-variable reference such as $(MARKETING_VERSION) is owned by the Xcode settings, so do not overwrite it.
675
704
  if m.group(2).strip().startswith("$("):
676
- log(f"skipped: {plist_file} — CFBundleShortVersionString references a build variable")
705
+ log(t("version_manager.skipped_plist", file=plist_file))
677
706
  continue
678
707
  new_text = _PLIST_VERSION_RE.sub(lambda mm: mm.group(1) + new_version + mm.group(3), text)
679
- write_file(plist_file, new_text)
680
- log(f"updated: {plist_file}")
708
+ write_synced(plist_file, new_text)
681
709
  found_plist = True
682
710
  else:
683
- log(f"WARNING: react-native: {ios_dir} not found — skipping")
711
+ log(t("version_manager.warn_rn_ios_missing", dir=ios_dir))
684
712
 
685
713
  gradle_file = Path(path_dir) / "android" / "app" / "build.gradle"
686
714
  if gradle_file.is_file():
687
715
  text = read_file(gradle_file)
688
716
  new_text = re.sub(r'versionName\s+"[^"]*"', f'versionName "{new_version}"', text)
689
- write_file(gradle_file, new_text)
690
- log(f"updated: {gradle_file}")
717
+ write_synced(gradle_file, new_text)
691
718
  else:
692
- log(f"WARNING: react-native: {gradle_file} not found — skipping")
719
+ log(t("version_manager.warn_rn_gradle_missing", file=gradle_file))
693
720
 
694
721
  if not found_plist and not gradle_file.is_file():
695
- log(f"WARNING: react-native: no target files found under {path_dir}")
722
+ log(t("version_manager.warn_rn_no_targets", dir=path_dir))
696
723
 
697
724
 
698
725
  def sync_for_type(project_type, new_version, version_code_getter):
699
726
  path_dir = get_type_path(project_type)
700
727
  handler = TYPE_HANDLERS.get(project_type)
701
728
  if handler is None:
702
- log(f"WARNING: unknown project type: {project_type} — skipping")
729
+ log(t("version_manager.warn_unknown_type", type=project_type))
703
730
  return
704
731
  handler.sync(path_dir, new_version, version_code_getter)
705
732
 
@@ -709,17 +736,17 @@ def sync_all_project_files(new_version):
709
736
  if not types:
710
737
  # No silent fallback: an unreadable project_types used to degrade to
711
738
  # "basic" and skip every sync without a word.
712
- raise SystemExit("ERROR: version.yml has no readable project_types — cannot sync project files")
739
+ raise SystemExit(t("version_manager.err_no_types_sync"))
713
740
  errors = []
714
- for t in types:
741
+ for ptype in types:
715
742
  # Keep syncing the other types when one fails; collect failures and report them via the exit code at the end.
716
743
  try:
717
- sync_for_type(t, new_version, get_reconciled_version_code)
744
+ sync_for_type(ptype, new_version, get_reconciled_version_code)
718
745
  except VersionSyncError as e:
719
- log(f"ERROR: {t}: {e}")
720
- errors.append(t)
746
+ log(t("version_manager.err_type", type=ptype, error=e))
747
+ errors.append(ptype)
721
748
  if errors:
722
- raise VersionSyncError(f"project file sync failed for: {', '.join(errors)}")
749
+ raise VersionSyncError(t("version_manager.err_sync_failed", types=", ".join(errors)))
723
750
 
724
751
 
725
752
  README_VERSION_LINE_RE = re.compile(
@@ -749,7 +776,7 @@ def update_readme_version(new_version, path="README.md"):
749
776
  return False
750
777
  lines[i + 1] = new_line
751
778
  write_file(p, "\n".join(lines))
752
- log(f"README.md version line -> v{new_version}")
779
+ log(t("version_manager.readme_updated", version=new_version))
753
780
  return True
754
781
  return False
755
782
 
@@ -859,7 +886,6 @@ TYPE_HANDLERS = {
859
886
  # Only types that need a build number compute version_code (this has a pubspec-adjusting side effect).
860
887
  "flutter": TypeHandler(read=_read_flutter, sync=lambda d, v, code: sync_flutter(d, v, code())),
861
888
  "react": _PACKAGE_JSON,
862
- "next": _PACKAGE_JSON,
863
889
  "node": _PACKAGE_JSON,
864
890
  "python": TypeHandler(read=_read_python, sync=lambda d, v, _code: sync_python(d, v)),
865
891
  "react-native": TypeHandler(read=_read_react_native, sync=lambda d, v, _code: sync_react_native(d, v)),
@@ -880,7 +906,7 @@ def get_project_file_version(project_type):
880
906
  if handler is not None:
881
907
  version = handler.read(path_dir)
882
908
  except Exception as e:
883
- log(f"WARNING: failed reading project file for {project_type}: {e}")
909
+ log(t("version_manager.warn_read_failed", type=project_type, error=e))
884
910
  version = None
885
911
 
886
912
  if not version:
@@ -892,32 +918,35 @@ def sync_versions():
892
918
  yml_version = get_current_version()
893
919
  types = get_project_types_csv()
894
920
  if not types:
895
- raise SystemExit("ERROR: version.yml has no readable project_types — cannot sync versions")
921
+ raise SystemExit(t("version_manager.err_no_types_check"))
896
922
  primary_type = types[0]
897
923
  project_version = get_project_file_version(primary_type)
898
924
 
899
- log("Version sync check")
900
- log(f" version.yml: {yml_version}")
901
- log(f" project file: {project_version}")
902
-
903
925
  if yml_version != project_version:
926
+ # The detail lines are only useful when the two disagree; an in-sync check prints a single line
927
+ # (this check runs several times per job, so a multi-line block each time is just noise).
928
+ log(t("version_manager.sync_check"))
929
+ log(t("version_manager.sync_yml_version", version=yml_version))
930
+ log(t("version_manager.sync_project_version", version=project_version))
904
931
  if validate_version(yml_version) and validate_version(project_version):
905
932
  higher = get_higher_version(yml_version, project_version)
906
- log(f"Version mismatch detected, syncing to higher version: {higher}")
933
+ log(t("version_manager.sync_mismatch", version=higher))
907
934
  if higher != yml_version:
908
935
  update_version_yml(higher)
909
936
  if higher != project_version:
910
937
  sync_all_project_files(higher)
911
938
  return higher
912
939
  else:
913
- log("WARNING: version format invalid, cannot sync")
940
+ log(t("version_manager.warn_format_invalid"))
914
941
  return yml_version
915
942
  else:
916
- types = get_project_types_csv()
917
- if types:
918
- log(f"Multi-type — reconciling all type files to version.yml version: {yml_version}")
919
- sync_all_project_files(yml_version)
920
- log(f"Version already in sync: {yml_version}")
943
+ # The primary type matches, but the other types' files may still lag behind; reconcile them quietly.
944
+ changed_before = len(_CHANGED_FILES)
945
+ sync_all_project_files(yml_version)
946
+ if len(_CHANGED_FILES) > changed_before:
947
+ log(t("version_manager.sync_reconciled", version=yml_version))
948
+ else:
949
+ log(t("version_manager.sync_ok", version=yml_version))
921
950
  return yml_version
922
951
 
923
952
 
@@ -952,7 +981,7 @@ def cmd_increment(args):
952
981
  require_version_yml()
953
982
  current_version = sync_versions()
954
983
  if not validate_version(current_version):
955
- log(f"ERROR: invalid version format: {current_version}")
984
+ log(t("version_manager.err_invalid_version", version=current_version))
956
985
  return 1
957
986
  bump = getattr(args, "bump", None) or "patch"
958
987
  new_version = increment_version(current_version, bump)
@@ -968,7 +997,7 @@ def cmd_set(args):
968
997
  require_version_yml()
969
998
  new_version = args.version
970
999
  if not validate_version(new_version):
971
- log(f"ERROR: invalid version format: {new_version} (must be x.y.z)")
1000
+ log(t("version_manager.err_invalid_version_xyz", version=new_version))
972
1001
  return 1
973
1002
  update_all_versions(new_version)
974
1003
  print(new_version)
@@ -1017,7 +1046,7 @@ def main(argv=None):
1017
1046
  try:
1018
1047
  return handler(args)
1019
1048
  except VersionSyncError as e:
1020
- log(f"ERROR: {e}")
1049
+ log(t("version_manager.err_generic", error=e))
1021
1050
  return 1
1022
1051
 
1023
1052
 
@@ -10,7 +10,7 @@
10
10
  # workflows call GitHub Copilot CLI, which consumes GitHub Copilot AI Credits.
11
11
  # Falls back to rule-based summaries whenever Copilot is off or unavailable.
12
12
  # ===
13
- # Supported project types: spring, flutter, next, react, react-native,
13
+ # Supported project types: spring, flutter, react, react-native,
14
14
  # react-native-expo, node, python, basic, go
15
15
  # Multi-type projects: list every type in the project_types array
16
16
  # -> the first entry is the primary type (project_types is the single source of truth)
@@ -20,7 +20,7 @@
20
20
  # Synced files per type:
21
21
  # - spring: build.gradle / build.gradle.kts / pom.xml
22
22
  # - flutter: pubspec.yaml
23
- # - next/react/node: package.json
23
+ # - react/node: package.json
24
24
  # - react-native: Info.plist + build.gradle
25
25
  # - react-native-expo: app.json
26
26
  # - python: pyproject.toml / setup.py
@@ -53,11 +53,13 @@ jobs:
53
53
  - name: Read copilot_ai option from version.yml
54
54
  id: copilot_options
55
55
  run: |
56
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
57
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
56
58
  # scoped to the metadata: -> template: -> options: chain so an
57
59
  # unrelated "copilot_ai:" key can never hijack the value
58
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")
59
61
  echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
60
- echo "copilot_ai option: $COPILOT_AI"
62
+ echo "$(m wf_common.copilot_ai_option copilot_ai="$COPILOT_AI")"
61
63
 
62
64
  - name: Install Copilot CLI
63
65
  if: steps.copilot_options.outputs.copilot_ai == 'true'
@@ -74,6 +76,8 @@ jobs:
74
76
  COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
75
77
  GH_TOKEN: ${{ github.token }}
76
78
  run: |
79
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
80
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
77
81
  PR_NUMBER=${{ github.event.pull_request.number }}
78
82
 
79
83
  git fetch origin {{MAIN_BRANCH}}
@@ -105,7 +109,7 @@ jobs:
105
109
  fi
106
110
  VERSION=$(python3 -c 'import sys; sys.path.insert(0, ".github/scripts"); from version_manager import increment_version; print(increment_version(sys.argv[1], sys.argv[2]))' "$CURRENT_VERSION" "${BUMP:-patch}" 2>/dev/null || echo "")
107
111
  VERSION=${VERSION:-Unreleased}
108
- echo "expected next version: $VERSION (current: ${CURRENT_VERSION:-none}, mode: $MODE, bump: ${BUMP:-patch})"
112
+ echo "$(m wf_aisum.expected_version version="$VERSION" current_version="${CURRENT_VERSION:-none}" mode="$MODE" bump="${BUMP:-patch}")"
109
113
 
110
114
  SUMMARY_RESULT=$(python3 .github/scripts/changelog_manager.py ai-summary \
111
115
  --commits-file commits.txt \
@@ -136,10 +140,10 @@ jobs:
136
140
  --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$MARKER\"))) | .id" 2>/dev/null | tail -n 1 || true)
137
141
  if [ -n "$COMMENT_ID" ]; then
138
142
  gh api -X PATCH "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" --input comment_payload.json > /dev/null \
139
- || echo "comment update failed — continuing (this workflow never blocks the PR)"
143
+ || echo "$(m wf_aisum.comment_update_failed)"
140
144
  else
141
145
  gh api -X POST "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" --input comment_payload.json > /dev/null \
142
- || echo "comment post failed — continuing (this workflow never blocks the PR)"
146
+ || echo "$(m wf_aisum.comment_post_failed)"
143
147
  fi
144
148
 
145
149
  - name: Link related issue via branch name
@@ -152,6 +156,7 @@ jobs:
152
156
  # AUTO-CHANGELOG-CONTROL).
153
157
  HEAD_REF: ${{ github.event.pull_request.head.ref }}
154
158
  run: |
159
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
155
160
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
156
161
  ISSUE_NUM=$(python3 .github/scripts/issue_helper.py extract-branch-issue "$HEAD_REF")
157
162