""" Release validation utilities. This module provides validation functions for release readiness. """ from pathlib import Path from typing import List, Tuple, Optional from ..git.manager import GitManager class ReleaseValidator: """Validates release readiness and requirements.""" def __init__(self, project_root: Optional[Path] = None): """Initialize release validator. Args: project_root: Root directory of the project """ self.project_root = project_root or Path.cwd() self.git_manager = GitManager(project_root) def validate_release_state(self, force: bool = False) -> Tuple[bool, List[str]]: """Validate that repository is ready for release. Args: force: Skip validation checks if True Returns: Tuple of (is_valid, list_of_issues) """ if force: return True, [] issues = [] # Git repository validation git_issues = self._validate_git_state() issues.extend(git_issues) # Project structure validation structure_issues = self._validate_project_structure() issues.extend(structure_issues) # Configuration validation config_issues = self._validate_configuration() issues.extend(config_issues) return len(issues) == 0, issues def _validate_git_state(self) -> List[str]: """Validate git repository state. Returns: List of git-related issues """ issues = [] status = self.git_manager.get_repository_status() if not status['is_repo']: issues.append("Not in a git repository") return issues if status['has_changes']: issues.append("Repository has uncommitted changes") if status['branch'] != 'main': issues.append(f"Not on main branch (currently on {status['branch']})") # Check if remote exists remote_url = self.git_manager.get_remote_url() if not remote_url: issues.append("No git remote 'origin' configured") return issues def _validate_project_structure(self) -> List[str]: """Validate project structure for releases. Returns: List of project structure issues """ issues = [] # Check for required files required_files = ['pyproject.toml'] for file_name in required_files: file_path = self.project_root / file_name if not file_path.exists(): issues.append(f"Missing required file: {file_name}") # Check for setuptools-scm configuration pyproject_path = self.project_root / 'pyproject.toml' if pyproject_path.exists(): try: import tomllib except ImportError: try: import tomli as tomllib except ImportError: issues.append("Cannot read pyproject.toml (tomllib/tomli not available)") return issues try: with open(pyproject_path, 'rb') as f: config = tomllib.load(f) # Check for setuptools-scm configuration build_system = config.get('build-system', {}) if 'setuptools-scm' not in str(build_system.get('requires', [])): issues.append("setuptools-scm not found in build-system.requires") # Check for dynamic version project_config = config.get('project', {}) if 'version' in project_config: issues.append("Static version found in project config. Use dynamic versioning with setuptools-scm.") dynamic = project_config.get('dynamic', []) if 'version' not in dynamic: issues.append("'version' not in project.dynamic. Add it for setuptools-scm.") except Exception as e: issues.append(f"Error reading pyproject.toml: {e}") return issues def _validate_configuration(self) -> List[str]: """Validate release configuration. Returns: List of configuration issues """ issues = [] # Check for environment variables that might be needed import os # Check for common auth tokens (warn, don't fail) auth_vars = ['GITEA_API_TOKEN', 'PYPI_TOKEN', 'GITHUB_TOKEN'] available_auth = [var for var in auth_vars if os.getenv(var)] if not available_auth: issues.append("No authentication tokens found in environment. " "Consider setting GITEA_API_TOKEN, PYPI_TOKEN, or GITHUB_TOKEN " "for package publishing.") return issues def validate_version_string(self, version_string: str) -> Tuple[bool, List[str]]: """Validate a version string for release. Args: version_string: Version string to validate Returns: Tuple of (is_valid, list_of_issues) """ issues = [] if not version_string: issues.append("Version string cannot be empty") return False, issues # Check basic format if not version_string.replace('.', '').replace('-', '').replace('+', '').replace('a', '').replace('b', '').replace('rc', '').isalnum(): issues.append("Version string contains invalid characters") # Check for development markers in release dev_markers = ['dev', '.dev', '+dev'] if any(marker in version_string.lower() for marker in dev_markers): issues.append("Development versions should not be released") # Check for reasonable version format (semantic versioning) try: from packaging import version version.Version(version_string) except Exception: issues.append("Version string is not valid according to PEP 440") # Check if version already exists as git tag tag_name = version_string if version_string.startswith('v') else f'v{version_string}' if self.git_manager.tag_exists(tag_name): issues.append(f"Git tag {tag_name} already exists") return len(issues) == 0, issues def get_validation_summary(self) -> dict: """Get a comprehensive validation summary. Returns: Dictionary with validation results """ is_valid, issues = self.validate_release_state() return { 'is_valid': is_valid, 'issues': issues, 'git_status': self.git_manager.get_repository_status(), 'recommendations': self._get_recommendations(issues) } def _get_recommendations(self, issues: List[str]) -> List[str]: """Get recommendations based on validation issues. Args: issues: List of validation issues Returns: List of recommendations """ recommendations = [] if any('uncommitted changes' in issue for issue in issues): recommendations.append("Commit or stash your changes before releasing") if any('not on main branch' in issue for issue in issues): recommendations.append("Switch to main branch: git checkout main") if any('setuptools-scm' in issue for issue in issues): recommendations.append("Configure setuptools-scm in pyproject.toml") if any('authentication' in issue.lower() for issue in issues): recommendations.append("Set up authentication tokens for package publishing") if not issues: recommendations.append("Repository is ready for release!") return recommendations