from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
CARGO_TOML = REPO_ROOT / "Cargo.toml"
def run(cmd: list[str], *, check: bool = True, capture: bool = False) -> str:
print(f" $ {' '.join(cmd)}")
result = subprocess.run(
cmd,
cwd=REPO_ROOT,
check=check,
text=True,
capture_output=capture,
)
return result.stdout.strip() if capture else ""
def fatal(msg: str) -> None:
print(f"\nerror: {msg}", file=sys.stderr)
sys.exit(1)
VERSION_RE = re.compile(
r'^(version\s*=\s*")(\d+\.\d+\.\d+)(")', re.MULTILINE
)
def read_current_version() -> str:
text = CARGO_TOML.read_text()
pkg_section = text.split("[dependencies]")[0] if "[dependencies]" in text else text
m = VERSION_RE.search(pkg_section)
if not m:
fatal("Could not find version in Cargo.toml")
return m.group(2)
def bump_patch(current: str) -> str:
major, minor, patch = map(int, current.split("."))
return f"{major}.{minor}.{patch + 1}"
def set_version_in_cargo_toml(new_version: str) -> None:
text = CARGO_TOML.read_text()
new_text, count = VERSION_RE.subn(
rf"\g<1>{new_version}\g<3>", text, count=1
)
if count == 0:
fatal("Failed to replace version in Cargo.toml")
CARGO_TOML.write_text(new_text)
def preflight() -> None:
if not (REPO_ROOT / ".git").exists():
fatal("Not a git repository")
status = run(["git", "status", "--porcelain"], capture=True)
if status:
fatal("Working tree is not clean. Commit or stash changes first.\n" + status)
branch = run(["git", "rev-parse", "--abbrev-ref", "HEAD"], capture=True)
if branch != "main":
fatal(f"Not on main branch (currently on {branch!r}). Switch to main first.")
run(["git", "fetch", "origin", "main"], check=False)
local = run(["git", "rev-parse", "HEAD"], capture=True)
remote = run(["git", "rev-parse", "origin/main"], capture=True, check=False)
if remote and local != remote:
fatal(
"Local main is not up to date with origin/main.\n"
" Run: git pull --rebase origin main"
)
def main() -> None:
parser = argparse.ArgumentParser(
description="Bump patch version and release cargo-depflame.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would happen without making changes",
)
args = parser.parse_args()
current = read_current_version()
new_version = bump_patch(current)
tag = f"v{new_version}"
print(f"\ncargo-depflame release")
print(f" {current} -> {new_version} ({tag})")
print()
if args.dry_run:
print("[dry-run] Would perform the following steps:")
print(f" 1. Update Cargo.toml version to {new_version}")
print(f" 2. Run cargo check (updates Cargo.lock)")
print(f" 3. Run cargo test")
print(f" 4. Commit: 'Release {tag}'")
print(f" 5. Tag: {tag}")
print(f" 6. Push commit and tag to origin")
print("\nNo changes made.")
return
print("Running pre-flight checks...")
preflight()
print()
existing_tags = run(["git", "tag", "-l", tag], capture=True)
if existing_tags:
fatal(f"Tag {tag} already exists")
print(f"Updating Cargo.toml to {new_version}...")
set_version_in_cargo_toml(new_version)
print("\nRunning cargo check...")
run(["cargo", "check"])
print("\nRunning cargo test...")
run(["cargo", "test"])
print("\nCommitting...")
run(["git", "add", "Cargo.toml", "Cargo.lock"])
run(["git", "commit", "-m", f"Release {tag}"])
print(f"\nTagging {tag}...")
run(["git", "tag", "-a", tag, "-m", f"Release {tag}"])
print("\nPushing to origin...")
run(["git", "push", "origin", "main"])
run(["git", "push", "origin", tag])
print(f"\nDone! Release {tag} has been pushed.")
print("The GitHub Actions release workflow will now:")
print(" - Build binaries for Linux, macOS, and Windows")
print(" - Create a GitHub Release with the binaries")
print(" - Publish to crates.io (if CARGO_REGISTRY_TOKEN is set)")
if __name__ == "__main__":
main()