Skip to content

implement version bumping script#53

Merged
ehwan merged 3 commits into
mainfrom
bump_version
Jun 18, 2026
Merged

implement version bumping script#53
ehwan merged 3 commits into
mainfrom
bump_version

Conversation

@ehwan

@ehwan ehwan commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Description

This Pull Request introduces a unified release manager script scripts/release.py to 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. Version Bump: Bumps the minor version of all workspace packages by +1 (setting the patch version to 0, excluding the example folder). Updates inter-workspace dependency constraints and rusty_lr_parser::target_rusty_lr_version() automatically.
  2. Bootstrap Test: Runs workspace-wide compiler and test verifications.
  3. Workspace Crate Publishing: Sequence-publishes isolated packages.
  4. Git Commit: Automatically stages and commits the version bump changes.

2. Quiet Testing Suite

  • Modified scripts/bootstrap_test.sh to suppress compile logs (--quiet) and capture cargo test stdout/stderr.
  • If all tests pass successfully, the output is completely silenced (deleted without printing).
  • If any test fails, the script outputs the full test runner logs loudly to aid in debugging, and immediately halts the pipeline.

3. Isolated Sequence Publishing & Safety Warning

  • For each crate, the script temporarily comments out other members in the root Cargo.toml to comply with cargo package verification.
  • Prompts the user (Confirm: Do you want to publish '<package> v<version>'? [y/N]) before executing cargo publish.
  • Added a high-visibility warning banner warning the user if the --allow-dirty flag is missing, as the version bump step leaves uncommitted edits in place during publish checks.

4. Automatic Git Commit

  • Upon a successful pipeline run (all checks and publishing succeed), the script stages all modifications and commits them with a detailed summary message.

@ehwan ehwan self-assigned this Jun 18, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/release.py
Comment on lines +265 to +302
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'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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)

Comment thread scripts/release.py
Comment on lines +231 to +253
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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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")

Comment thread scripts/release.py
Comment on lines +59 to +80
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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

Comment thread scripts/release.py
Comment on lines +82 to +89
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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

@ehwan
ehwan merged commit 2adb643 into main Jun 18, 2026
1 check passed
@ehwan
ehwan deleted the bump_version branch June 18, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant