Files
markitect-main/release.py
tegwick d8d823b101 Add complete Gitea package publishing support
 Features:
- GiteaPackageRegistry client for PyPI-compatible uploads
- Enhanced release.py with upload/registry commands
- New Makefile targets for Gitea publishing workflow
- Comprehensive documentation with examples

📦 New Commands:
- `release.py registry` - Show registry info & authentication
- `release.py upload` - Upload packages to Gitea
- `release.py publish --to-gitea` - Complete release + upload
- `make release-publish-gitea VERSION=x.y.z` - One-command release

🔧 Infrastructure:
- Automatic package detection (wheel + sdist)
- Dry-run support for safe testing
- Error handling and detailed feedback
- Authentication validation

📚 Documentation:
- PACKAGE_PUBLISHING.md with complete setup guide
- Usage examples and troubleshooting

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-08 21:06:03 +01:00

377 lines
14 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.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)
upload Upload packages to Gitea registry
registry Show Gitea package registry information
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
--to-gitea Upload to Gitea package registry
"""
import subprocess
import argparse
import sys
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Tuple
try:
from gitea.package_registry import GiteaPackageRegistry
GITEA_AVAILABLE = True
except ImportError:
GITEA_AVAILABLE = False
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 upload_to_gitea(self, dry_run: bool = False) -> bool:
"""Upload packages to Gitea package registry."""
if not GITEA_AVAILABLE:
print("❌ Gitea package registry not available (missing gitea module)")
return False
try:
registry = GiteaPackageRegistry()
print(f"📡 Uploading to Gitea registry: {registry.pypi_registry_url}")
# Find built packages
dist_dir = self.project_root / "dist"
if not dist_dir.exists():
print("❌ No dist/ directory found. Run 'build' command first.")
return False
wheel_files = list(dist_dir.glob("*.whl"))
sdist_files = list(dist_dir.glob("*.tar.gz"))
if not wheel_files and not sdist_files:
print("❌ No package files found in dist/")
return False
# Upload each package
success = True
for wheel_file in wheel_files:
# Find matching sdist
sdist_file = None
for sdist in sdist_files:
if wheel_file.stem.split('-')[0] == sdist.stem.split('-')[0]:
sdist_file = sdist
break
if not registry.upload_package(wheel_file, sdist_file, dry_run=dry_run):
success = False
# Upload any remaining sdists
uploaded_sdists = []
for wheel_file in wheel_files:
for sdist in sdist_files:
if wheel_file.stem.split('-')[0] == sdist.stem.split('-')[0]:
uploaded_sdists.append(sdist)
for sdist_file in sdist_files:
if sdist_file not in uploaded_sdists:
if not registry.upload_package(sdist_file, dry_run=dry_run):
success = False
return success
except Exception as e:
print(f"❌ Upload to Gitea failed: {e}")
return False
def show_gitea_registry_info(self):
"""Show Gitea package registry information."""
if not GITEA_AVAILABLE:
print("❌ Gitea package registry not available (missing gitea module)")
return
try:
registry = GiteaPackageRegistry()
info = registry.get_registry_info()
print("📦 Gitea Package Registry Information")
print("=" * 50)
print(f"Gitea URL: {info['gitea_url']}")
print(f"Repository: {info['repo_owner']}/{info['repo_name']}")
print(f"PyPI Registry URL: {info['pypi_registry_url']}")
print(f"Package List URL: {info['package_list_url']}")
print(f"Authentication Configured: {'' if info['auth_configured'] else ''}")
print(f"Authentication Valid: {'' if info['auth_valid'] else '' if info['auth_configured'] else 'N/A'}")
if info['auth_configured']:
try:
packages = registry.list_packages()
print(f"\nExisting Packages: {len(packages)}")
for package in packages[:5]: # Show first 5
print(f" - {package.get('name', 'unknown')} (type: {package.get('type', 'unknown')})")
except Exception as e:
print(f"\nError listing packages: {e}")
else:
print("\n Set GITEA_API_TOKEN environment variable for package management")
except Exception as e:
print(f"❌ Error getting registry info: {e}")
def publish_with_gitea(self, version: str, dry_run: bool = False) -> bool:
"""Complete release workflow including Gitea upload."""
if not self.publish_release(version):
return False
if not self.upload_to_gitea(dry_run=dry_run):
print("⚠️ Release completed but Gitea upload failed")
return False
print("🎉 Complete release with Gitea upload successful!")
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', 'upload', 'registry'],
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')
parser.add_argument('--to-gitea', action='store_true', help='Include Gitea package registry upload')
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)
if args.to_gitea:
manager.publish_with_gitea(args.version, args.dry_run)
else:
manager.publish_release(args.version)
elif args.command == 'upload':
manager.upload_to_gitea(args.dry_run)
elif args.command == 'registry':
manager.show_gitea_registry_info()
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()