|
import re |
|
import sys |
|
import subprocess |
|
import argparse |
|
from pathlib import Path |
|
from enum import Enum |
|
|
|
SCRIPT_DIR = Path(__file__).parent |
|
PROJECT_ROOT = SCRIPT_DIR.parent |
|
|
|
PYPROJECT_FILE = PROJECT_ROOT / "pyproject.toml" |
|
LOCK_FILE = PROJECT_ROOT / "uv.lock" |
|
|
|
|
|
class VersionType(Enum): |
|
MAJOR = "major" |
|
MINOR = "minor" |
|
PATCH = "patch" |
|
|
|
|
|
def get_current_version(): |
|
""" |
|
Extracts the current version from pyproject.toml |
|
|
|
Returns: |
|
tuple: The current version (major, minor, patch) |
|
""" |
|
with open(PYPROJECT_FILE, "r") as f: |
|
content = f.read() |
|
|
|
match = re.search(r'version = "(\d+)\.(\d+)\.(\d+)"', content) |
|
if not match: |
|
print("Error: Could not find version in pyproject.toml") |
|
sys.exit(1) |
|
|
|
return tuple(map(int, match.groups())) |
|
|
|
|
|
def bump_version(version_type: VersionType): |
|
""" |
|
Bumps the version based on the specified type (major, minor, patch). |
|
|
|
Args: |
|
version_type (VersionType): The type of version bump to perform |
|
|
|
Returns: |
|
str: The new version |
|
""" |
|
major, minor, patch = get_current_version() |
|
if version_type == VersionType.MAJOR: |
|
major += 1 |
|
minor = 0 |
|
patch = 0 |
|
elif version_type == VersionType.MINOR: |
|
minor += 1 |
|
patch = 0 |
|
elif version_type == VersionType.PATCH: |
|
patch += 1 |
|
|
|
return f"{major}.{minor}.{patch}" |
|
|
|
|
|
def update_pyproject_version(new_version, dry_run=False): |
|
""" |
|
Updates the version in pyproject.toml |
|
|
|
Args: |
|
new_version (str): The new version to set in pyproject.toml |
|
dry_run (bool): If True, don't actually write changes |
|
|
|
Returns: |
|
str: The new version |
|
""" |
|
with open(PYPROJECT_FILE, "r") as f: |
|
content = f.read() |
|
|
|
new_content = re.sub( |
|
r'version = "\d+\.\d+\.\d+"', f'version = "{new_version}"', content |
|
) |
|
|
|
if not dry_run: |
|
with open(PYPROJECT_FILE, "w") as f: |
|
f.write(new_content) |
|
print(f"Updated pyproject.toml to version {new_version}") |
|
else: |
|
print(f"[DRY RUN] Would update pyproject.toml to version {new_version}") |
|
|
|
|
|
def run_command(cmd, capture_output=False, dry_run=False): |
|
"""Runs a shell command and exits if it fails |
|
|
|
Args: |
|
cmd (list): The command to run |
|
capture_output (bool): Whether to capture the output of the command |
|
dry_run (bool): If True, don't actually run the command |
|
|
|
Returns: |
|
str: The output of the command |
|
""" |
|
if dry_run: |
|
print(f"[DRY RUN] Would run: {' '.join(cmd)}") |
|
return None |
|
|
|
print(f"Running: {' '.join(cmd)}") |
|
result = subprocess.run( |
|
cmd, capture_output=capture_output, text=True, cwd=PROJECT_ROOT |
|
) |
|
if result.returncode != 0: |
|
print(f"Error: {result.stderr}") |
|
sys.exit(1) |
|
return result.stdout.strip() if capture_output else None |
|
|
|
|
|
def is_working_directory_clean(): |
|
""" |
|
Checks if the git working directory is clean (no uncommitted changes) |
|
|
|
Returns: |
|
bool: True if working directory is clean, False otherwise |
|
""" |
|
result = subprocess.run( |
|
["git", "status", "--porcelain"], |
|
capture_output=True, |
|
text=True, |
|
cwd=PROJECT_ROOT, |
|
) |
|
|
|
if result.returncode != 0: |
|
print(f"Error checking git status: {result.stderr}") |
|
sys.exit(1) |
|
|
|
return result.stdout.strip() == "" |
|
|
|
|
|
def confirm_action(message): |
|
""" |
|
Asks the user to confirm an action |
|
|
|
Args: |
|
message (str): The message to display |
|
|
|
Returns: |
|
bool: True if user confirms, False otherwise |
|
""" |
|
response = input(f"{message} (y/n): ").lower().strip() |
|
return response in ("y", "yes") |
|
|
|
|
|
def main(): |
|
parser = argparse.ArgumentParser( |
|
description="Bump version and create release branch" |
|
) |
|
parser.add_argument( |
|
"version_type", |
|
choices=["major", "minor", "patch"], |
|
help="Type of version bump to perform", |
|
) |
|
parser.add_argument( |
|
"--dry-run", |
|
action="store_true", |
|
help="Show what would happen without making changes", |
|
) |
|
args = parser.parse_args() |
|
|
|
version_type = VersionType(args.version_type) |
|
dry_run = args.dry_run |
|
|
|
if dry_run: |
|
print("Running in dry-run mode. No changes will be made.") |
|
|
|
# Check if working directory is clean |
|
if not is_working_directory_clean(): |
|
print( |
|
"Error: Working directory is not clean. Commit or stash your changes first." |
|
) |
|
sys.exit(1) |
|
|
|
current_version = ".".join(map(str, get_current_version())) |
|
new_version = bump_version(version_type) |
|
release_branch = f"release/{new_version}" |
|
|
|
print(f"Current version: {current_version}") |
|
print(f"New version: {new_version}") |
|
print(f"Release branch: {release_branch}") |
|
|
|
if not dry_run and not confirm_action( |
|
f"Proceed with bumping version to {new_version} and creating release branch {release_branch}?" |
|
): |
|
print("Operation cancelled.") |
|
sys.exit(0) |
|
|
|
update_pyproject_version(new_version, dry_run) |
|
|
|
run_command(["uv", "lock"], dry_run=dry_run) |
|
run_command(["uv", "sync"], dry_run=dry_run) |
|
|
|
run_command(["git", "checkout", "-b", release_branch], dry_run=dry_run) |
|
|
|
run_command(["git", "add", "pyproject.toml", "uv.lock"], dry_run=dry_run) |
|
run_command( |
|
["git", "commit", "-m", f"Bump version to {new_version}"], dry_run=dry_run |
|
) |
|
|
|
if not dry_run and not confirm_action( |
|
f"Proceed with pushing the release branch {release_branch} to origin?" |
|
): |
|
print("Operation cancelled.") |
|
sys.exit(0) |
|
|
|
run_command(["git", "push", "origin", release_branch], dry_run=dry_run) |
|
|
|
if dry_run: |
|
print(f"[DRY RUN] Would create and push release branch '{release_branch}'") |
|
else: |
|
print( |
|
f"Release branch '{release_branch}' created and pushed. Open a PR to merge." |
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |