Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Laure Hugo <201583486+laure0303@users.noreply.github.com> Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com> Co-authored-by: Peter Evers <peter.evers@mistral.ai> Co-authored-by: Jules YZERD <newtonlormont@gmail.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
242 lines
8.8 KiB
Python
Executable file
242 lines
8.8 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Version bumping script for semver versioning.
|
|
|
|
This script increments the version in pyproject.toml based on the specified bump type:
|
|
- major: 1.0.0 -> 2.0.0
|
|
- minor: 1.0.0 -> 1.1.0
|
|
- micro/patch: 1.0.0 -> 1.0.1
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from datetime import date
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from typing import Literal, get_args
|
|
|
|
BumpType = Literal["major", "minor", "micro", "patch"]
|
|
BUMP_TYPES = get_args(BumpType)
|
|
|
|
|
|
def parse_version(version_str: str) -> tuple[int, int, int]:
|
|
match = re.match(r"^(\d+)\.(\d+)\.(\d+)$", version_str.strip())
|
|
if not match:
|
|
raise ValueError(f"Invalid version format: {version_str}")
|
|
|
|
return int(match.group(1)), int(match.group(2)), int(match.group(3))
|
|
|
|
|
|
def format_version(major: int, minor: int, patch: int) -> str:
|
|
return f"{major}.{minor}.{patch}"
|
|
|
|
|
|
def bump_version(version: str, bump_type: BumpType) -> str:
|
|
major, minor, patch = parse_version(version)
|
|
|
|
match bump_type:
|
|
case "major":
|
|
return format_version(major + 1, 0, 0)
|
|
case "minor":
|
|
return format_version(major, minor + 1, 0)
|
|
case "micro" | "patch":
|
|
return format_version(major, minor, patch + 1)
|
|
|
|
|
|
def update_hard_values_files(filepath: str, patterns: list[tuple[str, str]]) -> None:
|
|
path = Path(filepath)
|
|
|
|
if not path.exists():
|
|
raise FileNotFoundError(f"{filepath} not found in current directory")
|
|
|
|
for pattern, replacement in patterns:
|
|
content = path.read_text()
|
|
updated_content = re.sub(pattern, replacement, content, flags=re.MULTILINE)
|
|
|
|
if updated_content == content:
|
|
raise ValueError(f"pattern {pattern} not found in {filepath}")
|
|
|
|
path.write_text(updated_content)
|
|
|
|
print(f"Updated version in {filepath}")
|
|
|
|
|
|
def get_current_version() -> str:
|
|
pyproject_path = Path("pyproject.toml")
|
|
|
|
if not pyproject_path.exists():
|
|
raise FileNotFoundError("pyproject.toml not found in current directory")
|
|
|
|
content = pyproject_path.read_text()
|
|
|
|
version_match = re.search(r'^version = "([^"]+)"$', content, re.MULTILINE)
|
|
if not version_match:
|
|
raise ValueError("Version not found in pyproject.toml")
|
|
|
|
return version_match.group(1)
|
|
|
|
|
|
def scaffold_changelog(new_version: str) -> None:
|
|
changelog_path = Path("CHANGELOG.md")
|
|
|
|
if not changelog_path.exists():
|
|
raise FileNotFoundError("CHANGELOG.md not found in current directory")
|
|
|
|
content = changelog_path.read_text()
|
|
today = date.today().isoformat()
|
|
|
|
first_entry_match = re.search(r"^## \[[\d.]+\]", content, re.MULTILINE)
|
|
if not first_entry_match:
|
|
raise ValueError("Could not find version entry in CHANGELOG.md")
|
|
|
|
insert_position = first_entry_match.start()
|
|
|
|
new_entry = f"## [{new_version}] - {today}\n\n"
|
|
new_entry += "### Added\n\n"
|
|
new_entry += "### Changed\n\n"
|
|
new_entry += "### Fixed\n\n"
|
|
new_entry += "### Removed\n\n"
|
|
new_entry += "\n"
|
|
|
|
updated_content = content[:insert_position] + new_entry + content[insert_position:]
|
|
changelog_path.write_text(updated_content)
|
|
|
|
|
|
def scaffold_whats_new() -> None:
|
|
whats_new_path = Path("vibe/whats_new.md")
|
|
if not whats_new_path.exists():
|
|
raise FileNotFoundError("whats_new.md not found in current directory")
|
|
|
|
whats_new_path.write_text("")
|
|
|
|
|
|
def fill_release_notes(current_version: str, new_version: str, autofill: bool) -> None:
|
|
if not autofill:
|
|
print("Skipping CHANGELOG.md and whats_new.md auto-fill.")
|
|
return
|
|
|
|
print("Filling CHANGELOG.md and vibe/whats_new.md...")
|
|
prompt = f"""Fill both CHANGELOG.md and vibe/whats_new.md for version {new_version} in a single pass, reusing the same git history context for both files.
|
|
|
|
Step 1 — Gather context (do this once):
|
|
- Inspect git history for commits in origin/main that touch the `vibe` folder since version {current_version}.
|
|
- Build a single mental list of relevant changes. Do not mention commit hashes or PR numbers.
|
|
|
|
Step 2 — Fill CHANGELOG.md:
|
|
- Edit the section for version {new_version} that was just scaffolded at the top of the file.
|
|
- Follow the existing file convention: Keep a Changelog format with ### Added, ### Changed, ### Fixed, ### Removed. One bullet per line, concise. Match the tone and style of existing entries.
|
|
- Remove any subsection that has no bullets (leave no empty ### Added / ### Changed / etc).
|
|
|
|
Step 3 — Fill vibe/whats_new.md (reuse the same context, do NOT re-inspect git):
|
|
- This file is an in-app announcement shown to users on upgrade. It is NOT a changelog. Its only purpose is to advertise a handful of notable new things. Most releases warrant 0-3 bullets. If nothing is genuinely noteworthy to an end user, leave the file empty.
|
|
- Inclusion criteria (a bullet must meet ALL):
|
|
* Visible in the CLI/UI: a new command, key binding, screen, flag, or behavior the user will actually notice.
|
|
* Net-new capability or a meaningful UX improvement (not a tweak, polish, or fix unless it unblocks a real workflow).
|
|
* Worth interrupting the user to tell them about.
|
|
- Hard exclusions (never include, even if user-facing): bug fixes, small UI polish, copy changes, refactors, performance tweaks, dependency bumps, internal/API-only changes, config plumbing, telemetry, logging, build/CI, tests, docs.
|
|
- Be ruthless. When in doubt, leave it out. Prefer an empty file over a weak bullet. Do NOT pad the list to look substantial.
|
|
- Format (only if there is at least one qualifying item):
|
|
* First line: "# What's new in v{new_version}" (no other headings).
|
|
* Then up to 3 bullets, one line each: "- **Feature**: short summary" (e.g. "- **Interactive resume**: Added a /resume command to choose which session to resume").
|
|
* Do not copy or paraphrase the full changelog."""
|
|
try:
|
|
result = subprocess.run(
|
|
["vibe", "-p", prompt, "--auto-approve"],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError("Failed to auto-fill release notes")
|
|
except Exception:
|
|
print(
|
|
"Warning: failed to auto-fill CHANGELOG.md and whats_new.md, please fill them manually.",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
os.chdir(Path(__file__).parent.parent)
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Bump semver version in pyproject.toml",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
uv run scripts/bump_version.py major # 1.0.0 -> 2.0.0
|
|
uv run scripts/bump_version.py minor # 1.0.0 -> 1.1.0
|
|
uv run scripts/bump_version.py micro # 1.0.0 -> 1.0.1
|
|
uv run scripts/bump_version.py patch # 1.0.0 -> 1.0.1
|
|
""",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"bump_type", choices=BUMP_TYPES, help="Type of version bump to perform"
|
|
)
|
|
parser.add_argument(
|
|
"--no-autofill",
|
|
action="store_true",
|
|
help="Skip auto-filling CHANGELOG.md and whats_new.md via vibe -p",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
autofill = not args.no_autofill
|
|
|
|
try:
|
|
# Get current version
|
|
current_version = get_current_version()
|
|
print(f"Current version: {current_version}")
|
|
|
|
# Calculate new version
|
|
new_version = bump_version(current_version, args.bump_type)
|
|
print(f"New version: {new_version}\n")
|
|
|
|
# Update pyproject.toml
|
|
update_hard_values_files(
|
|
"pyproject.toml",
|
|
[(f'version = "{current_version}"', f'version = "{new_version}"')],
|
|
)
|
|
# Update extension.toml
|
|
update_hard_values_files(
|
|
"distribution/zed/extension.toml",
|
|
[
|
|
(f'version = "{current_version}"', f'version = "{new_version}"'),
|
|
(
|
|
f"releases/download/v{current_version}",
|
|
f"releases/download/v{new_version}",
|
|
),
|
|
(f"-{current_version}.zip", f"-{new_version}.zip"),
|
|
],
|
|
)
|
|
# Update vibe/core/__init__.py
|
|
update_hard_values_files(
|
|
"vibe/__init__.py",
|
|
[(f'__version__ = "{current_version}"', f'__version__ = "{new_version}"')],
|
|
)
|
|
# Update tests/acp/test_initialize.py
|
|
update_hard_values_files(
|
|
"tests/acp/test_initialize.py",
|
|
[(f'version="{current_version}"', f'version="{new_version}"')],
|
|
)
|
|
|
|
print()
|
|
scaffold_changelog(new_version=new_version)
|
|
scaffold_whats_new()
|
|
fill_release_notes(
|
|
current_version=current_version, new_version=new_version, autofill=autofill
|
|
)
|
|
print()
|
|
|
|
subprocess.run(["uv", "lock"], check=True)
|
|
|
|
print(f"\nSuccessfully bumped version from {current_version} to {new_version}")
|
|
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|