Files
markitect-main/release_simplified.py
tegwick 8249296a43 Convert to setuptools-scm for automatic version management
- Remove manual version management in favor of git tag-based versioning
- Simplify __version__.py to import from generated _version.py
- Add simplified release_simplified.py script
- Add _version.py to .gitignore (auto-generated)

🚀 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-08 20:23:16 +01:00

258 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""
MarkiTect Release Management Tool (setuptools-scm version)
This simplified script works with setuptools-scm for automatic version management.
Versions are automatically derived from git tags - no manual version bumping needed.
Usage:
python release_simplified.py [command] [options]
Commands:
status Show current release status
validate Validate current state for release
tag Create git tag for version (e.g., v0.8.0)
build Build release packages
publish Complete release workflow (tag + build + distribute)
Options:
--version VERSION Git tag version (e.g., 0.8.0, 1.0.0-rc1)
--dry-run Show what would be done without making changes
--force Force operation even with warnings
"""
import subprocess
import argparse
import sys
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Tuple
class SimpleReleaseManager:
"""Simplified release manager using setuptools-scm."""
def __init__(self, dry_run=False, force=False):
self.dry_run = dry_run
self.force = force
self.project_root = Path(__file__).parent.absolute()
def run_command(self, cmd: List[str], capture=True, check=True, skip_dry_run=False) -> subprocess.CompletedProcess:
"""Run a command with optional dry-run support."""
if self.dry_run and not skip_dry_run:
print(f"[DRY RUN] Would run: {' '.join(cmd)}")
return subprocess.CompletedProcess(cmd, 0, "", "")
return subprocess.run(cmd, capture_output=capture, text=True, check=check, cwd=self.project_root)
def get_current_version_from_scm(self) -> str:
"""Get current version using setuptools-scm."""
try:
result = self.run_command(['python', '-m', 'setuptools_scm'], skip_dry_run=True)
return result.stdout.strip()
except subprocess.CalledProcessError:
return "unknown"
def get_git_status(self) -> Dict[str, any]:
"""Get current git repository status."""
try:
# Get current branch
branch_result = self.run_command(['git', 'branch', '--show-current'], skip_dry_run=True)
current_branch = branch_result.stdout.strip()
# Check for uncommitted changes
status_result = self.run_command(['git', 'status', '--porcelain'], skip_dry_run=True)
has_changes = bool(status_result.stdout.strip())
# Get latest commit
commit_result = self.run_command(['git', 'rev-parse', '--short', 'HEAD'], skip_dry_run=True)
latest_commit = commit_result.stdout.strip()
# Get latest tag
try:
tag_result = self.run_command(['git', 'describe', '--tags', '--abbrev=0'], skip_dry_run=True)
latest_tag = tag_result.stdout.strip()
except subprocess.CalledProcessError:
latest_tag = None
return {
'is_repo': True,
'branch': current_branch,
'has_changes': has_changes,
'latest_commit': latest_commit,
'latest_tag': latest_tag
}
except subprocess.CalledProcessError:
return {'is_repo': False}
def validate_release_state(self) -> Tuple[bool, List[str]]:
"""Validate that the repository is ready for release."""
issues = []
git_status = self.get_git_status()
if not git_status['is_repo']:
issues.append("Not in a git repository")
else:
if git_status['has_changes'] and not self.force:
issues.append("Repository has uncommitted changes")
if git_status['branch'] != 'main' and not self.force:
issues.append(f"Not on main branch (currently on {git_status['branch']})")
return len(issues) == 0, issues
def create_git_tag(self, version: str, message: str = None):
"""Create and push git tag."""
if not version.startswith('v'):
tag_name = f"v{version}"
else:
tag_name = version
tag_message = message or f"Release {version}"
print(f"🏷️ Creating git tag {tag_name}")
# Create annotated tag
self.run_command(['git', 'tag', '-a', tag_name, '-m', tag_message])
print(f"✅ Tag {tag_name} created")
# Optionally push tag (can be done manually)
try:
print(f"📤 Pushing tag to origin...")
self.run_command(['git', 'push', 'origin', tag_name])
print(f"✅ Tag pushed to origin")
except subprocess.CalledProcessError as e:
print(f"⚠️ Could not push tag to origin: {e}")
print("You can push it manually with: git push origin " + tag_name)
def build_packages(self):
"""Build release packages using setuptools-scm."""
print(f"📦 Building packages (version will be auto-determined by setuptools-scm)")
# Clean previous builds
for pattern in ['build', 'dist', '*.egg-info']:
try:
self.run_command(['rm', '-rf', pattern])
except subprocess.CalledProcessError:
pass
# Build source distribution and wheel
print("Building packages...")
self.run_command(['python', '-m', 'build'], capture=False)
print("✅ Packages built successfully")
def show_status(self):
"""Show current release status."""
print("🔍 MarkiTect Release Status (setuptools-scm)")
print("=" * 60)
# Get version from setuptools-scm
scm_version = self.get_current_version_from_scm()
print(f"Current Version (setuptools-scm): {scm_version}")
git_status = self.get_git_status()
if git_status['is_repo']:
print(f"Git Branch: {git_status['branch']}")
print(f"Latest Commit: {git_status['latest_commit']}")
print(f"Latest Tag: {git_status['latest_tag'] or 'None'}")
print(f"Uncommitted Changes: {'Yes' if git_status['has_changes'] else 'No'}")
else:
print("Git Repository: Not available")
# Check build tools
print("\nBuild Tools:")
try:
self.run_command(['python', '-m', 'build', '--help'], skip_dry_run=True)
print("✅ build module available")
except (subprocess.CalledProcessError, FileNotFoundError):
print("❌ build module not available (pip install build)")
try:
self.run_command(['python', '-m', 'setuptools_scm'], skip_dry_run=True)
print("✅ setuptools-scm available")
except (subprocess.CalledProcessError, FileNotFoundError):
print("❌ setuptools-scm not available")
# Check existing packages
dist_dir = self.project_root / "dist"
if dist_dir.exists():
packages = list(dist_dir.glob("*"))
print(f"\nExisting Packages: {len(packages)} files in dist/")
for pkg in packages[-5:]: # Show last 5
print(f" - {pkg.name}")
else:
print("\nExisting Packages: None")
def publish_release(self, version: str):
"""Complete release workflow."""
print(f"🚀 Publishing release {version}")
# Validate state
is_valid, issues = self.validate_release_state()
if not is_valid and not self.force:
print("❌ Cannot publish release:")
for issue in issues:
print(f" - {issue}")
return False
# Create git tag (this determines the version for setuptools-scm)
self.create_git_tag(version)
# Build packages (setuptools-scm will use the tag for version)
self.build_packages()
print(f"✅ Release {version} completed!")
print("📦 Packages available in dist/")
print(f"🏷️ Git tag v{version} created")
return True
def main():
parser = argparse.ArgumentParser(
description="MarkiTect Release Management Tool (setuptools-scm)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__.split('\n\n')[1]
)
parser.add_argument('command', choices=['status', 'validate', 'tag', 'build', 'publish'],
help='Release command to execute')
parser.add_argument('--version', type=str, help='Target version for git tag (e.g., 0.8.0)')
parser.add_argument('--dry-run', action='store_true', help='Show what would be done')
parser.add_argument('--force', action='store_true', help='Force operation despite warnings')
args = parser.parse_args()
manager = SimpleReleaseManager(dry_run=args.dry_run, force=args.force)
try:
if args.command == 'status':
manager.show_status()
elif args.command == 'validate':
is_valid, issues = manager.validate_release_state()
if is_valid:
print("✅ Repository is ready for release")
else:
print("❌ Release validation failed:")
for issue in issues:
print(f" - {issue}")
sys.exit(1)
elif args.command == 'tag':
if not args.version:
print("❌ --version is required for tag command")
sys.exit(1)
manager.create_git_tag(args.version)
elif args.command == 'build':
manager.build_packages()
elif args.command == 'publish':
if not args.version:
print("❌ --version is required for publish command")
sys.exit(1)
manager.publish_release(args.version)
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()