project-auto-wizard 0.13.3 → 0.15.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.
- package/README.ja.md +2 -0
- package/README.ko.md +2 -0
- package/README.md +3 -1
- package/README.zh-CN.md +2 -0
- package/bin/project-auto-wizard.js +18 -4
- package/package.json +2 -2
- package/payload/config/breaking-changes.json +28 -14
- package/payload/config/wizard-prompts.yml +102 -46
- package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
- package/payload/flutter-app/ios/ExportOptions.plist +8 -8
- package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
- package/payload/scripts/changelog_manager.py +304 -265
- package/payload/scripts/issue_helper.py +54 -50
- package/payload/scripts/messages.py +2585 -0
- package/payload/scripts/truncate_release_notes.py +7 -6
- package/payload/scripts/version_manager.py +110 -107
- package/payload/version.yml.template +2 -1
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +23 -16
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +106 -63
- package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +55 -45
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +90 -72
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +59 -45
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
- package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
- package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
- package/src/cli/args.js +98 -52
- package/src/cli/help.js +8 -41
- package/src/commands/doctor.js +90 -89
- package/src/commands/dry-run.js +64 -43
- package/src/commands/full.js +79 -77
- package/src/commands/install-settings.js +16 -15
- package/src/commands/interactive-flutter.js +20 -19
- package/src/commands/interactive.js +135 -132
- package/src/commands/purge.js +47 -42
- package/src/commands/status.js +36 -36
- package/src/commands/uninstall.js +43 -41
- package/src/context.js +12 -11
- package/src/core/assets.js +12 -11
- package/src/core/baseline.js +27 -27
- package/src/core/branches.js +29 -27
- package/src/core/branding.js +7 -6
- package/src/core/breaking-check.js +22 -21
- package/src/core/breaking.js +15 -8
- package/src/core/copy/app-files.js +5 -4
- package/src/core/copy/flutter-app.js +8 -8
- package/src/core/copy/gitignore.js +58 -50
- package/src/core/copy/readme.js +102 -36
- package/src/core/copy/simple.js +16 -15
- package/src/core/copy/workflows.js +131 -124
- package/src/core/deploy-style.js +51 -48
- package/src/core/detect-fs.js +57 -57
- package/src/core/detect.js +67 -65
- package/src/core/errors.js +2 -2
- package/src/core/flutter-doctor.js +17 -16
- package/src/core/flutter-hooks.js +33 -32
- package/src/core/flutter-options.js +34 -33
- package/src/core/fsutil.js +12 -12
- package/src/core/installed-stores.js +4 -4
- package/src/core/logger.js +42 -40
- package/src/core/paths-resolve.js +88 -86
- package/src/core/paths.js +10 -10
- package/src/core/release-options.js +8 -8
- package/src/core/removal-exec.js +14 -13
- package/src/core/removal-plan.js +37 -37
- package/src/core/types.js +47 -47
- package/src/core/verify.js +44 -38
- package/src/core/version-yml.js +76 -68
- package/src/core/wizard-env.js +46 -46
- package/src/core/wizard-labels.js +63 -41
- package/src/i18n/catalog/en/cli.js +50 -0
- package/src/i18n/catalog/en/commands.js +188 -0
- package/src/i18n/catalog/en/copy.js +20 -0
- package/src/i18n/catalog/en/core.js +74 -0
- package/src/i18n/catalog/en/core2.js +15 -0
- package/src/i18n/catalog/en/ui.js +221 -0
- package/src/i18n/catalog/en.js +12 -0
- package/src/i18n/catalog/index.js +5 -0
- package/src/i18n/catalog/ko/cli.js +50 -0
- package/src/i18n/catalog/ko/commands.js +188 -0
- package/src/i18n/catalog/ko/copy.js +20 -0
- package/src/i18n/catalog/ko/core.js +74 -0
- package/src/i18n/catalog/ko/core2.js +15 -0
- package/src/i18n/catalog/ko/ui.js +221 -0
- package/src/i18n/catalog/ko.js +10 -0
- package/src/i18n/index.js +44 -0
- package/src/i18n/languages.js +9 -0
- package/src/index.js +132 -97
- package/src/ui/ansi.js +7 -7
- package/src/ui/banner.js +7 -6
- package/src/ui/env-plan.js +82 -81
- package/src/ui/prompts.js +97 -122
- package/src/ui/readline-engine.js +59 -58
- package/src/ui/status-cards.js +37 -31
- package/src/ui/summary.js +90 -89
|
@@ -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
|
|
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
|
-
#
|
|
49
|
-
# version.yml
|
|
50
|
-
#
|
|
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
|
|
54
|
-
#
|
|
55
|
-
# rc.1
|
|
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]
|
|
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
|
|
|
@@ -115,7 +117,7 @@ def _version_yml_path():
|
|
|
115
117
|
|
|
116
118
|
def require_version_yml():
|
|
117
119
|
if not _version_yml_path().is_file():
|
|
118
|
-
log("
|
|
120
|
+
log(t("version_manager.err_yml_missing"))
|
|
119
121
|
sys.exit(1)
|
|
120
122
|
|
|
121
123
|
|
|
@@ -236,7 +238,7 @@ def get_version_code():
|
|
|
236
238
|
require_version_yml()
|
|
237
239
|
code = read_scalar_key("version_code", None)
|
|
238
240
|
if code is None or code == "" or code == "null":
|
|
239
|
-
log("
|
|
241
|
+
log(t("version_manager.warn_code_missing"))
|
|
240
242
|
text = read_text()
|
|
241
243
|
if re.search(r'^version:', text, re.MULTILINE):
|
|
242
244
|
new_text = re.sub(
|
|
@@ -258,7 +260,7 @@ def set_version_code(new_code):
|
|
|
258
260
|
if current not in (None, "", "null"):
|
|
259
261
|
try:
|
|
260
262
|
if int(new_code) < int(current):
|
|
261
|
-
log(
|
|
263
|
+
log(t("version_manager.warn_code_regress", new_code=new_code, current=current))
|
|
262
264
|
except ValueError:
|
|
263
265
|
pass
|
|
264
266
|
text = read_text()
|
|
@@ -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'.
|
|
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,8 +317,8 @@ 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
|
-
#
|
|
319
|
-
log("
|
|
320
|
+
# Without the key nothing was written yet the call looked successful, so the same version came out every time.
|
|
321
|
+
log(t("version_manager.warn_version_missing"))
|
|
320
322
|
text = read_text()
|
|
321
323
|
line = f'version: "{new_version}"\n'
|
|
322
324
|
if re.search(r'^version_code:', text, re.MULTILINE):
|
|
@@ -325,7 +327,7 @@ def update_version_yml(new_version):
|
|
|
325
327
|
new_text = text.rstrip("\n") + "\n" + line
|
|
326
328
|
write_text(new_text)
|
|
327
329
|
if read_scalar_key("version") != new_version:
|
|
328
|
-
raise VersionSyncError(
|
|
330
|
+
raise VersionSyncError(t("version_manager.err_write_failed", version=new_version))
|
|
329
331
|
today = datetime.date.today().isoformat()
|
|
330
332
|
user = os.environ.get("GITHUB_ACTOR", "")
|
|
331
333
|
if not user:
|
|
@@ -365,16 +367,16 @@ _XML_TOKEN_RE = re.compile(
|
|
|
365
367
|
|
|
366
368
|
|
|
367
369
|
def _pom_text_span(text, path):
|
|
368
|
-
"""
|
|
369
|
-
<parent
|
|
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 #
|
|
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,24 +403,24 @@ def _pom_replace(text, path, value):
|
|
|
401
403
|
|
|
402
404
|
|
|
403
405
|
def sync_maven(path_dir, new_version):
|
|
404
|
-
"""
|
|
405
|
-
<parent><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
|
|
409
411
|
text = read_file(root_pom)
|
|
410
412
|
old_version = _pom_text(text, ["version"])
|
|
411
413
|
if old_version is None:
|
|
412
|
-
log(
|
|
414
|
+
log(t("version_manager.warn_spring_no_version", pom=root_pom))
|
|
413
415
|
return True
|
|
414
416
|
if "${" in old_version:
|
|
415
|
-
# ${revision}
|
|
416
|
-
log(
|
|
417
|
+
# CI-friendly versions such as ${revision} are managed in a property, so leave them alone.
|
|
418
|
+
log(t("version_manager.warn_spring_property", pom=root_pom, version=old_version))
|
|
417
419
|
return True
|
|
418
420
|
new_full = _keep_snapshot(old_version, new_version)
|
|
419
421
|
new_text, _ = _pom_replace(text, ["version"], new_full)
|
|
420
422
|
write_file(root_pom, new_text)
|
|
421
|
-
log(
|
|
423
|
+
log(t("version_manager.updated", path=root_pom))
|
|
422
424
|
|
|
423
425
|
root_artifact = _pom_text(text, ["artifactId"])
|
|
424
426
|
for child in sorted(Path(path_dir).glob("*/pom.xml")):
|
|
@@ -431,7 +433,7 @@ def sync_maven(path_dir, new_version):
|
|
|
431
433
|
if _pom_text(ctext, ["version"]) == old_version:
|
|
432
434
|
ctext, _ = _pom_replace(ctext, ["version"], new_full)
|
|
433
435
|
write_file(child, ctext)
|
|
434
|
-
log(
|
|
436
|
+
log(t("version_manager.updated", path=child))
|
|
435
437
|
return True
|
|
436
438
|
|
|
437
439
|
|
|
@@ -440,12 +442,12 @@ _GRADLE_SHARED_BLOCKS = ("allprojects", "subprojects")
|
|
|
440
442
|
|
|
441
443
|
|
|
442
444
|
def _gradle_code(line, quote=""):
|
|
443
|
-
"""
|
|
444
|
-
|
|
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
|
-
"""
|
|
473
|
+
"""Matches of the project version lines. Reading and syncing share this one place so they touch the same lines.
|
|
472
474
|
|
|
473
|
-
|
|
474
|
-
|
|
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)
|
|
@@ -507,27 +509,27 @@ def sync_spring(path_dir, new_version):
|
|
|
507
509
|
has_pom = sync_maven(path_dir, new_version)
|
|
508
510
|
if not candidates:
|
|
509
511
|
if not has_pom:
|
|
510
|
-
log(
|
|
512
|
+
log(t("version_manager.warn_spring_no_files", dir=path_dir))
|
|
511
513
|
return
|
|
512
514
|
for gradle_file in candidates:
|
|
513
515
|
text = read_file(gradle_file)
|
|
514
|
-
#
|
|
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
|
-
#
|
|
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():]
|
|
522
524
|
if not matches:
|
|
523
|
-
log(
|
|
525
|
+
log(t("version_manager.warn_spring_no_gradle_line", file=gradle_file))
|
|
524
526
|
continue
|
|
525
527
|
write_file(gradle_file, new_text)
|
|
526
|
-
log(
|
|
528
|
+
log(t("version_manager.updated", path=gradle_file))
|
|
527
529
|
|
|
528
530
|
|
|
529
531
|
def _pubspec_build_number(path_dir):
|
|
530
|
-
"""pubspec.yaml `version: x.y.z+N
|
|
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
|
|
540
|
-
|
|
541
|
-
|
|
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 = [
|
|
@@ -546,7 +548,7 @@ def get_reconciled_version_code():
|
|
|
546
548
|
if n is not None
|
|
547
549
|
]
|
|
548
550
|
if pubspec_codes and max(pubspec_codes) > code:
|
|
549
|
-
log(
|
|
551
|
+
log(t("version_manager.pubspec_ahead", build=max(pubspec_codes), code=code))
|
|
550
552
|
code = max(pubspec_codes)
|
|
551
553
|
set_version_code(code)
|
|
552
554
|
return code
|
|
@@ -555,7 +557,7 @@ def get_reconciled_version_code():
|
|
|
555
557
|
def sync_flutter(path_dir, new_version, version_code):
|
|
556
558
|
target = Path(path_dir) / "pubspec.yaml"
|
|
557
559
|
if not target.is_file():
|
|
558
|
-
log(
|
|
560
|
+
log(t("version_manager.warn_flutter_missing", target=target))
|
|
559
561
|
return
|
|
560
562
|
text = read_file(target)
|
|
561
563
|
full_version = f"{new_version}+{version_code}"
|
|
@@ -565,7 +567,7 @@ def sync_flutter(path_dir, new_version, version_code):
|
|
|
565
567
|
else:
|
|
566
568
|
new_text = text.rstrip("\n") + f"\nversion: {full_version}\n"
|
|
567
569
|
write_file(target, new_text)
|
|
568
|
-
log(
|
|
570
|
+
log(t("version_manager.updated", path=target))
|
|
569
571
|
|
|
570
572
|
|
|
571
573
|
def _json_indent(text):
|
|
@@ -580,20 +582,20 @@ def _json_indent(text):
|
|
|
580
582
|
|
|
581
583
|
def sync_json_version(target, new_version, key_path):
|
|
582
584
|
if not target.is_file():
|
|
583
|
-
log(
|
|
585
|
+
log(t("version_manager.warn_target_missing", target=target))
|
|
584
586
|
return
|
|
585
587
|
raw = read_file(target)
|
|
586
588
|
try:
|
|
587
589
|
data = json.loads(raw)
|
|
588
590
|
except json.JSONDecodeError as e:
|
|
589
|
-
#
|
|
590
|
-
raise VersionSyncError(
|
|
591
|
+
# Skipping and reporting success would silently leave the tag/version.yml and the package version out of step.
|
|
592
|
+
raise VersionSyncError(t("version_manager.err_not_json", target=target, error=e))
|
|
591
593
|
node = data
|
|
592
594
|
for k in key_path[:-1]:
|
|
593
595
|
node = node.setdefault(k, {})
|
|
594
596
|
node[key_path[-1]] = new_version
|
|
595
597
|
write_file(target, json.dumps(data, indent=_json_indent(raw), ensure_ascii=False) + "\n")
|
|
596
|
-
log(
|
|
598
|
+
log(t("version_manager.updated", path=target))
|
|
597
599
|
|
|
598
600
|
|
|
599
601
|
_TOML_HEADER_RE = re.compile(r'^[ \t]*\[+[ \t]*([^\]\n]+?)[ \t]*\]+[ \t]*(?:#.*)?$', re.MULTILINE)
|
|
@@ -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]
|
|
605
|
-
|
|
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
|
|
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
|
-
"""
|
|
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,21 +640,21 @@ 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"]
|
|
642
|
-
log(
|
|
643
|
+
# Setups that keep the version out of the file (e.g. dynamic = ["version"]) have nowhere to write it.
|
|
644
|
+
log(t("version_manager.warn_python_no_version", dir=path_dir))
|
|
643
645
|
return
|
|
644
646
|
for target, (start, end) in spans:
|
|
645
647
|
text = read_file(target)
|
|
646
648
|
write_file(target, text[:start] + new_version + text[end:])
|
|
647
|
-
log(
|
|
649
|
+
log(t("version_manager.updated", path=target))
|
|
648
650
|
|
|
649
651
|
|
|
650
652
|
_PLIST_VERSION_RE = re.compile(r'(<key>CFBundleShortVersionString</key>\s*<string>)([^<]*)(</string>)')
|
|
651
653
|
|
|
652
654
|
|
|
653
655
|
def _rn_app_plists(ios_dir):
|
|
654
|
-
"""
|
|
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,35 +671,35 @@ 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)
|
|
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
|
-
log(
|
|
676
|
+
log(t("version_manager.skipped_plist", file=plist_file))
|
|
675
677
|
continue
|
|
676
678
|
new_text = _PLIST_VERSION_RE.sub(lambda mm: mm.group(1) + new_version + mm.group(3), text)
|
|
677
679
|
write_file(plist_file, new_text)
|
|
678
|
-
log(
|
|
680
|
+
log(t("version_manager.updated", path=plist_file))
|
|
679
681
|
found_plist = True
|
|
680
682
|
else:
|
|
681
|
-
log(
|
|
683
|
+
log(t("version_manager.warn_rn_ios_missing", dir=ios_dir))
|
|
682
684
|
|
|
683
685
|
gradle_file = Path(path_dir) / "android" / "app" / "build.gradle"
|
|
684
686
|
if gradle_file.is_file():
|
|
685
687
|
text = read_file(gradle_file)
|
|
686
688
|
new_text = re.sub(r'versionName\s+"[^"]*"', f'versionName "{new_version}"', text)
|
|
687
689
|
write_file(gradle_file, new_text)
|
|
688
|
-
log(
|
|
690
|
+
log(t("version_manager.updated", path=gradle_file))
|
|
689
691
|
else:
|
|
690
|
-
log(
|
|
692
|
+
log(t("version_manager.warn_rn_gradle_missing", file=gradle_file))
|
|
691
693
|
|
|
692
694
|
if not found_plist and not gradle_file.is_file():
|
|
693
|
-
log(
|
|
695
|
+
log(t("version_manager.warn_rn_no_targets", dir=path_dir))
|
|
694
696
|
|
|
695
697
|
|
|
696
698
|
def sync_for_type(project_type, new_version, version_code_getter):
|
|
697
699
|
path_dir = get_type_path(project_type)
|
|
698
700
|
handler = TYPE_HANDLERS.get(project_type)
|
|
699
701
|
if handler is None:
|
|
700
|
-
log(
|
|
702
|
+
log(t("version_manager.warn_unknown_type", type=project_type))
|
|
701
703
|
return
|
|
702
704
|
handler.sync(path_dir, new_version, version_code_getter)
|
|
703
705
|
|
|
@@ -707,17 +709,17 @@ def sync_all_project_files(new_version):
|
|
|
707
709
|
if not types:
|
|
708
710
|
# No silent fallback: an unreadable project_types used to degrade to
|
|
709
711
|
# "basic" and skip every sync without a word.
|
|
710
|
-
raise SystemExit("
|
|
712
|
+
raise SystemExit(t("version_manager.err_no_types_sync"))
|
|
711
713
|
errors = []
|
|
712
|
-
for
|
|
713
|
-
#
|
|
714
|
+
for ptype in types:
|
|
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
|
-
sync_for_type(
|
|
717
|
+
sync_for_type(ptype, new_version, get_reconciled_version_code)
|
|
716
718
|
except VersionSyncError as e:
|
|
717
|
-
log(
|
|
718
|
-
errors.append(
|
|
719
|
+
log(t("version_manager.err_type", type=ptype, error=e))
|
|
720
|
+
errors.append(ptype)
|
|
719
721
|
if errors:
|
|
720
|
-
raise VersionSyncError(
|
|
722
|
+
raise VersionSyncError(t("version_manager.err_sync_failed", types=", ".join(errors)))
|
|
721
723
|
|
|
722
724
|
|
|
723
725
|
README_VERSION_LINE_RE = re.compile(
|
|
@@ -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
|
-
"""
|
|
730
|
+
"""Rewrite only the version line right below the AUTO-VERSION-SECTION marker.
|
|
729
731
|
|
|
730
|
-
README
|
|
731
|
-
|
|
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
|
|
@@ -747,7 +749,7 @@ def update_readme_version(new_version, path="README.md"):
|
|
|
747
749
|
return False
|
|
748
750
|
lines[i + 1] = new_line
|
|
749
751
|
write_file(p, "\n".join(lines))
|
|
750
|
-
log(
|
|
752
|
+
log(t("version_manager.readme_updated", version=new_version))
|
|
751
753
|
return True
|
|
752
754
|
return False
|
|
753
755
|
|
|
@@ -763,7 +765,7 @@ def update_all_versions(new_version):
|
|
|
763
765
|
# ===================================================================
|
|
764
766
|
|
|
765
767
|
def _read_spring(path_dir):
|
|
766
|
-
# build.gradle
|
|
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
|
-
"""
|
|
800
|
-
package.json version
|
|
801
|
-
$(MARKETING_VERSION)
|
|
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
|
-
"""
|
|
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
|
-
#
|
|
854
|
-
#
|
|
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
|
|
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,
|
|
@@ -878,7 +880,7 @@ def get_project_file_version(project_type):
|
|
|
878
880
|
if handler is not None:
|
|
879
881
|
version = handler.read(path_dir)
|
|
880
882
|
except Exception as e:
|
|
881
|
-
log(
|
|
883
|
+
log(t("version_manager.warn_read_failed", type=project_type, error=e))
|
|
882
884
|
version = None
|
|
883
885
|
|
|
884
886
|
if not version:
|
|
@@ -890,32 +892,32 @@ def sync_versions():
|
|
|
890
892
|
yml_version = get_current_version()
|
|
891
893
|
types = get_project_types_csv()
|
|
892
894
|
if not types:
|
|
893
|
-
raise SystemExit("
|
|
895
|
+
raise SystemExit(t("version_manager.err_no_types_check"))
|
|
894
896
|
primary_type = types[0]
|
|
895
897
|
project_version = get_project_file_version(primary_type)
|
|
896
898
|
|
|
897
|
-
log("
|
|
898
|
-
log(
|
|
899
|
-
log(
|
|
899
|
+
log(t("version_manager.sync_check"))
|
|
900
|
+
log(t("version_manager.sync_yml_version", version=yml_version))
|
|
901
|
+
log(t("version_manager.sync_project_version", version=project_version))
|
|
900
902
|
|
|
901
903
|
if yml_version != project_version:
|
|
902
904
|
if validate_version(yml_version) and validate_version(project_version):
|
|
903
905
|
higher = get_higher_version(yml_version, project_version)
|
|
904
|
-
log(
|
|
906
|
+
log(t("version_manager.sync_mismatch", version=higher))
|
|
905
907
|
if higher != yml_version:
|
|
906
908
|
update_version_yml(higher)
|
|
907
909
|
if higher != project_version:
|
|
908
910
|
sync_all_project_files(higher)
|
|
909
911
|
return higher
|
|
910
912
|
else:
|
|
911
|
-
log("
|
|
913
|
+
log(t("version_manager.warn_format_invalid"))
|
|
912
914
|
return yml_version
|
|
913
915
|
else:
|
|
914
916
|
types = get_project_types_csv()
|
|
915
917
|
if types:
|
|
916
|
-
log(
|
|
918
|
+
log(t("version_manager.sync_multi", version=yml_version))
|
|
917
919
|
sync_all_project_files(yml_version)
|
|
918
|
-
log(
|
|
920
|
+
log(t("version_manager.sync_ok", version=yml_version))
|
|
919
921
|
return yml_version
|
|
920
922
|
|
|
921
923
|
|
|
@@ -950,11 +952,11 @@ def cmd_increment(args):
|
|
|
950
952
|
require_version_yml()
|
|
951
953
|
current_version = sync_versions()
|
|
952
954
|
if not validate_version(current_version):
|
|
953
|
-
log(
|
|
955
|
+
log(t("version_manager.err_invalid_version", version=current_version))
|
|
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
|
|
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
|
|
|
@@ -966,7 +968,7 @@ def cmd_set(args):
|
|
|
966
968
|
require_version_yml()
|
|
967
969
|
new_version = args.version
|
|
968
970
|
if not validate_version(new_version):
|
|
969
|
-
log(
|
|
971
|
+
log(t("version_manager.err_invalid_version_xyz", version=new_version))
|
|
970
972
|
return 1
|
|
971
973
|
update_all_versions(new_version)
|
|
972
974
|
print(new_version)
|
|
@@ -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=
|
|
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
|
|
|
@@ -1014,7 +1017,7 @@ def main(argv=None):
|
|
|
1014
1017
|
try:
|
|
1015
1018
|
return handler(args)
|
|
1016
1019
|
except VersionSyncError as e:
|
|
1017
|
-
log(
|
|
1020
|
+
log(t("version_manager.err_generic", error=e))
|
|
1018
1021
|
return 1
|
|
1019
1022
|
|
|
1020
1023
|
|
|
@@ -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}}
|