jrl-cmakemodules-scripts 2.1.0__tar.gz → 2.3.0__tar.gz
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.
- {jrl_cmakemodules_scripts-2.1.0 → jrl_cmakemodules_scripts-2.3.0}/PKG-INFO +1 -1
- {jrl_cmakemodules_scripts-2.1.0 → jrl_cmakemodules_scripts-2.3.0}/jrl_cmakemodules_scripts.egg-info/PKG-INFO +1 -1
- {jrl_cmakemodules_scripts-2.1.0 → jrl_cmakemodules_scripts-2.3.0}/jrl_release.py +743 -74
- {jrl_cmakemodules_scripts-2.1.0 → jrl_cmakemodules_scripts-2.3.0}/pyproject.toml +1 -1
- {jrl_cmakemodules_scripts-2.1.0 → jrl_cmakemodules_scripts-2.3.0}/README.md +0 -0
- {jrl_cmakemodules_scripts-2.1.0 → jrl_cmakemodules_scripts-2.3.0}/jrl_cmakemodules_scripts.egg-info/SOURCES.txt +0 -0
- {jrl_cmakemodules_scripts-2.1.0 → jrl_cmakemodules_scripts-2.3.0}/jrl_cmakemodules_scripts.egg-info/dependency_links.txt +0 -0
- {jrl_cmakemodules_scripts-2.1.0 → jrl_cmakemodules_scripts-2.3.0}/jrl_cmakemodules_scripts.egg-info/entry_points.txt +0 -0
- {jrl_cmakemodules_scripts-2.1.0 → jrl_cmakemodules_scripts-2.3.0}/jrl_cmakemodules_scripts.egg-info/requires.txt +0 -0
- {jrl_cmakemodules_scripts-2.1.0 → jrl_cmakemodules_scripts-2.3.0}/jrl_cmakemodules_scripts.egg-info/top_level.txt +0 -0
- {jrl_cmakemodules_scripts-2.1.0 → jrl_cmakemodules_scripts-2.3.0}/setup.cfg +0 -0
|
@@ -46,17 +46,21 @@ uv run --no-project jrl_release.py --bump patch --git-commit --git-tag
|
|
|
46
46
|
| Option | Description |
|
|
47
47
|
| :--- | :--- |
|
|
48
48
|
| `--root <PATH>` | Project root (default: cwd). |
|
|
49
|
-
| `--bump
|
|
49
|
+
| `--bump {major,minor,patch}` | Bump version component. |
|
|
50
50
|
| `--update-version <X.Y.Z>` | Set a specific version. |
|
|
51
51
|
| `--dry-run` | Show changes without writing files. |
|
|
52
52
|
| `--short` | Print only the version string. |
|
|
53
|
-
| `--output-format
|
|
53
|
+
| `--output-format {text,json}` | Output format (default: text). |
|
|
54
54
|
| `--confirm` | Skip interactive prompts. |
|
|
55
55
|
| `--list-files` | List tracked files. |
|
|
56
56
|
| `--git-commit [MSG]` | Commit changes. Optional message (`{version}` placeholder). |
|
|
57
57
|
| `--git-tag [NAME]` | Create a tag. Optional name (`{version}` placeholder). |
|
|
58
58
|
| `--git-tag-message <MSG>` | Tag annotation (`{version}` placeholder). |
|
|
59
59
|
| `--sign-tag` | GPG-sign the tag (`git tag -s`). Needs a configured signing key. |
|
|
60
|
+
| `--push-tag` | Push the tag to the main repository. Only works with github for now. |
|
|
61
|
+
| `--git-archive` | Create a .tar.gz archive. |
|
|
62
|
+
| `--sign-archive` | GPG-sign the archive. Needs a configured signing key. |
|
|
63
|
+
| `--gh-release` | Create Github release. Needs a configured gh cli. |
|
|
60
64
|
|
|
61
65
|
**Git defaults**: commit `chore: bump version to {version}`, tag `v{version}`, tag message `Release version {version}`.
|
|
62
66
|
|
|
@@ -64,21 +68,37 @@ uv run --no-project jrl_release.py --bump patch --git-commit --git-tag
|
|
|
64
68
|
|
|
65
69
|
| File | Key |
|
|
66
70
|
| :--- | :--- |
|
|
67
|
-
| `package.xml` | `<version>` tag |
|
|
68
|
-
| `pyproject.toml` | `project.version` |
|
|
71
|
+
| `package.xml` | `<version> and <url>` tag |
|
|
72
|
+
| `pyproject.toml` | `project.version` and `project.urls` |
|
|
69
73
|
| `CHANGELOG.md` | First `## [X.Y.Z]` section (not Unreleased) |
|
|
70
74
|
| `pixi.toml` | `[workspace] version` |
|
|
71
75
|
| `pixi.lock` | Regenerated via `pixi list` |
|
|
72
76
|
| `CITATION.cff` | `version` key |
|
|
73
|
-
| `CMakeLists.txt` | `project(... VERSION X.Y.Z ...)` |
|
|
77
|
+
| `CMakeLists.txt` | `project(... VERSION X.Y.Z ... HOMEPAGE_URL ...)` |
|
|
78
|
+
| `debian/changelog` | `version = ...` |
|
|
79
|
+
| `conanfile.py` | `version = ...` |
|
|
74
80
|
|
|
75
81
|
> Requires `pixi` CLI if `pixi.lock` exists in the project root.
|
|
82
|
+
|
|
83
|
+
## Meta-packages
|
|
84
|
+
|
|
85
|
+
ROS meta-packages are handled automatically: every nested directory with a
|
|
86
|
+
`package.xml` is treated like the repository root — the full set of
|
|
87
|
+
supported files (`package.xml`, `pyproject.toml`, `CHANGELOG.md`, `pixi.toml`,
|
|
88
|
+
`CITATION.cff`, `CMakeLists.txt`, `debian/changelog`) is checked there too,
|
|
89
|
+
skipping any that don't exist in that nested directory.
|
|
90
|
+
Discovery skips hidden directories (`.git`, `.pixi`, `.venv`, …), git submodules,
|
|
91
|
+
and — honoring your `.gitignore` via `git check-ignore` — ignored dirs like
|
|
92
|
+
`build/` or `dist/`. All discovered files must agree on a single
|
|
93
|
+
version to bump version.
|
|
76
94
|
"""
|
|
77
95
|
|
|
78
96
|
import sys
|
|
79
97
|
import re
|
|
98
|
+
import os
|
|
80
99
|
import argparse
|
|
81
100
|
import datetime
|
|
101
|
+
import importlib.metadata
|
|
82
102
|
import json
|
|
83
103
|
import subprocess
|
|
84
104
|
import shutil
|
|
@@ -122,6 +142,8 @@ STYLE_NEW_VALUE = "green"
|
|
|
122
142
|
STYLE_UNCHANGED_VALUE = "dim"
|
|
123
143
|
STYLE_HIGHLIGHT = "cyan"
|
|
124
144
|
|
|
145
|
+
GITHUB_URL = "https://github.com/"
|
|
146
|
+
|
|
125
147
|
|
|
126
148
|
class VersionNotPresent(Exception):
|
|
127
149
|
"""Raised when a file exists but has no version field configured."""
|
|
@@ -132,6 +154,8 @@ class VersionNotPresent(Exception):
|
|
|
132
154
|
class VersionExtractor(ABC):
|
|
133
155
|
def __init__(self, file_path: Path):
|
|
134
156
|
self.file_path = file_path
|
|
157
|
+
# Display label; discovery overrides it with a root-relative path.
|
|
158
|
+
self.label = file_path.name
|
|
135
159
|
|
|
136
160
|
@abstractmethod
|
|
137
161
|
def get_version(self) -> str:
|
|
@@ -152,6 +176,17 @@ class VersionExtractor(ABC):
|
|
|
152
176
|
def path(self) -> str:
|
|
153
177
|
return str(self.file_path)
|
|
154
178
|
|
|
179
|
+
def get_url(self) -> str | None:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
def validate_url(self, url: str) -> str | None:
|
|
183
|
+
if match := re.match(GITHUB_URL + r"[^/]+/[^/]+", url.strip("/").lower()):
|
|
184
|
+
valid_url = match.group()
|
|
185
|
+
console.print(
|
|
186
|
+
f"[{STYLE_INFO}]Found url {valid_url} in '{self.file_path}'[/{STYLE_INFO}]"
|
|
187
|
+
)
|
|
188
|
+
return valid_url
|
|
189
|
+
|
|
155
190
|
|
|
156
191
|
class XmlVersionExtractor(VersionExtractor):
|
|
157
192
|
def get_version(self) -> str:
|
|
@@ -178,6 +213,16 @@ class XmlVersionExtractor(VersionExtractor):
|
|
|
178
213
|
with open(self.file_path, "w", encoding="utf-8") as f:
|
|
179
214
|
f.write(new_content)
|
|
180
215
|
|
|
216
|
+
def get_url(self) -> str | None:
|
|
217
|
+
if not self.file_path.exists():
|
|
218
|
+
return None
|
|
219
|
+
with open(self.file_path, "r", encoding="utf-8") as f:
|
|
220
|
+
content = f.read()
|
|
221
|
+
for url in re.findall(r"<url[^>]*>(.*?)</url>", content, re.I):
|
|
222
|
+
if valid_url := self.validate_url(url):
|
|
223
|
+
return valid_url
|
|
224
|
+
return None
|
|
225
|
+
|
|
181
226
|
|
|
182
227
|
class TomlVersionExtractor(VersionExtractor):
|
|
183
228
|
def __init__(self, file_path: Path, keys: List[str]):
|
|
@@ -216,6 +261,17 @@ class TomlVersionExtractor(VersionExtractor):
|
|
|
216
261
|
with open(self.file_path, "w", encoding="utf-8") as f:
|
|
217
262
|
tomlkit.dump(data, f)
|
|
218
263
|
|
|
264
|
+
def get_url(self) -> str | None:
|
|
265
|
+
if not self.file_path.exists():
|
|
266
|
+
return None
|
|
267
|
+
with open(self.file_path, "r", encoding="utf-8") as f:
|
|
268
|
+
data = tomlkit.load(f)
|
|
269
|
+
if "project" in data and "urls" in data["project"]:
|
|
270
|
+
for url in data["project"]["urls"].values():
|
|
271
|
+
if valid_url := self.validate_url(url):
|
|
272
|
+
return valid_url
|
|
273
|
+
return None
|
|
274
|
+
|
|
219
275
|
|
|
220
276
|
class YamlVersionExtractor(VersionExtractor):
|
|
221
277
|
def __init__(self, file_path: Path, keys: List[str]):
|
|
@@ -253,6 +309,24 @@ class YamlVersionExtractor(VersionExtractor):
|
|
|
253
309
|
self.yaml.dump(data, f)
|
|
254
310
|
|
|
255
311
|
|
|
312
|
+
class ConanfileVersionExtractor(VersionExtractor):
|
|
313
|
+
def get_version(self) -> str:
|
|
314
|
+
content = self.file_path.read_text()
|
|
315
|
+
if match := re.search(r'\s*version\s*=\s*["\']([^"\']+)["\']', content):
|
|
316
|
+
return match.group(1)
|
|
317
|
+
raise VersionNotPresent(f"Version not found in {self.name}")
|
|
318
|
+
|
|
319
|
+
def update_version(self, new_version: str) -> None:
|
|
320
|
+
pattern = re.compile(r'(\s*version\s*=\s*["\'])([^"\']+)(["\'])')
|
|
321
|
+
content = self.file_path.read_text()
|
|
322
|
+
if not pattern.search(content):
|
|
323
|
+
raise VersionNotPresent(f"Version not found in {self.name}")
|
|
324
|
+
new_content, count = pattern.subn(
|
|
325
|
+
lambda m: f"{m.group(1)}{new_version}{m.group(3)}", content, count=1
|
|
326
|
+
)
|
|
327
|
+
self.file_path.write_text(new_content)
|
|
328
|
+
|
|
329
|
+
|
|
256
330
|
class CMakeListsVersionExtractor(VersionExtractor):
|
|
257
331
|
"""Specialized extractor for CMakeLists.txt that uses cmake-parser
|
|
258
332
|
and handles both direct VERSION and variables (e.g., from package.xml)."""
|
|
@@ -288,7 +362,7 @@ class CMakeListsVersionExtractor(VersionExtractor):
|
|
|
288
362
|
ver = args[version_idx + 1]
|
|
289
363
|
# Check if it's a variable reference
|
|
290
364
|
if not ver.startswith("${"):
|
|
291
|
-
project_version = ver
|
|
365
|
+
project_version = ver.strip('"')
|
|
292
366
|
except ValueError:
|
|
293
367
|
pass
|
|
294
368
|
|
|
@@ -305,6 +379,15 @@ class CMakeListsVersionExtractor(VersionExtractor):
|
|
|
305
379
|
|
|
306
380
|
return self._get_version_regex(content)
|
|
307
381
|
|
|
382
|
+
def get_url(self) -> str | None:
|
|
383
|
+
if not self.file_path.exists():
|
|
384
|
+
return None
|
|
385
|
+
with open(self.file_path, "r", encoding="utf-8") as f:
|
|
386
|
+
content = f.read()
|
|
387
|
+
if match := re.search(r'HOMEPAGE_URL\s+"([^"]+)"', content):
|
|
388
|
+
return self.validate_url(match.group(1))
|
|
389
|
+
return None
|
|
390
|
+
|
|
308
391
|
def _get_command_args(self, node) -> List[str]:
|
|
309
392
|
"""Extract arguments from a cmake command node."""
|
|
310
393
|
args = []
|
|
@@ -316,16 +399,16 @@ class CMakeListsVersionExtractor(VersionExtractor):
|
|
|
316
399
|
|
|
317
400
|
def _get_version_regex(self, content: str) -> str:
|
|
318
401
|
"""Fallback regex-based version extraction."""
|
|
319
|
-
#
|
|
402
|
+
# Finds set(PROJECT_VERSION "X.Y.Z") or set(PROJECT_VERSION X.Y.Z)
|
|
320
403
|
fallback_pattern = re.compile(
|
|
321
|
-
r'set\s*\(\s*PROJECT_VERSION\s+"([0-9]+\.[0-9]+\.[0-9]+)"',
|
|
404
|
+
r'set\s*\(\s*PROJECT_VERSION\s+"?([0-9]+\.[0-9]+\.[0-9]+)"?\s*\)',
|
|
322
405
|
re.MULTILINE,
|
|
323
406
|
)
|
|
324
407
|
fallback_match = fallback_pattern.search(content)
|
|
325
408
|
|
|
326
409
|
# Also check if project() uses a literal version or variable
|
|
327
410
|
project_pattern = re.compile(
|
|
328
|
-
r
|
|
411
|
+
r'project\s*\([^)]*VERSION\s+"?([\d.]+|\$\{[^}]+\})"?', re.MULTILINE
|
|
329
412
|
)
|
|
330
413
|
project_match = project_pattern.search(content)
|
|
331
414
|
|
|
@@ -347,20 +430,22 @@ class CMakeListsVersionExtractor(VersionExtractor):
|
|
|
347
430
|
with open(self.file_path, "r", encoding="utf-8") as f:
|
|
348
431
|
content = f.read()
|
|
349
432
|
|
|
350
|
-
# Update
|
|
433
|
+
# 1. Update fallback version: matches set(PROJECT_VERSION 1.2.3) or set(PROJECT_VERSION "1.2.3")
|
|
434
|
+
# Always replaces it without quotes: set(PROJECT_VERSION 1.2.3)
|
|
351
435
|
fallback_pattern = re.compile(
|
|
352
|
-
r'(set\s*\(\s*PROJECT_VERSION\s+)"([0-9]+\.[0-9]+\.[0-9]+)"',
|
|
436
|
+
r'(set\s*\(\s*PROJECT_VERSION\s+)"?([0-9]+\.[0-9]+\.[0-9]+)"?\s*\)',
|
|
353
437
|
re.MULTILINE,
|
|
354
438
|
)
|
|
355
439
|
|
|
356
440
|
def repl_fallback(match):
|
|
357
|
-
return f
|
|
441
|
+
return f"{match.group(1)}{new_version})"
|
|
358
442
|
|
|
359
443
|
content = fallback_pattern.sub(repl_fallback, content, count=1)
|
|
360
444
|
|
|
361
|
-
#
|
|
445
|
+
# 2. Update literal version in project(): matches VERSION 1.2.3 or VERSION "1.2.3"
|
|
446
|
+
# Always replaces it without quotes: VERSION 1.2.3
|
|
362
447
|
project_pattern = re.compile(
|
|
363
|
-
r
|
|
448
|
+
r'(project\s*\([^)]*VERSION\s+)"?([\d.]+)"?', re.MULTILINE
|
|
364
449
|
)
|
|
365
450
|
|
|
366
451
|
def repl_project(match):
|
|
@@ -372,6 +457,114 @@ class CMakeListsVersionExtractor(VersionExtractor):
|
|
|
372
457
|
f.write(content)
|
|
373
458
|
|
|
374
459
|
|
|
460
|
+
class DebianChangelogVersionExtractor(VersionExtractor):
|
|
461
|
+
"""Specialized extractor for debian/changelog files.
|
|
462
|
+
|
|
463
|
+
Expects standard format: package (version) distribution; urgency=...
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
@property
|
|
467
|
+
def name(self) -> str:
|
|
468
|
+
return "debian/changelog"
|
|
469
|
+
|
|
470
|
+
def get_version(self) -> str:
|
|
471
|
+
"""Extracts and returns the clean upstream version string (omitting the Debian suffix)."""
|
|
472
|
+
with open(self.file_path, "r", encoding="utf-8") as f:
|
|
473
|
+
first_line = f.readline()
|
|
474
|
+
|
|
475
|
+
if not first_line:
|
|
476
|
+
raise VersionNotPresent(f"Changelog file {self.name} is empty")
|
|
477
|
+
|
|
478
|
+
match = re.match(r"^[a-zA-Z0-9.+_-]+\s+\(([^)]+)\)", first_line)
|
|
479
|
+
if match:
|
|
480
|
+
full_version = match.group(1)
|
|
481
|
+
# Splits on '-' or '~' to strip the packaging revision (e.g., '1.3.3-1debian1' -> '1.3.3')
|
|
482
|
+
return re.split(r"[-~]", full_version)[0]
|
|
483
|
+
|
|
484
|
+
raise VersionNotPresent(
|
|
485
|
+
f"Could not parse Debian version from first line of {self.name}"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
def _get_raw_full_version(self) -> str:
|
|
489
|
+
"""Internal helper to get the un-stripped full version string with its suffix."""
|
|
490
|
+
with open(self.file_path, "r", encoding="utf-8") as f:
|
|
491
|
+
first_line = f.readline()
|
|
492
|
+
match = re.match(r"^[a-zA-Z0-9.+_-]+\s+\(([^)]+)\)", first_line)
|
|
493
|
+
return match.group(1) if match else ""
|
|
494
|
+
|
|
495
|
+
def update_version(self, new_version: str) -> None:
|
|
496
|
+
# Compare core upstream versions safely to avoid duplicate entries
|
|
497
|
+
try:
|
|
498
|
+
if self.get_version() == re.split(r"[-~]", new_version)[0]:
|
|
499
|
+
return # Skip adding a duplicate entry
|
|
500
|
+
except VersionNotPresent:
|
|
501
|
+
pass
|
|
502
|
+
|
|
503
|
+
# Grab the raw full version string to parse its suffix before reading full file
|
|
504
|
+
try:
|
|
505
|
+
current_full_version = self._get_raw_full_version()
|
|
506
|
+
except Exception:
|
|
507
|
+
current_full_version = ""
|
|
508
|
+
|
|
509
|
+
with open(self.file_path, "r", encoding="utf-8") as f:
|
|
510
|
+
content = f.read()
|
|
511
|
+
|
|
512
|
+
# Parse the first line to reuse metadata
|
|
513
|
+
first_line = content.splitlines()[0] if content.strip() else ""
|
|
514
|
+
match = re.match(
|
|
515
|
+
r"^([a-zA-Z0-9.+_-]+)\s+\([^)]+\)\s+([^;]+);\s*(urgency=\w+)",
|
|
516
|
+
first_line,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
if match:
|
|
520
|
+
package_name = match.group(1)
|
|
521
|
+
distribution = match.group(2).strip()
|
|
522
|
+
urgency = match.group(3)
|
|
523
|
+
else:
|
|
524
|
+
package_name = "package"
|
|
525
|
+
distribution = "unstable"
|
|
526
|
+
urgency = "urgency=medium"
|
|
527
|
+
|
|
528
|
+
# Determine the final version string to write, preserving the suffix
|
|
529
|
+
new_upstream = re.split(r"[-~]", new_version)[0]
|
|
530
|
+
if "-" in new_version or "~" in new_version:
|
|
531
|
+
formatted_version = new_version
|
|
532
|
+
elif current_full_version and (
|
|
533
|
+
"-" in current_full_version or "~" in current_full_version
|
|
534
|
+
):
|
|
535
|
+
suffix_match = re.search(r"([-~].*)$", current_full_version)
|
|
536
|
+
suffix = suffix_match.group(1) if suffix_match else "-1"
|
|
537
|
+
formatted_version = f"{new_upstream}{suffix}"
|
|
538
|
+
else:
|
|
539
|
+
formatted_version = f"{new_upstream}-1"
|
|
540
|
+
|
|
541
|
+
# Fetch maintainer details
|
|
542
|
+
repo_dir = self.file_path.parent
|
|
543
|
+
name_ok, git_name = run_git_command(["config", "user.name"], cwd=repo_dir)
|
|
544
|
+
email_ok, git_email = run_git_command(["config", "user.email"], cwd=repo_dir)
|
|
545
|
+
|
|
546
|
+
if name_ok and email_ok:
|
|
547
|
+
maintainer = f"{git_name} <{git_email}>"
|
|
548
|
+
else:
|
|
549
|
+
maintainer = "Maintainer <maintainer@example.com>"
|
|
550
|
+
|
|
551
|
+
# Format RFC 5322 timestamp
|
|
552
|
+
now = datetime.datetime.now(datetime.timezone.utc).astimezone()
|
|
553
|
+
debian_date = now.strftime("%a, %d %b %Y %H:%M:%S %z")
|
|
554
|
+
|
|
555
|
+
# Write out using the compliant version with the preserved suffix
|
|
556
|
+
new_entry = (
|
|
557
|
+
f"{package_name} ({formatted_version}) {distribution}; {urgency}\n\n"
|
|
558
|
+
f" * New release: version {new_upstream}\n\n"
|
|
559
|
+
f" -- {maintainer} {debian_date}\n\n"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
updated_content = new_entry + content
|
|
563
|
+
|
|
564
|
+
with open(self.file_path, "w", encoding="utf-8") as f:
|
|
565
|
+
f.write(updated_content)
|
|
566
|
+
|
|
567
|
+
|
|
375
568
|
class ChangelogVersionExtractor(VersionExtractor):
|
|
376
569
|
def __init__(self, file_path: Path, pattern: str = ""):
|
|
377
570
|
super().__init__(file_path)
|
|
@@ -393,6 +586,8 @@ class ChangelogVersionExtractor(VersionExtractor):
|
|
|
393
586
|
|
|
394
587
|
today = datetime.date.today().isoformat()
|
|
395
588
|
|
|
589
|
+
# Insert new release section
|
|
590
|
+
|
|
396
591
|
pattern = r"^## \[Unreleased\]"
|
|
397
592
|
if not re.search(pattern, content, re.MULTILINE):
|
|
398
593
|
console.print(
|
|
@@ -404,12 +599,221 @@ class ChangelogVersionExtractor(VersionExtractor):
|
|
|
404
599
|
|
|
405
600
|
new_content = re.sub(pattern, replacement, content, count=1, flags=re.MULTILINE)
|
|
406
601
|
|
|
602
|
+
# Add new release link and update unreleased link
|
|
603
|
+
|
|
604
|
+
pattern = r"^\[Unreleased\]: (https://.+)/(v.+)\.\.\.HEAD$"
|
|
605
|
+
|
|
606
|
+
if match := re.search(pattern, new_content, re.MULTILINE):
|
|
607
|
+
url, old_version = match.groups()
|
|
608
|
+
replacement = f"[Unreleased]: {url}/v{new_version}...HEAD\n[{new_version}]: {url}/{old_version}...v{new_version}"
|
|
609
|
+
new_content = re.sub(pattern, replacement, new_content, flags=re.MULTILINE)
|
|
610
|
+
else:
|
|
611
|
+
console.print(
|
|
612
|
+
f"[{STYLE_WARNING}]Warning: Link definitions in CHANGELOG.md can't be updated automatically.[/{STYLE_WARNING}]"
|
|
613
|
+
)
|
|
614
|
+
|
|
407
615
|
with open(self.file_path, "w", encoding="utf-8") as f:
|
|
408
616
|
f.write(new_content)
|
|
409
617
|
|
|
410
|
-
console.print(
|
|
411
|
-
|
|
618
|
+
console.print(f"[{STYLE_INFO}]Updated CHANGELOG.md.[/{STYLE_INFO}]")
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def git_ignored_abs_dirs(repo_dir: Path) -> set:
|
|
622
|
+
"""Absolute paths of git-ignored directories for the repo containing ``repo_dir``.
|
|
623
|
+
|
|
624
|
+
Runs ``git ls-files --directory`` from ``repo_dir`` so git resolves the
|
|
625
|
+
enclosing repository itself; paths are reported relative to ``repo_dir`` and
|
|
626
|
+
resolved to absolute Paths. Empty when ``repo_dir`` is not inside a repo.
|
|
627
|
+
|
|
628
|
+
Used to prune ignored dirs (e.g. build/, output/) during discovery instead
|
|
629
|
+
of walking into them.
|
|
630
|
+
"""
|
|
631
|
+
try:
|
|
632
|
+
result = subprocess.run(
|
|
633
|
+
[
|
|
634
|
+
"git",
|
|
635
|
+
"ls-files",
|
|
636
|
+
"--others",
|
|
637
|
+
"--ignored",
|
|
638
|
+
"--exclude-standard",
|
|
639
|
+
"--directory",
|
|
640
|
+
"-z",
|
|
641
|
+
],
|
|
642
|
+
cwd=repo_dir,
|
|
643
|
+
capture_output=True,
|
|
644
|
+
text=True,
|
|
412
645
|
)
|
|
646
|
+
except (FileNotFoundError, OSError):
|
|
647
|
+
return set()
|
|
648
|
+
|
|
649
|
+
if result.returncode != 0:
|
|
650
|
+
return set()
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
(repo_dir / entry.rstrip("/")).resolve()
|
|
654
|
+
for entry in result.stdout.split("\0")
|
|
655
|
+
if entry
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def drop_git_ignored(root_dir: Path, paths: List[Path]) -> List[Path]:
|
|
660
|
+
"""Drop paths that git ignores, honoring each path's own enclosing repo.
|
|
661
|
+
|
|
662
|
+
Candidates may live in independent nested git repositories even when
|
|
663
|
+
``root_dir`` is not itself a repo (e.g. a plain folder holding several
|
|
664
|
+
checkouts). Anchoring a single ``git check-ignore`` at ``root_dir`` would
|
|
665
|
+
then bail out entirely and leak ignored build trees like ``output/`` into
|
|
666
|
+
the results. Instead, resolve the repository that actually contains each
|
|
667
|
+
candidate and run ``git check-ignore`` there. Paths outside any repo are
|
|
668
|
+
always kept.
|
|
669
|
+
"""
|
|
670
|
+
if not paths:
|
|
671
|
+
return paths
|
|
672
|
+
|
|
673
|
+
# Group candidates by the git repo top-level that contains them.
|
|
674
|
+
groups: Dict[Path, List[Path]] = {}
|
|
675
|
+
for p in paths:
|
|
676
|
+
ok, top = run_git_command(["rev-parse", "--show-toplevel"], p.parent)
|
|
677
|
+
if not ok or not top:
|
|
678
|
+
continue # not inside any repo -> always kept
|
|
679
|
+
groups.setdefault(Path(top).resolve(), []).append(p)
|
|
680
|
+
|
|
681
|
+
ignored: set = set()
|
|
682
|
+
for top_path, repo_paths in groups.items():
|
|
683
|
+
rel = [str(p.resolve().relative_to(top_path)) for p in repo_paths]
|
|
684
|
+
try:
|
|
685
|
+
result = subprocess.run(
|
|
686
|
+
["git", "check-ignore", "-z", "--stdin"],
|
|
687
|
+
cwd=top_path,
|
|
688
|
+
input="\0".join(rel) + "\0",
|
|
689
|
+
capture_output=True,
|
|
690
|
+
text=True,
|
|
691
|
+
)
|
|
692
|
+
except (FileNotFoundError, OSError):
|
|
693
|
+
continue # keep this repo's candidates on git failure
|
|
694
|
+
|
|
695
|
+
# 0 = some ignored, 1 = none ignored; anything else is an error, keep all.
|
|
696
|
+
if result.returncode not in (0, 1):
|
|
697
|
+
continue
|
|
698
|
+
|
|
699
|
+
ignored_rel = {chunk for chunk in result.stdout.split("\0") if chunk}
|
|
700
|
+
ignored.update(p for p, r in zip(repo_paths, rel) if r in ignored_rel)
|
|
701
|
+
|
|
702
|
+
return [p for p in paths if p not in ignored]
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def git_submodule_dirs(root_dir: Path) -> set:
|
|
706
|
+
"""Absolute paths of git submodule working trees (empty outside a git repo)."""
|
|
707
|
+
ok, top = run_git_command(["rev-parse", "--show-toplevel"], root_dir)
|
|
708
|
+
if not ok or not top:
|
|
709
|
+
return set()
|
|
710
|
+
top = Path(top)
|
|
711
|
+
|
|
712
|
+
ok, out = run_git_command(
|
|
713
|
+
["config", "--file", ".gitmodules", "-z", "--get-regexp", "path"], top
|
|
714
|
+
)
|
|
715
|
+
if not ok or not out:
|
|
716
|
+
return set()
|
|
717
|
+
|
|
718
|
+
dirs = set()
|
|
719
|
+
for record in out.split("\0"):
|
|
720
|
+
key, _, value = record.partition("\n")
|
|
721
|
+
if key.endswith(".path") and value:
|
|
722
|
+
dirs.add((top / value).resolve())
|
|
723
|
+
return dirs
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def drop_submodules(root_dir: Path, paths: List[Path]) -> List[Path]:
|
|
727
|
+
"""Drop paths inside git submodules (they are versioned independently)."""
|
|
728
|
+
if not paths:
|
|
729
|
+
return paths
|
|
730
|
+
subdirs = git_submodule_dirs(root_dir)
|
|
731
|
+
if not subdirs:
|
|
732
|
+
return paths
|
|
733
|
+
kept = []
|
|
734
|
+
for p in paths:
|
|
735
|
+
resolved = p.resolve()
|
|
736
|
+
if any(resolved == s or s in resolved.parents for s in subdirs):
|
|
737
|
+
continue
|
|
738
|
+
kept.append(p)
|
|
739
|
+
return kept
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def discover_package_roots(root_dir: Path) -> List[Path]:
|
|
743
|
+
"""Discover nested ROS package roots (dirs with a package.xml, root excluded).
|
|
744
|
+
|
|
745
|
+
Skips hidden directories (.git, .pixi, ...), git submodules, and — inside a
|
|
746
|
+
git repo — anything git ignores (.gitignore, global excludes), like `fd`.
|
|
747
|
+
|
|
748
|
+
Hidden and ignored directories are pruned during the walk so we never
|
|
749
|
+
descend into large trees like `.venv` or `build/`. Nested repositories are
|
|
750
|
+
handled too: each one's ignore rules are learned when its top-level is
|
|
751
|
+
reached, so an ignored `output/` build tree is pruned even when `root_dir`
|
|
752
|
+
itself is not a git repo.
|
|
753
|
+
"""
|
|
754
|
+
# Seed from root_dir's own repo (covers root being, or living inside, a repo).
|
|
755
|
+
ignored_abs = git_ignored_abs_dirs(root_dir)
|
|
756
|
+
candidates = []
|
|
757
|
+
for dirpath, dirnames, filenames in os.walk(root_dir):
|
|
758
|
+
current = Path(dirpath)
|
|
759
|
+
# A nested repo brings its own .gitignore; learn what it ignores before
|
|
760
|
+
# descending, so its build trees are pruned regardless of root_dir.
|
|
761
|
+
if current != root_dir and (current / ".git").exists():
|
|
762
|
+
ignored_abs |= git_ignored_abs_dirs(current)
|
|
763
|
+
kept = []
|
|
764
|
+
for d in dirnames:
|
|
765
|
+
if d.startswith("."):
|
|
766
|
+
continue
|
|
767
|
+
if ignored_abs and (current / d).resolve() in ignored_abs:
|
|
768
|
+
continue
|
|
769
|
+
kept.append(d)
|
|
770
|
+
dirnames[:] = kept
|
|
771
|
+
if current != root_dir and "package.xml" in filenames:
|
|
772
|
+
candidates.append(current / "package.xml")
|
|
773
|
+
candidates = drop_git_ignored(root_dir, candidates)
|
|
774
|
+
candidates = drop_submodules(root_dir, candidates)
|
|
775
|
+
return sorted({package_xml.parent for package_xml in candidates})
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def build_root_checks(root_dir: Path) -> List[VersionExtractor]:
|
|
779
|
+
"""Build the version extractors tracked at the repository root."""
|
|
780
|
+
return [
|
|
781
|
+
XmlVersionExtractor(root_dir / "package.xml"),
|
|
782
|
+
TomlVersionExtractor(root_dir / "pyproject.toml", ["project", "version"]),
|
|
783
|
+
ChangelogVersionExtractor(root_dir / "CHANGELOG.md", r""),
|
|
784
|
+
TomlVersionExtractor(root_dir / "pixi.toml", ["workspace", "version"]),
|
|
785
|
+
YamlVersionExtractor(root_dir / "CITATION.cff", ["version"]),
|
|
786
|
+
CMakeListsVersionExtractor(root_dir / "CMakeLists.txt"),
|
|
787
|
+
DebianChangelogVersionExtractor(root_dir / "debian/changelog"),
|
|
788
|
+
ConanfileVersionExtractor(root_dir / "conanfile.py"),
|
|
789
|
+
]
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def collect_version_checks(root_dir: Path) -> List[VersionExtractor]:
|
|
793
|
+
"""Root checks plus the same set of checks for each nested package.
|
|
794
|
+
|
|
795
|
+
Each nested package is treated exactly like the repository root: the same
|
|
796
|
+
file/key combinations are checked, and any that don't exist in the nested
|
|
797
|
+
package directory are simply skipped (VersionExtractor.check_file_exists()).
|
|
798
|
+
"""
|
|
799
|
+
checks = list(build_root_checks(root_dir))
|
|
800
|
+
seen_paths = {check.file_path for check in checks}
|
|
801
|
+
|
|
802
|
+
for package_root in discover_package_roots(root_dir):
|
|
803
|
+
for check in build_root_checks(package_root):
|
|
804
|
+
if check.file_path in seen_paths:
|
|
805
|
+
continue
|
|
806
|
+
checks.append(check)
|
|
807
|
+
seen_paths.add(check.file_path)
|
|
808
|
+
|
|
809
|
+
# Label nested files by their relative path so identical basenames stay distinct.
|
|
810
|
+
for check in checks:
|
|
811
|
+
try:
|
|
812
|
+
check.label = str(check.file_path.relative_to(root_dir))
|
|
813
|
+
except ValueError:
|
|
814
|
+
check.label = check.file_path.name
|
|
815
|
+
|
|
816
|
+
return checks
|
|
413
817
|
|
|
414
818
|
|
|
415
819
|
def validate_semver(version: str) -> str:
|
|
@@ -444,22 +848,25 @@ def bump_version(version: str, bump_type: str) -> str:
|
|
|
444
848
|
raise ValueError(f"Invalid bump type: {bump_type}")
|
|
445
849
|
|
|
446
850
|
|
|
447
|
-
def
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
errors
|
|
451
|
-
|
|
851
|
+
def collect_versions(
|
|
852
|
+
checks: List[VersionExtractor],
|
|
853
|
+
) -> Tuple[set, List[str]]:
|
|
854
|
+
"""Collect distinct versions and parse errors across all files. No console output."""
|
|
855
|
+
versions: set = set()
|
|
856
|
+
errors: List[str] = []
|
|
452
857
|
for check in checks:
|
|
453
858
|
if check.check_file_exists():
|
|
454
859
|
try:
|
|
455
|
-
|
|
456
|
-
versions_found.add(version)
|
|
860
|
+
versions.add(check.get_version())
|
|
457
861
|
except VersionNotPresent:
|
|
458
862
|
pass # file exists but has no version configured; skip
|
|
459
863
|
except Exception as e:
|
|
460
|
-
errors.append(f"{check.
|
|
864
|
+
errors.append(f"{check.label}: {e}")
|
|
865
|
+
return versions, errors
|
|
461
866
|
|
|
462
|
-
|
|
867
|
+
|
|
868
|
+
def report_version_problems(versions: set, errors: List[str]) -> None:
|
|
869
|
+
"""Print parse warnings and version-mismatch errors from collect_versions()."""
|
|
463
870
|
if errors:
|
|
464
871
|
console.print(
|
|
465
872
|
f"[{STYLE_WARNING}]Warning: Failed to parse version from some files:[/{STYLE_WARNING}]"
|
|
@@ -467,21 +874,25 @@ def get_current_version(checks: List[VersionExtractor]) -> Optional[str]:
|
|
|
467
874
|
for error in errors:
|
|
468
875
|
console.print(f" [{STYLE_MUTED}]• {error}[/{STYLE_MUTED}]")
|
|
469
876
|
|
|
470
|
-
if len(
|
|
471
|
-
return list(versions_found)[0]
|
|
472
|
-
elif len(versions_found) > 1:
|
|
877
|
+
if len(versions) > 1:
|
|
473
878
|
console.print(
|
|
474
|
-
f"[{
|
|
879
|
+
f"[{STYLE_INFO}]Tip:[/{STYLE_INFO}] use --update-version X.Y.Z to set a single version across all files."
|
|
475
880
|
)
|
|
476
881
|
console.print(
|
|
477
|
-
f"[{
|
|
882
|
+
f"[{STYLE_ERROR}]Error: version mismatch — multiple versions found: "
|
|
883
|
+
f"{', '.join(sorted(versions))}[/{STYLE_ERROR}]"
|
|
478
884
|
)
|
|
479
|
-
|
|
480
|
-
else:
|
|
885
|
+
elif not versions:
|
|
481
886
|
console.print(
|
|
482
887
|
f"[{STYLE_ERROR}]Error: No version found in any files.[/{STYLE_ERROR}]"
|
|
483
888
|
)
|
|
484
|
-
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def get_current_version(checks: List[VersionExtractor]) -> Optional[str]:
|
|
892
|
+
"""Consensus version across all files, reporting any problems."""
|
|
893
|
+
versions, errors = collect_versions(checks)
|
|
894
|
+
report_version_problems(versions, errors)
|
|
895
|
+
return next(iter(versions)) if len(versions) == 1 else None
|
|
485
896
|
|
|
486
897
|
|
|
487
898
|
def infer_change_type(
|
|
@@ -677,7 +1088,7 @@ def git_tag_version(
|
|
|
677
1088
|
custom_tag_name: Optional[str] = None,
|
|
678
1089
|
custom_tag_message: Optional[str] = None,
|
|
679
1090
|
sign: bool = False,
|
|
680
|
-
) -> bool:
|
|
1091
|
+
) -> Tuple[bool, str]:
|
|
681
1092
|
"""Create a git tag for the version.
|
|
682
1093
|
|
|
683
1094
|
When ``sign`` is True the tag is GPG-signed (``git tag -s``); otherwise an
|
|
@@ -688,7 +1099,7 @@ def git_tag_version(
|
|
|
688
1099
|
console.print(
|
|
689
1100
|
f"[{STYLE_WARNING}]Not a git repository, skipping git tag.[/{STYLE_WARNING}]"
|
|
690
1101
|
)
|
|
691
|
-
return False
|
|
1102
|
+
return False, ""
|
|
692
1103
|
|
|
693
1104
|
tag_name = (
|
|
694
1105
|
custom_tag_name.format(version=version) if custom_tag_name else f"v{version}"
|
|
@@ -704,7 +1115,7 @@ def git_tag_version(
|
|
|
704
1115
|
console.print(
|
|
705
1116
|
f"[{STYLE_WARNING}]Tag {tag_name} already exists.[/{STYLE_WARNING}]"
|
|
706
1117
|
)
|
|
707
|
-
return False
|
|
1118
|
+
return False, ""
|
|
708
1119
|
|
|
709
1120
|
if not auto_confirm:
|
|
710
1121
|
confirmed = Confirm.ask(
|
|
@@ -723,14 +1134,113 @@ def git_tag_version(
|
|
|
723
1134
|
)
|
|
724
1135
|
if success:
|
|
725
1136
|
console.print(f"[{STYLE_SUCCESS}]✓ Created tag: {tag_name}[/{STYLE_SUCCESS}]")
|
|
726
|
-
|
|
727
|
-
f"[{STYLE_MUTED}] To push: git push origin {tag_name}[/{STYLE_MUTED}]"
|
|
728
|
-
)
|
|
729
|
-
return True
|
|
1137
|
+
return True, tag_name
|
|
730
1138
|
else:
|
|
731
1139
|
console.print(f"[{STYLE_ERROR}]Failed to create tag: {output}[/{STYLE_ERROR}]")
|
|
1140
|
+
return False, ""
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def push_tag(root_dir: Path, tag_name: str, project_url: str) -> bool:
|
|
1144
|
+
url = project_url.replace(GITHUB_URL, "git@github.com:")
|
|
1145
|
+
git_args = ["push", url, tag_name]
|
|
1146
|
+
console.print(f"[{STYLE_MUTED}]$ git {' '.join(git_args)}")
|
|
1147
|
+
success, output = run_git_command(git_args, cwd=root_dir)
|
|
1148
|
+
if success:
|
|
1149
|
+
console.print(f"[{STYLE_SUCCESS}]✓ Pushed tag: {tag_name}[/{STYLE_SUCCESS}]")
|
|
1150
|
+
else:
|
|
1151
|
+
console.print(f"[{STYLE_ERROR}]Failed to push tag: {output}[/{STYLE_ERROR}]")
|
|
1152
|
+
return success
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def git_archive(
|
|
1156
|
+
root_dir: Path, archive_name: str, tag_name: str, sign: bool = False
|
|
1157
|
+
) -> bool:
|
|
1158
|
+
"""Create a .tar.gz archive from git for the version.
|
|
1159
|
+
|
|
1160
|
+
When ``sign`` is True, a detached GPG .tar.gz.sig file is added.
|
|
1161
|
+
"""
|
|
1162
|
+
git_args = ["archive", "--format", "tgz", "--output", archive_name, tag_name]
|
|
1163
|
+
console.print(f"[{STYLE_MUTED}]$ git {' '.join(git_args)}")
|
|
1164
|
+
success, output = run_git_command(git_args, cwd=root_dir)
|
|
1165
|
+
if not success:
|
|
1166
|
+
console.print(
|
|
1167
|
+
f"[{STYLE_ERROR}]Failed to create archive: {output}[/{STYLE_ERROR}]"
|
|
1168
|
+
)
|
|
732
1169
|
return False
|
|
733
1170
|
|
|
1171
|
+
if sign:
|
|
1172
|
+
call = [
|
|
1173
|
+
"gpg",
|
|
1174
|
+
"--detach-sign",
|
|
1175
|
+
"--armor",
|
|
1176
|
+
"-o",
|
|
1177
|
+
f"{archive_name}.sig",
|
|
1178
|
+
archive_name,
|
|
1179
|
+
]
|
|
1180
|
+
console.print(f"[{STYLE_MUTED}]$ {' '.join(call)}")
|
|
1181
|
+
result = subprocess.run(call, cwd=root_dir, capture_output=True, text=True)
|
|
1182
|
+
if result.returncode != 0:
|
|
1183
|
+
console.print(
|
|
1184
|
+
f"[{STYLE_ERROR}]Failed to sign archive: {result.stderr}[/{STYLE_ERROR}]"
|
|
1185
|
+
)
|
|
1186
|
+
return False
|
|
1187
|
+
|
|
1188
|
+
return True
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def gh_release(
|
|
1192
|
+
root_dir: Path,
|
|
1193
|
+
target_version: str,
|
|
1194
|
+
project_url: str,
|
|
1195
|
+
archive_name: str | None = None,
|
|
1196
|
+
sign: bool = False,
|
|
1197
|
+
dry_run: bool = False,
|
|
1198
|
+
) -> Tuple[bool, str]:
|
|
1199
|
+
"""Create a github release and return (success, output)."""
|
|
1200
|
+
|
|
1201
|
+
title = f"Release v{target_version}"
|
|
1202
|
+
repo = project_url.removeprefix(GITHUB_URL)
|
|
1203
|
+
call = ["gh", "release", "create", "--title", title, "--repo", repo]
|
|
1204
|
+
|
|
1205
|
+
# include latest release notes
|
|
1206
|
+
changelog = root_dir / "CHANGELOG.md"
|
|
1207
|
+
if changelog.is_file():
|
|
1208
|
+
notes = changelog.read_text().split("\n## ")
|
|
1209
|
+
if len(notes) >= 3: # 0: header, 1: unreleased, 2: latest
|
|
1210
|
+
today = datetime.date.today().isoformat()
|
|
1211
|
+
call = [
|
|
1212
|
+
*call,
|
|
1213
|
+
"--notes",
|
|
1214
|
+
notes[1].replace("[Unreleased]", f"[{target_version}] - {today}")
|
|
1215
|
+
if dry_run
|
|
1216
|
+
else notes[2],
|
|
1217
|
+
]
|
|
1218
|
+
else:
|
|
1219
|
+
console.print(
|
|
1220
|
+
f"[{STYLE_WARNING}]Warning: No release notes available in CHANGELOG.md yet.[/{STYLE_WARNING}]"
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
call = [*call, f"v{target_version}"]
|
|
1224
|
+
|
|
1225
|
+
# include .tar.gz archive and .tar.gz.sig signature
|
|
1226
|
+
if archive_name is not None:
|
|
1227
|
+
call = [*call, archive_name]
|
|
1228
|
+
if sign:
|
|
1229
|
+
call = [*call, f"{archive_name}.sig"]
|
|
1230
|
+
|
|
1231
|
+
print_call = "$ '" + "' '".join(c.replace("'", "\\'") for c in call) + "'"
|
|
1232
|
+
if dry_run:
|
|
1233
|
+
return True, print_call
|
|
1234
|
+
|
|
1235
|
+
console.print(f"[{STYLE_MUTED}]$ {print_call}")
|
|
1236
|
+
try:
|
|
1237
|
+
result = subprocess.run(call, cwd=root_dir, capture_output=True, text=True)
|
|
1238
|
+
if result.returncode != 0:
|
|
1239
|
+
return False, result.stderr.strip()
|
|
1240
|
+
return True, result.stdout.strip()
|
|
1241
|
+
except subprocess.CalledProcessError as e:
|
|
1242
|
+
return False, e.stderr or ""
|
|
1243
|
+
|
|
734
1244
|
|
|
735
1245
|
def update_pixi_lock(root_dir: Path, dry_run: bool = False) -> Optional[str]:
|
|
736
1246
|
"""Update pixi.lock file by running 'pixi list'.
|
|
@@ -799,9 +1309,10 @@ def create_backups(file_paths: List[Path]) -> Dict[Path, Path]:
|
|
|
799
1309
|
backups = {}
|
|
800
1310
|
temp_dir = Path(tempfile.mkdtemp(prefix="release_backup_"))
|
|
801
1311
|
|
|
802
|
-
|
|
1312
|
+
# Index-prefix the name so files sharing a basename don't collide.
|
|
1313
|
+
for index, file_path in enumerate(file_paths):
|
|
803
1314
|
if file_path.exists():
|
|
804
|
-
backup_path = temp_dir / file_path.name
|
|
1315
|
+
backup_path = temp_dir / f"{index:04d}_{file_path.name}"
|
|
805
1316
|
shutil.copy2(file_path, backup_path)
|
|
806
1317
|
backups[file_path] = backup_path
|
|
807
1318
|
|
|
@@ -856,16 +1367,16 @@ def list_version_files(checks: List[VersionExtractor]) -> None:
|
|
|
856
1367
|
else f"[{STYLE_ERROR}]✗[/{STYLE_ERROR}]"
|
|
857
1368
|
)
|
|
858
1369
|
file_type = check.__class__.__name__.replace("VersionExtractor", "")
|
|
859
|
-
table.add_row(check.
|
|
1370
|
+
table.add_row(check.label, str(check.file_path), exists, file_type)
|
|
860
1371
|
|
|
861
1372
|
console.print(table)
|
|
862
1373
|
sys.exit(0)
|
|
863
1374
|
|
|
864
1375
|
|
|
865
|
-
def handle_check_version(checks: List[VersionExtractor], args) ->
|
|
1376
|
+
def handle_check_version(checks: List[VersionExtractor], args) -> bool:
|
|
866
1377
|
"""Handle the --check-version command.
|
|
867
1378
|
|
|
868
|
-
Returns
|
|
1379
|
+
Returns True if all files agree on one version, False otherwise.
|
|
869
1380
|
"""
|
|
870
1381
|
results = []
|
|
871
1382
|
versions_found = set()
|
|
@@ -878,7 +1389,7 @@ def handle_check_version(checks: List[VersionExtractor], args) -> int:
|
|
|
878
1389
|
|
|
879
1390
|
for check in checks:
|
|
880
1391
|
result = {
|
|
881
|
-
"file": check.
|
|
1392
|
+
"file": check.label,
|
|
882
1393
|
"version": None,
|
|
883
1394
|
"status": "Unknown",
|
|
884
1395
|
"message": "",
|
|
@@ -911,13 +1422,14 @@ def handle_check_version(checks: List[VersionExtractor], args) -> int:
|
|
|
911
1422
|
consensus_version = "MISMATCH"
|
|
912
1423
|
|
|
913
1424
|
if args.output_format == "json":
|
|
1425
|
+
consistent = not errors and len(versions_found) == 1
|
|
914
1426
|
out_payload = {
|
|
915
1427
|
"consensus_version": consensus_version,
|
|
916
1428
|
"files": results,
|
|
917
|
-
"consistent":
|
|
1429
|
+
"consistent": consistent,
|
|
918
1430
|
}
|
|
919
1431
|
print(json.dumps(out_payload, indent=2))
|
|
920
|
-
return
|
|
1432
|
+
return consistent
|
|
921
1433
|
|
|
922
1434
|
# Standard Rich table output
|
|
923
1435
|
table = Table(title="Version Check Summary", box=box.ROUNDED)
|
|
@@ -946,7 +1458,7 @@ def handle_check_version(checks: List[VersionExtractor], args) -> int:
|
|
|
946
1458
|
):
|
|
947
1459
|
version_display = f"[{STYLE_SUCCESS}]{res['version']}[/{STYLE_SUCCESS}]"
|
|
948
1460
|
elif consensus_version == "MISMATCH":
|
|
949
|
-
version_display = f"[{
|
|
1461
|
+
version_display = f"[{STYLE_ERROR}]{res['version']}[/{STYLE_ERROR}]"
|
|
950
1462
|
|
|
951
1463
|
table.add_row(res["file"], version_display, status_style, res["message"])
|
|
952
1464
|
|
|
@@ -956,27 +1468,53 @@ def handle_check_version(checks: List[VersionExtractor], args) -> int:
|
|
|
956
1468
|
if args.short and consensus_version and consensus_version != "MISMATCH":
|
|
957
1469
|
print(consensus_version)
|
|
958
1470
|
|
|
1471
|
+
tag_check_failed = False
|
|
1472
|
+
tag_format_is_valid = False
|
|
1473
|
+
if hasattr(args, "check_tag") and args.check_tag:
|
|
1474
|
+
if not args.check_tag.startswith("v"):
|
|
1475
|
+
tag_check_failed = True
|
|
1476
|
+
errors = True
|
|
1477
|
+
else:
|
|
1478
|
+
tag_version = args.check_tag.lstrip("v")
|
|
1479
|
+
if consensus_version and consensus_version != "MISMATCH":
|
|
1480
|
+
if consensus_version != tag_version:
|
|
1481
|
+
tag_check_failed = True
|
|
1482
|
+
tag_format_is_valid = True
|
|
1483
|
+
errors = True
|
|
1484
|
+
|
|
959
1485
|
if errors:
|
|
960
1486
|
if len(versions_found) > 1:
|
|
961
1487
|
console.print(
|
|
962
|
-
f"\n[{
|
|
1488
|
+
f"\n[{STYLE_INFO}]Tip:[/{STYLE_INFO}] use --update-version X.Y.Z to set a single version across all files."
|
|
963
1489
|
)
|
|
1490
|
+
console.print(
|
|
1491
|
+
f"[{STYLE_ERROR_STRONG}]FAILURE:[/{STYLE_ERROR_STRONG}] version mismatch — conflicting versions: {', '.join(sorted(versions_found))}"
|
|
1492
|
+
)
|
|
1493
|
+
elif tag_check_failed:
|
|
1494
|
+
if not tag_format_is_valid:
|
|
1495
|
+
console.print(
|
|
1496
|
+
f"\n[{STYLE_ERROR_STRONG}]FAILURE:[/{STYLE_ERROR_STRONG}] Tag version ({args.check_tag}) format is invalid. It should start with 'v' and be a semantic version."
|
|
1497
|
+
)
|
|
1498
|
+
else:
|
|
1499
|
+
console.print(
|
|
1500
|
+
f"\n[{STYLE_ERROR_STRONG}]FAILURE:[/{STYLE_ERROR_STRONG}] Tag version ({args.check_tag.lstrip('v')}) does not match consensus version ({consensus_version})."
|
|
1501
|
+
)
|
|
964
1502
|
else:
|
|
965
1503
|
console.print(
|
|
966
|
-
f"\n[{STYLE_ERROR_STRONG}]FAILURE:[/{STYLE_ERROR_STRONG}]
|
|
1504
|
+
f"\n[{STYLE_ERROR_STRONG}]FAILURE:[/{STYLE_ERROR_STRONG}] parsing errors encountered."
|
|
967
1505
|
)
|
|
968
|
-
return
|
|
1506
|
+
return False
|
|
969
1507
|
elif not versions_found:
|
|
970
1508
|
console.print(
|
|
971
1509
|
f"\n[{STYLE_ERROR_STRONG}]FAILURE:[/{STYLE_ERROR_STRONG}] No version files found in {args.root}."
|
|
972
1510
|
)
|
|
973
|
-
return
|
|
1511
|
+
return False
|
|
974
1512
|
else:
|
|
975
1513
|
if not args.short:
|
|
976
1514
|
console.print(
|
|
977
1515
|
f"\n[{STYLE_SUCCESS_STRONG}]SUCCESS:[/{STYLE_SUCCESS_STRONG}] All files match version [{STYLE_SUCCESS}]{consensus_version}[/{STYLE_SUCCESS}]."
|
|
978
1516
|
)
|
|
979
|
-
return
|
|
1517
|
+
return True
|
|
980
1518
|
|
|
981
1519
|
|
|
982
1520
|
def perform_version_updates(
|
|
@@ -999,7 +1537,7 @@ def perform_version_updates(
|
|
|
999
1537
|
try:
|
|
1000
1538
|
if dry_run:
|
|
1001
1539
|
curr = check.get_version()
|
|
1002
|
-
dry_run_rows.append((check.
|
|
1540
|
+
dry_run_rows.append((check.label, curr, target_version))
|
|
1003
1541
|
else:
|
|
1004
1542
|
try:
|
|
1005
1543
|
old_version = check.get_version()
|
|
@@ -1008,20 +1546,20 @@ def perform_version_updates(
|
|
|
1008
1546
|
except Exception:
|
|
1009
1547
|
old_version = "?"
|
|
1010
1548
|
check.update_version(target_version)
|
|
1011
|
-
dry_run_rows.append((check.
|
|
1549
|
+
dry_run_rows.append((check.label, old_version, target_version))
|
|
1012
1550
|
line = Text()
|
|
1013
|
-
line.append(f" {check.
|
|
1551
|
+
line.append(f" {check.label:<28}", style="cyan")
|
|
1014
1552
|
line.append(old_version, style=STYLE_OLD_VALUE)
|
|
1015
1553
|
line.append(" → ", style="dim")
|
|
1016
1554
|
line.append(target_version, style=STYLE_NEW_VALUE)
|
|
1017
1555
|
console.print(line)
|
|
1018
|
-
updated_files.append(check.
|
|
1556
|
+
updated_files.append(check.label)
|
|
1019
1557
|
updated_file_paths.append(str(check.file_path))
|
|
1020
1558
|
except VersionNotPresent:
|
|
1021
1559
|
pass # file exists but has no version configured; skip
|
|
1022
1560
|
except Exception as e:
|
|
1023
1561
|
console.print(
|
|
1024
|
-
f"[{STYLE_ERROR}]Failed to update {check.
|
|
1562
|
+
f"[{STYLE_ERROR}]Failed to update {check.label}: {e}[/{STYLE_ERROR}]"
|
|
1025
1563
|
)
|
|
1026
1564
|
if not dry_run:
|
|
1027
1565
|
failed = True
|
|
@@ -1044,14 +1582,14 @@ def show_dry_run_panel(
|
|
|
1044
1582
|
console.print(f" [dim]{'─' * 44}[/dim]")
|
|
1045
1583
|
for name, old, new in dry_run_rows:
|
|
1046
1584
|
line = Text()
|
|
1047
|
-
line.append(f" {name:<
|
|
1585
|
+
line.append(f" {name:<28}", style="cyan")
|
|
1048
1586
|
line.append(old, style=STYLE_OLD_VALUE)
|
|
1049
1587
|
line.append(" → ", style="dim")
|
|
1050
1588
|
line.append(new, style=STYLE_NEW_VALUE)
|
|
1051
1589
|
console.print(line)
|
|
1052
1590
|
if pixi_lock_would_update:
|
|
1053
1591
|
line = Text()
|
|
1054
|
-
line.append(f" {'pixi.lock':<
|
|
1592
|
+
line.append(f" {'pixi.lock':<28}", style="cyan")
|
|
1055
1593
|
line.append("regenerated via pixi list", style="dim")
|
|
1056
1594
|
console.print(line)
|
|
1057
1595
|
|
|
@@ -1070,7 +1608,7 @@ def show_result_panel(
|
|
|
1070
1608
|
"""Display a polished summary of completed version updates."""
|
|
1071
1609
|
if pixi_lock_updated:
|
|
1072
1610
|
line = Text()
|
|
1073
|
-
line.append(f" {'pixi.lock':<
|
|
1611
|
+
line.append(f" {'pixi.lock':<28}", style="cyan")
|
|
1074
1612
|
line.append("regenerated via pixi list", style="dim")
|
|
1075
1613
|
console.print(line)
|
|
1076
1614
|
console.print()
|
|
@@ -1164,6 +1702,28 @@ def main():
|
|
|
1164
1702
|
action="store_true",
|
|
1165
1703
|
help="GPG-sign the git tag (uses 'git tag -s' instead of '-a'). Requires a configured signing key (git config user.signingkey). Only meaningful with --git-tag.",
|
|
1166
1704
|
)
|
|
1705
|
+
parser.add_argument(
|
|
1706
|
+
"--push-tag",
|
|
1707
|
+
action="store_true",
|
|
1708
|
+
help="Push the tag to the main repository. Only works with github for now.",
|
|
1709
|
+
)
|
|
1710
|
+
|
|
1711
|
+
parser.add_argument(
|
|
1712
|
+
"--git-archive",
|
|
1713
|
+
action="store_true",
|
|
1714
|
+
help="Create a .tar.gz archive of the project with git",
|
|
1715
|
+
)
|
|
1716
|
+
parser.add_argument(
|
|
1717
|
+
"--sign-archive",
|
|
1718
|
+
action="store_true",
|
|
1719
|
+
help="GPG-sign the git archive. Requires a configured signing key. Only meaningful with --git-archive.",
|
|
1720
|
+
)
|
|
1721
|
+
parser.add_argument(
|
|
1722
|
+
"--gh-release",
|
|
1723
|
+
action="store_true",
|
|
1724
|
+
help="Create Github release. Requires a configured gh cli.",
|
|
1725
|
+
)
|
|
1726
|
+
|
|
1167
1727
|
parser.add_argument(
|
|
1168
1728
|
"--short",
|
|
1169
1729
|
action="store_true",
|
|
@@ -1177,6 +1737,11 @@ def main():
|
|
|
1177
1737
|
)
|
|
1178
1738
|
|
|
1179
1739
|
group = parser.add_mutually_exclusive_group(required=True)
|
|
1740
|
+
group.add_argument(
|
|
1741
|
+
"--version",
|
|
1742
|
+
action="store_true",
|
|
1743
|
+
help="Show jrl-cmakemodules-scripts version and exit",
|
|
1744
|
+
)
|
|
1180
1745
|
group.add_argument(
|
|
1181
1746
|
"--check-version", action="store_true", help="Check versions across files."
|
|
1182
1747
|
)
|
|
@@ -1196,7 +1761,18 @@ def main():
|
|
|
1196
1761
|
help="Bump the project version.",
|
|
1197
1762
|
)
|
|
1198
1763
|
|
|
1764
|
+
parser.add_argument(
|
|
1765
|
+
"--check-tag",
|
|
1766
|
+
type=str,
|
|
1767
|
+
help="Check that the consensus version matches the given tag (e.g., v2.15.0). Only meaningful with --check-version.",
|
|
1768
|
+
)
|
|
1769
|
+
|
|
1199
1770
|
args = parser.parse_args()
|
|
1771
|
+
|
|
1772
|
+
if args.version:
|
|
1773
|
+
print(importlib.metadata.version("jrl_cmakemodules_scripts"))
|
|
1774
|
+
sys.exit(0)
|
|
1775
|
+
|
|
1200
1776
|
root_dir = args.root
|
|
1201
1777
|
|
|
1202
1778
|
# Redirect console output to stderr for clean stdout with json/short
|
|
@@ -1206,6 +1782,9 @@ def main():
|
|
|
1206
1782
|
else:
|
|
1207
1783
|
console = Console()
|
|
1208
1784
|
|
|
1785
|
+
if args.check_tag and not args.check_version:
|
|
1786
|
+
parser.error("--check-tag requires --check-version")
|
|
1787
|
+
|
|
1209
1788
|
if args.update_version:
|
|
1210
1789
|
try:
|
|
1211
1790
|
if not re.match(r"^\d+\.\d+\.\d+$", args.update_version):
|
|
@@ -1219,14 +1798,7 @@ def main():
|
|
|
1219
1798
|
)
|
|
1220
1799
|
sys.exit(1)
|
|
1221
1800
|
|
|
1222
|
-
checks: List[VersionExtractor] =
|
|
1223
|
-
XmlVersionExtractor(root_dir / "package.xml"),
|
|
1224
|
-
TomlVersionExtractor(root_dir / "pyproject.toml", ["project", "version"]),
|
|
1225
|
-
ChangelogVersionExtractor(root_dir / "CHANGELOG.md", r""),
|
|
1226
|
-
TomlVersionExtractor(root_dir / "pixi.toml", ["workspace", "version"]),
|
|
1227
|
-
YamlVersionExtractor(root_dir / "CITATION.cff", ["version"]),
|
|
1228
|
-
CMakeListsVersionExtractor(root_dir / "CMakeLists.txt"),
|
|
1229
|
-
]
|
|
1801
|
+
checks: List[VersionExtractor] = collect_version_checks(root_dir)
|
|
1230
1802
|
|
|
1231
1803
|
if args.list_files:
|
|
1232
1804
|
if args.output_format == "json":
|
|
@@ -1234,7 +1806,7 @@ def main():
|
|
|
1234
1806
|
for check in checks:
|
|
1235
1807
|
files_list.append(
|
|
1236
1808
|
{
|
|
1237
|
-
"name": check.
|
|
1809
|
+
"name": check.label,
|
|
1238
1810
|
"path": str(check.file_path),
|
|
1239
1811
|
"exists": check.check_file_exists(),
|
|
1240
1812
|
"type": check.__class__.__name__.replace(
|
|
@@ -1248,14 +1820,15 @@ def main():
|
|
|
1248
1820
|
sys.exit(0)
|
|
1249
1821
|
|
|
1250
1822
|
if args.check_version:
|
|
1251
|
-
sys.exit(handle_check_version(checks, args))
|
|
1823
|
+
sys.exit(0 if handle_check_version(checks, args) else 1)
|
|
1252
1824
|
|
|
1253
1825
|
current_version = None
|
|
1254
1826
|
new_version_str = None
|
|
1255
1827
|
|
|
1256
1828
|
if args.update_version:
|
|
1257
1829
|
new_version_str = args.update_version
|
|
1258
|
-
|
|
1830
|
+
versions, _ = collect_versions(checks)
|
|
1831
|
+
current_version = next(iter(versions)) if len(versions) == 1 else None
|
|
1259
1832
|
if not args.dry_run:
|
|
1260
1833
|
console.print(
|
|
1261
1834
|
f"[{STYLE_INFO}]Updating versions to {new_version_str} in {root_dir}...[/{STYLE_INFO}]"
|
|
@@ -1370,6 +1943,22 @@ def main():
|
|
|
1370
1943
|
elif args.short:
|
|
1371
1944
|
print(target_version)
|
|
1372
1945
|
|
|
1946
|
+
project_url = ""
|
|
1947
|
+
archive_name = ""
|
|
1948
|
+
if args.push_tag:
|
|
1949
|
+
for check in checks:
|
|
1950
|
+
if url := check.get_url():
|
|
1951
|
+
project_url = url
|
|
1952
|
+
break
|
|
1953
|
+
else:
|
|
1954
|
+
where = "CMakeLists.txt, package.xml, or pyproject.toml"
|
|
1955
|
+
console.print(
|
|
1956
|
+
f"[{STYLE_ERROR_STRONG}]Error: can't push tag without a github homepage in {where}.[/{STYLE_ERROR_STRONG}]"
|
|
1957
|
+
)
|
|
1958
|
+
sys.exit(1)
|
|
1959
|
+
if args.git_archive:
|
|
1960
|
+
archive_name = f"{project_url.split('/')[-1]}-{target_version}.tar.gz"
|
|
1961
|
+
|
|
1373
1962
|
if args.dry_run:
|
|
1374
1963
|
pixi_lock_would_update = (root_dir / "pixi.lock").exists()
|
|
1375
1964
|
|
|
@@ -1388,6 +1977,7 @@ def main():
|
|
|
1388
1977
|
)
|
|
1389
1978
|
git_lines.append(f"$ git add {' '.join(rel_paths) if rel_paths else '-u'}")
|
|
1390
1979
|
git_lines.append(f"$ git commit -m '{commit_message}'")
|
|
1980
|
+
|
|
1391
1981
|
if args.git_tag is not None:
|
|
1392
1982
|
custom_tag_name = None if args.git_tag is True else args.git_tag
|
|
1393
1983
|
tag_name = (
|
|
@@ -1402,6 +1992,30 @@ def main():
|
|
|
1402
1992
|
)
|
|
1403
1993
|
tag_flag = "-s" if args.sign_tag else "-a"
|
|
1404
1994
|
git_lines.append(f"$ git tag {tag_flag} {tag_name} -m '{tag_message}'")
|
|
1995
|
+
if args.push_tag:
|
|
1996
|
+
url = project_url.replace(GITHUB_URL, "git@github.com:")
|
|
1997
|
+
git_lines.append(f"$ git push {url} {tag_name}")
|
|
1998
|
+
|
|
1999
|
+
if args.git_archive:
|
|
2000
|
+
git_lines.append(
|
|
2001
|
+
f"$ git archive --format tgz --output {archive_name} {tag_name}"
|
|
2002
|
+
)
|
|
2003
|
+
|
|
2004
|
+
if args.sign_archive:
|
|
2005
|
+
git_lines.append(
|
|
2006
|
+
f"$ gpg --detach-sign --armor -o {archive_name}.sig {archive_name}"
|
|
2007
|
+
)
|
|
2008
|
+
|
|
2009
|
+
if args.gh_release:
|
|
2010
|
+
_, call = gh_release(
|
|
2011
|
+
root_dir,
|
|
2012
|
+
target_version,
|
|
2013
|
+
project_url,
|
|
2014
|
+
archive_name,
|
|
2015
|
+
args.sign_archive,
|
|
2016
|
+
dry_run=True,
|
|
2017
|
+
)
|
|
2018
|
+
git_lines.append(call)
|
|
1405
2019
|
|
|
1406
2020
|
show_dry_run_panel(dry_run_rows, pixi_lock_would_update, git_lines)
|
|
1407
2021
|
sys.exit(0)
|
|
@@ -1420,6 +2034,31 @@ def main():
|
|
|
1420
2034
|
f"[{STYLE_WARNING}]Warning: --sign-tag has no effect without --git-tag.[/{STYLE_WARNING}]"
|
|
1421
2035
|
)
|
|
1422
2036
|
|
|
2037
|
+
if args.gh_release and not args.push_tag:
|
|
2038
|
+
console.print(
|
|
2039
|
+
f"[{STYLE_WARNING}]Warning: --gh-release has no effect without --push-tag.[/{STYLE_WARNING}]"
|
|
2040
|
+
)
|
|
2041
|
+
|
|
2042
|
+
if args.sign_archive and args.git_archive is None:
|
|
2043
|
+
console.print(
|
|
2044
|
+
f"[{STYLE_WARNING}]Warning: --sign-archive has no effect without --git-archive.[/{STYLE_WARNING}]"
|
|
2045
|
+
)
|
|
2046
|
+
|
|
2047
|
+
if args.git_archive and (root_dir / archive_name).is_file():
|
|
2048
|
+
console.print(
|
|
2049
|
+
f"[{STYLE_WARNING_STRONG}]Warning: {archive_name} already exists ![/{STYLE_WARNING_STRONG}]"
|
|
2050
|
+
)
|
|
2051
|
+
if Confirm.ask(
|
|
2052
|
+
"[bold]Remove this file (and the .sig if it is also there) and continue ?[/bold]",
|
|
2053
|
+
default=True,
|
|
2054
|
+
):
|
|
2055
|
+
(root_dir / archive_name).unlink()
|
|
2056
|
+
sig = root_dir / f"{archive_name}.sig"
|
|
2057
|
+
if sig.is_file():
|
|
2058
|
+
sig.unlink()
|
|
2059
|
+
else:
|
|
2060
|
+
sys.exit(1)
|
|
2061
|
+
|
|
1423
2062
|
if args.git_commit is not None:
|
|
1424
2063
|
custom_message = None if args.git_commit is True else args.git_commit
|
|
1425
2064
|
git_commit_version(
|
|
@@ -1432,7 +2071,7 @@ def main():
|
|
|
1432
2071
|
|
|
1433
2072
|
if args.git_tag is not None:
|
|
1434
2073
|
custom_tag_name = None if args.git_tag is True else args.git_tag
|
|
1435
|
-
git_tag_version(
|
|
2074
|
+
_success, tag_name = git_tag_version(
|
|
1436
2075
|
root_dir,
|
|
1437
2076
|
target_version,
|
|
1438
2077
|
args.confirm,
|
|
@@ -1441,6 +2080,36 @@ def main():
|
|
|
1441
2080
|
args.sign_tag,
|
|
1442
2081
|
)
|
|
1443
2082
|
|
|
2083
|
+
if args.push_tag:
|
|
2084
|
+
success = push_tag(root_dir, tag_name, project_url)
|
|
2085
|
+
if not success:
|
|
2086
|
+
sys.exit(1)
|
|
2087
|
+
|
|
2088
|
+
if args.git_archive and archive_name:
|
|
2089
|
+
success = git_archive(root_dir, archive_name, tag_name, args.sign_archive)
|
|
2090
|
+
if not success:
|
|
2091
|
+
sys.exit(1)
|
|
2092
|
+
|
|
2093
|
+
if args.gh_release:
|
|
2094
|
+
if args.git_tag is None:
|
|
2095
|
+
console.print(
|
|
2096
|
+
f"[{STYLE_WARNING}]Warning: --gh-release has no effect without --git-tag.[/{STYLE_WARNING}]"
|
|
2097
|
+
)
|
|
2098
|
+
elif not args.push_tag:
|
|
2099
|
+
console.print(
|
|
2100
|
+
f"[{STYLE_WARNING}]Warning: --gh-release has no effect without --push-tag.[/{STYLE_WARNING}]"
|
|
2101
|
+
)
|
|
2102
|
+
else:
|
|
2103
|
+
success, _ = gh_release(
|
|
2104
|
+
root_dir,
|
|
2105
|
+
target_version,
|
|
2106
|
+
project_url,
|
|
2107
|
+
archive_name,
|
|
2108
|
+
args.sign_archive,
|
|
2109
|
+
)
|
|
2110
|
+
if not success:
|
|
2111
|
+
sys.exit(1)
|
|
2112
|
+
|
|
1444
2113
|
|
|
1445
2114
|
if __name__ == "__main__":
|
|
1446
2115
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|