feat: implement modular capability system with automatic discovery
- 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>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Utilities for release management.
|
||||
|
||||
This module provides utility functions for version management,
|
||||
validation, and other common operations.
|
||||
"""
|
||||
|
||||
from .version import VersionManager
|
||||
from .validation import ReleaseValidator
|
||||
|
||||
__all__ = ["VersionManager", "ReleaseValidator"]
|
||||
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
Version management utilities.
|
||||
|
||||
This module provides utilities for working with versions and setuptools-scm.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
from packaging import version
|
||||
|
||||
|
||||
class VersionManager:
|
||||
"""Utilities for version management with setuptools-scm."""
|
||||
|
||||
def __init__(self, project_root: Optional[Path] = None):
|
||||
"""Initialize version manager.
|
||||
|
||||
Args:
|
||||
project_root: Root directory of the project
|
||||
"""
|
||||
self.project_root = project_root or Path.cwd()
|
||||
|
||||
def get_current_version(self) -> str:
|
||||
"""Get current version using setuptools-scm.
|
||||
|
||||
Returns:
|
||||
Current version string or "unknown" if unavailable
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['python', '-m', 'setuptools_scm'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=self.project_root
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError:
|
||||
return "unknown"
|
||||
|
||||
def parse_version(self, version_string: str) -> Dict[str, Any]:
|
||||
"""Parse a version string and return components.
|
||||
|
||||
Args:
|
||||
version_string: Version string to parse
|
||||
|
||||
Returns:
|
||||
Dictionary with version components
|
||||
"""
|
||||
try:
|
||||
v = version.Version(version_string)
|
||||
return {
|
||||
'major': v.major,
|
||||
'minor': v.minor,
|
||||
'micro': v.micro,
|
||||
'is_prerelease': v.is_prerelease,
|
||||
'is_devrelease': v.is_devrelease,
|
||||
'local': v.local,
|
||||
'public': v.public,
|
||||
'base_version': v.base_version,
|
||||
}
|
||||
except version.InvalidVersion:
|
||||
return {'error': f'Invalid version: {version_string}'}
|
||||
|
||||
def is_development_version(self, version_string: Optional[str] = None) -> bool:
|
||||
"""Check if version is a development version.
|
||||
|
||||
Args:
|
||||
version_string: Version to check. If None, uses current version.
|
||||
|
||||
Returns:
|
||||
True if development version, False otherwise
|
||||
"""
|
||||
if version_string is None:
|
||||
version_string = self.get_current_version()
|
||||
|
||||
try:
|
||||
v = version.Version(version_string)
|
||||
return v.is_devrelease or 'dev' in version_string.lower()
|
||||
except version.InvalidVersion:
|
||||
return True # Assume unknown versions are dev
|
||||
|
||||
def compare_versions(self, version1: str, version2: str) -> int:
|
||||
"""Compare two version strings.
|
||||
|
||||
Args:
|
||||
version1: First version to compare
|
||||
version2: Second version to compare
|
||||
|
||||
Returns:
|
||||
-1 if version1 < version2, 0 if equal, 1 if version1 > version2
|
||||
"""
|
||||
try:
|
||||
v1 = version.Version(version1)
|
||||
v2 = version.Version(version2)
|
||||
|
||||
if v1 < v2:
|
||||
return -1
|
||||
elif v1 > v2:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
except version.InvalidVersion:
|
||||
# Fallback to string comparison
|
||||
if version1 < version2:
|
||||
return -1
|
||||
elif version1 > version2:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_next_version(self, current_version: str, bump_type: str = 'patch') -> str:
|
||||
"""Get the next version based on bump type.
|
||||
|
||||
Args:
|
||||
current_version: Current version string
|
||||
bump_type: Type of bump ('major', 'minor', 'patch')
|
||||
|
||||
Returns:
|
||||
Next version string
|
||||
|
||||
Raises:
|
||||
ValueError: If bump_type is invalid
|
||||
"""
|
||||
try:
|
||||
v = version.Version(current_version)
|
||||
major, minor, micro = v.major, v.minor, v.micro
|
||||
|
||||
if bump_type == 'major':
|
||||
return f"{major + 1}.0.0"
|
||||
elif bump_type == 'minor':
|
||||
return f"{major}.{minor + 1}.0"
|
||||
elif bump_type == 'patch':
|
||||
return f"{major}.{minor}.{micro + 1}"
|
||||
else:
|
||||
raise ValueError(f"Invalid bump type: {bump_type}")
|
||||
|
||||
except version.InvalidVersion:
|
||||
raise ValueError(f"Cannot parse version: {current_version}")
|
||||
|
||||
def suggest_version(self, current_version: Optional[str] = None) -> Dict[str, str]:
|
||||
"""Suggest next version options.
|
||||
|
||||
Args:
|
||||
current_version: Current version. If None, gets from setuptools-scm.
|
||||
|
||||
Returns:
|
||||
Dictionary with version suggestions
|
||||
"""
|
||||
if current_version is None:
|
||||
current_version = self.get_current_version()
|
||||
|
||||
if current_version == "unknown":
|
||||
return {
|
||||
'error': 'Cannot determine current version',
|
||||
'suggestion': 'Consider creating an initial tag like v0.1.0'
|
||||
}
|
||||
|
||||
try:
|
||||
# Strip development version info to get base
|
||||
v = version.Version(current_version)
|
||||
base_version = v.base_version
|
||||
|
||||
return {
|
||||
'current': current_version,
|
||||
'base': base_version,
|
||||
'patch': self.get_next_version(base_version, 'patch'),
|
||||
'minor': self.get_next_version(base_version, 'minor'),
|
||||
'major': self.get_next_version(base_version, 'major'),
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'error': str(e),
|
||||
'current': current_version
|
||||
}
|
||||
|
||||
def validate_version_format(self, version_string: str) -> bool:
|
||||
"""Validate if a version string follows semantic versioning.
|
||||
|
||||
Args:
|
||||
version_string: Version string to validate
|
||||
|
||||
Returns:
|
||||
True if valid semantic version, False otherwise
|
||||
"""
|
||||
try:
|
||||
version.Version(version_string)
|
||||
return True
|
||||
except version.InvalidVersion:
|
||||
return False
|
||||
Reference in New Issue
Block a user