mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(release): version detection accepts manifest variants, never crashes the sweep
_pyproject_version read pyproject.toml with no error handling, so a non-Python project failed the release-manager cycle every interval forever. _project_version now probes pyproject.toml, package.json, Cargo.toml, then a bare VERSION file; missing or unparseable manifests are skipped and no manifest degrades to an empty version.
This commit is contained in:
@@ -12,11 +12,15 @@ gate state) is layered on top of these primitives in Task 3.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import TYPE_CHECKING, Any, Literal
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
BumpKind = Literal["major", "minor", "patch"]
|
BumpKind = Literal["major", "minor", "patch"]
|
||||||
|
|
||||||
@@ -439,12 +443,49 @@ def _run_git(root: Path, args: list[str]) -> str:
|
|||||||
return result.stdout
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
def _pyproject_version(root: Path) -> str:
|
_TOML_VERSION_RE = re.compile(r'^version\s*=\s*"([^"]+)"', re.MULTILINE)
|
||||||
text = (root / "pyproject.toml").read_text(encoding="utf-8")
|
|
||||||
match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
|
|
||||||
|
def _toml_version(path: Path) -> str:
|
||||||
|
match = _TOML_VERSION_RE.search(path.read_text(encoding="utf-8"))
|
||||||
return match.group(1) if match else ""
|
return match.group(1) if match else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _package_json_version(path: Path) -> str:
|
||||||
|
version = json.loads(path.read_text(encoding="utf-8")).get("version")
|
||||||
|
return version if isinstance(version, str) else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _version_file_version(path: Path) -> str:
|
||||||
|
lines = path.read_text(encoding="utf-8").strip().splitlines()
|
||||||
|
return lines[0].strip() if lines else ""
|
||||||
|
|
||||||
|
|
||||||
|
_VERSION_PROBES: tuple[tuple[str, Callable[[Path], str]], ...] = (
|
||||||
|
("pyproject.toml", _toml_version),
|
||||||
|
("package.json", _package_json_version),
|
||||||
|
("Cargo.toml", _toml_version),
|
||||||
|
("VERSION", _version_file_version),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _project_version(root: Path) -> str:
|
||||||
|
"""Current version from the repo's own manifest — first probe that
|
||||||
|
yields one wins (pyproject.toml, package.json, Cargo.toml, VERSION).
|
||||||
|
|
||||||
|
Missing or unparseable manifests are skipped, never raised: a non-Python
|
||||||
|
layout must degrade to "" (readiness reports the gap) instead of
|
||||||
|
crashing the release-manager sweep every cycle."""
|
||||||
|
for name, probe in _VERSION_PROBES:
|
||||||
|
try:
|
||||||
|
version = probe(root / name)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
continue
|
||||||
|
if version:
|
||||||
|
return version
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _last_tag(root: Path) -> str | None:
|
def _last_tag(root: Path) -> str | None:
|
||||||
tag = _run_git(root, ["describe", "--tags", "--abbrev=0"]).strip()
|
tag = _run_git(root, ["describe", "--tags", "--abbrev=0"]).strip()
|
||||||
return tag or None
|
return tag or None
|
||||||
@@ -607,7 +648,7 @@ def gather_snapshot(
|
|||||||
(the ``tag_drift`` gap). None ⇒ degenerate/unsplit project, baseline stays
|
(the ``tag_drift`` gap). None ⇒ degenerate/unsplit project, baseline stays
|
||||||
``last_tag`` (unchanged behavior).
|
``last_tag`` (unchanged behavior).
|
||||||
"""
|
"""
|
||||||
version = _pyproject_version(root)
|
version = _project_version(root)
|
||||||
tag = _last_tag(root)
|
tag = _last_tag(root)
|
||||||
prod_tip = _rev_parse(root, f"origin/{prod_branch}") if prod_branch else None
|
prod_tip = _rev_parse(root, f"origin/{prod_branch}") if prod_branch else None
|
||||||
last_tag_sha = _rev_parse(root, f"{tag}^{{commit}}") if tag else None
|
last_tag_sha = _rev_parse(root, f"{tag}^{{commit}}") if tag else None
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from roboco.services.release_readiness import (
|
|||||||
CommitInfo,
|
CommitInfo,
|
||||||
_commits_since,
|
_commits_since,
|
||||||
_draft_changelog,
|
_draft_changelog,
|
||||||
|
_project_version,
|
||||||
_run_git,
|
_run_git,
|
||||||
classify_changes,
|
classify_changes,
|
||||||
derive_bump,
|
derive_bump,
|
||||||
@@ -164,3 +165,41 @@ def test_draft_changelog_falls_back_to_transcription_without_curation() -> None:
|
|||||||
draft = _draft_changelog("0.25.0", changes, "2026-07-15", empty)
|
draft = _draft_changelog("0.25.0", changes, "2026-07-15", empty)
|
||||||
assert "### Added" in draft
|
assert "### Added" in draft
|
||||||
assert "- shiny thing" in draft
|
assert "- shiny thing" in draft
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# _project_version — manifest-variant probing, never raises on a non-Python
|
||||||
|
# layout (a missing/unparseable manifest must not crash the readiness sweep).
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_version_reads_pyproject(tmp_path: Path) -> None:
|
||||||
|
(tmp_path / "pyproject.toml").write_text('version = "1.2.3"\n', encoding="utf-8")
|
||||||
|
assert _project_version(tmp_path) == "1.2.3"
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_version_falls_back_to_package_json(tmp_path: Path) -> None:
|
||||||
|
(tmp_path / "package.json").write_text('{"version": "4.5.6"}', encoding="utf-8")
|
||||||
|
assert _project_version(tmp_path) == "4.5.6"
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_version_falls_back_to_cargo_toml(tmp_path: Path) -> None:
|
||||||
|
(tmp_path / "Cargo.toml").write_text(
|
||||||
|
'[package]\nname = "x"\nversion = "7.8.9"\n', encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert _project_version(tmp_path) == "7.8.9"
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_version_falls_back_to_version_file(tmp_path: Path) -> None:
|
||||||
|
(tmp_path / "VERSION").write_text("2.0.0\n", encoding="utf-8")
|
||||||
|
assert _project_version(tmp_path) == "2.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_version_empty_when_no_manifest(tmp_path: Path) -> None:
|
||||||
|
assert _project_version(tmp_path) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_version_skips_unparseable_manifest(tmp_path: Path) -> None:
|
||||||
|
(tmp_path / "package.json").write_text("{not json", encoding="utf-8")
|
||||||
|
(tmp_path / "VERSION").write_text("3.1.4\n", encoding="utf-8")
|
||||||
|
assert _project_version(tmp_path) == "3.1.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user