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>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -96,3 +96,4 @@ ISSUES.index
|
|||||||
|
|
||||||
# Test artifacts and temporary files
|
# Test artifacts and temporary files
|
||||||
tmp/
|
tmp/
|
||||||
|
markitect/_version.py
|
||||||
|
|||||||
@@ -1,123 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
Version information for MarkiTect.
|
Version information for MarkiTect.
|
||||||
|
|
||||||
This module provides version and release information for the MarkiTect package.
|
This module provides version information using setuptools-scm.
|
||||||
Version information is sourced from pyproject.toml and git metadata when available.
|
Version is automatically derived from git tags.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
try:
|
||||||
import subprocess
|
from ._version import version as __version__
|
||||||
from pathlib import Path
|
except ImportError:
|
||||||
from typing import Optional
|
# Fallback when _version.py is not available (e.g., during development without setuptools-scm)
|
||||||
|
__version__ = "unknown"
|
||||||
|
|
||||||
# Base version from pyproject.toml
|
def get_version():
|
||||||
__version__ = "0.5.0"
|
"""Get the current version string."""
|
||||||
|
return __version__
|
||||||
def get_git_commit_hash() -> Optional[str]:
|
|
||||||
"""Get the current git commit hash if available."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'rev-parse', '--short', 'HEAD'],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
cwd=Path(__file__).parent.parent
|
|
||||||
)
|
|
||||||
return result.stdout.strip()
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_git_branch() -> Optional[str]:
|
|
||||||
"""Get the current git branch if available."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'branch', '--show-current'],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
cwd=Path(__file__).parent.parent
|
|
||||||
)
|
|
||||||
return result.stdout.strip()
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_git_tag() -> Optional[str]:
|
|
||||||
"""Get the current git tag if available."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'describe', '--tags', '--exact-match'],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
cwd=Path(__file__).parent.parent
|
|
||||||
)
|
|
||||||
return result.stdout.strip()
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def is_development_version() -> bool:
|
|
||||||
"""Check if this is a development version (has uncommitted changes)."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'status', '--porcelain'],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
cwd=Path(__file__).parent.parent
|
|
||||||
)
|
|
||||||
return bool(result.stdout.strip())
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_version_info() -> dict:
|
|
||||||
"""Get comprehensive version information."""
|
|
||||||
git_commit = get_git_commit_hash()
|
|
||||||
git_branch = get_git_branch()
|
|
||||||
git_tag = get_git_tag()
|
|
||||||
is_dev = is_development_version()
|
|
||||||
|
|
||||||
# Build version string
|
|
||||||
version_parts = [__version__]
|
|
||||||
|
|
||||||
if git_tag and git_tag != f"v{__version__}":
|
|
||||||
# If we have a different tag, use it
|
|
||||||
version_parts = [git_tag.lstrip('v')]
|
|
||||||
|
|
||||||
if git_commit:
|
|
||||||
if is_dev:
|
|
||||||
version_parts.append(f"dev+{git_commit}")
|
|
||||||
elif not git_tag:
|
|
||||||
version_parts.append(f"+{git_commit}")
|
|
||||||
|
|
||||||
if is_dev and not git_commit:
|
|
||||||
version_parts.append("dev")
|
|
||||||
|
|
||||||
full_version = ".".join(version_parts)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"version": __version__,
|
|
||||||
"full_version": full_version,
|
|
||||||
"git_commit": git_commit,
|
|
||||||
"git_branch": git_branch,
|
|
||||||
"git_tag": git_tag,
|
|
||||||
"is_development": is_dev,
|
|
||||||
"is_git_repo": git_commit is not None
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_release_info() -> dict:
|
|
||||||
"""Get release information."""
|
|
||||||
version_info = get_version_info()
|
|
||||||
|
|
||||||
release_type = "development" if version_info["is_development"] else "release"
|
|
||||||
if version_info["git_tag"]:
|
|
||||||
release_type = "tagged-release"
|
|
||||||
elif version_info["git_commit"] and not version_info["is_development"]:
|
|
||||||
release_type = "commit-build"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"release_type": release_type,
|
|
||||||
"build_from": version_info["git_branch"] or "unknown",
|
|
||||||
"commit": version_info["git_commit"] or "unknown",
|
|
||||||
"clean_build": not version_info["is_development"],
|
|
||||||
**version_info
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61.0"]
|
requires = ["setuptools>=64", "setuptools-scm>=8"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "markitect"
|
name = "markitect"
|
||||||
version = "0.7.0"
|
dynamic = ["version"]
|
||||||
description = "Advanced Markdown engine for structured content"
|
description = "Advanced Markdown engine for structured content"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
@@ -100,3 +100,6 @@ module = [
|
|||||||
"yaml.*"
|
"yaml.*"
|
||||||
]
|
]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
write_to = "markitect/_version.py"
|
||||||
|
|||||||
258
release_simplified.py
Normal file
258
release_simplified.py
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
#!/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()
|
||||||
Reference in New Issue
Block a user