mirror of
https://github.com/hanzoui/workflow-templates-broken-zap-2026-06-08.git
synced 2026-10-30 20:44:35 +00:00
305 lines
12 KiB
Python
305 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Auto-bump sub-package versions only when the root pyproject.toml version has been
|
|
changed (e.g. by the PR author). Otherwise skip bumping to avoid unnecessary
|
|
version churn on every template-only PR.
|
|
"""
|
|
import json
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Set, List, Dict
|
|
|
|
ROOT = Path.cwd()
|
|
|
|
def run_git(args: List[str]) -> str:
|
|
return subprocess.check_output(["git", *args], cwd=ROOT).decode().strip()
|
|
|
|
|
|
def root_version_changed() -> bool:
|
|
"""
|
|
Return True if the root pyproject.toml version was changed compared to the
|
|
merge-base with the default branch (origin/main). Only then do we run auto-bump.
|
|
"""
|
|
try:
|
|
base = run_git(["merge-base", "HEAD", "origin/main"])
|
|
except subprocess.CalledProcessError:
|
|
try:
|
|
base = run_git(["merge-base", "HEAD", "main"])
|
|
except subprocess.CalledProcessError:
|
|
return True # fallback: allow bump when merge-base unavailable
|
|
try:
|
|
current = get_current_version("meta")
|
|
base_content = run_git(["show", f"{base}:pyproject.toml"])
|
|
match = re.search(r'^version\s*=\s*"([^"]+)"', base_content, re.MULTILINE)
|
|
base_version = match.group(1) if match else ""
|
|
return current != base_version
|
|
except Exception:
|
|
return True # fallback: allow bump on parse/git errors
|
|
|
|
def get_current_version(pkg: str) -> str:
|
|
"""Get current version of a package from its pyproject.toml"""
|
|
if pkg == "meta":
|
|
path = Path("pyproject.toml")
|
|
else:
|
|
path = Path(f"packages/{pkg}/pyproject.toml")
|
|
|
|
if not path.exists():
|
|
return "0.0.0"
|
|
|
|
text = path.read_text()
|
|
match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
|
|
return match.group(1) if match else "0.0.0"
|
|
|
|
def find_last_version_bump_commit(pkg: str, current_version: str) -> str:
|
|
"""Find the commit where this package's version was last bumped to current_version"""
|
|
if pkg == "meta":
|
|
file_path = "pyproject.toml"
|
|
else:
|
|
file_path = f"packages/{pkg}/pyproject.toml"
|
|
|
|
try:
|
|
# Get commit history for the pyproject.toml file
|
|
log_output = run_git(["log", "--oneline", "--follow", "--", file_path])
|
|
|
|
for line in log_output.split('\n'):
|
|
if not line.strip():
|
|
continue
|
|
commit_hash = line.split()[0]
|
|
|
|
# Check version in this commit
|
|
try:
|
|
old_content = run_git(["show", f"{commit_hash}:{file_path}"])
|
|
old_version_match = re.search(r'^version\s*=\s*"([^"]+)"', old_content, re.MULTILINE)
|
|
if old_version_match:
|
|
old_version = old_version_match.group(1)
|
|
if old_version == current_version:
|
|
# This is where this version was introduced
|
|
return commit_hash
|
|
except:
|
|
continue
|
|
|
|
# Fallback: if we can't find the exact version bump, use HEAD~10 or first commit
|
|
try:
|
|
return run_git(["rev-list", "--max-count=1", "HEAD~10"])
|
|
except:
|
|
return run_git(["rev-list", "--max-count=1", "HEAD"])
|
|
|
|
except:
|
|
# Ultimate fallback
|
|
return "HEAD~1"
|
|
|
|
def get_files_affecting_package(pkg: str, since_commit: str) -> List[str]:
|
|
"""Get files that affect a specific package since the given commit, excluding CI auto-commits"""
|
|
try:
|
|
# Get all files changed since the reference commit
|
|
committed_files = set(run_git(["diff", f"{since_commit}..HEAD", "--name-only"]).split('\n'))
|
|
|
|
# Also check for unstaged changes in working directory (important for core package manifest.json)
|
|
# This handles the case where sync_bundles.py modified manifest.json but it's not yet committed
|
|
unstaged_set = set()
|
|
try:
|
|
unstaged_output = run_git(["diff", "--name-only", "HEAD"])
|
|
unstaged_set = set(unstaged_output.split('\n')) if unstaged_output.strip() else set()
|
|
except:
|
|
pass # If we can't get unstaged files, continue with committed changes only
|
|
|
|
# Combine committed and unstaged changes
|
|
all_changed_files = committed_files | unstaged_set
|
|
|
|
# Filter out files from CI auto-commits by checking commit messages
|
|
affecting_files = []
|
|
for file in all_changed_files:
|
|
file = file.strip()
|
|
if not file:
|
|
continue
|
|
|
|
# For unstaged files, skip the commit check and include them directly
|
|
# (they are new changes from sync_bundles.py that haven't been committed yet)
|
|
if file in unstaged_set:
|
|
affecting_files.append(file)
|
|
continue
|
|
|
|
# For committed files, check if they were modified by CI auto-commits
|
|
try:
|
|
last_commit = run_git(["log", "-1", "--format=%s", f"{since_commit}..HEAD", "--", file]).strip()
|
|
# Skip files that were last modified by CI auto-commits
|
|
if last_commit.startswith("Auto-bump package versions") or last_commit.startswith("chore: bump version"):
|
|
continue
|
|
except:
|
|
pass # If we can't get commit info, include the file
|
|
|
|
affecting_files.append(file)
|
|
|
|
# Filter to only files that affect this package
|
|
filtered_files = []
|
|
|
|
bundles = json.loads(Path("bundles.json").read_text()) if Path("bundles.json").exists() else {}
|
|
bundle_mapping = {
|
|
"media-api": "media_api",
|
|
"media-video": "media_video",
|
|
"media-image": "media_image",
|
|
"media-other": "media_other"
|
|
}
|
|
|
|
# Find which bundle this package corresponds to
|
|
pkg_bundle = None
|
|
for bundle_name, bundle_pkg in bundle_mapping.items():
|
|
if bundle_pkg == pkg:
|
|
pkg_bundle = bundle_name
|
|
break
|
|
|
|
for file in affecting_files:
|
|
file = file.strip()
|
|
if not file:
|
|
continue
|
|
|
|
# Direct package directory changes
|
|
if file.startswith(f"packages/{pkg}/"):
|
|
filtered_files.append(file)
|
|
# Core package affects meta
|
|
elif pkg == "meta" and (file.startswith("packages/") or file == "pyproject.toml"):
|
|
filtered_files.append(file)
|
|
# Template files affecting this package's bundle
|
|
elif pkg_bundle and file.startswith("templates/"):
|
|
template_name = Path(file).stem.split('-')[0]
|
|
if pkg_bundle in bundles and template_name in bundles[pkg_bundle]:
|
|
filtered_files.append(file)
|
|
# bundles.json changes affecting this package's bundle
|
|
elif pkg_bundle and file == "bundles.json":
|
|
try:
|
|
# First check if bundles.json existed in the old commit
|
|
run_git(["cat-file", "-e", f"{since_commit}:bundles.json"])
|
|
old_bundles = json.loads(run_git(["show", f"{since_commit}:bundles.json"]))
|
|
if bundles.get(pkg_bundle) != old_bundles.get(pkg_bundle):
|
|
filtered_files.append(file)
|
|
except subprocess.CalledProcessError:
|
|
# bundles.json didn't exist in old commit, so this is a new file affecting all bundles
|
|
filtered_files.append(file)
|
|
except:
|
|
# Other error, assume it affects this package
|
|
filtered_files.append(file)
|
|
|
|
return filtered_files
|
|
except:
|
|
return []
|
|
|
|
def get_changed_packages() -> Set[str]:
|
|
"""Determine which packages need version bumps based on changes since their last version bump"""
|
|
try:
|
|
packages = ["core", "media_api", "media_video", "media_image", "media_other", "meta"]
|
|
affected = set()
|
|
|
|
for pkg in packages:
|
|
current_version = get_current_version(pkg)
|
|
last_bump_commit = find_last_version_bump_commit(pkg, current_version)
|
|
affecting_files = get_files_affecting_package(pkg, last_bump_commit)
|
|
|
|
if affecting_files:
|
|
affected.add(pkg)
|
|
print(f"Package {pkg} needs bump: {len(affecting_files)} files changed since version {current_version}")
|
|
for f in affecting_files[:5]: # Show first 5 files
|
|
print(f" - {f}")
|
|
if len(affecting_files) > 5:
|
|
print(f" ... and {len(affecting_files) - 5} more files")
|
|
else:
|
|
print(f"Package {pkg} up to date since version {current_version}")
|
|
|
|
# If any non-meta packages changed, also bump meta
|
|
if affected - {"meta"}:
|
|
affected.add("meta")
|
|
|
|
return affected
|
|
except Exception as e:
|
|
print(f"Error in change detection: {e}")
|
|
return {"core", "media_api", "media_video", "media_image", "media_other", "meta"}
|
|
|
|
def bump_versions(packages: Set[str]) -> None:
|
|
# Auto-bump individual packages (core, media-api, etc.) but not the root meta package
|
|
for pkg in packages:
|
|
if pkg == "meta":
|
|
continue # Root pyproject.toml version is manually controlled
|
|
|
|
paths = [f"packages/{pkg}/pyproject.toml"]
|
|
|
|
for path_str in paths:
|
|
path = Path(path_str)
|
|
if path.exists():
|
|
text = path.read_text()
|
|
|
|
def bump_version_match(match):
|
|
version = match.group(1)
|
|
parts = version.split('.')
|
|
if len(parts) >= 3:
|
|
parts[2] = str(int(parts[2]) + 1)
|
|
elif len(parts) == 2:
|
|
parts.append("1")
|
|
else:
|
|
parts = [parts[0], "0", "1"]
|
|
return f'version = "{".".join(parts)}"'
|
|
|
|
# Only bump the project version, not tool versions like ruff target-version
|
|
updated = re.sub(r'^version\s*=\s*"([^"]+)"', bump_version_match, text, flags=re.MULTILINE)
|
|
path.write_text(updated)
|
|
|
|
def update_dependencies() -> None:
|
|
"""Update root meta package dependencies to match auto-bumped individual packages"""
|
|
changed_packages = get_changed_packages()
|
|
non_meta_packages = changed_packages - {"meta"}
|
|
|
|
version_re = re.compile(r'^version\s*=\s*"([^"]+)"', re.MULTILINE)
|
|
versions = {}
|
|
|
|
pyprojects = {
|
|
"core": "packages/core/pyproject.toml",
|
|
"media_api": "packages/media_api/pyproject.toml",
|
|
"media_video": "packages/media_video/pyproject.toml",
|
|
"media_image": "packages/media_image/pyproject.toml",
|
|
"media_other": "packages/media_other/pyproject.toml",
|
|
}
|
|
|
|
# Get versions for packages that were auto-bumped
|
|
for pkg, path in pyprojects.items():
|
|
if pkg in non_meta_packages and Path(path).exists():
|
|
text = Path(path).read_text()
|
|
match = version_re.search(text)
|
|
if match:
|
|
versions[pkg] = match.group(1)
|
|
|
|
if not versions:
|
|
return
|
|
|
|
# Update root pyproject.toml dependencies to match bumped package versions
|
|
meta_path = "pyproject.toml"
|
|
if Path(meta_path).exists():
|
|
text = Path(meta_path).read_text()
|
|
|
|
for pkg, version in versions.items():
|
|
pip_name = f"hanzo-studio-workflow-templates-{pkg.replace('_', '-')}"
|
|
pattern = rf'("{re.escape(pip_name)})==([0-9.]+)(")'
|
|
replacement = rf'\g<1>=={version}\g<3>'
|
|
text = re.sub(pattern, replacement, text)
|
|
|
|
Path(meta_path).write_text(text)
|
|
|
|
if __name__ == "__main__":
|
|
if not root_version_changed():
|
|
print("Root pyproject.toml version unchanged; skipping auto-bump.")
|
|
print("")
|
|
exit(0)
|
|
|
|
packages = get_changed_packages()
|
|
print(f"Detected changed packages: {sorted(packages)}")
|
|
|
|
non_meta_packages = packages - {"meta"}
|
|
|
|
if non_meta_packages:
|
|
bump_versions(packages)
|
|
update_dependencies()
|
|
print(f"Auto-bumped packages and updated dependencies: {sorted(non_meta_packages)}")
|
|
|
|
# Output all packages that need building (including meta if changed)
|
|
if packages:
|
|
print(" ".join(sorted(packages)))
|
|
else:
|
|
print("") |