- Move release management to capabilities/release-management/ with complete Makefile - Create automatic capability discovery system in scripts/capability_discovery.mk - Add capability-manager subagent for managing modular architecture - Implement target delegation system enabling capability-name-target patterns - Create Makefiles for markitect-content, markitect-utils, and issue-facade capabilities - Remove legacy release management code and documentation from main project - Update main Makefile to use capability discovery and delegation - Add comprehensive capability status, help, and management targets The capability system provides: - Automatic discovery of capabilities with Makefiles - Clean target delegation without conflicts - Modular architecture following established patterns - Comprehensive help and status reporting - Zero-conflict capability integration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
230 lines
7.6 KiB
Python
230 lines
7.6 KiB
Python
"""
|
|
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 |