haiku.rag/scripts/bump_version.py
Yiorgis Gozadinos eb9995e9b7
Ship a Claude Code plugin with the haiku-rag skill
claude-plugin/ holds the plugin manifest, the server configuration
(haiku-rag mcp --stdio, the configuration decides the database) and a
skill that says when to reach for the knowledge base and how to move
from a search result to a document, a section, an answer or a
computation. A repo-root marketplace manifest makes
`claude plugin marketplace add ggozad/haiku.rag` work. The skill
pre-approves every tool the server registers, and a test keeps the two
in step.

The manifest carries the package version, which bump_version.py now
rewrites: a versioned plugin updates only on a bump, so the installed
skill stays in step with the haiku-rag release the user has.

Refs #599
2026-09-04 13:00:15 +03:00

195 lines
6.1 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Version bumping script for haiku.rag workspace.
Updates version in all pyproject.toml files, the Claude Code plugin manifest
and CHANGELOG.md.
"""
import re
import subprocess
import sys
from datetime import date
from pathlib import Path
def get_current_version(file_path: Path) -> str:
"""Extract current version from pyproject.toml."""
content = file_path.read_text()
match = re.search(r'^version = "([^"]+)"', content, re.MULTILINE)
if not match:
raise ValueError(f"Could not find version in {file_path}")
return match.group(1)
def update_version_in_file(file_path: Path, new_version: str) -> None:
"""Update version in a pyproject.toml file."""
content = file_path.read_text()
updated = re.sub(
r'^version = "[^"]+"', f'version = "{new_version}"', content, flags=re.MULTILINE
)
file_path.write_text(updated)
print(f"✓ Updated {file_path.relative_to(Path.cwd())}")
def update_dependency_version(file_path: Path, new_version: str) -> None:
"""Update haiku.rag-slim dependency version in root pyproject.toml."""
content = file_path.read_text()
updated = re.sub(
r"(haiku\.rag-slim\[.*?\])==[0-9.]+", rf"\1=={new_version}", content
)
file_path.write_text(updated)
print(f"✓ Updated haiku.rag-slim dependency in {file_path.relative_to(Path.cwd())}")
def update_example_dependencies(file_path: Path, new_version: str) -> None:
"""Update haiku.rag and haiku.rag-slim dependency versions in example pyproject.toml files."""
content = file_path.read_text()
# Update haiku.rag-slim (with or without extras) >= X.Y.Z
updated = re.sub(
r"(haiku\.rag-slim(?:\[.*?\])?)>=[0-9.]+", rf"\1>={new_version}", content
)
# Update haiku.rag >= X.Y.Z
updated = re.sub(r"(haiku\.rag)>=[0-9.]+", rf"\1>={new_version}", updated)
file_path.write_text(updated)
print(f"✓ Updated example dependencies in {file_path.relative_to(Path.cwd())}")
def update_plugin_version(file_path: Path, new_version: str) -> None:
"""Update the version in the Claude Code plugin manifest."""
content = file_path.read_text()
updated = re.sub(
r'^(\s*"version": )"[^"]+"',
rf'\1"{new_version}"',
content,
flags=re.MULTILINE,
)
file_path.write_text(updated)
print(f"✓ Updated {file_path.relative_to(Path.cwd())}")
def update_changelog(changelog_path: Path, new_version: str) -> None:
"""Update CHANGELOG.md with new version."""
content = changelog_path.read_text()
today = date.today().isoformat()
# Replace [Unreleased] with new version
updated = re.sub(
r"## \[Unreleased\]",
f"## [Unreleased]\n\n## [{new_version}] - {today}",
content,
count=1,
)
# Update comparison links
# Find the old [Unreleased] link
old_unreleased_match = re.search(
r"\[Unreleased\]: https://github\.com/ggozad/haiku\.rag/compare/(.+?)\.\.\.HEAD",
updated,
)
if old_unreleased_match:
prev_version = old_unreleased_match.group(1)
# Update [Unreleased] link
updated = re.sub(
r"\[Unreleased\]: https://github\.com/ggozad/haiku\.rag/compare/.+?\.\.\.HEAD",
f"[Unreleased]: https://github.com/ggozad/haiku.rag/compare/{new_version}...HEAD",
updated,
)
# Add new version link after [Unreleased]
updated = re.sub(
r"(\[Unreleased\]: https://github\.com/ggozad/haiku\.rag/compare/[^\n]+\n)",
f"\\1[{new_version}]: https://github.com/ggozad/haiku.rag/compare/{prev_version}...{new_version}\n",
updated,
)
changelog_path.write_text(updated)
print(f"✓ Updated {changelog_path.relative_to(Path.cwd())}")
def main():
if len(sys.argv) != 2:
print("Usage: python scripts/bump_version.py <new_version>")
print("Example: python scripts/bump_version.py 0.14.0")
sys.exit(1)
new_version = sys.argv[1]
# Validate version format
if not re.match(r"^\d+\.\d+\.\d+$", new_version):
print(f"Error: Invalid version format '{new_version}'")
print("Version must be in format: X.Y.Z (e.g., 0.14.0)")
sys.exit(1)
root = Path(__file__).parent.parent
# Files to update
pyproject_files = [
root / "pyproject.toml",
root / "haiku_rag_slim" / "pyproject.toml",
root / "evaluations" / "pyproject.toml",
]
example_pyproject_files = [
root / "app" / "backend" / "pyproject.toml",
]
plugin_file = root / "claude-plugin" / ".claude-plugin" / "plugin.json"
changelog_file = root / "CHANGELOG.md"
# Check all files exist
for file in (
pyproject_files + example_pyproject_files + [plugin_file, changelog_file]
):
if not file.exists():
print(f"Error: {file} not found")
sys.exit(1)
# Get current version from root pyproject.toml
current_version = get_current_version(pyproject_files[0])
print(f"Current version: {current_version}")
print(f"New version: {new_version}")
print()
# Confirm
response = input("Proceed with version bump? [y/N] ")
if response.lower() != "y":
print("Aborted.")
sys.exit(0)
print()
# Update all pyproject.toml files
for file in pyproject_files:
update_version_in_file(file, new_version)
# Update haiku.rag-slim dependency version in root pyproject.toml
update_dependency_version(pyproject_files[0], new_version)
# Update example project dependencies
for file in example_pyproject_files:
update_example_dependencies(file, new_version)
update_plugin_version(plugin_file, new_version)
# Update CHANGELOG.md
update_changelog(changelog_file, new_version)
# Run uv sync to update lock file
print()
print("Running uv sync...")
try:
subprocess.run(["uv", "sync"], check=True, cwd=root)
print("✓ Lock file updated")
except subprocess.CalledProcessError as e:
print(f"Error: uv sync failed with exit code {e.returncode}")
sys.exit(1)
print()
print(f"✓ Version bumped from {current_version} to {new_version}")
if __name__ == "__main__":
main()