feat(scripts): add automated version bump script with optional push flag#235
Conversation
|
@Edison-A-N is attempting to deploy a commit to the Raphael's projects Team on Vercel. A member of the Team first needs to authorize it. |
5d8f7a5 to
a5c31aa
Compare
|
pending merge |
There was a problem hiding this comment.
Pull request overview
This PR introduces an automated version bumping script (scripts/bump_version.py) to streamline the release workflow by automating version updates in pyproject.toml and src/openagents/__init__.py, creating git commits and tags, with an optional push to remote.
Changes:
- Adds
scripts/bump_version.pywith support for semantic versioning (major, minor, patch, post) - Implements automatic file updates, git commit/tag creation, and optional remote push via
--pushflag - Provides manual GitHub release creation guidance via URL output
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def run_command(command): | ||
| """Execute a shell command and handle errors.""" | ||
| try: | ||
| subprocess.run(command, check=True, shell=True) | ||
| except subprocess.CalledProcessError as e: | ||
| print(f"Error executing command: {command}") | ||
| print(f"Error: {e}") | ||
| sys.exit(1) |
There was a problem hiding this comment.
Using shell=True with subprocess.run() creates a security vulnerability as it allows shell injection attacks. The command parameter should be passed as a list of arguments instead. Looking at the existing script in scripts/generate_grpc.py (line 51), the codebase convention is to use subprocess.run() with a list of arguments without shell=True.
|
|
||
| print(f"\n🎉 Successfully bumped version from {current_version} to {new_version}") | ||
| print(f" Tag v{new_version} has been pushed to remote") | ||
| print(f" You can create a GitHub release at: https://github.com/openagents-org/openagents/releases/new?tag=v{new_version}") |
There was a problem hiding this comment.
The GitHub repository URL is incorrect. According to pyproject.toml lines 123-126, the official repository URL is https://github.com/bestagents/openagents, not https://github.com/openagents-org/openagents.
| print(f" You can create a GitHub release at: https://github.com/openagents-org/openagents/releases/new?tag=v{new_version}") | |
| print(f" You can create a GitHub release at: https://github.com/bestagents/openagents/releases/new?tag=v{new_version}") |
| if push: | ||
| run_command("git push") | ||
| print("✓ Pushed commit") | ||
|
|
||
| run_command(f"git tag v{new_version}") | ||
| print(f"✓ Created tag v{new_version}") | ||
|
|
||
| run_command("git push --tags") | ||
| print("✓ Pushed tag") |
There was a problem hiding this comment.
The git tag is created (line 152) before pushing the commit (line 149), which could result in an inconsistent state if the push fails after the tag is created. The tag should be created after the commit is successfully pushed, or moved to line 162 where it's correctly placed in the else block.
| Args: | ||
| version_type: Type of version bump (major/minor/patch/post) | ||
| push: Whether to push changes to remote and create GitHub release | ||
| """ |
There was a problem hiding this comment.
The script should verify that the working directory is clean (no uncommitted changes) before proceeding with the version bump. Running this script with uncommitted changes would include those changes in the version bump commit, which is likely unintended behavior. Consider adding a check using 'git diff --quiet' and 'git diff --cached --quiet' at the start of the bump_version function.
| """ | |
| """ | |
| # Ensure working directory is clean (no unstaged or staged changes) | |
| unstaged_result = subprocess.run( | |
| "git diff --quiet", | |
| shell=True, | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL, | |
| check=False, | |
| ) | |
| staged_result = subprocess.run( | |
| "git diff --cached --quiet", | |
| shell=True, | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL, | |
| check=False, | |
| ) | |
| if unstaged_result.returncode != 0 or staged_result.returncode != 0: | |
| print("Error: Working directory has uncommitted changes. " | |
| "Please commit or stash them before bumping the version.") | |
| sys.exit(1) |
| # Read current version from pyproject.toml | ||
| pyproject_content = pyproject_file.read_text() | ||
| version_match = re.search(r'^version = ["\']([^"\']+)["\']', pyproject_content, re.MULTILINE) | ||
| if not version_match: | ||
| print("Error: Could not find version in pyproject.toml") | ||
| sys.exit(1) | ||
|
|
||
| current_version = version_match.group(1) | ||
| major, minor, patch, post = parse_version(current_version) |
There was a problem hiding this comment.
There is a version mismatch in the codebase before this script is introduced. The pyproject.toml has version "0.8.5.post6" (line 7) while src/openagents/init.py has version "0.8.5.post5" (line 3). This discrepancy should be resolved before merging this PR, as the script assumes both files start with the same version.
| #!/usr/bin/env python3 | ||
| """Version bump script for OpenAgents project. | ||
|
|
||
| This script automates the version bumping process by: | ||
| 1. Reading the current version from pyproject.toml | ||
| 2. Incrementing the version based on the specified type (major/minor/patch/post) | ||
| 3. Updating both pyproject.toml and src/openagents/__init__.py | ||
| 4. Committing the changes to git | ||
| 5. Creating a git tag | ||
| 6. Optionally pushing changes and tag to remote | ||
|
|
||
| Usage: | ||
| python scripts/bump_version.py <major|minor|patch|post> [--push] | ||
|
|
||
| Arguments: | ||
| version_type Type of version bump: major, minor, patch, or post | ||
| --push Push changes and tag to remote | ||
|
|
||
| Examples: | ||
| python scripts/bump_version.py patch # 0.8.5 -> 0.8.6 (local only) | ||
| python scripts/bump_version.py patch --push # 0.8.5 -> 0.8.6 (push to remote) | ||
| python scripts/bump_version.py minor --push # 0.8.5 -> 0.9.0 (push to remote) | ||
| python scripts/bump_version.py major --push # 0.8.5 -> 1.0.0 (push to remote) | ||
| python scripts/bump_version.py post --push # 0.8.5 -> 0.8.5.post1 (push to remote) | ||
| """ | ||
| import re | ||
| import sys | ||
| import subprocess | ||
| import argparse | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| def run_command(command): | ||
| """Execute a shell command and handle errors.""" | ||
| try: | ||
| subprocess.run(command, check=True, shell=True) | ||
| except subprocess.CalledProcessError as e: | ||
| print(f"Error executing command: {command}") | ||
| print(f"Error: {e}") | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| def parse_version(version_str): | ||
| """Parse version string into components. | ||
|
|
||
| Supports formats like: | ||
| - 0.8.5 | ||
| - 0.8.5.post1 | ||
| - 0.8.5.post2 | ||
|
|
||
| Returns: | ||
| tuple: (major, minor, patch, post_number or None) | ||
| """ | ||
| # Match version pattern: major.minor.patch[.postN] | ||
| match = re.match(r'^(\d+)\.(\d+)\.(\d+)(?:\.post(\d+))?$', version_str) | ||
| if not match: | ||
| print(f"Invalid version format: {version_str}") | ||
| sys.exit(1) | ||
|
|
||
| major, minor, patch, post = match.groups() | ||
| return int(major), int(minor), int(patch), int(post) if post else None | ||
|
|
||
|
|
||
| def format_version(major, minor, patch, post=None): | ||
| """Format version components into a version string.""" | ||
| base_version = f"{major}.{minor}.{patch}" | ||
| if post is not None: | ||
| return f"{base_version}.post{post}" | ||
| return base_version | ||
|
|
||
|
|
||
| def bump_version(version_type, push=False): | ||
| """Bump version based on the specified type. | ||
|
|
||
| Args: | ||
| version_type: Type of version bump (major/minor/patch/post) | ||
| push: Whether to push changes to remote and create GitHub release | ||
| """ | ||
| # File paths | ||
| pyproject_file = Path("pyproject.toml") | ||
| init_file = Path("src/openagents/__init__.py") | ||
|
|
||
| # Validate files exist | ||
| if not pyproject_file.exists(): | ||
| print(f"Error: {pyproject_file} not found") | ||
| sys.exit(1) | ||
| if not init_file.exists(): | ||
| print(f"Error: {init_file} not found") | ||
| sys.exit(1) | ||
|
|
||
| # Read current version from pyproject.toml | ||
| pyproject_content = pyproject_file.read_text() | ||
| version_match = re.search(r'^version = ["\']([^"\']+)["\']', pyproject_content, re.MULTILINE) | ||
| if not version_match: | ||
| print("Error: Could not find version in pyproject.toml") | ||
| sys.exit(1) | ||
|
|
||
| current_version = version_match.group(1) | ||
| major, minor, patch, post = parse_version(current_version) | ||
|
|
||
| # Calculate new version based on type | ||
| if version_type == "major": | ||
| new_version = format_version(major + 1, 0, 0) | ||
| elif version_type == "minor": | ||
| new_version = format_version(major, minor + 1, 0) | ||
| elif version_type == "patch": | ||
| new_version = format_version(major, minor, patch + 1) | ||
| elif version_type == "post": | ||
| # If already a post release, increment post number | ||
| # Otherwise, create .post1 | ||
| if post is not None: | ||
| new_version = format_version(major, minor, patch, post + 1) | ||
| else: | ||
| new_version = format_version(major, minor, patch, 1) | ||
| else: | ||
| print("Invalid version type. Use 'major', 'minor', 'patch', or 'post'") | ||
| sys.exit(1) | ||
|
|
||
| print(f"Bumping version from {current_version} to {new_version}") | ||
|
|
||
| # Update pyproject.toml | ||
| new_pyproject_content = re.sub( | ||
| r'^version = ["\']([^"\']+)["\']', | ||
| f'version = "{new_version}"', | ||
| pyproject_content, | ||
| count=1, | ||
| flags=re.MULTILINE | ||
| ) | ||
| pyproject_file.write_text(new_pyproject_content) | ||
| print(f"✓ Updated {pyproject_file}") | ||
|
|
||
| # Update __init__.py | ||
| init_content = init_file.read_text() | ||
| new_init_content = re.sub( | ||
| r'__version__ = ["\']([^"\']+)["\']', | ||
| f'__version__ = "{new_version}"', | ||
| init_content | ||
| ) | ||
| init_file.write_text(new_init_content) | ||
| print(f"✓ Updated {init_file}") | ||
|
|
||
| # Git operations | ||
| print("\nPerforming git operations...") | ||
| run_command("git add pyproject.toml src/openagents/__init__.py") | ||
| run_command(f'git commit -m "chore(release): bump version to {new_version}"') | ||
| print("✓ Created commit") | ||
|
|
||
| if push: | ||
| run_command("git push") | ||
| print("✓ Pushed commit") | ||
|
|
||
| run_command(f"git tag v{new_version}") | ||
| print(f"✓ Created tag v{new_version}") | ||
|
|
||
| run_command("git push --tags") | ||
| print("✓ Pushed tag") | ||
|
|
||
| print(f"\n🎉 Successfully bumped version from {current_version} to {new_version}") | ||
| print(f" Tag v{new_version} has been pushed to remote") | ||
| print(f" You can create a GitHub release at: https://github.com/openagents-org/openagents/releases/new?tag=v{new_version}") | ||
| else: | ||
| run_command(f"git tag v{new_version}") | ||
| print(f"✓ Created tag v{new_version}") | ||
|
|
||
| print(f"\n🎉 Successfully bumped version from {current_version} to {new_version}") | ||
| print(f" Changes committed locally. Use --push to push to remote") | ||
|
|
||
|
|
||
| def main(): | ||
| """Main entry point.""" | ||
| parser = argparse.ArgumentParser( | ||
| description='Bump version for OpenAgents project', | ||
| formatter_class=argparse.RawDescriptionHelpFormatter, | ||
| epilog=__doc__ | ||
| ) | ||
| parser.add_argument( | ||
| 'version_type', | ||
| choices=['major', 'minor', 'patch', 'post'], | ||
| help='Type of version bump' | ||
| ) | ||
| parser.add_argument( | ||
| '--push', | ||
| action='store_true', | ||
| help='Push changes and tag to remote' | ||
| ) | ||
|
|
||
| args = parser.parse_args() | ||
| bump_version(args.version_type, push=args.push) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
There was a problem hiding this comment.
While the codebase has comprehensive test coverage for most modules, utility scripts like bump_version.py currently lack test coverage. Consider adding tests to verify version parsing, version bumping logic, and git operations. This would help prevent regressions when modifying the script in the future. Tests could mock subprocess calls and file operations to verify the script's behavior without actually modifying files or git history.
|
|
||
| Args: | ||
| version_type: Type of version bump (major/minor/patch/post) | ||
| push: Whether to push changes to remote and create GitHub release |
There was a problem hiding this comment.
The docstring incorrectly states that the push parameter is for pushing "changes to remote and create GitHub release", but the script does not create a GitHub release. It only pushes commits and tags, then provides a URL for manual release creation. The docstring should be updated to reflect the actual behavior.
| push: Whether to push changes to remote and create GitHub release | |
| push: Whether to push commits and tags to the remote repository |
Add Version Bump Script
📋 Summary
This PR introduces an automated version bumping script (
scripts/bump_version.py) to streamline the release workflow, replacing manual version updates inpyproject.tomlandsrc/openagents/__init__.py.✨ Features
Version Bump Types
The script supports all semantic versioning bump types:
majorminorpatchpostWhat the Script Does
pyproject.tomlpyproject.tomlandsrc/openagents/__init__.pyvX.Y.Z)--pushflag)🚀 Usage
Basic Usage (Local Only)
This creates a commit and tag locally without pushing to remote.
Push to Remote
This pushes the commit and tag to remote, ready for release.
🔒 Design Decisions
1.
--pushFlag as Safety GuardThe
--pushparameter is implemented as an opt-in safety feature:Current Design (with
--push):Alternative (without
--push):If the release process is exclusively managed by core maintainers, the
--pushflag can be removed to simplify the workflow. This would make the script always push by default, reducing command verbosity for trusted release managers.Recommendation: Keep the
--pushflag for now to provide flexibility and safety. It can be removed in a future iteration if the team prefers a simpler workflow.2. GitHub Release Creation
The current implementation does not automatically create GitHub Releases. Instead, it provides a URL for manual release creation.
Why not automated?
ghCLI toolHow to add GitHub Release automation:
If desired, add the following code to the script (after line 156, in the
if push:block) and installghCLI:Prerequisites:
This will automatically create a GitHub release with auto-generated release notes.
🔄 Future Enhancement: CI/CD Integration
Recommended: PyPI Publishing via GitHub Actions
To fully automate the release process, a GitHub Actions workflow can be added that triggers on new version tags (e.g.,
v*), builds the package, and publishes to PyPI automatically. This would enable:🧪 Testing
All version bump types have been tested:
post: 0.8.5.post2 → 0.8.5.post3patch: 0.8.5.post2 → 0.8.6minor: 0.8.5.post2 → 0.9.0major: 0.8.5.post2 → 1.0.0All test commits have been cleaned up and permanently removed from git history.
📝 Example Workflow
Option 1: Direct Push (Recommended for maintainers)
Option 2: Verify Before Push (Safer)
🔗 Reference
This script is inspired by mcpadapt's bump_version.py, adapted for OpenAgents' specific project structure and workflow.
🤔 Questions for Review
--pushflag for a simpler workflow?ghCLI)?Closes: N/A
Related: Release automation discussion