"""Shared version management utilities for build scripts. This module provides a single source of truth for version reading to avoid duplication between different build scripts. """ import re from pathlib import Path def get_project_root() -> Path: """Get the project root directory. Returns: Path to project root (parent of build/scripts) """ return Path(__file__).parent.parent.parent def get_current_version() -> str: """Read version from __init__.py. This is the single source of truth for version information. All build scripts and version management tools use this function. Returns: Current version string from __init__.py Raises: ValueError: If __version__ cannot be found in __init__.py """ project_root = get_project_root() init_file = project_root / "src" / "webdrop_bridge" / "__init__.py" if not init_file.exists(): raise FileNotFoundError(f"Cannot find __init__.py at {init_file}") content = init_file.read_text(encoding="utf-8") match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content) if not match: raise ValueError( f"Could not find __version__ in {init_file}. " 'Expected: __version__ = "X.Y.Z"' ) return match.group(1) def extract_release_notes(changelog_content: str, version: str) -> str | None: """Extract the notes for a specific version from changelog content. Args: changelog_content: Full text of CHANGELOG.md version: Version to extract, e.g. "0.9.1" Returns: The section content for that version, or None if not found. """ version_header = re.compile( rf"^##\s*\[?{re.escape(version)}\]?(?:\s*-\s*.+)?\s*$", re.MULTILINE, ) match = version_header.search(changelog_content) if not match: return None section_start = match.end() next_header = re.search(r"^##\s+", changelog_content[section_start:], re.MULTILINE) section_end = section_start + next_header.start() if next_header else len(changelog_content) section = changelog_content[section_start:section_end].strip() return section or None def get_release_notes(version: str, project_root: Path | None = None) -> str: """Build a readable release body for publishing. Prefers the matching version section from CHANGELOG.md. If no changelog entry exists yet, falls back to a generic but user-facing description. Args: version: Release version string. project_root: Optional project root override for testing. Returns: Release notes text suitable for Forgejo/GitHub release bodies. """ root = project_root or get_project_root() changelog_file = root / "CHANGELOG.md" if changelog_file.exists(): content = changelog_file.read_text(encoding="utf-8") notes = extract_release_notes(content, version) if notes: return f"## WebDrop Bridge v{version}\n\n{notes}" return ( f"## WebDrop Bridge v{version}\n\n" "This release package contains the latest improvements, fixes, " "and installer updates for this version." )