|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Copyright (c) Microsoft Corporation. All rights reserved. |
| 4 | +Licensed under the MIT License. |
| 5 | +""" |
| 6 | + |
| 7 | +import argparse |
| 8 | +import subprocess |
| 9 | +import sys |
| 10 | +from pathlib import Path |
| 11 | +from typing import Dict, List |
| 12 | + |
| 13 | +import tomllib |
| 14 | + |
| 15 | + |
| 16 | +def get_packages_dir() -> Path: |
| 17 | + """Get the packages directory relative to the script location.""" |
| 18 | + script_dir = Path(__file__).parent |
| 19 | + return script_dir.parent / "packages" |
| 20 | + |
| 21 | + |
| 22 | +def find_packages() -> List[Path]: |
| 23 | + """Find all package directories containing pyproject.toml.""" |
| 24 | + packages_dir = get_packages_dir() |
| 25 | + packages: List[Path] = [] |
| 26 | + |
| 27 | + for item in packages_dir.iterdir(): |
| 28 | + if item.is_dir() and (item / "pyproject.toml").exists(): |
| 29 | + packages.append(item) |
| 30 | + |
| 31 | + return sorted(packages) |
| 32 | + |
| 33 | + |
| 34 | +def dry_run_version_bump(package_path: Path, bump_type: str) -> str: |
| 35 | + """Run a dry-run version bump to see what the new version would be.""" |
| 36 | + try: |
| 37 | + result = subprocess.run( |
| 38 | + ["uv", "version", "--bump", bump_type, "--dry-run"], |
| 39 | + cwd=package_path, |
| 40 | + capture_output=True, |
| 41 | + text=True, |
| 42 | + check=True, |
| 43 | + ) |
| 44 | + # Extract the version from the output |
| 45 | + # Handle multiple formats: |
| 46 | + # Format 1: "Would bump version from X.Y.Z to A.B.C" |
| 47 | + # Format 2: "package-name X.Y.Z => A.B.C" |
| 48 | + # Format 3: Just "A.B.C" |
| 49 | + output = result.stdout.strip() |
| 50 | + |
| 51 | + if " to " in output: |
| 52 | + return output.split(" to ")[-1] |
| 53 | + elif " => " in output: |
| 54 | + return output.split(" => ")[-1] |
| 55 | + else: |
| 56 | + # Fallback: extract version from the end of the output |
| 57 | + return output.split()[-1] |
| 58 | + except subprocess.CalledProcessError as e: |
| 59 | + print(f" ✗ Failed to dry-run bump {package_path.name}: {e.stderr}") |
| 60 | + sys.exit(1) |
| 61 | + |
| 62 | + |
| 63 | +def bump_package_version(package_path: Path, bump_type: str, verbose: bool = False) -> str: |
| 64 | + """Bump the version of a package and return the new version.""" |
| 65 | + print(f"Bumping {package_path.name} version ({bump_type})...") |
| 66 | + |
| 67 | + try: |
| 68 | + result = subprocess.run( |
| 69 | + ["uv", "version", "--bump", bump_type], |
| 70 | + cwd=package_path, |
| 71 | + capture_output=not verbose, |
| 72 | + text=True, |
| 73 | + check=True, |
| 74 | + ) |
| 75 | + print(f" ✓ {package_path.name}: {result.stdout.strip()}") |
| 76 | + return get_package_version(package_path) |
| 77 | + except subprocess.CalledProcessError as e: |
| 78 | + print(f" ✗ Failed to bump {package_path.name}: {e.stderr}") |
| 79 | + sys.exit(1) |
| 80 | + |
| 81 | + |
| 82 | +def get_package_version(package_path: Path) -> str: |
| 83 | + """Extract version from pyproject.toml.""" |
| 84 | + pyproject_path = package_path / "pyproject.toml" |
| 85 | + |
| 86 | + try: |
| 87 | + with open(pyproject_path, "rb") as f: |
| 88 | + data = tomllib.load(f) |
| 89 | + return data["project"]["version"] |
| 90 | + except (KeyError, tomllib.TOMLDecodeError, OSError) as e: |
| 91 | + print(f"Error reading version from {pyproject_path}: {e}") |
| 92 | + sys.exit(1) |
| 93 | + |
| 94 | + |
| 95 | +def create_release_branch(version: str, verbose: bool = False) -> str: |
| 96 | + """Create a new release branch.""" |
| 97 | + branch_name = f"release_{version}" |
| 98 | + |
| 99 | + try: |
| 100 | + # Create and switch to new branch |
| 101 | + subprocess.run(["git", "checkout", "-b", branch_name], check=True, capture_output=not verbose) |
| 102 | + print(f"Created and switched to branch: {branch_name}") |
| 103 | + |
| 104 | + # Add all changes |
| 105 | + subprocess.run(["git", "add", "."], check=True, capture_output=not verbose) |
| 106 | + |
| 107 | + # Commit changes |
| 108 | + subprocess.run(["git", "commit", "-m", f"Release version {version}"], check=True, capture_output=not verbose) |
| 109 | + print(f"Committed changes for release {version}") |
| 110 | + |
| 111 | + return branch_name |
| 112 | + except subprocess.CalledProcessError as e: |
| 113 | + print(f"Error creating release branch: {e}") |
| 114 | + sys.exit(1) |
| 115 | + |
| 116 | + |
| 117 | +def main() -> None: |
| 118 | + """Main script entry point.""" |
| 119 | + parser = argparse.ArgumentParser( |
| 120 | + description="Release script for Microsoft Teams Python SDK", |
| 121 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 122 | + epilog=""" |
| 123 | +Version bump types: |
| 124 | + major - Increment major version (1.0.0 -> 2.0.0) |
| 125 | + minor - Increment minor version (1.0.0 -> 1.1.0) |
| 126 | + patch - Increment patch version (1.0.0 -> 1.0.1) |
| 127 | + stable - Remove pre-release suffix (1.0.0a1 -> 1.0.0) |
| 128 | + alpha - Add/increment alpha pre-release (1.0.0 -> 1.0.0a1) |
| 129 | + beta - Add/increment beta pre-release (1.0.0 -> 1.0.0b1) |
| 130 | + rc - Add/increment release candidate (1.0.0 -> 1.0.0rc1) |
| 131 | + post - Add/increment post-release (1.0.0 -> 1.0.0.post1) |
| 132 | + dev - Add/increment dev release (1.0.0 -> 1.0.0.dev1) |
| 133 | + """, |
| 134 | + ) |
| 135 | + |
| 136 | + parser.add_argument( |
| 137 | + "bump_type", |
| 138 | + choices=["major", "minor", "patch", "stable", "alpha", "beta", "rc", "post", "dev"], |
| 139 | + help="Type of version bump to perform", |
| 140 | + ) |
| 141 | + parser.add_argument( |
| 142 | + "-v", |
| 143 | + "--verbose", |
| 144 | + action="store_true", |
| 145 | + help="Show detailed output from commands", |
| 146 | + ) |
| 147 | + |
| 148 | + args = parser.parse_args() |
| 149 | + |
| 150 | + # Find all packages |
| 151 | + packages = find_packages() |
| 152 | + if not packages: |
| 153 | + print("No packages found in packages/ directory") |
| 154 | + sys.exit(1) |
| 155 | + |
| 156 | + print(f"Found {len(packages)} packages:") |
| 157 | + for pkg in packages: |
| 158 | + print(f" - {pkg.name}") |
| 159 | + print() |
| 160 | + |
| 161 | + # First, do a dry-run to check all packages would have the same version |
| 162 | + print("Running dry-run to check version consistency...") |
| 163 | + dry_run_versions: Dict[str, str] = {} |
| 164 | + for package in packages: |
| 165 | + new_version = dry_run_version_bump(package, args.bump_type) |
| 166 | + dry_run_versions[package.name] = new_version |
| 167 | + print(f" {package.name}: {get_package_version(package)} -> {new_version}") |
| 168 | + |
| 169 | + # Check if all packages would have the same version |
| 170 | + unique_dry_run_versions = set(dry_run_versions.values()) |
| 171 | + if len(unique_dry_run_versions) != 1: |
| 172 | + print("\n❌ ERROR: Packages would have different versions after bump:") |
| 173 | + for pkg, ver in dry_run_versions.items(): |
| 174 | + print(f" {pkg}: {ver}") |
| 175 | + print("\nAll packages must have the same version. Please fix version inconsistencies first.") |
| 176 | + sys.exit(1) |
| 177 | + |
| 178 | + target_version = next(iter(unique_dry_run_versions)) |
| 179 | + print(f"\n✓ All packages will be bumped to: {target_version}") |
| 180 | + print("\nProceeding with actual version bump...") |
| 181 | + |
| 182 | + # Now do the actual version bump |
| 183 | + versions: Dict[str, str] = {} |
| 184 | + for package in packages: |
| 185 | + new_version = bump_package_version(package, args.bump_type, args.verbose) |
| 186 | + versions[package.name] = new_version |
| 187 | + |
| 188 | + # Verify all packages have the same version (should always pass now) |
| 189 | + unique_versions = set(versions.values()) |
| 190 | + if len(unique_versions) != 1: |
| 191 | + print("❌ CRITICAL ERROR: Packages have different versions after bump (this should not happen):") |
| 192 | + for pkg, ver in versions.items(): |
| 193 | + print(f" {pkg}: {ver}") |
| 194 | + sys.exit(1) |
| 195 | + |
| 196 | + # Use the first version as the release version |
| 197 | + release_version = next(iter(unique_versions)) |
| 198 | + print(f"\nAll packages bumped to version: {release_version}") |
| 199 | + |
| 200 | + # Ask user about creating branch |
| 201 | + response = input("\nWould you like to create a release branch (y/N): ").strip().lower() |
| 202 | + |
| 203 | + if response in ("y", "yes"): |
| 204 | + branch_name = create_release_branch(release_version, args.verbose) |
| 205 | + print(f"\n✓ Release {release_version} is ready!") |
| 206 | + print(f" Branch: {branch_name}") |
| 207 | + else: |
| 208 | + print(f"\nVersion bump complete. Release version: {release_version}") |
| 209 | + print("You can manually commit and create a branch/PR when ready.") |
| 210 | + |
| 211 | + |
| 212 | +if __name__ == "__main__": |
| 213 | + main() |
0 commit comments