microsoft/TypeAgent

Public

mirrored fromhttps://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fb22ace26e4ae2010db0861d7afaa20698e14528

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

python/ta/tools/release.py

271lines · modecode

1#!/usr/bin/env python3
2# Copyright (c) Microsoft Corporation.
3# Licensed under the MIT License.
4
5"""
6Release automation script for the TypeAgent Python package.
7
8This script:
91. Bumps the patch version (3rd part) in pyproject.toml
102. Commits the change
113. Creates a git tag in the format v{major}.{minor}.{patch}-py
124. Pushes the tags to trigger the GitHub Actions release workflow
13
14Usage:
15 python tools/release.py [--dry-run] [--help]
16"""
17
18import argparse
19import re
20import subprocess
21import sys
22from pathlib import Path
23from typing import Tuple
24
25
26def run_command(cmd: list[str], dry_run: bool = False) -> Tuple[int, str]:
27 """
28 Run a shell command and return (exit_code, output).
29
30 Args:
31 cmd: Command as a list of strings
32 dry_run: If True, print what would be run without executing
33
34 Returns:
35 Tuple of (exit_code, output_string)
36 """
37 cmd_str = " ".join(cmd)
38
39 if dry_run:
40 print(f"[DRY RUN] Would run: {cmd_str}")
41 return 0, ""
42
43 print(f"Running: {cmd_str}")
44
45 try:
46 result = subprocess.run(cmd, capture_output=True, text=True, check=False)
47
48 if result.stdout:
49 print(result.stdout.strip())
50 if result.stderr:
51 print(f"stderr: {result.stderr.strip()}", file=sys.stderr)
52
53 return result.returncode, result.stdout.strip()
54
55 except Exception as e:
56 print(f"Error running command: {e}", file=sys.stderr)
57 return 1, str(e)
58
59
60def parse_version(version_str: str) -> Tuple[int, int, int]:
61 """
62 Parse a semantic version string into (major, minor, patch).
63
64 Args:
65 version_str: Version string like "0.1.3"
66
67 Returns:
68 Tuple of (major, minor, patch) as integers
69
70 Raises:
71 ValueError: If version format is invalid
72 """
73 match = re.match(r"^(\d+)\.(\d+)\.(\d+)$", version_str.strip())
74 if not match:
75 raise ValueError(f"Invalid version format: {version_str}")
76
77 return int(match.group(1)), int(match.group(2)), int(match.group(3))
78
79
80def format_version(major: int, minor: int, patch: int) -> str:
81 """Format version components back into a version string."""
82 return f"{major}.{minor}.{patch}"
83
84
85def get_current_version(pyproject_path: Path) -> str:
86 """
87 Extract the current version from pyproject.toml.
88
89 Args:
90 pyproject_path: Path to the pyproject.toml file
91
92 Returns:
93 Current version string
94
95 Raises:
96 FileNotFoundError: If pyproject.toml doesn't exist
97 ValueError: If version field is not found or invalid
98 """
99 if not pyproject_path.exists():
100 raise FileNotFoundError(f"pyproject.toml not found at {pyproject_path}")
101
102 content = pyproject_path.read_text(encoding="utf-8")
103
104 # Look for version = "x.y.z" in the [project] section
105 version_match = re.search(
106 r'^version\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE
107 )
108
109 if not version_match:
110 raise ValueError("Version field not found in pyproject.toml")
111
112 return version_match.group(1)
113
114
115def update_version_in_pyproject(
116 pyproject_path: Path, new_version: str, dry_run: bool = False
117) -> None:
118 """
119 Update the version in pyproject.toml.
120
121 Args:
122 pyproject_path: Path to the pyproject.toml file
123 new_version: New version string to set
124 dry_run: If True, show what would be changed without modifying the file
125 """
126 content = pyproject_path.read_text(encoding="utf-8")
127
128 # Replace the version field
129 new_content = re.sub(
130 r'^(version\s*=\s*["\'])[^"\']+(["\'])',
131 rf"\g<1>{new_version}\g<2>",
132 content,
133 flags=re.MULTILINE,
134 )
135
136 if content == new_content:
137 raise ValueError("Failed to update version in pyproject.toml")
138
139 if dry_run:
140 print(f"[DRY RUN] Would update version to {new_version} in {pyproject_path}")
141 return
142
143 pyproject_path.write_text(new_content, encoding="utf-8")
144 print(f"Updated version to {new_version} in {pyproject_path}")
145
146
147def check_git_status() -> bool:
148 """
149 Check if the git working directory is clean.
150
151 Returns:
152 True if working directory is clean, False otherwise
153 """
154 exit_code, output = run_command(["git", "status", "--porcelain"])
155
156 if exit_code != 0:
157 print("Error: Failed to check git status", file=sys.stderr)
158 return False
159
160 # If there's any output, the working directory is not clean
161 return len(output.strip()) == 0
162
163
164def main():
165 parser = argparse.ArgumentParser(
166 description="Automate the release process for TypeAgent Python package",
167 formatter_class=argparse.RawDescriptionHelpFormatter,
168 epilog="""
169This script will:
1701. Bump the patch version in pyproject.toml
1712. Commit the change with message "Bump version to X.Y.Z"
1723. Create a git tag "vX.Y.Z-py"
1734. Push the tags to trigger the release workflow
174
175The script must be run from the python/ta directory.
176 """,
177 )
178
179 parser.add_argument(
180 "--dry-run",
181 action="store_true",
182 help="Show what would be done without making changes",
183 )
184
185 args = parser.parse_args()
186
187 # Ensure we're in the right directory
188 current_dir = Path.cwd()
189 expected_files = ["pyproject.toml", "tools"]
190
191 for file_name in expected_files:
192 if not (current_dir / file_name).exists():
193 print(
194 f"Error: {file_name} not found. Please run this script from the python/ta directory.",
195 file=sys.stderr,
196 )
197 return 1
198
199 pyproject_path = current_dir / "pyproject.toml"
200
201 # Check git status (unless dry run)
202 if not args.dry_run and not check_git_status():
203 print(
204 "Error: Git working directory is not clean. Please commit or stash changes first.",
205 file=sys.stderr,
206 )
207 return 1
208
209 try:
210 # Get current version
211 current_version = get_current_version(pyproject_path)
212 print(f"Current version: {current_version}")
213
214 # Parse and bump version
215 major, minor, patch = parse_version(current_version)
216 new_patch = patch + 1
217 new_version = format_version(major, minor, new_patch)
218
219 print(f"New version: {new_version}")
220
221 # Update pyproject.toml
222 update_version_in_pyproject(pyproject_path, new_version, args.dry_run)
223
224 # Git commit
225 exit_code, _ = run_command(["git", "add", "pyproject.toml"], args.dry_run)
226
227 if exit_code != 0:
228 print("Error: Failed to stage pyproject.toml", file=sys.stderr)
229 return 1
230
231 commit_message = f"Bump version to {new_version}"
232 exit_code, _ = run_command(
233 ["git", "commit", "-m", commit_message], args.dry_run
234 )
235
236 if exit_code != 0:
237 print("Error: Failed to commit changes", file=sys.stderr)
238 return 1
239
240 # Create git tag
241 tag_name = f"v{new_version}-py"
242 exit_code, _ = run_command(["git", "tag", tag_name], args.dry_run)
243
244 if exit_code != 0:
245 print(f"Error: Failed to create tag {tag_name}", file=sys.stderr)
246 return 1
247
248 # Push tags
249 exit_code, _ = run_command(["git", "push", "--tags"], args.dry_run)
250
251 if exit_code != 0:
252 print("Error: Failed to push tags", file=sys.stderr)
253 return 1
254
255 if args.dry_run:
256 print(f"\n[DRY RUN] Release process completed successfully!")
257 print(f"Would have created tag: {tag_name}")
258 else:
259 print(f"\nRelease process completed successfully!")
260 print(f"Created tag: {tag_name}")
261 print(f"The GitHub Actions release workflow should now be triggered.")
262
263 return 0
264
265 except Exception as e:
266 print(f"Error: {e}", file=sys.stderr)
267 return 1
268
269
270if __name__ == "__main__":
271 sys.exit(main())
272