Skip to content

WEB3-464: [WIP] Add release automation scripts #613

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/venv
93 changes: 93 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Release Automation Scripts

This directory contains scripts to automate the risc0-ethereum release process as described in [RELEASE.md](../RELEASE.md).

## Quick Start

### Prerequisites

- Python 3.7+
- Git
- Bash

### Setup

1. Set up the Python environment:
```bash
scripts/setup-venv.sh
```

2. Run a release preparation (dry-run first!):
```bash
# Prepare release 2.1.0 (dry run)
scripts/prepare-release.sh 2.1.0 --dry-run

# Actually run it
scripts/prepare-release.sh 2.1.0
```

3. Prepare next development version:
```bash
# Prepare next dev version 2.2.0-alpha.1 (dry run)
scripts/prepare-next-version.sh 2.2.0 --dry-run

# Actually run it
scripts/prepare-next-version.sh 2.2.0
```

## Main Orchestration Scripts

### `prepare-release.sh`
Automates the complete release branch preparation process.

**Usage:**
```bash
scripts/prepare-release.sh <release_version> [options]

Arguments:
release_version Version to release (e.g., 2.1.0)

Options:
--risc0-version RISC Zero monorepo version to use (default: 2.0)
--dry-run Show changes without applying them
--help Show help message

Examples:
scripts/prepare-release.sh 2.1.0
scripts/prepare-release.sh 2.1.0 --risc0-version 2.0 --dry-run
```

**What it does:**
1. Creates release branch (e.g., `release-2.1`)
2. Updates all Cargo.toml versions to release version
3. Updates contract version strings
4. Updates branch references from `main` to release branch
5. Converts risc0 git dependencies to version dependencies
6. Updates .gitignore to include Cargo.lock files
7. Removes main branch warning from README.md
8. Updates CHANGELOG.md to mark current version as released

### `prepare-next-version.sh`
Prepares the main branch for the next development cycle.

**Usage:**
```bash
scripts/prepare-next-version.sh <next_version> [options]

Arguments:
next_version Next development version (e.g., 2.2.0)
Script automatically appends '-alpha.1'

Options:
--dry-run Show changes without applying them
--help Show help message

Examples:
scripts/prepare-next-version.sh 2.2.0
scripts/prepare-next-version.sh 2.2.0 --dry-run
```

**What it does:**
1. Updates all Cargo.toml versions to next alpha version
2. Updates contract version strings to next alpha version
3. Adds new "Unreleased" section to CHANGELOG.md
175 changes: 175 additions & 0 deletions scripts/bump-cargo-versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
Cargo version bump script for risc0-ethereum release automation.

This script updates versions across all workspace Cargo.toml files.
Supports both release mode (x.y.z) and next development mode (x.y+1.0-alpha.1).
"""

import argparse
import re
import sys
import toml
from pathlib import Path
from typing import List, Tuple, Optional


def parse_version(version_str: str) -> Tuple[int, int, int, Optional[str]]:
"""Parse semantic version string into components."""
pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$'
match = re.match(pattern, version_str)
if not match:
raise ValueError(f"Invalid version format: {version_str}")

major, minor, patch, prerelease = match.groups()
return int(major), int(minor), int(patch), prerelease


def format_version(major: int, minor: int, patch: int, prerelease: Optional[str] = None) -> str:
"""Format version components back to string."""
version = f"{major}.{minor}.{patch}"
if prerelease:
version += f"-{prerelease}"
return version


def get_next_release_version(current_version: str) -> str:
"""Convert alpha version to release version (remove prerelease suffix)."""
major, minor, patch, prerelease = parse_version(current_version)
if prerelease and 'alpha' in prerelease:
# Remove alpha suffix for release
return format_version(major, minor, patch)
else:
# Already a release version, just return as-is
return current_version


def get_next_dev_version(current_version: str) -> str:
"""Get next development version (increment minor, set patch to 0, add alpha.1)."""
major, minor, patch, prerelease = parse_version(current_version)
# For next dev version, increment minor and reset patch
return format_version(major, minor + 1, 0, "alpha.1")


def find_cargo_toml_files() -> List[Path]:
"""Find all Cargo.toml files that need version updates."""
root = Path('.')

# Use the grep command from RELEASE.md to find files
import subprocess
result = subprocess.run([
'grep', '-rl', '^version = "', '--include=Cargo.toml',
'--exclude-dir=./lib', '--exclude-dir=./examples',
'--exclude-dir=./crates/ffi/guests', '--exclude-dir=./target', '.'
], capture_output=True, text=True, cwd=root)

if result.returncode != 0:
return []

files = [Path(f.strip()) for f in result.stdout.strip().split('\n') if f.strip()]
return [f for f in files if f.exists()]


def update_cargo_toml(file_path: Path, new_version: str, dry_run: bool = False) -> bool:
"""Update version in a Cargo.toml file."""
if not file_path.exists():
print(f"Warning: {file_path} does not exist")
return False

try:
content = file_path.read_text()
data = toml.loads(content)

# Check if this file has a version field
if 'package' in data and 'version' in data['package']:
old_version = data['package']['version']
if old_version != new_version:
print(f" {file_path}: {old_version} -> {new_version}")
if not dry_run:
data['package']['version'] = new_version
file_path.write_text(toml.dumps(data))
return True

# Check workspace version
if 'workspace' in data and 'package' in data['workspace'] and 'version' in data['workspace']['package']:
old_version = data['workspace']['package']['version']
if old_version != new_version:
print(f" {file_path} (workspace): {old_version} -> {new_version}")
if not dry_run:
data['workspace']['package']['version'] = new_version
file_path.write_text(toml.dumps(data))
return True

except Exception as e:
print(f"Error updating {file_path}: {e}")
return False

return False


def main():
parser = argparse.ArgumentParser(description='Bump Cargo.toml versions for risc0-ethereum release')
parser.add_argument('version', help='Target version (e.g., 2.1.0 or auto)')
parser.add_argument('--mode', choices=['release', 'next-dev'], required=True,
help='release: prepare for release, next-dev: prepare for next development cycle')
parser.add_argument('--dry-run', action='store_true', help='Show changes without applying them')

args = parser.parse_args()

# Read current version from workspace Cargo.toml
workspace_cargo = Path('Cargo.toml')
if not workspace_cargo.exists():
print("Error: Cargo.toml not found in current directory")
sys.exit(1)

try:
workspace_data = toml.loads(workspace_cargo.read_text())
current_version = workspace_data['workspace']['package']['version']
print(f"Current version: {current_version}")
except Exception as e:
print(f"Error reading workspace Cargo.toml: {e}")
sys.exit(1)

# Determine target version
if args.version == 'auto':
if args.mode == 'release':
target_version = get_next_release_version(current_version)
else: # next-dev
target_version = get_next_dev_version(current_version)
else:
target_version = args.version

print(f"Target version: {target_version}")

if args.dry_run:
print("\n=== DRY RUN MODE - No changes will be made ===")

# Validate target version format
try:
parse_version(target_version)
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)

changes_made = False

# Update Cargo.toml files
print(f"\nUpdating Cargo.toml files:")
cargo_files = find_cargo_toml_files()
if not cargo_files:
print("No Cargo.toml files found")
else:
for file_path in cargo_files:
if update_cargo_toml(file_path, target_version, args.dry_run):
changes_made = True

if not changes_made:
print("\nNo changes needed - versions are already up to date")
elif args.dry_run:
print(f"\nDry run complete. Run without --dry-run to apply changes.")
else:
print(f"\nCargo version bump complete: {current_version} -> {target_version}")


if __name__ == '__main__':
main()
87 changes: 87 additions & 0 deletions scripts/bump-contract-versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
Contract version bump script for risc0-ethereum release automation.

This script updates version strings in Solidity contract files.
"""

import argparse
import re
import sys
from pathlib import Path
from typing import List


def find_contract_files() -> List[Path]:
"""Find contract files that contain version strings."""
return [
Path('contracts/src/groth16/RiscZeroGroth16Verifier.sol'),
Path('contracts/src/RiscZeroSetVerifier.sol')
]


def update_contract_version(file_path: Path, new_version: str, dry_run: bool = False) -> bool:
"""Update version string in a Solidity contract file."""
if not file_path.exists():
print(f"Warning: {file_path} does not exist")
return False

try:
content = file_path.read_text()

# Pattern to match version strings in contracts
pattern = r'string public constant VERSION = "([^"]+)";'

changes_made = False
def replace_version(match):
nonlocal changes_made
old_version = match.group(1)
if old_version != new_version:
print(f" {file_path}: {old_version} -> {new_version}")
changes_made = True
return f'string public constant VERSION = "{new_version}";'
return match.group(0)

new_content = re.sub(pattern, replace_version, content)

if changes_made and not dry_run:
file_path.write_text(new_content)

return changes_made

except Exception as e:
print(f"Error updating {file_path}: {e}")
return False


def main():
parser = argparse.ArgumentParser(description='Bump contract version strings for risc0-ethereum release')
parser.add_argument('version', help='Target version (e.g., 2.1.0)')
parser.add_argument('--dry-run', action='store_true', help='Show changes without applying them')

args = parser.parse_args()

if args.dry_run:
print("=== DRY RUN MODE - No changes will be made ===")

print(f"Target version: {args.version}")

changes_made = False

# Update contract files
print(f"\nUpdating contract version strings:")
contract_files = find_contract_files()
for file_path in contract_files:
if update_contract_version(file_path, args.version, args.dry_run):
changes_made = True

if not changes_made:
print("\nNo changes needed - contract versions are already up to date")
elif args.dry_run:
print(f"\nDry run complete. Run without --dry-run to apply changes.")
else:
print(f"\nContract version bump complete -> {args.version}")


if __name__ == '__main__':
main()
Loading
Loading