fix(release): centralize pubspec version parsing

This commit is contained in:
edde746
2026-07-12 08:42:24 +02:00
parent 2e830d3f97
commit c0d8951677
4 changed files with 146 additions and 5 deletions
+2 -4
View File
@@ -32,10 +32,8 @@ jobs:
- name: Extract current build number
id: get_build
run: |
# Extract current build number from pubspec.yaml
CURRENT_VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //')
CURRENT_BUILD=$(echo "$CURRENT_VERSION" | cut -d'+' -f2)
NEW_BUILD=$((CURRENT_BUILD + 1))
CURRENT_BUILD=$(python3 scripts/pubspec_version.py --build-number pubspec.yaml)
NEW_BUILD=$((10#$CURRENT_BUILD + 1))
echo "current_build=$CURRENT_BUILD" >> $GITHUB_OUTPUT
echo "new_build=$NEW_BUILD" >> $GITHUB_OUTPUT
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Read and validate the top-level version from a Flutter pubspec."""
import argparse
from pathlib import Path
import re
import sys
_TOP_LEVEL_VERSION = re.compile(r"^version:[ \t]*(.*)$")
_VERSION = re.compile(
r"(?P<version>"
r"(?:0|[1-9][0-9]*)\."
r"(?:0|[1-9][0-9]*)\."
r"(?:0|[1-9][0-9]*)\+"
r"(?P<build>[0-9]+)"
r")"
)
def parse_pubspec_version(contents: str) -> tuple[str, str]:
values = []
for line in contents.splitlines():
match = _TOP_LEVEL_VERSION.match(line)
if match:
value = re.sub(r"[ \t]+#.*$", "", match.group(1)).strip()
values.append(value)
if len(values) != 1:
raise ValueError(
f"expected one top-level 'version:' field, found {len(values)}"
)
match = _VERSION.fullmatch(values[0])
if not match:
raise ValueError(
"top-level version must use major.minor.patch+numeric-build syntax; "
f"got {values[0]!r}"
)
return match.group("version"), match.group("build")
def main() -> int:
parser = argparse.ArgumentParser(
description="Read and validate the top-level pubspec version."
)
parser.add_argument(
"pubspec",
nargs="?",
type=Path,
default=Path("pubspec.yaml"),
help="path to pubspec.yaml (default: ./pubspec.yaml)",
)
parser.add_argument(
"--build-number",
action="store_true",
help="print only the numeric build metadata",
)
args = parser.parse_args()
try:
contents = args.pubspec.read_text(encoding="utf-8")
version, build = parse_pubspec_version(contents)
except (OSError, UnicodeError, ValueError) as error:
print(f"Error: {args.pubspec}: {error}", file=sys.stderr)
return 1
print(build if args.build_number else version)
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -22,7 +22,7 @@ create_changelogs() {
fi
local version_code
version_code=$(grep -E 'version:' "$PROJECT_ROOT/pubspec.yaml" | sed -E 's/.*\+([0-9]+).*/\1/')
version_code=$(python3 "$SCRIPT_DIR/pubspec_version.py" --build-number "$PROJECT_ROOT/pubspec.yaml")
local prompt="Below is a changelog for a cross-platform Flutter app (iOS, Android, macOS, Linux, Windows). Return ONLY the entries relevant to the given platform. Keep the same format (section headers + bullet points). If a section has no relevant entries, omit it entirely. If an entry is not platform-specific, include it. You MUST stay under the character limit. Aggressively drop less important entries and consolidate similar ones to fit. Count your output characters before responding. Output nothing else."
local notes
+69
View File
@@ -0,0 +1,69 @@
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(SCRIPT_DIR))
from pubspec_version import parse_pubspec_version
SCRIPT = SCRIPT_DIR / "pubspec_version.py"
class ParsePubspecVersionTest(unittest.TestCase):
def test_ignores_duplicate_nested_version_keys(self) -> None:
contents = """\
name: example
dependencies:
first:
version: 9.9.9+999
version: 2.8.0+119
metadata:
version: malformed
"""
self.assertEqual(parse_pubspec_version(contents), ("2.8.0+119", "119"))
def test_rejects_malformed_versions(self) -> None:
for value in ("2.8", "2.8.0", "02.8.0+1", "2.8.0+build.1"):
with self.subTest(value=value), self.assertRaises(ValueError):
parse_pubspec_version(f"version: {value}\n")
def test_rejects_missing_top_level_version(self) -> None:
contents = """\
name: example
dependency:
version: 2.8.0+119
"""
with self.assertRaisesRegex(ValueError, "found 0"):
parse_pubspec_version(contents)
def test_accepts_numeric_build_metadata(self) -> None:
self.assertEqual(
parse_pubspec_version("version: 10.20.30+0007 # release build\n"),
("10.20.30+0007", "0007"),
)
def test_cli_prints_build_number(self) -> None:
with tempfile.TemporaryDirectory() as directory:
pubspec = Path(directory) / "pubspec.yaml"
pubspec.write_text("version: 1.2.3+456\n", encoding="utf-8")
result = subprocess.run(
[sys.executable, str(SCRIPT), "--build-number", str(pubspec)],
check=False,
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0)
self.assertEqual(result.stdout, "456\n")
self.assertEqual(result.stderr, "")
if __name__ == "__main__":
unittest.main()