diff --git a/release/create_release_pull_request.py b/release/create_release_pull_request.py index 09cc121c3..5dfed0ed4 100755 --- a/release/create_release_pull_request.py +++ b/release/create_release_pull_request.py @@ -2,10 +2,12 @@ import argparse import os import subprocess +import sys from inspect import getsourcefile from os.path import abspath import semver +import tomlkit from utils.validate_version import assert_version_is_not_published @@ -15,31 +17,73 @@ SCRIPTS_DIR = os.path.join(REPO_ROOT_DIR, "scripts") SCRIPT_FILENAME = os.path.basename(getsourcefile(lambda: 0)) -def overwrite_version(version): +def get_current_version(): + """Read the current version from Cargo.toml using tomlkit""" toml_path = os.path.join(REPO_ROOT_DIR, "Cargo.toml") - with open(toml_path, "r") as file: - lines = file.readlines() - - # This will preserve line order. - current_version = None - for i, line in enumerate(lines): - if line.startswith("version ="): - current_version = line.split("=")[1].strip().strip('"') - if current_version == version: - print(f"Already at version {version}.") - exit(1) - lines[i] = f"version = \"{version}\"\n" - break - - if current_version is None: - print("The `version` field is not present in Cargo.toml.") - exit(1) + + try: + with open(toml_path, "r") as file: + doc = tomlkit.parse(file.read()) + + if "package" in doc and "version" in doc["package"]: + return doc["package"]["version"] + else: + print("Error: The `version` field is not present in Cargo.toml [package] section.") + sys.exit(1) + except FileNotFoundError: + print(f"Error: Cargo.toml not found at {toml_path}") + sys.exit(1) + except Exception as e: + print(f"Error: Failed to parse Cargo.toml: {e}") + sys.exit(1) + +def overwrite_version(version, dry_run=False): + """ + Update version in Cargo.toml using tomlkit. + Preserves formatting, comments, and structure. + """ + toml_path = os.path.join(REPO_ROOT_DIR, "Cargo.toml") + + try: + with open(toml_path, "r") as file: + content = file.read() + doc = tomlkit.parse(content) + except FileNotFoundError: + print(f"Error: Cargo.toml not found at {toml_path}") + sys.exit(1) + except Exception as e: + print(f"Error: Failed to parse Cargo.toml: {e}") + sys.exit(1) + + # Check if [package] section exists + if "package" not in doc: + print("Error: [package] section not found in Cargo.toml") + sys.exit(1) + + # Check if version field exists + if "version" not in doc["package"]: + print("Error: version field not found in [package] section") + sys.exit(1) + + current_version = doc["package"]["version"] + + if current_version == version: + print(f"Already at version {version}.") + sys.exit(1) commit_message = f"chore(deps): change version from {current_version} with {version}" - print(commit_message) + print(f"Updating version in Cargo.toml: {current_version} -> {version}") + if dry_run: + print("Dry-run mode: Skipping version file write and commit.") + return + + # Update version in the document + doc["package"]["version"] = version + + # Write back to file (preserves formatting and comments) with open(toml_path, "w") as file: - file.writelines(lines) + file.write(tomlkit.dumps(doc)) # Update VRL version in Cargo.lock subprocess.run(["cargo", "update", "-p", "vrl"], check=True, cwd=REPO_ROOT_DIR) @@ -47,6 +91,35 @@ def overwrite_version(version): subprocess.run(["git", "commit", "-a", "-m", commit_message], check=True, cwd=REPO_ROOT_DIR) +def resolve_version(version_arg): + """ + Resolve version argument to actual version string. + Supports: + - Exact version (e.g., "1.2.3") + - "major" - bump major version + - "minor" - bump minor version + - "patch" - bump patch version + """ + bump_types = ["major", "minor", "patch"] + + if version_arg.lower() in bump_types: + current_version_str = get_current_version() + current_version = semver.VersionInfo.parse(current_version_str) + + if version_arg.lower() == "major": + new_version = current_version.bump_major() + elif version_arg.lower() == "minor": + new_version = current_version.bump_minor() + elif version_arg.lower() == "patch": + new_version = current_version.bump_patch() + + new_version_str = str(new_version) + print(f"Bumping {version_arg} version: {current_version_str} -> {new_version_str}") + return new_version_str + else: + # Assume it's an exact version + return version_arg + def validate_version(version): try: semver.VersionInfo.parse(version) @@ -56,8 +129,11 @@ def validate_version(version): assert_version_is_not_published(version) -def generate_changelog(): +def generate_changelog(dry_run=False): print("Generating changelog...") + if dry_run: + print("Dry-run mode: Skipping changelog generation and commit.") + return subprocess.run(["./generate_release_changelog.sh", "--no-prompt"], check=True, cwd=SCRIPTS_DIR) subprocess.run(["git", "commit", "-a", "-m", "chore(releasing): generate changelog"], check=True, @@ -65,15 +141,21 @@ def generate_changelog(): def create_branch(branch_name, dry_run=False): print(f"Creating branch: {branch_name}") + if dry_run: + print("Dry-run mode: Skipping branch creation.") + return subprocess.run(["git", "checkout", "-b", branch_name], check=True, cwd=REPO_ROOT_DIR) - if not dry_run: - subprocess.run(["git", "push", "-u", "origin", branch_name], - check=True, - cwd=REPO_ROOT_DIR) + subprocess.run(["git", "push", "-u", "origin", branch_name], + check=True, + cwd=REPO_ROOT_DIR) -def create_pull_request(branch_name, new_version, dry_run=False): +def create_pull_request(branch_name, new_version, issue_link=None, dry_run=False): title = f"chore(releasing): Prepare {new_version} release" body = f"Generated with {SCRIPT_FILENAME}" + + if issue_link: + body += f"\n\nRelated issue: {issue_link}" + print(f"Creating pull request with title: {title}") if dry_run: print("Dry-run mode: Skipping PR creation.") @@ -87,24 +169,33 @@ def create_pull_request(branch_name, new_version, dry_run=False): def main(): parser = argparse.ArgumentParser(description="Prepare a new release") - parser.add_argument("version", help="The new version to release") + parser.add_argument("version", help="The new version to release (e.g., '1.2.3', 'major', 'minor', or 'patch')") + parser.add_argument("--issue", "-i", dest="issue_link", + help="GitHub issue link to include in the PR body (e.g., 'https://github.com/owner/repo/issues/123')") parser.add_argument("--dry-run", action="store_true", - help="Run the script without making remote changes") + help="Run the script without making any changes (read-only)") args = parser.parse_args() - new_version = args.version + # Resolve version (could be exact version or bump type) + new_version = resolve_version(args.version) dry_run = args.dry_run + issue_link = args.issue_link + + if not dry_run: + validate_version(new_version) - validate_version(new_version) branch_name = f"prepare-{new_version}-release" create_branch(branch_name, dry_run) - overwrite_version(new_version) - generate_changelog() - subprocess.run(["git", "push"], check=True, cwd=REPO_ROOT_DIR) - create_pull_request(branch_name, new_version, dry_run) + overwrite_version(new_version, dry_run) + generate_changelog(dry_run) + + if not dry_run: + subprocess.run(["git", "push"], check=True, cwd=REPO_ROOT_DIR) + + create_pull_request(branch_name, new_version, issue_link, dry_run) if dry_run: - print("Dry-run completed. No actual remote changes were made.") + print("\nDry-run completed. No changes were made.") if __name__ == "__main__": main() diff --git a/release/requirements.txt b/release/requirements.txt index 2db6103f8..cf5167adb 100644 --- a/release/requirements.txt +++ b/release/requirements.txt @@ -1,2 +1,3 @@ requests semver +tomlkit