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
RELEASE_DIR = os.path.dirname(abspath(getsourcefile(lambda: 0)))
REPO_ROOT_DIR = os.path.dirname(RELEASE_DIR)
CHANGELOG_DIR = os.path.join(REPO_ROOT_DIR, "changelog.d")
SCRIPTS_DIR = os.path.join(REPO_ROOT_DIR, "scripts")
SCRIPT_FILENAME = os.path.basename(getsourcefile(lambda: 0))
def get_current_version():
toml_path = os.path.join(REPO_ROOT_DIR, "Cargo.toml")
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):
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)
if "package" not in doc:
print("Error: [package] section not found in Cargo.toml")
sys.exit(1)
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(f"Updating version in Cargo.toml: {current_version} -> {version}")
if dry_run:
print("Dry-run mode: Skipping version file write and commit.")
return
doc["package"]["version"] = version
with open(toml_path, "w") as file:
file.write(tomlkit.dumps(doc))
subprocess.run(["cargo", "update", "-p", "vrl"], check=True, cwd=REPO_ROOT_DIR)
subprocess.run(["git", "commit", "-a", "-m", commit_message], check=True, cwd=REPO_ROOT_DIR)
def resolve_version(version_arg):
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:
return version_arg
def validate_version(version):
try:
semver.VersionInfo.parse(version)
except ValueError:
print(f"Invalid version: {version}. Please provide a valid SemVer string.")
exit(1)
assert_version_is_not_published(version)
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,
cwd=REPO_ROOT_DIR)
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)
subprocess.run(["git", "push", "-u", "origin", branch_name],
check=True,
cwd=REPO_ROOT_DIR)
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.")
else:
try:
subprocess.run(
["gh", "pr", "create", "--title", title, "--body", body, "--head", branch_name,
"--base", "main", "--label", "no-changelog"], check=True, cwd=REPO_ROOT_DIR)
except subprocess.CalledProcessError as e:
print(f"Failed to create pull request: {e}")
def main():
parser = argparse.ArgumentParser(description="Prepare a new 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 any changes (read-only)")
args = parser.parse_args()
new_version = resolve_version(args.version)
dry_run = args.dry_run
issue_link = args.issue_link
if not dry_run:
validate_version(new_version)
branch_name = f"prepare-{new_version}-release"
create_branch(branch_name, 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("\nDry-run completed. No changes were made.")
if __name__ == "__main__":
main()