honcho-api / scripts /update_version.py
rrizwan98
Honcho self-hosted deployment for HF Spaces
66227af
Raw
History Blame Contribute Delete
23.8 kB
#!/usr/bin/env uv run python
"""
Honcho Version Update Script
This script helps update version numbers across the Honcho repository.
It handles the main API, Python SDK, and TypeScript SDK in a single operation.
"""
import json
import os
import re
import subprocess
import sys
import tempfile
from datetime import datetime
class VersionUpdater:
def __init__(self, base_path: str):
self.base_path = base_path
def get_current_versions(self) -> dict[str, str]:
"""Get current version numbers from the repository."""
versions = {}
# Main API version
with open(os.path.join(self.base_path, "pyproject.toml")) as f:
for line in f:
if line.startswith("version = "):
versions["api"] = line.split('"')[1]
break
# Python SDK version
with open(os.path.join(self.base_path, "sdks/python/pyproject.toml")) as f:
for line in f:
if line.startswith("version = "):
versions["python_sdk"] = line.split('"')[1]
break
# TypeScript SDK version
with open(os.path.join(self.base_path, "sdks/typescript/package.json")) as f:
data = json.load(f)
versions["typescript_sdk"] = data["version"]
return versions
def get_all_versions_from_editor(
self, current_versions: dict[str, str]
) -> dict[str, dict[str, str]]:
"""Open editor to get all version updates at once."""
template = f"""# Honcho Version Update
# Enter new version numbers below. Leave blank to skip updating that component.
#
# MAIN API
# Current version: {current_versions["api"]}
API_VERSION=
# API Changelog (use ### for section headers: Added, Changed, Fixed, etc.)
# PYTHON SDK
# Current version: {current_versions["python_sdk"]}
PYTHON_VERSION=
# Python SDK Changelog
# TYPESCRIPT SDK
# Current version: {current_versions["typescript_sdk"]}
TYPESCRIPT_VERSION=
# TypeScript SDK Changelog
# Lines starting with # are comments and will be ignored
"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write(template)
temp_file = f.name
# Open in vim
subprocess.call(["vim", temp_file])
# Parse the file
with open(temp_file) as f:
content = f.read()
os.unlink(temp_file)
# Extract all versions and changelogs
updates = {}
# Parse API version
api_match = re.search(r"^API_VERSION=(.*)$", content, re.MULTILINE)
if api_match and api_match.group(1).strip():
changelog = self._extract_changelog_between(
content, "API_VERSION=", "PYTHON_VERSION="
)
updates["api"] = {
"version": api_match.group(1).strip(),
"changelog": self._clean_changelog_sections(changelog),
}
# Parse Python SDK version
python_match = re.search(r"^PYTHON_VERSION=(.*)$", content, re.MULTILINE)
if python_match and python_match.group(1).strip():
changelog = self._extract_changelog_between(
content, "PYTHON_VERSION=", "TYPESCRIPT_VERSION="
)
updates["python_sdk"] = {
"version": python_match.group(1).strip(),
"changelog": self._clean_changelog_sections(changelog),
}
# Parse TypeScript SDK version
ts_match = re.search(r"^TYPESCRIPT_VERSION=(.*)$", content, re.MULTILINE)
if ts_match and ts_match.group(1).strip():
changelog = self._extract_changelog_between(
content, "TYPESCRIPT_VERSION=", None
)
updates["typescript_sdk"] = {
"version": ts_match.group(1).strip(),
"changelog": self._clean_changelog_sections(changelog),
}
return updates
def _extract_changelog_between(
self, content: str, start_marker: str, end_marker: str | None
) -> str:
"""Extract changelog content between markers."""
lines = content.split("\n")
changelog_lines = []
in_section = False
for line in lines:
if start_marker in line:
in_section = True
continue
if end_marker and end_marker in line:
break
if in_section:
# Skip comment lines but keep markdown headers
if line.strip().startswith("# ") and not line.strip().startswith("###"):
continue
if line.strip() == "#":
continue
changelog_lines.append(line)
# Remove trailing empty lines
while changelog_lines and not changelog_lines[-1].strip():
changelog_lines.pop()
return "\n".join(changelog_lines).strip()
def _clean_changelog_sections(self, changelog: str) -> str:
"""Remove empty changelog sections."""
sections = ["Added", "Changed", "Fixed", "Deprecated", "Removed", "Security"]
lines = changelog.split("\n")
cleaned_lines = []
current_section = None
section_has_content = False
section_start_idx = -1
for i, line in enumerate(lines):
# Check if this is a section header
is_section_header = False
for section in sections:
if line.strip() == f"### {section}":
# If we have a previous section, decide whether to keep it
if (
current_section is not None
and section_start_idx != -1
and section_has_content
):
# Keep the section
cleaned_lines.extend(lines[section_start_idx:i])
# Start tracking new section
current_section = section
section_start_idx = i
section_has_content = False
is_section_header = True
break
if (
not is_section_header
and current_section is not None
and line.strip()
and not line.strip().startswith("#")
):
section_has_content = True
# Handle the last section
if (
current_section is not None
and section_start_idx != -1
and section_has_content
):
cleaned_lines.extend(lines[section_start_idx:])
# If no sections were found, return original
if not cleaned_lines and "###" not in changelog:
return changelog
return "\n".join(cleaned_lines).strip()
def update_all(
self, updates: dict[str, dict[str, str]], current_versions: dict[str, str]
):
"""Update all components that have new versions."""
# Update API if specified
if "api" in updates:
print(f"\nUpdating API version to {updates['api']['version']}...")
self.update_api_version(
updates["api"]["version"], updates["api"]["changelog"]
)
# Update compatibility guide for new API version
self._update_compatibility_guide_for_api(
updates["api"]["version"],
updates.get("python_sdk", {}).get(
"version", current_versions["python_sdk"]
),
updates.get("typescript_sdk", {}).get(
"version", current_versions["typescript_sdk"]
),
)
# Update Python SDK if specified
if "python_sdk" in updates:
print(
f"Updating Python SDK version to {updates['python_sdk']['version']}..."
)
self.update_python_sdk_version(
updates["python_sdk"]["version"], updates["python_sdk"]["changelog"]
)
# Update TypeScript SDK if specified
if "typescript_sdk" in updates:
print(
f"Updating TypeScript SDK version to {updates['typescript_sdk']['version']}..."
)
self.update_typescript_sdk_version(
updates["typescript_sdk"]["version"],
updates["typescript_sdk"]["changelog"],
)
def update_api_version(self, new_version: str, changelog: str):
"""Update main API version across all files."""
updates = [
# Simple replacements
{
"file": "pyproject.toml",
"pattern": r'version = "[^"]*"',
"replacement": f'version = "{new_version}"',
},
{
"file": "src/main.py",
"pattern": r'version="[^"]*"',
"replacement": f'version="{new_version}"',
},
{
"file": "README.md",
"pattern": r"Version-\d+\.\d+\.\d+-blue",
"replacement": f"Version-{new_version}-blue",
},
# docs.json is handled separately to only update same major version
]
# Apply simple updates
for update in updates:
file_path = os.path.join(self.base_path, update["file"])
with open(file_path) as f:
content = f.read()
content = re.sub(update["pattern"], update["replacement"], content)
with open(file_path, "w") as f:
f.write(content)
# Update docs.json - only update same major version
self._update_docs_json(new_version)
# Update CHANGELOG.md (prepend new entry)
self._update_changelog_md(new_version, changelog)
# Update docs changelog (MDX format)
self._update_docs_changelog(new_version, changelog, "api")
def update_python_sdk_version(self, new_version: str, changelog: str):
"""Update Python SDK version."""
updates = [
{
"file": "sdks/python/pyproject.toml",
"pattern": r'version = "[^"]*"',
"replacement": f'version = "{new_version}"',
},
{
"file": "sdks/python/src/honcho/__init__.py",
"pattern": r'__version__ = "[^"]*"',
"replacement": f'__version__ = "{new_version}"',
},
]
for update in updates:
file_path = os.path.join(self.base_path, update["file"])
with open(file_path) as f:
content = f.read()
content = re.sub(update["pattern"], update["replacement"], content)
with open(file_path, "w") as f:
f.write(content)
# Update SDK's own CHANGELOG.md
self._update_sdk_changelog(new_version, changelog, "sdks/python/CHANGELOG.md")
# Update docs changelog
self._update_docs_changelog(new_version, changelog, "python_sdk")
# Update compatibility guide SDK version
self._update_compatibility_guide("python", new_version)
def update_typescript_sdk_version(self, new_version: str, changelog: str):
"""Update TypeScript SDK version."""
# Update package.json
file_path = os.path.join(self.base_path, "sdks/typescript/package.json")
with open(file_path) as f:
data = json.load(f)
data["version"] = new_version
with open(file_path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n") # Add trailing newline
# Update SDK's own CHANGELOG.md
self._update_sdk_changelog(
new_version, changelog, "sdks/typescript/CHANGELOG.md"
)
# Update docs changelog
self._update_docs_changelog(new_version, changelog, "typescript_sdk")
# Update compatibility guide SDK version
self._update_compatibility_guide("typescript", new_version)
def _update_docs_json(self, new_version: str):
"""Update docs.json - only update versions with same major version."""
file_path = os.path.join(self.base_path, "docs/docs.json")
with open(file_path) as f:
data = json.load(f)
# Get major version of new version
new_major = new_version.split(".")[0]
# Update only matching major versions
if "navigation" in data and "versions" in data["navigation"]:
for version_entry in data["navigation"]["versions"]:
if "version" in version_entry:
current_version = version_entry["version"].lstrip("v")
current_major = current_version.split(".")[0]
if current_major == new_major:
version_entry["version"] = f"v{new_version}"
with open(file_path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
def _update_sdk_changelog(self, version: str, changelog: str, relative_path: str):
"""Update an SDK's CHANGELOG.md file."""
file_path = os.path.join(self.base_path, relative_path)
with open(file_path) as f:
content = f.read()
# Find the position after the header
header_end = content.find("\n## [")
if header_end == -1:
header_end = content.find("\n##")
if header_end == -1:
# No existing entries, add after title section
header_end = content.find("and this project adheres to")
if header_end != -1:
header_end = content.find("\n", header_end)
# Create new entry with proper formatting
date = datetime.now().strftime("%Y-%m-%d")
# Ensure changelog content is properly formatted
if changelog.strip():
formatted_changelog = changelog.strip()
else:
formatted_changelog = "### Changed\n\n- Updated version"
new_entry = f"\n\n## [{version}] - {date}\n\n{formatted_changelog}\n"
# Insert the new entry
new_content = content[:header_end] + new_entry + content[header_end:]
with open(file_path, "w") as f:
f.write(new_content)
def _update_changelog_md(self, version: str, changelog: str):
"""Update the main CHANGELOG.md file."""
file_path = os.path.join(self.base_path, "CHANGELOG.md")
with open(file_path) as f:
content = f.read()
# Find the position after the header
header_end = content.find("\n## [")
if header_end == -1:
header_end = content.find("\n##")
if header_end == -1:
# No existing entries, add after title
header_end = content.find("\n", content.find("# Changelog"))
# Create new entry with proper formatting
date = datetime.now().strftime("%Y-%m-%d")
# Ensure changelog content is properly formatted
if changelog.strip():
formatted_changelog = changelog.strip()
else:
formatted_changelog = "### Changed\n\n- Updated version"
new_entry = f"\n\n## [{version}] - {date}\n\n{formatted_changelog}\n"
# Insert the new entry
new_content = content[:header_end] + new_entry + content[header_end:]
with open(file_path, "w") as f:
f.write(new_content)
def _update_docs_changelog(self, version: str, changelog: str, component: str):
"""Update the docs/changelog/introduction.mdx file."""
file_path = os.path.join(self.base_path, "docs/changelog/introduction.mdx")
with open(file_path) as f:
content = f.read()
if component == "api":
# Find the Honcho API tab content
tab_start = content.find('<Tab title="Honcho API">')
if tab_start == -1:
return
# Find where to insert (after the Tab opening)
insert_pos = content.find("\n", tab_start) + 1
# Format the changelog with proper indentation
indented_changelog = "\n".join(
" " + line if line.strip() else ""
for line in changelog.strip().split("\n")
)
# Create the new update entry
new_entry = f""" <Update label="v{version} (Current)">
{indented_changelog}
</Update>
"""
# Remove (Current) from previous entries
# Use a more specific pattern to avoid replacing in other contexts
content = re.sub(
r'(<Update label="v[^"]*) \(Current\)(")', r"\1\2", content
)
# Insert the new entry right after the Tab line
content = content[:insert_pos] + new_entry + content[insert_pos:]
elif component == "python_sdk":
# Update Python SDK section
content = self._update_sdk_changelog_section(
content, "Python SDK", version, changelog
)
elif component == "typescript_sdk":
# Update TypeScript SDK section
content = self._update_sdk_changelog_section(
content, "TypeScript SDK", version, changelog
)
with open(file_path, "w") as f:
f.write(content)
def _update_sdk_changelog_section(
self, content: str, sdk_title: str, version: str, changelog: str
):
"""Update a specific SDK section in the changelog."""
tab_pattern = f'<Tab title="{sdk_title}">'
tab_start = content.find(tab_pattern)
if tab_start == -1:
return content
# Format the changelog with proper indentation
indented_changelog = "\n".join(
" " + line if line.strip() else ""
for line in changelog.strip().split("\n")
)
# Create new update entry
new_entry = f""" <Update label="v{version} (Current)">
{indented_changelog}
</Update>
"""
# Remove (Current) from previous SDK entries
# More precise pattern to avoid issues
pattern = rf'(<Tab title="{sdk_title}">.*?<Update label="v[^"]*) \(Current\)(".*?</Tab>)'
content = re.sub(pattern, r"\1\2", content, flags=re.DOTALL)
# Find where to insert the new entry
# Look for the line after the SDK link (e.g., [Python SDK](...))
tab_pos = content.find(tab_pattern)
if tab_pos == -1:
return content
# Find the end of the SDK link line
link_start = content.find("[", tab_pos)
if link_start != -1:
link_end = content.find("\n", link_start)
if link_end != -1:
insert_pos = link_end + 1
content = content[:insert_pos] + new_entry + content[insert_pos:]
return content
def _update_compatibility_guide(self, sdk_type: str, version: str):
"""Update the compatibility guide with new SDK version."""
file_path = os.path.join(
self.base_path, "docs/changelog/compatibility-guide.mdx"
)
with open(file_path) as f:
content = f.read()
if sdk_type == "typescript":
# Update in the card
content = re.sub(
r'(<Card title="TypeScript SDK".*?Compatible Version:\*\*) v[\d.]+',
rf"\1 v{version}",
content,
flags=re.DOTALL,
)
# Update in the install command
content = re.sub(
r"npm install @honcho-ai/sdk@[\d.]+",
f"npm install @honcho-ai/sdk@{version}",
content,
)
elif sdk_type == "python":
# Update in the card
content = re.sub(
r'(<Card title="Python SDK".*?Compatible Version:\*\*) v[\d.]+',
rf"\1 v{version}",
content,
flags=re.DOTALL,
)
# Update in the install command
content = re.sub(
r"pip install honcho-ai==[\d.]+",
f"pip install honcho-ai=={version}",
content,
)
with open(file_path, "w") as f:
f.write(content)
def _update_compatibility_guide_for_api(
self, api_version: str, python_version: str, typescript_version: str
):
"""Update compatibility guide when API version changes."""
file_path = os.path.join(
self.base_path, "docs/changelog/compatibility-guide.mdx"
)
with open(file_path) as f:
content = f.read()
# Update the current API version header
content = re.sub(
r"### Honcho API v[\d.]+ \(Current\)",
f"### Honcho API v{api_version} (Current)",
content,
)
# Find the table and update/add entry
table_match = re.search(
r"(\| Honcho API Version.*?\n\|[-| ]+\n)(.*?)(\n\n|$)", content, re.DOTALL
)
if table_match:
header = table_match.group(1)
rows = table_match.group(2)
after_table = table_match.group(3)
# Update existing current version
rows = re.sub(
r"v[\d.]+ \(Current\)",
lambda m: m.group(0).replace(" (Current)", ""),
rows,
)
# Add new row at the top
new_row = f"| v{api_version} (Current) | v{typescript_version} | v{python_version} |"
# Reconstruct table
new_table = header + new_row + "\n" + rows + after_table
# Replace in content
content = (
content[: table_match.start()]
+ new_table
+ content[table_match.end() :]
)
with open(file_path, "w") as f:
f.write(content)
def main():
# Get the parent directory of the scripts folder (the project root)
script_dir = os.path.dirname(os.path.abspath(__file__))
base_path = os.path.dirname(script_dir)
updater = VersionUpdater(base_path)
# Get current versions
current_versions = updater.get_current_versions()
print("Honcho Version Updater")
print("=" * 50)
print("\nCurrent versions:")
print(f" Main API: {current_versions['api']}")
print(f" Python SDK: {current_versions['python_sdk']}")
print(f" TypeScript SDK: {current_versions['typescript_sdk']}")
print()
print("Opening editor for version updates...")
print("Leave version fields blank to skip updating that component.")
print()
# Get all updates at once
updates = updater.get_all_versions_from_editor(current_versions)
if not updates:
print("No versions specified. Exiting...")
sys.exit(0)
# Show what will be updated
print("\nThe following components will be updated:")
for component, info in updates.items():
component_name = {
"api": "Main API",
"python_sdk": "Python SDK",
"typescript_sdk": "TypeScript SDK",
}[component]
print(f" {component_name}: {current_versions[component]}{info['version']}")
# Confirm
response = input("\nProceed with updates? (y/n): ").strip().lower()
if response != "y":
print("Cancelled.")
sys.exit(0)
# Apply all updates
updater.update_all(updates, current_versions)
print("\nVersion updates complete!")
print("\nDon't forget to:")
print(" - Review the changes with `git diff`")
print(" - Commit the changes")
print(" - Create git tags for the new versions")
print(" - Push the changes and tags")
if __name__ == "__main__":
main()