5a558eb09e
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled
392 lines
12 KiB
Python
392 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate changelog entries by analyzing git history of pyproject.toml version changes.
|
|
|
|
This script:
|
|
1. Finds commits that changed the version in pyproject.toml
|
|
2. Extracts version numbers from each commit
|
|
3. Generates GitHub compare URLs
|
|
4. Summarizes changes between versions
|
|
|
|
Usage:
|
|
python scripts/generate_changelog.py # Show all version history
|
|
python scripts/generate_changelog.py --since 2.0.0 # Show versions since 2.0.0
|
|
python scripts/generate_changelog.py --format mdx # Output in Fern MDX table format
|
|
"""
|
|
|
|
import argparse
|
|
import re
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
|
|
@dataclass
|
|
class VersionEntry:
|
|
version: str
|
|
commit_hash: str
|
|
date: str
|
|
summary: str = ""
|
|
|
|
|
|
REPO_URL = "https://github.com/comet-ml/opik"
|
|
PYPROJECT_PATH = "sdks/opik_optimizer/pyproject.toml"
|
|
|
|
|
|
def run_git_command(args: list[str], cwd: Path | None = None) -> str:
|
|
"""Run a git command and return stdout."""
|
|
result = subprocess.run(
|
|
["git"] + args,
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=cwd,
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"Git command failed: {result.stderr}")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def get_repo_root() -> Path:
|
|
"""Get the git repository root directory."""
|
|
script_dir = Path(__file__).parent
|
|
# Navigate up to find .git directory
|
|
current = script_dir
|
|
while current != current.parent:
|
|
if (current / ".git").exists():
|
|
return current
|
|
current = current.parent
|
|
raise RuntimeError("Not in a git repository")
|
|
|
|
|
|
def extract_version_from_content(content: str) -> str | None:
|
|
"""Extract version string from pyproject.toml content."""
|
|
match = re.search(r'^version\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE)
|
|
return match.group(1) if match else None
|
|
|
|
|
|
def get_file_at_commit(commit: str, filepath: str, repo_root: Path) -> str | None:
|
|
"""Get file content at a specific commit."""
|
|
try:
|
|
return run_git_command(["show", f"{commit}:{filepath}"], cwd=repo_root)
|
|
except RuntimeError:
|
|
return None
|
|
|
|
|
|
def get_version_commits(repo_root: Path, limit: int = 50) -> list[VersionEntry]:
|
|
"""Find commits that changed the version in pyproject.toml."""
|
|
# Get commits that touched pyproject.toml
|
|
log_output = run_git_command(
|
|
[
|
|
"log",
|
|
"--oneline",
|
|
"--format=%H|%ad|%s",
|
|
"--date=short",
|
|
f"-{limit}",
|
|
"--",
|
|
PYPROJECT_PATH,
|
|
],
|
|
cwd=repo_root,
|
|
)
|
|
|
|
if not log_output:
|
|
return []
|
|
|
|
entries = []
|
|
seen_versions: set[str] = set()
|
|
prev_version: str | None = None
|
|
|
|
for line in log_output.strip().split("\n"):
|
|
if not line:
|
|
continue
|
|
|
|
parts = line.split("|", 2)
|
|
if len(parts) < 3:
|
|
continue
|
|
|
|
commit_hash, date, message = parts
|
|
commit_hash = commit_hash.strip()
|
|
|
|
# Get version at this commit
|
|
content = get_file_at_commit(commit_hash, PYPROJECT_PATH, repo_root)
|
|
if not content:
|
|
continue
|
|
|
|
version = extract_version_from_content(content)
|
|
if not version:
|
|
continue
|
|
|
|
# Skip if we've already seen this version (we want the first commit for each version)
|
|
if version in seen_versions:
|
|
continue
|
|
|
|
# Check if version actually changed from previous
|
|
if prev_version is None or version != prev_version:
|
|
seen_versions.add(version)
|
|
entries.append(
|
|
VersionEntry(
|
|
version=version,
|
|
commit_hash=commit_hash,
|
|
date=date,
|
|
summary=message,
|
|
)
|
|
)
|
|
prev_version = version
|
|
|
|
return entries
|
|
|
|
|
|
def get_commits_between(
|
|
repo_root: Path, from_hash: str, to_hash: str
|
|
) -> list[tuple[str, str]]:
|
|
"""Get commit messages between two hashes (for the optimizer subdirectory only)."""
|
|
try:
|
|
log_output = run_git_command(
|
|
[
|
|
"log",
|
|
"--oneline",
|
|
"--format=%h|%s",
|
|
f"{from_hash}..{to_hash}",
|
|
"--",
|
|
"sdks/opik_optimizer/",
|
|
],
|
|
cwd=repo_root,
|
|
)
|
|
if not log_output:
|
|
return []
|
|
|
|
commits = []
|
|
for line in log_output.strip().split("\n"):
|
|
if "|" in line:
|
|
hash_short, message = line.split("|", 1)
|
|
commits.append((hash_short.strip(), message.strip()))
|
|
return commits
|
|
except RuntimeError:
|
|
return []
|
|
|
|
|
|
def generate_compare_url(from_hash: str, to_hash: str) -> str:
|
|
"""Generate GitHub compare URL."""
|
|
return f"{REPO_URL}/compare/{from_hash}...{to_hash}"
|
|
|
|
|
|
def format_mdx_table(entries: list[VersionEntry], repo_root: Path) -> str:
|
|
"""Format version entries as Fern MDX table."""
|
|
lines = [
|
|
"| Date | Version | Highlights |",
|
|
"| --- | --- | --- |",
|
|
]
|
|
|
|
for i, entry in enumerate(entries):
|
|
# Get the previous version's commit for comparison
|
|
if i + 1 < len(entries):
|
|
prev_hash = entries[i + 1].commit_hash
|
|
compare_url = generate_compare_url(prev_hash, entry.commit_hash)
|
|
link = f"[Commits →]({compare_url})"
|
|
else:
|
|
# First version - link to single commit
|
|
link = f"[Commit →]({REPO_URL}/commit/{entry.commit_hash})"
|
|
|
|
# Get commits between versions for summary
|
|
if i + 1 < len(entries):
|
|
commits = get_commits_between(
|
|
repo_root, entries[i + 1].commit_hash, entry.commit_hash
|
|
)
|
|
if commits and len(commits) <= 10:
|
|
# Summarize commit messages directly
|
|
summary = summarize_commits(commits)
|
|
elif commits:
|
|
# Extract themes for large releases
|
|
summary = extract_themes_from_commits(commits, entry.version)
|
|
else:
|
|
summary = f"Version {entry.version} release."
|
|
else:
|
|
summary = clean_commit_message(entry.summary)
|
|
|
|
# Format date
|
|
date_str = f"**{entry.date}**"
|
|
|
|
# Build highlights with link
|
|
highlights = f"{summary} {link}"
|
|
|
|
lines.append(f"| {date_str} | `{entry.version}` | {highlights} |")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def clean_commit_message(msg: str, linkify_prs: bool = True) -> str:
|
|
"""Clean up commit message prefixes like [NA][SDK]."""
|
|
# Remove [TAG] prefixes
|
|
msg = re.sub(r"^\s*\[[^\]]+\]\s*", "", msg)
|
|
msg = re.sub(r"^\s*\[[^\]]+\]\s*", "", msg) # Run twice for [NA][SDK] style
|
|
# Clean up common prefixes
|
|
for prefix in ["chore:", "feat:", "fix:", "docs:", "refactor:", "test:"]:
|
|
if msg.lower().startswith(prefix):
|
|
msg = msg[len(prefix) :].strip()
|
|
# Remove "Optimizer" prefix (common in commit messages)
|
|
if msg.startswith("Optimizer "):
|
|
msg = msg[10:]
|
|
if msg.startswith("Opik Optimizer "):
|
|
msg = msg[15:]
|
|
# Capitalize first letter
|
|
if msg and msg[0].islower():
|
|
msg = msg[0].upper() + msg[1:]
|
|
# Convert PR numbers to links
|
|
if linkify_prs:
|
|
msg = re.sub(r"\(#(\d+)\)", rf"[#\1]({REPO_URL}/pull/\1)", msg)
|
|
return msg.strip()
|
|
|
|
|
|
def summarize_commits(commits: list[tuple[str, str]], max_items: int = 5) -> str:
|
|
"""Create a summary from commit messages."""
|
|
# Filter out merge commits and clean up messages
|
|
messages = []
|
|
for _, msg in commits:
|
|
# Skip merge commits
|
|
if msg.startswith("Merge"):
|
|
continue
|
|
# Clean up the message
|
|
cleaned = clean_commit_message(msg)
|
|
if cleaned:
|
|
messages.append(cleaned)
|
|
|
|
if not messages:
|
|
return "Various improvements and fixes."
|
|
|
|
# Take first few unique messages
|
|
unique_messages = list(dict.fromkeys(messages))[:max_items]
|
|
return ", ".join(unique_messages) + "."
|
|
|
|
|
|
def extract_themes_from_commits(commits: list[tuple[str, str]], version: str) -> str:
|
|
"""Extract key themes from a large set of commits."""
|
|
# Collect all cleaned messages
|
|
messages = []
|
|
for _, msg in commits:
|
|
if msg.startswith("Merge"):
|
|
continue
|
|
cleaned = clean_commit_message(msg, linkify_prs=False)
|
|
if cleaned:
|
|
messages.append(cleaned.lower())
|
|
|
|
if not messages:
|
|
return f"Version {version} release."
|
|
|
|
# Count keyword occurrences to identify themes
|
|
themes = []
|
|
keywords = {
|
|
"validation": "validation dataset support",
|
|
"benchmark": "benchmark improvements",
|
|
"agent": "agent support enhancements",
|
|
"multimodal": "multimodal support",
|
|
"test": "test infrastructure updates",
|
|
"fix": "bug fixes",
|
|
"refactor": "code refactoring",
|
|
"mcp": "MCP tooling updates",
|
|
"cache": "caching improvements",
|
|
"dataset": "dataset handling improvements",
|
|
"gepa": "GEPA optimizer updates",
|
|
"hierarchical": "Hierarchical Reflective Optimizer updates",
|
|
"fewshot": "Few-Shot Optimizer updates",
|
|
"evolutionary": "Evolutionary Optimizer updates",
|
|
"metaprompt": "MetaPrompt Optimizer updates",
|
|
}
|
|
|
|
for keyword, theme in keywords.items():
|
|
if any(keyword in m for m in messages):
|
|
themes.append(theme)
|
|
if len(themes) >= 3:
|
|
break
|
|
|
|
if themes:
|
|
return ", ".join(themes).capitalize() + "."
|
|
return f"Version {version} release with various improvements."
|
|
|
|
|
|
def format_plain(entries: list[VersionEntry], repo_root: Path) -> str:
|
|
"""Format version entries as plain text."""
|
|
lines = []
|
|
for i, entry in enumerate(entries):
|
|
lines.append(f"\n{'=' * 60}")
|
|
lines.append(f"Version: {entry.version}")
|
|
lines.append(f"Date: {entry.date}")
|
|
lines.append(f"Commit: {entry.commit_hash}")
|
|
|
|
if i + 1 < len(entries):
|
|
prev_hash = entries[i + 1].commit_hash
|
|
lines.append(
|
|
f"Compare: {generate_compare_url(prev_hash, entry.commit_hash)}"
|
|
)
|
|
|
|
# Show commits in this version
|
|
commits = get_commits_between(repo_root, prev_hash, entry.commit_hash)
|
|
if commits:
|
|
lines.append(f"\nCommits ({len(commits)}):")
|
|
for hash_short, msg in commits[:15]:
|
|
lines.append(f" {hash_short} {msg}")
|
|
if len(commits) > 15:
|
|
lines.append(f" ... and {len(commits) - 15} more")
|
|
else:
|
|
lines.append(f"Commit URL: {REPO_URL}/commit/{entry.commit_hash}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate changelog entries from git history"
|
|
)
|
|
parser.add_argument(
|
|
"--since",
|
|
type=str,
|
|
help="Only show versions since this version (e.g., 2.0.0)",
|
|
)
|
|
parser.add_argument(
|
|
"--format",
|
|
choices=["plain", "mdx"],
|
|
default="plain",
|
|
help="Output format (default: plain)",
|
|
)
|
|
parser.add_argument(
|
|
"--limit",
|
|
type=int,
|
|
default=50,
|
|
help="Maximum commits to search (default: 50)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
repo_root = get_repo_root()
|
|
print(f"Repository root: {repo_root}")
|
|
print(f"Scanning {PYPROJECT_PATH} for version changes...\n")
|
|
|
|
entries = get_version_commits(repo_root, limit=args.limit)
|
|
|
|
if not entries:
|
|
print("No version entries found.")
|
|
return
|
|
|
|
# Filter by --since if provided
|
|
if args.since:
|
|
filtered = []
|
|
for entry in entries:
|
|
filtered.append(entry)
|
|
if entry.version == args.since:
|
|
break
|
|
entries = filtered
|
|
|
|
print(f"Found {len(entries)} version(s):\n")
|
|
|
|
if args.format == "mdx":
|
|
print(format_mdx_table(entries, repo_root))
|
|
else:
|
|
print(format_plain(entries, repo_root))
|
|
|
|
print("\n" + "=" * 60)
|
|
print("To update the changelog, copy the relevant entries to:")
|
|
print(
|
|
" apps/opik-documentation/documentation/fern/docs/agent_optimization/getting_started/changelog.mdx"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|