implement version bumping script#53
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new Python release manager script (scripts/release.py) to automate version bumping, bootstrap testing, git commits, and crate publishing. It also updates scripts/bootstrap_test.sh to run tests quietly and capture output on failure. The review feedback highlights several critical improvement opportunities in the release script, including making the Cargo.toml workspace parser and package info parser more robust, avoiding the dangerous use of git add . by staging only modified files, and handling potential parsing exceptions when calculating bumped versions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| def comment_out_workspace_members(content, keep_member): | ||
| """Modify the workspace members array, commenting out all members except keep_member.""" | ||
| lines = content.splitlines() | ||
| new_lines = [] | ||
| in_members = False | ||
| for line in lines: | ||
| line_stripped = line.strip() | ||
| if 'members' in line_stripped and '=' in line_stripped: | ||
| in_members = True | ||
| new_lines.append(line) | ||
| continue | ||
| if in_members and line_stripped.startswith(']'): | ||
| in_members = False | ||
| new_lines.append(line) | ||
| continue | ||
|
|
||
| if in_members: | ||
| match = re.search(r'["\']([^"\']+)["\']', line_stripped) | ||
| if match: | ||
| member = match.group(1) | ||
| indent = line[:len(line) - len(line.lstrip())] | ||
| content_part = line.lstrip() | ||
| if member != keep_member: | ||
| if not content_part.startswith('#'): | ||
| new_lines.append(f"{indent}# {content_part}") | ||
| else: | ||
| new_lines.append(line) | ||
| else: | ||
| if content_part.startswith('#'): | ||
| uncommented = content_part[1:].lstrip() | ||
| new_lines.append(f"{indent}{uncommented}") | ||
| else: | ||
| new_lines.append(line) | ||
| else: | ||
| new_lines.append(line) | ||
| else: | ||
| new_lines.append(line) | ||
| return '\n'.join(new_lines) + '\n' |
There was a problem hiding this comment.
The current implementation of comment_out_workspace_members relies on line-by-line parsing with a simple state machine. If members is defined on a single line (e.g., members = ["rusty_lr", "rusty_lr_core"]) or if there are trailing comments, the state in_members will remain True for the rest of the file. This will corrupt the Cargo.toml file by commenting out random lines in other sections. Using a regex-based approach with re.DOTALL is much more robust and handles both single-line and multi-line formats safely.
def comment_out_workspace_members(content, keep_member):
"""Modify the workspace members array, commenting out all members except keep_member."""
def replace_members(match):
prefix = match.group(1)
members_content = match.group(2)
suffix = match.group(3)
new_lines = []
for line in members_content.splitlines(keepends=True):
line_stripped = line.strip()
match_member = re.search(r'["\']([^"\']+)["\']', line_stripped)
if match_member:
member = match_member.group(1)
indent = line[:len(line) - len(line.lstrip())]
content_part = line.lstrip()
if member != keep_member:
if not content_part.startswith('#'):
new_lines.append(f"{indent}# {content_part}")
else:
new_lines.append(line)
else:
if content_part.startswith('#'):
uncommented = content_part[1:].lstrip()
new_lines.append(f"{indent}{uncommented}")
else:
new_lines.append(line)
else:
new_lines.append(line)
return prefix + "".join(new_lines) + suffix
# Match 'members = [ ... ]' across multiple lines
pattern = r'(\\bmembers\\s*=\\s*\\[)([^\\]]*)(\\])'
return re.sub(pattern, replace_members, content, flags=re.DOTALL)| def run_git_commit(root_dir, package_map): | ||
| """Stages and commits all version changes to git with a descriptive release message.""" | ||
| print("=== STARTING GIT COMMIT ===") | ||
|
|
||
| # Construct a detailed commit message outlining the bumped versions | ||
| commit_msg = "Release: Bump versions and execute cargo publish\n\nVersion updates:\n" | ||
| for name, info in package_map.items(): | ||
| commit_msg += f"- {name}: {info['old_version']} -> {info['new_version']}\n" | ||
|
|
||
| try: | ||
| # Run 'git add .' | ||
| print("Running 'git add .'...") | ||
| subprocess.run(["git", "add", "."], cwd=root_dir, check=True) | ||
|
|
||
| # Run 'git commit -m ...' | ||
| print("Running 'git commit -m ...'") | ||
| subprocess.run(["git", "commit", "-m", commit_msg], cwd=root_dir, check=True) | ||
|
|
||
| print("Git commit completed successfully!") | ||
| except subprocess.CalledProcessError as e: | ||
| print(f"Error during git commit: {e}") | ||
| print("=== GIT COMMIT COMPLETE ===\n") | ||
|
|
There was a problem hiding this comment.
Using git add . in a release script is dangerous because it stages all changes in the working directory, including unrelated modifications or untracked files. It is much safer to explicitly stage only the files that were modified by this script (i.e., the Cargo.toml files of the bumped packages and rusty_lr_parser/src/lib.rs).
def run_git_commit(root_dir, package_map):
"""Stages and commits all version changes to git with a descriptive release message."""
print("=== STARTING GIT COMMIT ===")
# Construct a detailed commit message outlining the bumped versions
commit_msg = "Release: Bump versions and execute cargo publish\n\nVersion updates:\n"
for name, info in package_map.items():
commit_msg += f"- {name}: {info['old_version']} -> {info['new_version']}\n"
try:
# Stage only the files modified by the release script
files_to_add = [info['file_path'] for info in package_map.values()]
lib_rs_path = os.path.join(root_dir, 'rusty_lr_parser', 'src', 'lib.rs')
if os.path.exists(lib_rs_path):
files_to_add.append(lib_rs_path)
print("Staging modified files...")
subprocess.run(["git", "add"] + files_to_add, cwd=root_dir, check=True)
# Run 'git commit -m ...'
print("Running 'git commit -m ...'")
subprocess.run(["git", "commit", "-m", commit_msg], cwd=root_dir, check=True)
print("Git commit completed successfully!")
except subprocess.CalledProcessError as e:
print(f"Error during git commit: {e}")
print("=== GIT COMMIT COMPLETE ===\n")| def parse_package_info(file_path): | ||
| """Parse name and version from a Cargo.toml file under the [package] section.""" | ||
| with open(file_path, 'r', encoding='utf-8') as f: | ||
| content = f.read() | ||
|
|
||
| package_name = None | ||
| package_version = None | ||
| current_section = None | ||
|
|
||
| for line in content.splitlines(): | ||
| line_stripped = line.strip() | ||
| if line_stripped.startswith('[') and line_stripped.endswith(']'): | ||
| current_section = line_stripped[1:-1].strip() | ||
| elif current_section == 'package': | ||
| if line_stripped.startswith('name'): | ||
| parts = line_stripped.split('=', 1) | ||
| package_name = parts[1].strip().strip('"').strip("'") | ||
| elif line_stripped.startswith('version'): | ||
| parts = line_stripped.split('=', 1) | ||
| package_version = parts[1].strip().strip('"').strip("'") | ||
|
|
||
| return package_name, package_version |
There was a problem hiding this comment.
The current parser uses line_stripped.startswith('name') and line_stripped.startswith('version'). This can incorrectly match other keys starting with those prefixes (e.g., name-of-something or version.workspace). Splitting the line by = first and checking for an exact match on the key is much more robust.
def parse_package_info(file_path):
"""Parse name and version from a Cargo.toml file under the [package] section."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
package_name = None
package_version = None
current_section = None
for line in content.splitlines():
line_stripped = line.strip()
if line_stripped.startswith('[') and line_stripped.endswith(']'):
current_section = line_stripped[1:-1].strip()
elif current_section == 'package':
if '=' in line_stripped:
key, val = [x.strip() for x in line_stripped.split('=', 1)]
if key == 'name':
package_name = val.strip('"').strip("'")
elif key == 'version':
package_version = val.strip('"').strip("'")
return package_name, package_version| def calculate_bumped_version(version_str): | ||
| """Increment minor version by 1 and set patch version to 0 (e.g., 3.36.0 -> 3.37.0).""" | ||
| parts = version_str.split('.') | ||
| if len(parts) >= 2: | ||
| major = parts[0] | ||
| minor = int(parts[1]) | ||
| return f"{major}.{minor + 1}.0" | ||
| return version_str |
There was a problem hiding this comment.
If the version string does not conform to standard semver or contains non-integer components in the minor version, int(parts[1]) will raise a ValueError and crash the script. Wrapping this in a try-except block makes the script more resilient.
| def calculate_bumped_version(version_str): | |
| """Increment minor version by 1 and set patch version to 0 (e.g., 3.36.0 -> 3.37.0).""" | |
| parts = version_str.split('.') | |
| if len(parts) >= 2: | |
| major = parts[0] | |
| minor = int(parts[1]) | |
| return f"{major}.{minor + 1}.0" | |
| return version_str | |
| def calculate_bumped_version(version_str): | |
| """Increment minor version by 1 and set patch version to 0 (e.g., 3.36.0 -> 3.37.0).""" | |
| parts = version_str.split('.') | |
| if len(parts) >= 2: | |
| major = parts[0] | |
| try: | |
| minor = int(parts[1]) | |
| return f"{major}.{minor + 1}.0" | |
| except ValueError: | |
| pass | |
| return version_str |
Description
This Pull Request introduces a unified release manager script
scripts/release.pyto automate version bumping, workspace validation, and sequential crate publishing in a secure and interactive manner.Additionally, it removes obsolete separate versioning/publishing scripts and configures the bootstrap test runner to run silently on success, outputting details only on compilation or test failures.
Key Features
1. Release Pipeline Command (
scripts/release.py all)Enforces a strict execution pipeline in the following order:
+1(setting the patch version to0, excluding theexamplefolder). Updates inter-workspace dependency constraints andrusty_lr_parser::target_rusty_lr_version()automatically.2. Quiet Testing Suite
scripts/bootstrap_test.shto suppress compile logs (--quiet) and capturecargo teststdout/stderr.3. Isolated Sequence Publishing & Safety Warning
Cargo.tomlto comply with cargo package verification.Confirm: Do you want to publish '<package> v<version>'? [y/N]) before executingcargo publish.--allow-dirtyflag is missing, as the version bump step leaves uncommitted edits in place during publish checks.4. Automatic Git Commit