microsoft/TypeAgent

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
6589deaef62f063bb4ea424e9d04ef00922f0c79

Branches

Tags

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

Clone

HTTPS

Download ZIP

python/ta/tools/release.py

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