mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
build: sync agent hook manifests to repo semver
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
5f361f4eb5
commit
c5d795c2af
11 changed files with 814 additions and 586 deletions
|
|
@ -5,20 +5,20 @@
|
|||
},
|
||||
"metadata": {
|
||||
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
|
||||
"version": "0.1.0"
|
||||
"version": "0.10.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "headroom",
|
||||
"source": "./plugins/headroom-agent-hooks",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"version": "0.1.0",
|
||||
"version": "0.10.0",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/JerrettDavis/headroom"
|
||||
"url": "https://github.com/chopratejas/headroom"
|
||||
},
|
||||
"homepage": "https://github.com/JerrettDavis/headroom",
|
||||
"repository": "https://github.com/JerrettDavis/headroom",
|
||||
"homepage": "https://github.com/chopratejas/headroom",
|
||||
"repository": "https://github.com/chopratejas/headroom",
|
||||
"keywords": [
|
||||
"headroom",
|
||||
"hooks",
|
||||
|
|
|
|||
10
.github/plugin/marketplace.json
vendored
10
.github/plugin/marketplace.json
vendored
|
|
@ -5,20 +5,20 @@
|
|||
},
|
||||
"metadata": {
|
||||
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
|
||||
"version": "0.1.0"
|
||||
"version": "0.10.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "headroom",
|
||||
"source": "./plugins/headroom-agent-hooks",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"version": "0.1.0",
|
||||
"version": "0.10.0",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/JerrettDavis/headroom"
|
||||
"url": "https://github.com/chopratejas/headroom"
|
||||
},
|
||||
"homepage": "https://github.com/JerrettDavis/headroom",
|
||||
"repository": "https://github.com/JerrettDavis/headroom",
|
||||
"homepage": "https://github.com/chopratejas/headroom",
|
||||
"repository": "https://github.com/chopratejas/headroom",
|
||||
"keywords": [
|
||||
"headroom",
|
||||
"hooks",
|
||||
|
|
|
|||
437
.gitignore
vendored
437
.gitignore
vendored
|
|
@ -1,218 +1,219 @@
|
|||
# Private scripts (contain credentials). Allowlist checked-in helpers below.
|
||||
scripts/
|
||||
!scripts/
|
||||
scripts/*
|
||||
!scripts/install.sh
|
||||
!scripts/install.ps1
|
||||
!scripts/version-sync.py
|
||||
!scripts/changelog-gen.py
|
||||
!scripts/verify-versions.py
|
||||
!scripts/tests/
|
||||
!scripts/README.md
|
||||
!scripts/repro_codex_replay.py
|
||||
!scripts/fixtures/
|
||||
!scripts/fixtures/*.json
|
||||
|
||||
# Swift SDK (separate repo)
|
||||
swift/
|
||||
|
||||
# Local planning docs (never commit)
|
||||
ENTERPRISE_HARDENING.md
|
||||
|
||||
# Audit/scan outputs (contain security findings — never commit)
|
||||
bandit_result.txt
|
||||
pip_audit_result.txt
|
||||
ruff_result.txt
|
||||
reqs.txt
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.env.*
|
||||
!.env.act.example
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.python-version
|
||||
|
||||
# Secrets and API keys - NEVER commit these
|
||||
*.pem
|
||||
*.key
|
||||
secrets.json
|
||||
credentials.json
|
||||
.secrets
|
||||
api_keys.txt
|
||||
.anthropic
|
||||
.openai
|
||||
|
||||
# IDE and editors
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.spyproject
|
||||
.spyderproject
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
*.ipynb
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
._*
|
||||
|
||||
# Thumbnails
|
||||
Icon?
|
||||
._*
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Linux
|
||||
*~
|
||||
|
||||
# Local configuration
|
||||
local_settings.py
|
||||
*.local.py
|
||||
*.local.json
|
||||
*.local.yaml
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
logs/
|
||||
log/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
|
||||
# Benchmark results (keep framework, not results)
|
||||
.benchmarks/
|
||||
benchmark_results.json
|
||||
benchmark_results/
|
||||
|
||||
# DeepEval cache
|
||||
.deepeval/
|
||||
|
||||
# Headroom specific
|
||||
headroom.db
|
||||
headroom_*.db
|
||||
*.jsonl
|
||||
!tests/fixtures/*.jsonl
|
||||
|
||||
# Documentation build
|
||||
docs/_build/
|
||||
site/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Ruff
|
||||
.ruff_cache/
|
||||
|
||||
# pyright
|
||||
pyrightconfig.json
|
||||
|
||||
# Editor backup files
|
||||
*~
|
||||
\#*\#
|
||||
.\#*
|
||||
|
||||
# Local development configuration
|
||||
CLAUDE.md
|
||||
|
||||
# Vitals provenance data
|
||||
.vitals/
|
||||
|
||||
# Superpowers working files (plans, specs, brainstorming)
|
||||
# docs/spec/ (lives in git - git-versioned living specification)
|
||||
|
||||
# Managed platform (separate private repo)
|
||||
headroom-managed/
|
||||
|
||||
# Local act testing (never commit test tokens)
|
||||
/.env.act
|
||||
.actrc.local
|
||||
|
||||
# Release metadata artifact
|
||||
.releaseetadata
|
||||
|
||||
# uv lockfile: regenerated locally; not committed
|
||||
uv.lock
|
||||
# Private scripts (contain credentials). Allowlist checked-in helpers below.
|
||||
scripts/
|
||||
!scripts/
|
||||
scripts/*
|
||||
!scripts/install.sh
|
||||
!scripts/install.ps1
|
||||
!scripts/version-sync.py
|
||||
!scripts/sync-plugin-versions.py
|
||||
!scripts/changelog-gen.py
|
||||
!scripts/verify-versions.py
|
||||
!scripts/tests/
|
||||
!scripts/README.md
|
||||
!scripts/repro_codex_replay.py
|
||||
!scripts/fixtures/
|
||||
!scripts/fixtures/*.json
|
||||
|
||||
# Swift SDK (separate repo)
|
||||
swift/
|
||||
|
||||
# Local planning docs (never commit)
|
||||
ENTERPRISE_HARDENING.md
|
||||
|
||||
# Audit/scan outputs (contain security findings — never commit)
|
||||
bandit_result.txt
|
||||
pip_audit_result.txt
|
||||
ruff_result.txt
|
||||
reqs.txt
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.env.*
|
||||
!.env.act.example
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.python-version
|
||||
|
||||
# Secrets and API keys - NEVER commit these
|
||||
*.pem
|
||||
*.key
|
||||
secrets.json
|
||||
credentials.json
|
||||
.secrets
|
||||
api_keys.txt
|
||||
.anthropic
|
||||
.openai
|
||||
|
||||
# IDE and editors
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.spyproject
|
||||
.spyderproject
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
*.ipynb
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
._*
|
||||
|
||||
# Thumbnails
|
||||
Icon?
|
||||
._*
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Linux
|
||||
*~
|
||||
|
||||
# Local configuration
|
||||
local_settings.py
|
||||
*.local.py
|
||||
*.local.json
|
||||
*.local.yaml
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
logs/
|
||||
log/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
|
||||
# Benchmark results (keep framework, not results)
|
||||
.benchmarks/
|
||||
benchmark_results.json
|
||||
benchmark_results/
|
||||
|
||||
# DeepEval cache
|
||||
.deepeval/
|
||||
|
||||
# Headroom specific
|
||||
headroom.db
|
||||
headroom_*.db
|
||||
*.jsonl
|
||||
!tests/fixtures/*.jsonl
|
||||
|
||||
# Documentation build
|
||||
docs/_build/
|
||||
site/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Ruff
|
||||
.ruff_cache/
|
||||
|
||||
# pyright
|
||||
pyrightconfig.json
|
||||
|
||||
# Editor backup files
|
||||
*~
|
||||
\#*\#
|
||||
.\#*
|
||||
|
||||
# Local development configuration
|
||||
CLAUDE.md
|
||||
|
||||
# Vitals provenance data
|
||||
.vitals/
|
||||
|
||||
# Superpowers working files (plans, specs, brainstorming)
|
||||
# docs/spec/ (lives in git - git-versioned living specification)
|
||||
|
||||
# Managed platform (separate private repo)
|
||||
headroom-managed/
|
||||
|
||||
# Local act testing (never commit test tokens)
|
||||
/.env.act
|
||||
.actrc.local
|
||||
|
||||
# Release metadata artifact
|
||||
.releaseetadata
|
||||
|
||||
# uv lockfile: regenerated locally; not committed
|
||||
uv.lock
|
||||
|
|
|
|||
|
|
@ -1,17 +1,25 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
exclude: ^experiments/
|
||||
- id: ruff-format
|
||||
exclude: ^experiments/
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.14.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
args: [--ignore-missing-imports]
|
||||
exclude: ^experiments/
|
||||
pass_filenames: false
|
||||
entry: mypy headroom
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: sync-plugin-versions
|
||||
name: Sync plugin versions
|
||||
entry: python scripts/sync-plugin-versions.py
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
exclude: ^experiments/
|
||||
- id: ruff-format
|
||||
exclude: ^experiments/
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.14.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
args: [--ignore-missing-imports]
|
||||
exclude: ^experiments/
|
||||
pass_filenames: false
|
||||
entry: mypy headroom
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ def _marketplace_source() -> str:
|
|||
repo_root = Path(__file__).resolve().parents[2]
|
||||
if (repo_root / ".claude-plugin" / "marketplace.json").exists():
|
||||
return str(repo_root)
|
||||
return "JerrettDavis/headroom"
|
||||
return "chopratejas/headroom"
|
||||
|
||||
|
||||
def _run_checked(command: list[str], *, action: str) -> None:
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"name": "headroom",
|
||||
"version": "0.1.0",
|
||||
"version": "0.10.0",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/JerrettDavis/headroom"
|
||||
"url": "https://github.com/chopratejas/headroom"
|
||||
},
|
||||
"homepage": "https://github.com/JerrettDavis/headroom",
|
||||
"repository": "https://github.com/JerrettDavis/headroom",
|
||||
"homepage": "https://github.com/chopratejas/headroom",
|
||||
"repository": "https://github.com/chopratejas/headroom",
|
||||
"keywords": [
|
||||
"headroom",
|
||||
"hooks",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"name": "headroom",
|
||||
"version": "0.1.0",
|
||||
"version": "0.10.0",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/JerrettDavis/headroom"
|
||||
"url": "https://github.com/chopratejas/headroom"
|
||||
},
|
||||
"homepage": "https://github.com/JerrettDavis/headroom",
|
||||
"repository": "https://github.com/JerrettDavis/headroom",
|
||||
"homepage": "https://github.com/chopratejas/headroom",
|
||||
"repository": "https://github.com/chopratejas/headroom",
|
||||
"keywords": [
|
||||
"headroom",
|
||||
"hooks",
|
||||
|
|
|
|||
55
scripts/sync-plugin-versions.py
Normal file
55
scripts/sync-plugin-versions.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Sync plugin manifest versions to the repo's computed release semver."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from headroom.release_version import ( # noqa: E402
|
||||
compute_release_version,
|
||||
determine_bump_level,
|
||||
find_latest_release_tag,
|
||||
get_canonical_version,
|
||||
list_release_commits,
|
||||
list_release_tags,
|
||||
)
|
||||
|
||||
|
||||
def compute_repo_semver(root: Path) -> str:
|
||||
"""Return the npm-style semver for the repo's next release."""
|
||||
tags = list_release_tags(root)
|
||||
previous_tag = find_latest_release_tag(tags) or ""
|
||||
level = determine_bump_level(list_release_commits(root, previous_tag))
|
||||
info = compute_release_version(
|
||||
canonical_version=get_canonical_version(root),
|
||||
level=level,
|
||||
tags=tags,
|
||||
)
|
||||
return info.npm_version
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = ROOT
|
||||
version = compute_repo_semver(root)
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(root / "scripts" / "version-sync.py"),
|
||||
"--root",
|
||||
str(root),
|
||||
"--version",
|
||||
version,
|
||||
"--plugin-manifests-only",
|
||||
],
|
||||
cwd=root,
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,194 +1,291 @@
|
|||
"""Tests for version-sync.py."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_project(tmp_path: Path) -> dict[str, Path]:
|
||||
"""Create a temporary project with all versioned files."""
|
||||
# Create directory structure
|
||||
root = tmp_path / "project"
|
||||
headroom = root / "headroom"
|
||||
headroom.mkdir(parents=True)
|
||||
plugins = root / "plugins"
|
||||
openclaw = plugins / "openclaw"
|
||||
openclaw.mkdir(parents=True)
|
||||
sdk = root / "sdk"
|
||||
typescript = sdk / "typescript"
|
||||
typescript.mkdir(parents=True)
|
||||
|
||||
# pyproject.toml
|
||||
pyproject = root / "pyproject.toml"
|
||||
pyproject.write_text('[project]\nversion = "0.5.25"\n')
|
||||
|
||||
# headroom/_version.py
|
||||
version_py = headroom / "_version.py"
|
||||
version_py.write_text('"""Package version metadata."""\n\n__version__ = "0.5.25"\n')
|
||||
|
||||
# plugins/openclaw/package.json
|
||||
openclaw_pkg = openclaw / "package.json"
|
||||
openclaw_pkg.write_text(json.dumps({"name": "test", "version": "0.5.25"}))
|
||||
|
||||
# sdk/typescript/package.json
|
||||
typescript_pkg = typescript / "package.json"
|
||||
typescript_pkg.write_text(json.dumps({"name": "test", "version": "0.5.25"}))
|
||||
|
||||
return {
|
||||
"root": root,
|
||||
"pyproject": pyproject,
|
||||
"version_py": version_py,
|
||||
"openclaw_pkg": openclaw_pkg,
|
||||
"typescript_pkg": typescript_pkg,
|
||||
}
|
||||
|
||||
|
||||
def test_version_sync_explicit_version(temp_project: dict[str, Path]) -> None:
|
||||
"""Test --version flag updates all files."""
|
||||
root = temp_project["root"]
|
||||
script = Path(__file__).parent.parent / "version-sync.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--root", str(root), "--version", "0.7.0"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
|
||||
# Verify pyproject.toml
|
||||
pyproject_content = temp_project["pyproject"].read_text()
|
||||
assert 'version = "0.7.0"' in pyproject_content
|
||||
|
||||
# Verify headroom/_version.py
|
||||
version_py_content = temp_project["version_py"].read_text()
|
||||
assert '__version__ = "0.7.0"' in version_py_content
|
||||
|
||||
# Verify plugins/openclaw/package.json
|
||||
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
|
||||
assert openclaw_pkg["version"] == "0.7.0"
|
||||
|
||||
# Verify sdk/typescript/package.json
|
||||
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
|
||||
assert typescript_pkg["version"] == "0.7.0"
|
||||
|
||||
# Verify .releaseetadata was created
|
||||
release_metadata = root / ".releaseetadata"
|
||||
assert release_metadata.exists()
|
||||
metadata = json.loads(release_metadata.read_text())
|
||||
assert metadata["version"] == "0.7.0"
|
||||
assert metadata["packages"]["pypi"] == "0.7.0"
|
||||
assert metadata["packages"]["npm-sdk"] == "0.7.0"
|
||||
assert metadata["packages"]["npm-openclaw"] == "0.7.0"
|
||||
|
||||
|
||||
def test_bump_patch(temp_project: dict[str, Path]) -> None:
|
||||
"""Test --bump patch bumps 0.5.25 to 0.5.26."""
|
||||
root = temp_project["root"]
|
||||
script = Path(__file__).parent.parent / "version-sync.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--root", str(root), "--bump", "patch"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
|
||||
# Verify all files updated to 0.5.26
|
||||
pyproject_content = temp_project["pyproject"].read_text()
|
||||
assert 'version = "0.5.26"' in pyproject_content
|
||||
|
||||
version_py_content = temp_project["version_py"].read_text()
|
||||
assert '__version__ = "0.5.26"' in version_py_content
|
||||
|
||||
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
|
||||
assert openclaw_pkg["version"] == "0.5.26"
|
||||
|
||||
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
|
||||
assert typescript_pkg["version"] == "0.5.26"
|
||||
|
||||
|
||||
def test_bump_minor(temp_project: dict[str, Path]) -> None:
|
||||
"""Test --bump minor bumps 0.5.25 to 0.6.0."""
|
||||
root = temp_project["root"]
|
||||
script = Path(__file__).parent.parent / "version-sync.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--root", str(root), "--bump", "minor"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
|
||||
# Verify all files updated to 0.6.0
|
||||
pyproject_content = temp_project["pyproject"].read_text()
|
||||
assert 'version = "0.6.0"' in pyproject_content
|
||||
|
||||
version_py_content = temp_project["version_py"].read_text()
|
||||
assert '__version__ = "0.6.0"' in version_py_content
|
||||
|
||||
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
|
||||
assert openclaw_pkg["version"] == "0.6.0"
|
||||
|
||||
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
|
||||
assert typescript_pkg["version"] == "0.6.0"
|
||||
|
||||
|
||||
def test_bump_major(temp_project: dict[str, Path]) -> None:
|
||||
"""Test --bump major bumps 0.5.25 to 1.0.0."""
|
||||
root = temp_project["root"]
|
||||
script = Path(__file__).parent.parent / "version-sync.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--root", str(root), "--bump", "major"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
|
||||
# Verify all files updated to 1.0.0
|
||||
pyproject_content = temp_project["pyproject"].read_text()
|
||||
assert 'version = "1.0.0"' in pyproject_content
|
||||
|
||||
version_py_content = temp_project["version_py"].read_text()
|
||||
assert '__version__ = "1.0.0"' in version_py_content
|
||||
|
||||
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
|
||||
assert openclaw_pkg["version"] == "1.0.0"
|
||||
|
||||
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
|
||||
assert typescript_pkg["version"] == "1.0.0"
|
||||
|
||||
|
||||
def test_release_metadata_written(temp_project: dict[str, Path]) -> None:
|
||||
"""Test .releaseetadata is written correctly."""
|
||||
root = temp_project["root"]
|
||||
script = Path(__file__).parent.parent / "version-sync.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--root", str(root), "--version", "0.6.0"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
|
||||
release_metadata = root / ".releaseetadata"
|
||||
assert release_metadata.exists()
|
||||
|
||||
metadata = json.loads(release_metadata.read_text())
|
||||
assert metadata == {
|
||||
"version": "0.6.0",
|
||||
"packages": {
|
||||
"pypi": "0.6.0",
|
||||
"npm-sdk": "0.6.0",
|
||||
"npm-openclaw": "0.6.0",
|
||||
},
|
||||
}
|
||||
"""Tests for version-sync.py."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_project(tmp_path: Path) -> dict[str, Path]:
|
||||
"""Create a temporary project with all versioned files."""
|
||||
# Create directory structure
|
||||
root = tmp_path / "project"
|
||||
headroom = root / "headroom"
|
||||
headroom.mkdir(parents=True)
|
||||
repo_claude_plugin = root / ".claude-plugin"
|
||||
repo_claude_plugin.mkdir(parents=True)
|
||||
repo_github_plugin = root / ".github" / "plugin"
|
||||
repo_github_plugin.mkdir(parents=True)
|
||||
plugins = root / "plugins"
|
||||
openclaw = plugins / "openclaw"
|
||||
openclaw.mkdir(parents=True)
|
||||
agent_hooks_claude = plugins / "headroom-agent-hooks" / ".claude-plugin"
|
||||
agent_hooks_claude.mkdir(parents=True)
|
||||
agent_hooks_github = plugins / "headroom-agent-hooks" / ".github" / "plugin"
|
||||
agent_hooks_github.mkdir(parents=True)
|
||||
sdk = root / "sdk"
|
||||
typescript = sdk / "typescript"
|
||||
typescript.mkdir(parents=True)
|
||||
|
||||
# pyproject.toml
|
||||
pyproject = root / "pyproject.toml"
|
||||
pyproject.write_text('[project]\nversion = "0.5.25"\n')
|
||||
|
||||
# headroom/_version.py
|
||||
version_py = headroom / "_version.py"
|
||||
version_py.write_text('"""Package version metadata."""\n\n__version__ = "0.5.25"\n')
|
||||
|
||||
# plugins/openclaw/package.json
|
||||
openclaw_pkg = openclaw / "package.json"
|
||||
openclaw_pkg.write_text(json.dumps({"name": "test", "version": "0.5.25"}))
|
||||
|
||||
repo_claude_marketplace = repo_claude_plugin / "marketplace.json"
|
||||
repo_claude_marketplace.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"metadata": {"name": "claude-marketplace", "version": "0.1.0"},
|
||||
"plugins": [{"name": "headroom-agent-hooks", "version": "0.1.0"}],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
repo_github_marketplace = repo_github_plugin / "marketplace.json"
|
||||
repo_github_marketplace.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"metadata": {"name": "copilot-marketplace", "version": "0.1.0"},
|
||||
"plugins": [{"name": "headroom-agent-hooks", "version": "0.1.0"}],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
claude_plugin = agent_hooks_claude / "plugin.json"
|
||||
claude_plugin.write_text(json.dumps({"name": "headroom-agent-hooks", "version": "0.1.0"}))
|
||||
|
||||
github_plugin = agent_hooks_github / "plugin.json"
|
||||
github_plugin.write_text(json.dumps({"name": "headroom-agent-hooks", "version": "0.1.0"}))
|
||||
|
||||
# sdk/typescript/package.json
|
||||
typescript_pkg = typescript / "package.json"
|
||||
typescript_pkg.write_text(json.dumps({"name": "test", "version": "0.5.25"}))
|
||||
|
||||
return {
|
||||
"root": root,
|
||||
"pyproject": pyproject,
|
||||
"version_py": version_py,
|
||||
"openclaw_pkg": openclaw_pkg,
|
||||
"repo_claude_marketplace": repo_claude_marketplace,
|
||||
"repo_github_marketplace": repo_github_marketplace,
|
||||
"claude_plugin": claude_plugin,
|
||||
"github_plugin": github_plugin,
|
||||
"typescript_pkg": typescript_pkg,
|
||||
}
|
||||
|
||||
|
||||
def test_version_sync_explicit_version(temp_project: dict[str, Path]) -> None:
|
||||
"""Test --version flag updates all files."""
|
||||
root = temp_project["root"]
|
||||
script = Path(__file__).parent.parent / "version-sync.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--root", str(root), "--version", "0.7.0"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
|
||||
# Verify pyproject.toml
|
||||
pyproject_content = temp_project["pyproject"].read_text()
|
||||
assert 'version = "0.7.0"' in pyproject_content
|
||||
|
||||
# Verify headroom/_version.py
|
||||
version_py_content = temp_project["version_py"].read_text()
|
||||
assert '__version__ = "0.7.0"' in version_py_content
|
||||
|
||||
# Verify plugins/openclaw/package.json
|
||||
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
|
||||
assert openclaw_pkg["version"] == "0.7.0"
|
||||
|
||||
# Verify sdk/typescript/package.json
|
||||
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
|
||||
assert typescript_pkg["version"] == "0.7.0"
|
||||
|
||||
repo_claude_marketplace = json.loads(temp_project["repo_claude_marketplace"].read_text())
|
||||
assert repo_claude_marketplace["metadata"]["version"] == "0.7.0"
|
||||
assert repo_claude_marketplace["plugins"][0]["version"] == "0.7.0"
|
||||
|
||||
repo_github_marketplace = json.loads(temp_project["repo_github_marketplace"].read_text())
|
||||
assert repo_github_marketplace["metadata"]["version"] == "0.7.0"
|
||||
assert repo_github_marketplace["plugins"][0]["version"] == "0.7.0"
|
||||
|
||||
claude_plugin = json.loads(temp_project["claude_plugin"].read_text())
|
||||
assert claude_plugin["version"] == "0.7.0"
|
||||
|
||||
github_plugin = json.loads(temp_project["github_plugin"].read_text())
|
||||
assert github_plugin["version"] == "0.7.0"
|
||||
|
||||
# Verify .releaseetadata was created
|
||||
release_metadata = root / ".releaseetadata"
|
||||
assert release_metadata.exists()
|
||||
metadata = json.loads(release_metadata.read_text())
|
||||
assert metadata["version"] == "0.7.0"
|
||||
assert metadata["packages"]["pypi"] == "0.7.0"
|
||||
assert metadata["packages"]["npm-sdk"] == "0.7.0"
|
||||
assert metadata["packages"]["npm-openclaw"] == "0.7.0"
|
||||
assert metadata["packages"]["agent-hooks-plugin"] == "0.7.0"
|
||||
|
||||
|
||||
def test_bump_patch(temp_project: dict[str, Path]) -> None:
|
||||
"""Test --bump patch bumps 0.5.25 to 0.5.26."""
|
||||
root = temp_project["root"]
|
||||
script = Path(__file__).parent.parent / "version-sync.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--root", str(root), "--bump", "patch"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
|
||||
# Verify all files updated to 0.5.26
|
||||
pyproject_content = temp_project["pyproject"].read_text()
|
||||
assert 'version = "0.5.26"' in pyproject_content
|
||||
|
||||
version_py_content = temp_project["version_py"].read_text()
|
||||
assert '__version__ = "0.5.26"' in version_py_content
|
||||
|
||||
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
|
||||
assert openclaw_pkg["version"] == "0.5.26"
|
||||
|
||||
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
|
||||
assert typescript_pkg["version"] == "0.5.26"
|
||||
|
||||
claude_plugin = json.loads(temp_project["claude_plugin"].read_text())
|
||||
assert claude_plugin["version"] == "0.5.26"
|
||||
|
||||
|
||||
def test_bump_minor(temp_project: dict[str, Path]) -> None:
|
||||
"""Test --bump minor bumps 0.5.25 to 0.6.0."""
|
||||
root = temp_project["root"]
|
||||
script = Path(__file__).parent.parent / "version-sync.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--root", str(root), "--bump", "minor"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
|
||||
# Verify all files updated to 0.6.0
|
||||
pyproject_content = temp_project["pyproject"].read_text()
|
||||
assert 'version = "0.6.0"' in pyproject_content
|
||||
|
||||
version_py_content = temp_project["version_py"].read_text()
|
||||
assert '__version__ = "0.6.0"' in version_py_content
|
||||
|
||||
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
|
||||
assert openclaw_pkg["version"] == "0.6.0"
|
||||
|
||||
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
|
||||
assert typescript_pkg["version"] == "0.6.0"
|
||||
|
||||
github_plugin = json.loads(temp_project["github_plugin"].read_text())
|
||||
assert github_plugin["version"] == "0.6.0"
|
||||
|
||||
|
||||
def test_bump_major(temp_project: dict[str, Path]) -> None:
|
||||
"""Test --bump major bumps 0.5.25 to 1.0.0."""
|
||||
root = temp_project["root"]
|
||||
script = Path(__file__).parent.parent / "version-sync.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--root", str(root), "--bump", "major"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
|
||||
# Verify all files updated to 1.0.0
|
||||
pyproject_content = temp_project["pyproject"].read_text()
|
||||
assert 'version = "1.0.0"' in pyproject_content
|
||||
|
||||
version_py_content = temp_project["version_py"].read_text()
|
||||
assert '__version__ = "1.0.0"' in version_py_content
|
||||
|
||||
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
|
||||
assert openclaw_pkg["version"] == "1.0.0"
|
||||
|
||||
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
|
||||
assert typescript_pkg["version"] == "1.0.0"
|
||||
|
||||
repo_claude_marketplace = json.loads(temp_project["repo_claude_marketplace"].read_text())
|
||||
assert repo_claude_marketplace["metadata"]["version"] == "1.0.0"
|
||||
|
||||
|
||||
def test_release_metadata_written(temp_project: dict[str, Path]) -> None:
|
||||
"""Test .releaseetadata is written correctly."""
|
||||
root = temp_project["root"]
|
||||
script = Path(__file__).parent.parent / "version-sync.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--root", str(root), "--version", "0.6.0"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
|
||||
release_metadata = root / ".releaseetadata"
|
||||
assert release_metadata.exists()
|
||||
|
||||
metadata = json.loads(release_metadata.read_text())
|
||||
assert metadata == {
|
||||
"version": "0.6.0",
|
||||
"packages": {
|
||||
"pypi": "0.6.0",
|
||||
"npm-sdk": "0.6.0",
|
||||
"npm-openclaw": "0.6.0",
|
||||
"agent-hooks-plugin": "0.6.0",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_plugin_manifests_only_leaves_package_versions_unchanged(
|
||||
temp_project: dict[str, Path],
|
||||
) -> None:
|
||||
"""Test plugin-only sync leaves canonical package versions alone."""
|
||||
root = temp_project["root"]
|
||||
script = Path(__file__).parent.parent / "version-sync.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script),
|
||||
"--root",
|
||||
str(root),
|
||||
"--version",
|
||||
"0.8.0",
|
||||
"--plugin-manifests-only",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
||||
assert 'version = "0.5.25"' in temp_project["pyproject"].read_text()
|
||||
assert '__version__ = "0.5.25"' in temp_project["version_py"].read_text()
|
||||
assert json.loads(temp_project["openclaw_pkg"].read_text())["version"] == "0.5.25"
|
||||
assert json.loads(temp_project["typescript_pkg"].read_text())["version"] == "0.5.25"
|
||||
assert json.loads(temp_project["claude_plugin"].read_text())["version"] == "0.8.0"
|
||||
assert (
|
||||
json.loads(temp_project["repo_github_marketplace"].read_text())["metadata"]["version"]
|
||||
== "0.8.0"
|
||||
)
|
||||
assert not (root / ".releaseetadata").exists()
|
||||
|
|
|
|||
|
|
@ -1,138 +1,190 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Synchronize version across all headroom packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib
|
||||
|
||||
|
||||
def get_version_from_pyproject(root: Path) -> str:
|
||||
"""Read version from pyproject.toml."""
|
||||
pyproject_path = root / "pyproject.toml"
|
||||
with open(pyproject_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
return data["project"]["version"]
|
||||
|
||||
|
||||
def bump_version(version: str, bump_type: str) -> str:
|
||||
"""Bump version according to bump_type (major, minor, patch)."""
|
||||
major, minor, patch = map(int, version.split("."))
|
||||
if bump_type == "major":
|
||||
major += 1
|
||||
minor = 0
|
||||
patch = 0
|
||||
elif bump_type == "minor":
|
||||
minor += 1
|
||||
patch = 0
|
||||
elif bump_type == "patch":
|
||||
patch += 1
|
||||
return f"{major}.{minor}.{patch}"
|
||||
|
||||
|
||||
def update_version_py(root: Path, version: str) -> None:
|
||||
"""Update headroom/_version.py with new version."""
|
||||
version_py_path = root / "headroom" / "_version.py"
|
||||
content = version_py_path.read_text(encoding="utf-8")
|
||||
updated = re.sub(
|
||||
r'__version__ = "[^"]+"',
|
||||
f'__version__ = "{version}"',
|
||||
content,
|
||||
)
|
||||
version_py_path.write_text(updated, encoding="utf-8")
|
||||
|
||||
|
||||
def update_package_json(file_path: Path, version: str) -> None:
|
||||
"""Update a package.json version field."""
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
data["version"] = version
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def update_openclaw_package_json(file_path: Path, version: str, sdk_version: str) -> None:
|
||||
"""Update openclaw package.json version and headroom-ai dependency range."""
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
data["version"] = version
|
||||
if "dependencies" in data and "headroom-ai" in data["dependencies"]:
|
||||
data["dependencies"]["headroom-ai"] = f"^{sdk_version}"
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def update_pyproject_version(root: Path, version: str) -> None:
|
||||
"""Update pyproject.toml version."""
|
||||
pyproject_path = root / "pyproject.toml"
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
updated = re.sub(
|
||||
r'^version = "[^"]+"',
|
||||
f'version = "{version}"',
|
||||
content,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
pyproject_path.write_text(updated, encoding="utf-8")
|
||||
|
||||
|
||||
def write_release_metadata(root: Path, version: str) -> None:
|
||||
"""Write .releaseetadata JSON file."""
|
||||
metadata = {
|
||||
"version": version,
|
||||
"packages": {
|
||||
"pypi": version,
|
||||
"npm-sdk": version,
|
||||
"npm-openclaw": version,
|
||||
},
|
||||
}
|
||||
metadata_path = root / ".releaseetadata"
|
||||
with open(metadata_path, "w", encoding="utf-8") as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Synchronize version across headroom packages")
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
type=Path,
|
||||
default=Path(__file__).parent.parent,
|
||||
help="Root directory of the project",
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--version", help="Explicit version to set (e.g., 0.6.0)")
|
||||
group.add_argument(
|
||||
"--bump",
|
||||
choices=["major", "minor", "patch"],
|
||||
help="Bump version from pyproject.toml",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version:
|
||||
version = args.version
|
||||
elif args.bump:
|
||||
base_version = get_version_from_pyproject(args.root)
|
||||
version = bump_version(base_version, args.bump)
|
||||
else:
|
||||
version = get_version_from_pyproject(args.root)
|
||||
|
||||
# Update all versioned files
|
||||
update_pyproject_version(args.root, version)
|
||||
update_version_py(args.root, version)
|
||||
update_openclaw_package_json(
|
||||
args.root / "plugins" / "openclaw" / "package.json", version, version
|
||||
)
|
||||
update_package_json(args.root / "sdk" / "typescript" / "package.json", version)
|
||||
write_release_metadata(args.root, version)
|
||||
|
||||
print(f"Version synchronized to {version}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
#!/usr/bin/env python3
|
||||
"""Synchronize version across all headroom packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib
|
||||
|
||||
|
||||
def get_version_from_pyproject(root: Path) -> str:
|
||||
"""Read version from pyproject.toml."""
|
||||
pyproject_path = root / "pyproject.toml"
|
||||
with open(pyproject_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
return data["project"]["version"]
|
||||
|
||||
|
||||
def bump_version(version: str, bump_type: str) -> str:
|
||||
"""Bump version according to bump_type (major, minor, patch)."""
|
||||
major, minor, patch = map(int, version.split("."))
|
||||
if bump_type == "major":
|
||||
major += 1
|
||||
minor = 0
|
||||
patch = 0
|
||||
elif bump_type == "minor":
|
||||
minor += 1
|
||||
patch = 0
|
||||
elif bump_type == "patch":
|
||||
patch += 1
|
||||
return f"{major}.{minor}.{patch}"
|
||||
|
||||
|
||||
def update_version_py(root: Path, version: str) -> None:
|
||||
"""Update headroom/_version.py with new version."""
|
||||
version_py_path = root / "headroom" / "_version.py"
|
||||
content = version_py_path.read_text(encoding="utf-8")
|
||||
updated = re.sub(
|
||||
r'__version__ = "[^"]+"',
|
||||
f'__version__ = "{version}"',
|
||||
content,
|
||||
)
|
||||
version_py_path.write_text(updated, encoding="utf-8")
|
||||
|
||||
|
||||
def update_package_json(file_path: Path, version: str) -> None:
|
||||
"""Update a package.json version field."""
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
data["version"] = version
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def update_plugin_manifest(file_path: Path, version: str) -> None:
|
||||
"""Update a plugin.json version field."""
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
data["version"] = version
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def update_marketplace_manifest(file_path: Path, version: str) -> None:
|
||||
"""Update marketplace metadata and plugin entry versions."""
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
metadata = data.get("metadata")
|
||||
if isinstance(metadata, dict):
|
||||
metadata["version"] = version
|
||||
plugins = data.get("plugins")
|
||||
if isinstance(plugins, list):
|
||||
for plugin in plugins:
|
||||
if isinstance(plugin, dict):
|
||||
plugin["version"] = version
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def update_plugin_versions(root: Path, version: str) -> None:
|
||||
"""Update marketplace and plugin manifest versions."""
|
||||
update_marketplace_manifest(root / ".claude-plugin" / "marketplace.json", version)
|
||||
update_marketplace_manifest(root / ".github" / "plugin" / "marketplace.json", version)
|
||||
update_plugin_manifest(
|
||||
root / "plugins" / "headroom-agent-hooks" / ".claude-plugin" / "plugin.json", version
|
||||
)
|
||||
update_plugin_manifest(
|
||||
root / "plugins" / "headroom-agent-hooks" / ".github" / "plugin" / "plugin.json",
|
||||
version,
|
||||
)
|
||||
|
||||
|
||||
def update_openclaw_package_json(file_path: Path, version: str, sdk_version: str) -> None:
|
||||
"""Update openclaw package.json version and headroom-ai dependency range."""
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
data["version"] = version
|
||||
if "dependencies" in data and "headroom-ai" in data["dependencies"]:
|
||||
data["dependencies"]["headroom-ai"] = f"^{sdk_version}"
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def update_pyproject_version(root: Path, version: str) -> None:
|
||||
"""Update pyproject.toml version."""
|
||||
pyproject_path = root / "pyproject.toml"
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
updated = re.sub(
|
||||
r'^version = "[^"]+"',
|
||||
f'version = "{version}"',
|
||||
content,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
pyproject_path.write_text(updated, encoding="utf-8")
|
||||
|
||||
|
||||
def write_release_metadata(root: Path, version: str) -> None:
|
||||
"""Write .releaseetadata JSON file."""
|
||||
metadata = {
|
||||
"version": version,
|
||||
"packages": {
|
||||
"pypi": version,
|
||||
"npm-sdk": version,
|
||||
"npm-openclaw": version,
|
||||
"agent-hooks-plugin": version,
|
||||
},
|
||||
}
|
||||
metadata_path = root / ".releaseetadata"
|
||||
with open(metadata_path, "w", encoding="utf-8") as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Synchronize version across headroom packages")
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
type=Path,
|
||||
default=Path(__file__).parent.parent,
|
||||
help="Root directory of the project",
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--version", help="Explicit version to set (e.g., 0.6.0)")
|
||||
group.add_argument(
|
||||
"--bump",
|
||||
choices=["major", "minor", "patch"],
|
||||
help="Bump version from pyproject.toml",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plugin-manifests-only",
|
||||
action="store_true",
|
||||
help="Only update marketplace/plugin manifest versions",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version:
|
||||
version = args.version
|
||||
elif args.bump:
|
||||
base_version = get_version_from_pyproject(args.root)
|
||||
version = bump_version(base_version, args.bump)
|
||||
else:
|
||||
version = get_version_from_pyproject(args.root)
|
||||
|
||||
if args.plugin_manifests_only:
|
||||
update_plugin_versions(args.root, version)
|
||||
print(f"Plugin versions synchronized to {version}")
|
||||
return
|
||||
|
||||
# Update all versioned files
|
||||
update_pyproject_version(args.root, version)
|
||||
update_version_py(args.root, version)
|
||||
update_openclaw_package_json(
|
||||
args.root / "plugins" / "openclaw" / "package.json", version, version
|
||||
)
|
||||
update_package_json(args.root / "sdk" / "typescript" / "package.json", version)
|
||||
update_plugin_versions(args.root, version)
|
||||
write_release_metadata(args.root, version)
|
||||
|
||||
print(f"Version synchronized to {version}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -38,3 +38,18 @@ def test_marketplace_entry_points_to_plugin_root() -> None:
|
|||
assert plugin_root.is_dir()
|
||||
assert (plugin_root / ".claude-plugin" / "plugin.json").is_file()
|
||||
assert (plugin_root / "hooks" / "hooks.json").is_file()
|
||||
|
||||
|
||||
def test_plugin_metadata_points_to_upstream_repo() -> None:
|
||||
expected_repo = "https://github.com/chopratejas/headroom"
|
||||
marketplace = _load_json(".claude-plugin/marketplace.json")
|
||||
claude = _load_json("plugins/headroom-agent-hooks/.claude-plugin/plugin.json")
|
||||
assert isinstance(marketplace, dict)
|
||||
assert isinstance(claude, dict)
|
||||
plugin = marketplace["plugins"][0]
|
||||
assert plugin["author"]["url"] == expected_repo
|
||||
assert plugin["homepage"] == expected_repo
|
||||
assert plugin["repository"] == expected_repo
|
||||
assert claude["author"]["url"] == expected_repo
|
||||
assert claude["homepage"] == expected_repo
|
||||
assert claude["repository"] == expected_repo
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue