project-auto-wizard 0.12.2 → 0.13.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.
- package/README.ja.md +165 -0
- package/README.ko.md +165 -0
- package/README.md +107 -286
- package/README.zh-CN.md +165 -0
- package/bin/project-auto-wizard.js +1 -2
- package/package.json +9 -3
- package/payload/config/breaking-changes.json +24 -0
- package/payload/config/wizard-prompts.yml +45 -0
- package/payload/scripts/changelog_manager.py +141 -46
- package/payload/scripts/issue_helper.py +34 -5
- package/payload/scripts/version_manager.py +482 -107
- package/payload/version.yml.template +5 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +39 -9
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +165 -38
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +14 -5
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +220 -6
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +89 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +78 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +72 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +62 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +51 -13
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +75 -11
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +52 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +60 -11
- package/payload/workflows/go/PROJECT-GO-CI.yaml +10 -1
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +25 -3
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +83 -9
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +70 -6
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +25 -3
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +84 -10
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +13 -3
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +153 -30
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +47 -5
- package/src/cli/args.js +33 -24
- package/src/cli/help.js +16 -4
- package/src/commands/doctor.js +26 -60
- package/src/commands/dry-run.js +59 -5
- package/src/commands/full.js +135 -55
- package/src/commands/install-settings.js +51 -0
- package/src/commands/interactive-flutter.js +1 -1
- package/src/commands/interactive.js +135 -69
- package/src/commands/purge.js +50 -21
- package/src/commands/status.js +18 -12
- package/src/commands/uninstall.js +22 -22
- package/src/context.js +5 -5
- package/src/core/baseline.js +20 -5
- package/src/core/branches.js +19 -1
- package/src/core/branding.js +1 -1
- package/src/core/breaking-check.js +14 -19
- package/src/core/breaking.js +7 -3
- package/src/core/copy/app-files.js +28 -0
- package/src/core/copy/gitignore.js +20 -12
- package/src/core/copy/readme.js +24 -5
- package/src/core/copy/simple.js +52 -13
- package/src/core/copy/workflows.js +133 -111
- package/src/core/deploy-style.js +83 -11
- package/src/core/detect-fs.js +127 -24
- package/src/core/detect.js +177 -47
- package/src/core/errors.js +3 -0
- package/src/core/flutter-doctor.js +49 -0
- package/src/core/flutter-hooks.js +29 -0
- package/src/core/flutter-options.js +4 -3
- package/src/core/fsutil.js +23 -6
- package/src/core/logger.js +55 -19
- package/src/core/paths-resolve.js +50 -47
- package/src/core/paths.js +26 -1
- package/src/core/release-options.js +23 -0
- package/src/core/removal-exec.js +67 -0
- package/src/core/removal-plan.js +75 -19
- package/src/core/types.js +136 -0
- package/src/core/verify.js +85 -19
- package/src/core/version-yml.js +42 -8
- package/src/core/wizard-env.js +21 -14
- package/src/core/wizard-labels.js +34 -25
- package/src/index.js +128 -60
- package/src/ui/ansi.js +3 -1
- package/src/ui/banner.js +12 -8
- package/src/ui/env-plan.js +68 -44
- package/src/ui/prompts.js +22 -12
- package/src/ui/readline-engine.js +109 -22
- package/src/ui/status-cards.js +10 -5
- package/src/ui/summary.js +71 -33
- package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
|
@@ -5,13 +5,15 @@ version_manager.py — general-purpose version management script (stdlib only).
|
|
|
5
5
|
This script is copied into user repos (.github/scripts/) by project-auto-wizard
|
|
6
6
|
and runs standalone on GitHub Actions ubuntu runners (python3, no third-party deps).
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
(version_manager.sh). Behavioral equivalence with that script is the design goal:
|
|
8
|
+
Design rules:
|
|
10
9
|
- version.yml is the single source of truth for `version` and `version_code`.
|
|
11
10
|
- version.yml is edited via line-based regex replacements that preserve all
|
|
12
11
|
comments and formatting (never rewritten wholesale, never parsed with a YAML lib).
|
|
13
12
|
- Versions are synced out to type-specific project files (build.gradle,
|
|
14
13
|
pubspec.yaml, package.json, pyproject.toml, Info.plist, app.json, ...).
|
|
14
|
+
- increment/set also rewrite the README.md version line under the
|
|
15
|
+
AUTO-VERSION-SECTION marker, so the release commit (and its tag) already
|
|
16
|
+
shows the new version.
|
|
15
17
|
|
|
16
18
|
Usage:
|
|
17
19
|
version_manager.py get # current version (synced)
|
|
@@ -23,7 +25,8 @@ Usage:
|
|
|
23
25
|
|
|
24
26
|
Contract:
|
|
25
27
|
- The LAST line printed to stdout is always the value (callers do `| tail -n 1`).
|
|
26
|
-
- Exit 0 on success; exit 1 on validation failure
|
|
28
|
+
- Exit 0 on success; exit 1 on validation failure, missing version.yml,
|
|
29
|
+
or when a version could not be written (e.g. invalid package.json).
|
|
27
30
|
"""
|
|
28
31
|
|
|
29
32
|
import argparse
|
|
@@ -33,10 +36,35 @@ import os
|
|
|
33
36
|
import re
|
|
34
37
|
import sys
|
|
35
38
|
from pathlib import Path
|
|
39
|
+
from typing import Callable, NamedTuple
|
|
36
40
|
|
|
37
41
|
VERSION_YML = "version.yml"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class VersionSyncError(Exception):
|
|
45
|
+
"""version.yml·프로젝트 파일에 버전을 쓰지 못했다 — exit 1로 알린다."""
|
|
38
46
|
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
|
39
47
|
|
|
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 값과 첫 릴리스 때 읽은 값이 어긋난다.
|
|
51
|
+
_CORE_VERSION_RE = re.compile(r"^v?(\d+\.\d+\.\d+)(?:[-+][0-9A-Za-z.+-]*)?$")
|
|
52
|
+
|
|
53
|
+
# Gradle·Maven의 -SNAPSHOT은 특정 버전의 프리릴리스가 아니라 "개발 중 빌드" 표식이다(배포 저장소 선택,
|
|
54
|
+
# 산출물 이름에 쓰인다). 그래서 버전을 올려도 떼지 않고 새 버전 뒤에 그대로 붙인다.
|
|
55
|
+
# rc.1·beta.1 같은 프리릴리스는 그 버전 전용 표식이라 새 x.y.z로 릴리스할 때 버린다.
|
|
56
|
+
SNAPSHOT_SUFFIX = "-SNAPSHOT"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def core_version(value):
|
|
60
|
+
"""x.y.z[-pre][+build] → x.y.z. 버전 형식이 아니면 None."""
|
|
61
|
+
m = _CORE_VERSION_RE.match(str(value or "").strip())
|
|
62
|
+
return m.group(1) if m else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _keep_snapshot(old_value, new_version):
|
|
66
|
+
return new_version + SNAPSHOT_SUFFIX if str(old_value).endswith(SNAPSHOT_SUFFIX) else new_version
|
|
67
|
+
|
|
40
68
|
|
|
41
69
|
def log(message):
|
|
42
70
|
"""Non-value logging output, written to stderr (mirroring the bash
|
|
@@ -185,12 +213,15 @@ def get_project_types_csv():
|
|
|
185
213
|
def get_type_path(project_type, project_types_list=None):
|
|
186
214
|
"""Return project_paths.<type> if set, else '.' (repo root)."""
|
|
187
215
|
text = read_text()
|
|
188
|
-
|
|
216
|
+
# The wizard writes trailing comments on both the key line and value lines
|
|
217
|
+
# (e.g. `project_paths: # ...`, ` flutter: "app" # app/pubspec.yaml`).
|
|
218
|
+
m = re.search(r'^project_paths:[ \t]*(?:#[^\n]*)?\n((?:[ \t]+.+\n?)+)', text, re.MULTILINE)
|
|
189
219
|
if not m:
|
|
190
220
|
return "."
|
|
191
221
|
block = m.group(1)
|
|
192
222
|
km = re.search(
|
|
193
|
-
r'^[ \t]+["\']?' + re.escape(project_type)
|
|
223
|
+
r'^[ \t]+["\']?' + re.escape(project_type)
|
|
224
|
+
+ r'["\']?:[ \t]*["\']?([^"\'#\n]+?)["\']?[ \t]*(?:#.*)?$',
|
|
194
225
|
block,
|
|
195
226
|
re.MULTILINE,
|
|
196
227
|
)
|
|
@@ -283,7 +314,18 @@ def get_higher_version(v1, v2):
|
|
|
283
314
|
|
|
284
315
|
|
|
285
316
|
def update_version_yml(new_version):
|
|
286
|
-
write_scalar_key("version", new_version)
|
|
317
|
+
if not write_scalar_key("version", new_version):
|
|
318
|
+
# 키가 없으면 아무것도 쓰지 않은 채 성공처럼 끝나 매번 같은 버전이 나왔다.
|
|
319
|
+
log("WARNING: version field missing in version.yml, adding it")
|
|
320
|
+
text = read_text()
|
|
321
|
+
line = f'version: "{new_version}"\n'
|
|
322
|
+
if re.search(r'^version_code:', text, re.MULTILINE):
|
|
323
|
+
new_text = re.sub(r'^(?=version_code:)', lambda _m: line, text, count=1, flags=re.MULTILINE)
|
|
324
|
+
else:
|
|
325
|
+
new_text = text.rstrip("\n") + "\n" + line
|
|
326
|
+
write_text(new_text)
|
|
327
|
+
if read_scalar_key("version") != new_version:
|
|
328
|
+
raise VersionSyncError(f"failed to write version {new_version} to version.yml")
|
|
287
329
|
today = datetime.date.today().isoformat()
|
|
288
330
|
user = os.environ.get("GITHUB_ACTOR", "")
|
|
289
331
|
if not user:
|
|
@@ -316,24 +358,200 @@ def _write_nested_scalar(key, value):
|
|
|
316
358
|
# Project file sync (type-specific)
|
|
317
359
|
# ===================================================================
|
|
318
360
|
|
|
361
|
+
_XML_TOKEN_RE = re.compile(
|
|
362
|
+
r'<!--.*?-->|<!\[CDATA\[.*?\]\]>|<\?.*?\?>|<![^>]*>|<(/?)([A-Za-z_][\w.:-]*)[^>]*?(/?)>',
|
|
363
|
+
re.DOTALL,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _pom_text_span(text, path):
|
|
368
|
+
"""루트(<project>) 기준 경로의 요소 텍스트 구간 (start, end). 없으면 None.
|
|
369
|
+
<parent>·<dependencies> 안의 <version>은 다른 아티팩트 버전이라 깊이로 구분한다."""
|
|
370
|
+
stack = []
|
|
371
|
+
start = None
|
|
372
|
+
target = list(path)
|
|
373
|
+
for m in _XML_TOKEN_RE.finditer(text):
|
|
374
|
+
name = m.group(2)
|
|
375
|
+
if not name:
|
|
376
|
+
continue # 주석·CDATA·선언
|
|
377
|
+
if m.group(1): # 닫는 태그
|
|
378
|
+
if start is not None and stack[1:] == target:
|
|
379
|
+
return start, m.start()
|
|
380
|
+
if stack:
|
|
381
|
+
stack.pop()
|
|
382
|
+
continue
|
|
383
|
+
if m.group(3): # <tag/>
|
|
384
|
+
continue
|
|
385
|
+
stack.append(name)
|
|
386
|
+
if stack[1:] == target:
|
|
387
|
+
start = m.end()
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _pom_text(text, path):
|
|
392
|
+
span = _pom_text_span(text, path)
|
|
393
|
+
return text[span[0]:span[1]].strip() if span else None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _pom_replace(text, path, value):
|
|
397
|
+
span = _pom_text_span(text, path)
|
|
398
|
+
if not span:
|
|
399
|
+
return text, False
|
|
400
|
+
return text[:span[0]] + value + text[span[1]:], True
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def sync_maven(path_dir, new_version):
|
|
404
|
+
"""pom.xml의 프로젝트 자신의 <version>만 바꾼다. 하위 모듈은 루트를 가리키는
|
|
405
|
+
<parent><version>(과 루트와 같던 자기 <version>)만 따라 올린다."""
|
|
406
|
+
root_pom = Path(path_dir) / "pom.xml"
|
|
407
|
+
if not root_pom.is_file():
|
|
408
|
+
return False
|
|
409
|
+
text = read_file(root_pom)
|
|
410
|
+
old_version = _pom_text(text, ["version"])
|
|
411
|
+
if old_version is None:
|
|
412
|
+
log(f"WARNING: spring: {root_pom} has no project <version> (inherited from parent?) — skipping")
|
|
413
|
+
return True
|
|
414
|
+
if "${" in old_version:
|
|
415
|
+
# ${revision} 같은 CI-friendly 버전은 프로퍼티 쪽에서 관리하므로 건드리지 않는다.
|
|
416
|
+
log(f"WARNING: spring: {root_pom} <version> is a property ({old_version}) — skipping")
|
|
417
|
+
return True
|
|
418
|
+
new_full = _keep_snapshot(old_version, new_version)
|
|
419
|
+
new_text, _ = _pom_replace(text, ["version"], new_full)
|
|
420
|
+
write_file(root_pom, new_text)
|
|
421
|
+
log(f"updated: {root_pom}")
|
|
422
|
+
|
|
423
|
+
root_artifact = _pom_text(text, ["artifactId"])
|
|
424
|
+
for child in sorted(Path(path_dir).glob("*/pom.xml")):
|
|
425
|
+
ctext = read_file(child)
|
|
426
|
+
if _pom_text(ctext, ["parent", "artifactId"]) != root_artifact:
|
|
427
|
+
continue
|
|
428
|
+
if _pom_text(ctext, ["parent", "version"]) != old_version:
|
|
429
|
+
continue
|
|
430
|
+
ctext, _ = _pom_replace(ctext, ["parent", "version"], new_full)
|
|
431
|
+
if _pom_text(ctext, ["version"]) == old_version:
|
|
432
|
+
ctext, _ = _pom_replace(ctext, ["version"], new_full)
|
|
433
|
+
write_file(child, ctext)
|
|
434
|
+
log(f"updated: {child}")
|
|
435
|
+
return True
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
_GRADLE_VERSION_RE = re.compile(r"""^([ \t]*version[ \t]*=[ \t]*)(['"])([^'"\n]*)\2""", re.MULTILINE)
|
|
439
|
+
_GRADLE_SHARED_BLOCKS = ("allprojects", "subprojects")
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _gradle_code(line, quote=""):
|
|
443
|
+
"""한 줄에서 주석을 떼고 따옴표 안 글자는 공백으로 바꾼다. url 'https://…'의 `//`나
|
|
444
|
+
문자열 속 중괄호가 블록 깊이 계산을 흐트러뜨리지 않게 한다. detect.js와 같은 규칙.
|
|
445
|
+
|
|
446
|
+
quote는 이 줄이 시작될 때 열려 있던 따옴표다. 삼중따옴표(\"\"\" ''')는 여러 줄에 걸치므로
|
|
447
|
+
(코드, 줄 끝에서 열려 있는 따옴표)를 돌려줘 다음 줄이 이어받게 한다. 한 줄짜리 따옴표는
|
|
448
|
+
줄 끝에서 닫힌 것으로 본다."""
|
|
449
|
+
out, i = [], 0
|
|
450
|
+
while i < len(line):
|
|
451
|
+
ch = line[i]
|
|
452
|
+
if quote:
|
|
453
|
+
if ch == "\\" and i + 1 < len(line):
|
|
454
|
+
out.append(" "); i += 2; continue
|
|
455
|
+
if line.startswith(quote, i):
|
|
456
|
+
out.append(quote); i += len(quote); quote = ""; continue
|
|
457
|
+
out.append(" ")
|
|
458
|
+
elif line.startswith(('"""', "'''"), i):
|
|
459
|
+
quote = line[i:i + 3]; out.append(quote); i += 3; continue
|
|
460
|
+
elif ch in "'\"":
|
|
461
|
+
quote = ch; out.append(ch)
|
|
462
|
+
elif line.startswith("//", i):
|
|
463
|
+
break
|
|
464
|
+
else:
|
|
465
|
+
out.append(ch)
|
|
466
|
+
i += 1
|
|
467
|
+
return "".join(out), quote if len(quote) == 3 else ""
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _gradle_version_matches(text):
|
|
471
|
+
"""프로젝트 버전 줄의 match 목록. 읽기와 동기화가 이 한 곳을 공유해 같은 줄만 다룬다.
|
|
472
|
+
|
|
473
|
+
들여쓰지 않은 `version =`이 있으면 그것만 쓴다. 없을 때만 allprojects/subprojects 블록 안의
|
|
474
|
+
들여쓴 줄을 인정한다 — `node { version = '20.11.0' }` 같은 플러그인 설정 블록을
|
|
475
|
+
프로젝트 버전으로 오인하면 버전이 뛰고 빌드 설정이 깨진다."""
|
|
476
|
+
top, shared = [], []
|
|
477
|
+
stack = [] # 열린 블록 이름. 줄 끝이 `{`로 끝나는 줄의 마지막 단어
|
|
478
|
+
quote = "" # 여러 줄 문자열 안이면 그 따옴표. 문자열 속 줄은 코드가 아니다
|
|
479
|
+
pos = 0
|
|
480
|
+
for line in text.splitlines(keepends=True):
|
|
481
|
+
m = None if quote else _GRADLE_VERSION_RE.match(line)
|
|
482
|
+
if m:
|
|
483
|
+
m = _GRADLE_VERSION_RE.match(text, pos)
|
|
484
|
+
if not m.group(1)[:1].isspace():
|
|
485
|
+
top.append(m)
|
|
486
|
+
elif any(b in _GRADLE_SHARED_BLOCKS for b in stack):
|
|
487
|
+
shared.append(m)
|
|
488
|
+
code, quote = _gradle_code(line, quote)
|
|
489
|
+
name = re.search(r"(\w+)\s*\{[^{}]*$", code)
|
|
490
|
+
for ch in code:
|
|
491
|
+
if ch == "{":
|
|
492
|
+
stack.append(name.group(1) if name else "")
|
|
493
|
+
elif ch == "}" and stack:
|
|
494
|
+
stack.pop()
|
|
495
|
+
pos += len(line)
|
|
496
|
+
return top or shared
|
|
497
|
+
|
|
498
|
+
|
|
319
499
|
def sync_spring(path_dir, new_version):
|
|
320
|
-
"""Look for build.gradle or build.gradle.kts under path_dir (root of that dir, like bash's maxdepth 2)
|
|
500
|
+
"""Look for build.gradle or build.gradle.kts under path_dir (root of that dir, like bash's maxdepth 2),
|
|
501
|
+
and pom.xml for Maven projects."""
|
|
321
502
|
candidates = []
|
|
322
503
|
for name in ("build.gradle", "build.gradle.kts"):
|
|
323
504
|
for p in [Path(path_dir) / name] + list(Path(path_dir).glob("*/" + name)):
|
|
324
505
|
if p.is_file():
|
|
325
506
|
candidates.append(p)
|
|
507
|
+
has_pom = sync_maven(path_dir, new_version)
|
|
326
508
|
if not candidates:
|
|
327
|
-
|
|
509
|
+
if not has_pom:
|
|
510
|
+
log(f"WARNING: spring: no build.gradle(.kts) or pom.xml found under {path_dir} — skipping")
|
|
328
511
|
return
|
|
329
512
|
for gradle_file in candidates:
|
|
330
513
|
text = read_file(gradle_file)
|
|
331
|
-
|
|
332
|
-
|
|
514
|
+
# 줄 시작의 `version =`만 프로젝트 버전이다. 앵커가 없으면 kotlin_version 같은
|
|
515
|
+
# 의존성 버전 변수까지 함께 바뀌어 빌드가 깨진다.
|
|
516
|
+
matches = _gradle_version_matches(text)
|
|
517
|
+
new_text = text
|
|
518
|
+
# 뒤에서부터 바꿔 앞쪽 span이 밀리지 않게 한다.
|
|
519
|
+
for m in reversed(matches):
|
|
520
|
+
replaced = f"{m.group(1)}{m.group(2)}{_keep_snapshot(m.group(3), new_version)}{m.group(2)}"
|
|
521
|
+
new_text = new_text[:m.start()] + replaced + new_text[m.end():]
|
|
522
|
+
if not matches:
|
|
523
|
+
log(f"WARNING: spring: no `version = '...'` line in {gradle_file} — skipping")
|
|
524
|
+
continue
|
|
333
525
|
write_file(gradle_file, new_text)
|
|
334
526
|
log(f"updated: {gradle_file}")
|
|
335
527
|
|
|
336
528
|
|
|
529
|
+
def _pubspec_build_number(path_dir):
|
|
530
|
+
"""pubspec.yaml `version: x.y.z+N`의 N. 없으면 None."""
|
|
531
|
+
target = Path(path_dir) / "pubspec.yaml"
|
|
532
|
+
if not target.is_file():
|
|
533
|
+
return None
|
|
534
|
+
m = re.search(r'^version:[ \t]*[^\s#+]+\+(\d+)', read_file(target), re.MULTILINE)
|
|
535
|
+
return int(m.group(1)) if m else None
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def get_reconciled_version_code():
|
|
539
|
+
"""version.yml의 version_code와 pubspec.yaml의 +N 중 큰 값.
|
|
540
|
+
로컬 수동 배포 등으로 pubspec 쪽이 앞서 있으면 스토어는 더 작은 build number를
|
|
541
|
+
거부하므로, 그 값을 version.yml에 반영해 역행을 막는다."""
|
|
542
|
+
code = int(get_version_code())
|
|
543
|
+
types = get_project_types_csv()
|
|
544
|
+
pubspec_codes = [
|
|
545
|
+
n for n in (_pubspec_build_number(get_type_path(t)) for t in types if t == "flutter")
|
|
546
|
+
if n is not None
|
|
547
|
+
]
|
|
548
|
+
if pubspec_codes and max(pubspec_codes) > code:
|
|
549
|
+
log(f"pubspec.yaml build number {max(pubspec_codes)} is ahead of version_code {code} — adopting it")
|
|
550
|
+
code = max(pubspec_codes)
|
|
551
|
+
set_version_code(code)
|
|
552
|
+
return code
|
|
553
|
+
|
|
554
|
+
|
|
337
555
|
def sync_flutter(path_dir, new_version, version_code):
|
|
338
556
|
target = Path(path_dir) / "pubspec.yaml"
|
|
339
557
|
if not target.is_file():
|
|
@@ -350,49 +568,115 @@ def sync_flutter(path_dir, new_version, version_code):
|
|
|
350
568
|
log(f"updated: {target}")
|
|
351
569
|
|
|
352
570
|
|
|
571
|
+
def _json_indent(text):
|
|
572
|
+
"""Indentation used by an existing JSON file — the first indented line after a newline
|
|
573
|
+
(e.g. 4 spaces or a tab). Single-line JSON stays on one line (None).
|
|
574
|
+
Rewriting with a fixed indent turns every version bump into a whole-file diff."""
|
|
575
|
+
if "\n" not in text.strip():
|
|
576
|
+
return None
|
|
577
|
+
m = re.search(r'\n([ \t]+)\S', text)
|
|
578
|
+
return m.group(1) if m else 2
|
|
579
|
+
|
|
580
|
+
|
|
353
581
|
def sync_json_version(target, new_version, key_path):
|
|
354
582
|
if not target.is_file():
|
|
355
583
|
log(f"WARNING: {target} not found — skipping")
|
|
356
584
|
return
|
|
585
|
+
raw = read_file(target)
|
|
357
586
|
try:
|
|
358
|
-
data = json.loads(
|
|
587
|
+
data = json.loads(raw)
|
|
359
588
|
except json.JSONDecodeError as e:
|
|
360
|
-
|
|
361
|
-
|
|
589
|
+
# 건너뛰고 성공으로 끝내면 태그·version.yml과 패키지 버전이 조용히 어긋난다.
|
|
590
|
+
raise VersionSyncError(f"{target} is not valid JSON ({e}) — cannot write version")
|
|
362
591
|
node = data
|
|
363
592
|
for k in key_path[:-1]:
|
|
364
593
|
node = node.setdefault(k, {})
|
|
365
594
|
node[key_path[-1]] = new_version
|
|
366
|
-
write_file(target, json.dumps(data, indent=
|
|
595
|
+
write_file(target, json.dumps(data, indent=_json_indent(raw), ensure_ascii=False) + "\n")
|
|
367
596
|
log(f"updated: {target}")
|
|
368
597
|
|
|
369
598
|
|
|
599
|
+
_TOML_HEADER_RE = re.compile(r'^[ \t]*\[+[ \t]*([^\]\n]+?)[ \t]*\]+[ \t]*(?:#.*)?$', re.MULTILINE)
|
|
600
|
+
_TOML_VERSION_RE = re.compile(r'^[ \t]*version[ \t]*=[ \t]*([\'"])([^\'"\n]*)\1', re.MULTILINE)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _pyproject_version_span(text):
|
|
604
|
+
"""[project] 또는 [tool.poetry] 섹션의 version 값 구간. [tool.*] 등 다른 섹션의
|
|
605
|
+
`version =`은 도구 설정이라 패키지 버전으로 쓰지 않는다."""
|
|
606
|
+
headers = list(_TOML_HEADER_RE.finditer(text))
|
|
607
|
+
for i, h in enumerate(headers):
|
|
608
|
+
if h.group(1) not in ("project", "tool.poetry"):
|
|
609
|
+
continue
|
|
610
|
+
end = headers[i + 1].start() if i + 1 < len(headers) else len(text)
|
|
611
|
+
m = _TOML_VERSION_RE.search(text, h.end(), end)
|
|
612
|
+
if m:
|
|
613
|
+
return m.start(2), m.end(2)
|
|
614
|
+
return None
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
# setup(version="x.y.z"). python_requires·python_version 같은 다른 키는 앞 글자로 거른다.
|
|
618
|
+
_SETUP_PY_VERSION_RE = re.compile(r"""(?<![\w.])version\s*=\s*(['"])([^'"]+)\1""")
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _python_version_spans(path_dir):
|
|
622
|
+
"""버전을 읽고 쓰는 파일과 값 구간 목록. 읽을 때는 앞쪽(pyproject.toml → setup.py)이 우선이다.
|
|
623
|
+
setup.py만 쓰는 프로젝트도 설치 때 버전을 읽으므로, 릴리스 때도 같은 파일을 읽고 써야 한다."""
|
|
624
|
+
spans = []
|
|
625
|
+
pyproject = Path(path_dir) / "pyproject.toml"
|
|
626
|
+
if pyproject.is_file():
|
|
627
|
+
span = _pyproject_version_span(read_file(pyproject))
|
|
628
|
+
if span:
|
|
629
|
+
spans.append((pyproject, span))
|
|
630
|
+
setup_py = Path(path_dir) / "setup.py"
|
|
631
|
+
if setup_py.is_file():
|
|
632
|
+
m = _SETUP_PY_VERSION_RE.search(read_file(setup_py))
|
|
633
|
+
if m:
|
|
634
|
+
spans.append((setup_py, (m.start(2), m.end(2))))
|
|
635
|
+
return spans
|
|
636
|
+
|
|
637
|
+
|
|
370
638
|
def sync_python(path_dir, new_version):
|
|
371
|
-
|
|
372
|
-
if not
|
|
373
|
-
|
|
639
|
+
spans = _python_version_spans(path_dir)
|
|
640
|
+
if not spans:
|
|
641
|
+
# dynamic = ["version"] 등 버전을 파일에 두지 않는 구성은 쓸 곳이 없다.
|
|
642
|
+
log(f"WARNING: python: no version in pyproject.toml [project]/[tool.poetry] or setup.py under {path_dir} — skipping")
|
|
374
643
|
return
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
644
|
+
for target, (start, end) in spans:
|
|
645
|
+
text = read_file(target)
|
|
646
|
+
write_file(target, text[:start] + new_version + text[end:])
|
|
647
|
+
log(f"updated: {target}")
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
_PLIST_VERSION_RE = re.compile(r'(<key>CFBundleShortVersionString</key>\s*<string>)([^<]*)(</string>)')
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _rn_app_plists(ios_dir):
|
|
654
|
+
"""앱 타깃의 Info.plist만 이름순으로. Pods·빌드 산출물·테스트 타깃은 우리 버전이 아니다.
|
|
655
|
+
읽기와 동기화가 같은 파일 집합을 보도록 한 곳에서 고른다."""
|
|
656
|
+
skip = {"Pods", "build"}
|
|
657
|
+
return [
|
|
658
|
+
p for p in sorted(ios_dir.glob("*/Info.plist"))
|
|
659
|
+
if p.parent.name not in skip and not p.parent.name.endswith("Tests")
|
|
660
|
+
]
|
|
379
661
|
|
|
380
662
|
|
|
381
663
|
def sync_react_native(path_dir, new_version):
|
|
382
664
|
ios_dir = Path(path_dir) / "ios"
|
|
383
665
|
found_plist = False
|
|
384
666
|
if ios_dir.is_dir():
|
|
385
|
-
for plist_file in ios_dir
|
|
667
|
+
for plist_file in _rn_app_plists(ios_dir):
|
|
386
668
|
text = read_file(plist_file)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
669
|
+
m = _PLIST_VERSION_RE.search(text)
|
|
670
|
+
if not m:
|
|
671
|
+
continue
|
|
672
|
+
# $(MARKETING_VERSION) 같은 빌드 변수 참조는 Xcode 설정이 원본이라 덮어쓰지 않는다.
|
|
673
|
+
if m.group(2).strip().startswith("$("):
|
|
674
|
+
log(f"skipped: {plist_file} — CFBundleShortVersionString references a build variable")
|
|
675
|
+
continue
|
|
676
|
+
new_text = _PLIST_VERSION_RE.sub(lambda mm: mm.group(1) + new_version + mm.group(3), text)
|
|
677
|
+
write_file(plist_file, new_text)
|
|
678
|
+
log(f"updated: {plist_file}")
|
|
679
|
+
found_plist = True
|
|
396
680
|
else:
|
|
397
681
|
log(f"WARNING: react-native: {ios_dir} not found — skipping")
|
|
398
682
|
|
|
@@ -411,24 +695,11 @@ def sync_react_native(path_dir, new_version):
|
|
|
411
695
|
|
|
412
696
|
def sync_for_type(project_type, new_version, version_code_getter):
|
|
413
697
|
path_dir = get_type_path(project_type)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
elif project_type == "flutter":
|
|
417
|
-
sync_flutter(path_dir, new_version, version_code_getter())
|
|
418
|
-
elif project_type in ("react", "next", "node"):
|
|
419
|
-
sync_json_version(Path(path_dir) / "package.json", new_version, ["version"])
|
|
420
|
-
elif project_type == "python":
|
|
421
|
-
sync_python(path_dir, new_version)
|
|
422
|
-
elif project_type == "react-native":
|
|
423
|
-
sync_react_native(path_dir, new_version)
|
|
424
|
-
elif project_type == "react-native-expo":
|
|
425
|
-
sync_json_version(Path(path_dir) / "app.json", new_version, ["expo", "version"])
|
|
426
|
-
elif project_type == "basic":
|
|
427
|
-
pass
|
|
428
|
-
elif project_type == "go":
|
|
429
|
-
pass
|
|
430
|
-
else:
|
|
698
|
+
handler = TYPE_HANDLERS.get(project_type)
|
|
699
|
+
if handler is None:
|
|
431
700
|
log(f"WARNING: unknown project type: {project_type} — skipping")
|
|
701
|
+
return
|
|
702
|
+
handler.sync(path_dir, new_version, version_code_getter)
|
|
432
703
|
|
|
433
704
|
|
|
434
705
|
def sync_all_project_files(new_version):
|
|
@@ -437,74 +708,175 @@ def sync_all_project_files(new_version):
|
|
|
437
708
|
# No silent fallback: an unreadable project_types used to degrade to
|
|
438
709
|
# "basic" and skip every sync without a word.
|
|
439
710
|
raise SystemExit("ERROR: version.yml has no readable project_types — cannot sync project files")
|
|
711
|
+
errors = []
|
|
440
712
|
for t in types:
|
|
441
|
-
|
|
713
|
+
# 한 타입이 실패해도 나머지는 맞춰 두고, 실패는 끝에서 모아 종료 코드로 알린다.
|
|
714
|
+
try:
|
|
715
|
+
sync_for_type(t, new_version, get_reconciled_version_code)
|
|
716
|
+
except VersionSyncError as e:
|
|
717
|
+
log(f"ERROR: {t}: {e}")
|
|
718
|
+
errors.append(t)
|
|
719
|
+
if errors:
|
|
720
|
+
raise VersionSyncError(f"project file sync failed for: {', '.join(errors)}")
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
README_VERSION_LINE_RE = re.compile(
|
|
724
|
+
r"^(##[^:\n]*:[ \t]*)v?\d+\.\d+\.\d+([ \t]*\(\d{4}-\d{2}-\d{2}\))?[ \t]*$", re.MULTILINE)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def update_readme_version(new_version, path="README.md"):
|
|
728
|
+
"""AUTO-VERSION-SECTION 마커 바로 아래 버전 줄만 새 버전으로 바꾼다.
|
|
729
|
+
|
|
730
|
+
README 갱신 워크플로우는 태그를 만든 뒤에 돌기 때문에, 버전 확정 커밋에 README가 함께
|
|
731
|
+
들어가지 않으면 태그 시점 README가 한 버전 전을 가리킨다. 마커·줄 삽입 같은 나머지 일은
|
|
732
|
+
그 워크플로우에 맡기고, 여기서는 이미 있는 줄만 고친다(날짜 표기 여부도 그대로 둔다)."""
|
|
733
|
+
p = Path(path)
|
|
734
|
+
if not p.is_file():
|
|
735
|
+
return False
|
|
736
|
+
text = read_file(p)
|
|
737
|
+
lines = text.split("\n")
|
|
738
|
+
for i, line in enumerate(lines[:-1]):
|
|
739
|
+
if "AUTO-VERSION-SECTION" not in line:
|
|
740
|
+
continue
|
|
741
|
+
m = README_VERSION_LINE_RE.match(lines[i + 1])
|
|
742
|
+
if not m:
|
|
743
|
+
return False
|
|
744
|
+
date = f" ({datetime.date.today().isoformat()})" if m.group(2) else ""
|
|
745
|
+
new_line = f"{m.group(1)}v{new_version}{date}"
|
|
746
|
+
if new_line == lines[i + 1]:
|
|
747
|
+
return False
|
|
748
|
+
lines[i + 1] = new_line
|
|
749
|
+
write_file(p, "\n".join(lines))
|
|
750
|
+
log(f"README.md version line -> v{new_version}")
|
|
751
|
+
return True
|
|
752
|
+
return False
|
|
442
753
|
|
|
443
754
|
|
|
444
755
|
def update_all_versions(new_version):
|
|
445
756
|
update_version_yml(new_version)
|
|
446
757
|
sync_all_project_files(new_version)
|
|
758
|
+
update_readme_version(new_version)
|
|
447
759
|
|
|
448
760
|
|
|
449
761
|
# ===================================================================
|
|
450
|
-
# Project file -> version read-back
|
|
762
|
+
# Project file -> version read-back, and the per-type handler table
|
|
451
763
|
# ===================================================================
|
|
452
764
|
|
|
765
|
+
def _read_spring(path_dir):
|
|
766
|
+
# build.gradle에 버전이 없으면(allprojects 설정만 있는 경우 등) build.gradle.kts → pom.xml 순으로 이어 본다.
|
|
767
|
+
for name in ("build.gradle", "build.gradle.kts"):
|
|
768
|
+
p = Path(path_dir) / name
|
|
769
|
+
if p.is_file():
|
|
770
|
+
for m in _gradle_version_matches(read_file(p)):
|
|
771
|
+
version = core_version(m.group(3))
|
|
772
|
+
if version:
|
|
773
|
+
return version
|
|
774
|
+
pom = Path(path_dir) / "pom.xml"
|
|
775
|
+
if pom.is_file():
|
|
776
|
+
return core_version(_pom_text(read_file(pom), ["version"]))
|
|
777
|
+
return None
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _read_flutter(path_dir):
|
|
781
|
+
p = Path(path_dir) / "pubspec.yaml"
|
|
782
|
+
if p.is_file():
|
|
783
|
+
text = p.read_text(encoding="utf-8")
|
|
784
|
+
m = re.search(r'^version:\s*([^\s#]+)', text, re.MULTILINE)
|
|
785
|
+
if m:
|
|
786
|
+
return core_version(m.group(1))
|
|
787
|
+
return None
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _read_package_json(path_dir):
|
|
791
|
+
p = Path(path_dir) / "package.json"
|
|
792
|
+
if p.is_file():
|
|
793
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
794
|
+
return core_version(data.get("version"))
|
|
795
|
+
return None
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
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가 아닌 값은 건너뛴다."""
|
|
802
|
+
ios_dir = Path(path_dir) / "ios"
|
|
803
|
+
if ios_dir.is_dir():
|
|
804
|
+
for plist in _rn_app_plists(ios_dir):
|
|
805
|
+
m = _PLIST_VERSION_RE.search(read_file(plist))
|
|
806
|
+
version = core_version(m.group(2)) if m else None
|
|
807
|
+
if version:
|
|
808
|
+
return version
|
|
809
|
+
gradle_file = Path(path_dir) / "android" / "app" / "build.gradle"
|
|
810
|
+
if gradle_file.is_file():
|
|
811
|
+
m = re.search(r'versionName\s+"([^"]+)"', read_file(gradle_file))
|
|
812
|
+
if m:
|
|
813
|
+
return core_version(m.group(1))
|
|
814
|
+
return None
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _read_expo(path_dir):
|
|
818
|
+
p = Path(path_dir) / "app.json"
|
|
819
|
+
if p.is_file():
|
|
820
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
821
|
+
return core_version((data.get("expo") or {}).get("version"))
|
|
822
|
+
return None
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _read_python(path_dir):
|
|
826
|
+
for target, (start, end) in _python_version_spans(path_dir):
|
|
827
|
+
version = core_version(read_file(target)[start:end])
|
|
828
|
+
if version:
|
|
829
|
+
return version
|
|
830
|
+
return None
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _read_none(path_dir):
|
|
834
|
+
return None
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def _sync_none(path_dir, new_version, version_code_getter):
|
|
838
|
+
pass
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
class TypeHandler(NamedTuple):
|
|
842
|
+
"""타입별 버전 파일 처리. read(path_dir)는 파일의 버전(없으면 None),
|
|
843
|
+
sync(path_dir, new_version, version_code_getter)는 새 버전을 파일에 쓴다."""
|
|
844
|
+
read: Callable
|
|
845
|
+
sync: Callable
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
_PACKAGE_JSON = TypeHandler(
|
|
849
|
+
read=_read_package_json,
|
|
850
|
+
sync=lambda d, v, _code: sync_json_version(Path(d) / "package.json", v, ["version"]),
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
# 새 타입은 여기 한 줄만 추가하면 읽기(sync 비교)와 쓰기가 함께 따라온다.
|
|
854
|
+
# 버전 파일이 없는 타입(basic·go)은 version.yml만 쓰도록 빈 핸들러를 둔다.
|
|
855
|
+
TYPE_HANDLERS = {
|
|
856
|
+
"spring": TypeHandler(read=_read_spring, sync=lambda d, v, _code: sync_spring(d, v)),
|
|
857
|
+
# build number가 필요한 타입만 version_code를 계산한다 (pubspec 조정 부수효과가 있다).
|
|
858
|
+
"flutter": TypeHandler(read=_read_flutter, sync=lambda d, v, code: sync_flutter(d, v, code())),
|
|
859
|
+
"react": _PACKAGE_JSON,
|
|
860
|
+
"next": _PACKAGE_JSON,
|
|
861
|
+
"node": _PACKAGE_JSON,
|
|
862
|
+
"python": TypeHandler(read=_read_python, sync=lambda d, v, _code: sync_python(d, v)),
|
|
863
|
+
"react-native": TypeHandler(read=_read_react_native, sync=lambda d, v, _code: sync_react_native(d, v)),
|
|
864
|
+
"react-native-expo": TypeHandler(
|
|
865
|
+
read=_read_expo,
|
|
866
|
+
sync=lambda d, v, _code: sync_json_version(Path(d) / "app.json", v, ["expo", "version"]),
|
|
867
|
+
),
|
|
868
|
+
"basic": TypeHandler(read=_read_none, sync=_sync_none),
|
|
869
|
+
"go": TypeHandler(read=_read_none, sync=_sync_none),
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
|
|
453
873
|
def get_project_file_version(project_type):
|
|
454
874
|
path_dir = get_type_path(project_type)
|
|
875
|
+
handler = TYPE_HANDLERS.get(project_type)
|
|
455
876
|
version = None
|
|
456
877
|
try:
|
|
457
|
-
if
|
|
458
|
-
|
|
459
|
-
p = Path(path_dir) / name
|
|
460
|
-
if p.is_file():
|
|
461
|
-
text = p.read_text(encoding="utf-8")
|
|
462
|
-
m = re.search(r"^\s*version\s*=\s*['\"](\d+\.\d+\.\d+)['\"]", text, re.MULTILINE)
|
|
463
|
-
if m:
|
|
464
|
-
version = m.group(1)
|
|
465
|
-
break
|
|
466
|
-
elif project_type == "flutter":
|
|
467
|
-
p = Path(path_dir) / "pubspec.yaml"
|
|
468
|
-
if p.is_file():
|
|
469
|
-
text = p.read_text(encoding="utf-8")
|
|
470
|
-
m = re.search(r'^version:\s*([^\s#]+)', text, re.MULTILINE)
|
|
471
|
-
if m:
|
|
472
|
-
version = m.group(1).split("+")[0]
|
|
473
|
-
elif project_type in ("react", "next", "node"):
|
|
474
|
-
p = Path(path_dir) / "package.json"
|
|
475
|
-
if p.is_file():
|
|
476
|
-
data = json.loads(p.read_text(encoding="utf-8"))
|
|
477
|
-
version = data.get("version")
|
|
478
|
-
elif project_type == "react-native":
|
|
479
|
-
ios_dir = Path(path_dir) / "ios"
|
|
480
|
-
plist = None
|
|
481
|
-
if ios_dir.is_dir():
|
|
482
|
-
plists = list(ios_dir.rglob("Info.plist"))
|
|
483
|
-
plist = plists[0] if plists else None
|
|
484
|
-
if plist is not None:
|
|
485
|
-
text = plist.read_text(encoding="utf-8")
|
|
486
|
-
m = re.search(r'<key>CFBundleShortVersionString</key>\s*<string>([^<]*)</string>', text)
|
|
487
|
-
if m:
|
|
488
|
-
version = m.group(1)
|
|
489
|
-
else:
|
|
490
|
-
gradle_file = Path(path_dir) / "android" / "app" / "build.gradle"
|
|
491
|
-
if gradle_file.is_file():
|
|
492
|
-
text = gradle_file.read_text(encoding="utf-8")
|
|
493
|
-
m = re.search(r'versionName\s+"([^"]+)"', text)
|
|
494
|
-
if m:
|
|
495
|
-
version = m.group(1)
|
|
496
|
-
elif project_type == "react-native-expo":
|
|
497
|
-
p = Path(path_dir) / "app.json"
|
|
498
|
-
if p.is_file():
|
|
499
|
-
data = json.loads(p.read_text(encoding="utf-8"))
|
|
500
|
-
version = (data.get("expo") or {}).get("version")
|
|
501
|
-
elif project_type == "python":
|
|
502
|
-
p = Path(path_dir) / "pyproject.toml"
|
|
503
|
-
if p.is_file():
|
|
504
|
-
text = p.read_text(encoding="utf-8")
|
|
505
|
-
m = re.search(r'^version\s*=\s*"(\d+\.\d+\.\d+)"', text, re.MULTILINE)
|
|
506
|
-
if m:
|
|
507
|
-
version = m.group(1)
|
|
878
|
+
if handler is not None:
|
|
879
|
+
version = handler.read(path_dir)
|
|
508
880
|
except Exception as e:
|
|
509
881
|
log(f"WARNING: failed reading project file for {project_type}: {e}")
|
|
510
882
|
version = None
|
|
@@ -560,14 +932,14 @@ def cmd_get(args):
|
|
|
560
932
|
|
|
561
933
|
def cmd_get_code(args):
|
|
562
934
|
require_version_yml()
|
|
563
|
-
code =
|
|
935
|
+
code = get_reconciled_version_code()
|
|
564
936
|
print(code)
|
|
565
937
|
return 0
|
|
566
938
|
|
|
567
939
|
|
|
568
940
|
def cmd_increment_code(args):
|
|
569
941
|
require_version_yml()
|
|
570
|
-
current =
|
|
942
|
+
current = get_reconciled_version_code()
|
|
571
943
|
new_code = current + 1
|
|
572
944
|
set_version_code(new_code)
|
|
573
945
|
print(new_code)
|
|
@@ -582,11 +954,10 @@ def cmd_increment(args):
|
|
|
582
954
|
return 1
|
|
583
955
|
bump = getattr(args, "bump", None) or "patch"
|
|
584
956
|
new_version = increment_version(current_version, bump)
|
|
957
|
+
# build number를 먼저 올려야 이어지는 pubspec 동기화에 새 값이 함께 기록된다.
|
|
958
|
+
set_version_code(get_reconciled_version_code() + 1)
|
|
585
959
|
update_all_versions(new_version)
|
|
586
960
|
|
|
587
|
-
current_code = int(get_version_code())
|
|
588
|
-
set_version_code(current_code + 1)
|
|
589
|
-
|
|
590
961
|
print(new_version)
|
|
591
962
|
return 0
|
|
592
963
|
|
|
@@ -640,7 +1011,11 @@ def main(argv=None):
|
|
|
640
1011
|
"sync": cmd_sync,
|
|
641
1012
|
}
|
|
642
1013
|
handler = handlers[args.command]
|
|
643
|
-
|
|
1014
|
+
try:
|
|
1015
|
+
return handler(args)
|
|
1016
|
+
except VersionSyncError as e:
|
|
1017
|
+
log(f"ERROR: {e}")
|
|
1018
|
+
return 1
|
|
644
1019
|
|
|
645
1020
|
|
|
646
1021
|
if __name__ == "__main__":
|