mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
one release path, and the docs say the one that runs
RELEASE.md described GitHub Actions, GitHub secrets and three bin/ scripts that each took a PyPI token off the environment. None of that publishes this repo — .hanzo/workflows/publish-pypi.yml does, reading the token from KMS — so the scripts are gone and the page describes the lane, its tag mapping, and what to check before a tag makes a version permanent. LLM.md carries the published state: hanzoai 3.2.13 with get_keys/get_tools, and the KMS route that a 404 will otherwise misreport as a missing secret.
This commit is contained in:
@@ -12,21 +12,31 @@ OUT, never duplicate. Full spec: `~/work/hanzo/SDK-ARCHITECTURE.md`.
|
||||
|
||||
## Install / run
|
||||
```bash
|
||||
uv pip install "hanzoai @ git+https://github.com/hanzoai/python-sdk" # the current client
|
||||
pip install hanzoai==3.2.13 # the current client
|
||||
pip install hanzo # agents + MCP + orchestration helpers
|
||||
uv sync --all-packages # dev: whole workspace
|
||||
uv run pytest tests/ -v # tests
|
||||
```
|
||||
|
||||
**PyPI is behind, and the README says so instead of pretending.** `hanzoai` on
|
||||
PyPI is **3.2.1**; this tree is 3.2.11, and v3.2.3 … v3.2.10 are tagged on the
|
||||
forge with nothing published for any of them. The gap is not cosmetic:
|
||||
3.2.1 was cut before the default version left the operation ids, so it carries
|
||||
**PyPI serves this tree again.** `hanzoai` had sat at **3.2.1** while v3.2.3 …
|
||||
v3.2.12 were tagged with nothing published, and the gap was not cosmetic: 3.2.1
|
||||
predates the default version leaving the operation ids, so it carries
|
||||
`get_v1_keys`/`get_v1_tools` (182 api modules, 2172 models) plus the retired flat
|
||||
`hanzoai/api` tree, while this one carries `get_keys`/`get_tools`. A README that
|
||||
printed `pip install hanzoai` above a `get_keys()` quickstart would be handing
|
||||
the reader a client that cannot run it, so the install line is the git one until
|
||||
a publish lands.
|
||||
`hanzoai/api` tree. **3.2.13** carries `get_keys`/`get_tools` and is what the
|
||||
README's quickstart runs on. The same tag published 13 sibling packages that had
|
||||
also drifted behind — `hanzo-mcp` 0.15.14, `hanzo-tools` 0.3.5, `hanzo-kms`
|
||||
1.1.1, `hanzo-network` 0.1.4, seven `hanzo-tools-*`, and first releases of
|
||||
`hanzo-flags` and `hanzo-research`.
|
||||
|
||||
**What had stopped every one of them** is worth keeping, because it reads like a
|
||||
missing secret and is not one. The job asked KMS for
|
||||
`/v1/kms/orgs/hanzo/secrets/<path>/<key>` and parsed `.secret.value`. KMS serves
|
||||
`GET /v1/kms/secrets/<path>/<key>?env=<env>` and answers `{"env","name","value"}`,
|
||||
taking the org from the caller's own token claim. A wrong route 404s exactly like
|
||||
an unseeded path, so the log said "KMS holds no PyPI token" about a token that was
|
||||
sitting at `hanzo`/`prod`/`python-sdk-publish`/`PYPI_TOKEN` the whole time. The
|
||||
route the kms-operator uses is the one that answers; check a read against a secret
|
||||
known to exist before believing a 404.
|
||||
|
||||
## Console-script law — only the native binary is called `hanzo`
|
||||
|
||||
@@ -253,8 +263,8 @@ have. It reads `analytics_api` now, off the client.
|
||||
seven hand-written modules beside it (`config`, `mcp`, `protocols`, `session`, `zap`, …).
|
||||
`pkg/hanzoai/{api,models}` is GONE from git; if a checkout still shows `resources/` or
|
||||
`types/` on disk they are untracked leftovers of that surface, and the wheel excludes them
|
||||
— a built 3.2.11 wheel is 2660 files of `hanzoai/cloud/` and those seven modules, nothing
|
||||
else.
|
||||
— the published 3.2.13 wheel is 2667 files of `hanzoai/cloud/` and those seven modules,
|
||||
nothing else.
|
||||
- `pkg/hanzo/src/hanzo/cli.py` — the `hanzo` CLI command tree.
|
||||
- `pkg/hanzo-mcp/` — MCP server; tools via `[project.entry-points."hanzo.tools"]`.
|
||||
- `pkg/hanzo-tools-*/` — one concern each; 37 of the 38 register a `TOOLS` list under the
|
||||
|
||||
+35
-176
@@ -1,194 +1,53 @@
|
||||
# Release Process for Hanzo Python SDK
|
||||
# Releasing
|
||||
|
||||
This document describes the automated release process for all Python packages in this repository.
|
||||
One lane: `.hanzo/workflows/publish-pypi.yml`, on the git.hanzo.ai runner fleet.
|
||||
It reads the PyPI token from KMS at publish time — nothing else in this repo
|
||||
uploads, and no token is ever handed to a shell here.
|
||||
|
||||
## 📦 Packages
|
||||
Bump the version, commit, push a tag:
|
||||
|
||||
The following Python packages are managed in this repository:
|
||||
|
||||
- **hanzo** - Core Hanzo AI SDK
|
||||
- **hanzo-network** - Network utilities for Hanzo AI
|
||||
- **hanzo-mcp** - Model Context Protocol implementation
|
||||
- **hanzo-agents** - Agent framework for Hanzo AI
|
||||
- **hanzo-memory** - Memory management for AI agents
|
||||
- **hanzo-aci** - AI Chain Infrastructure
|
||||
- **hanzo-repl** - REPL for Hanzo AI
|
||||
|
||||
## 🚀 Automated Release Process
|
||||
|
||||
### CI/CD Pipeline
|
||||
|
||||
The release process is fully automated through GitHub Actions with two methods:
|
||||
|
||||
#### Method 1: Automatic Version Detection (Recommended)
|
||||
**Every push to `main` branch automatically:**
|
||||
1. Runs all tests
|
||||
2. Checks each package's version against PyPI
|
||||
3. Publishes any packages with new versions
|
||||
4. Skips packages already published
|
||||
|
||||
**To release this way:**
|
||||
```bash
|
||||
# Update version in pyproject.toml
|
||||
# Commit and push to main
|
||||
git add -A
|
||||
git commit -m "bump: hanzo-mcp to v1.2.3"
|
||||
# pkg/hanzo-mcp/pyproject.toml -> version = "0.15.15"
|
||||
git commit -am "hanzo-mcp 0.15.15"
|
||||
git push origin main
|
||||
# CI automatically publishes if version is new!
|
||||
git tag hanzo-mcp-0.15.15 && git push origin hanzo-mcp-0.15.15
|
||||
```
|
||||
|
||||
#### Method 2: Tag-based Release
|
||||
1. **Tests Run First**: All tests must pass before any package is published
|
||||
2. **Tag-based Triggers**: Pushing a tag triggers the release process
|
||||
3. **Automatic PyPI Upload**: Packages are automatically built and uploaded to PyPI
|
||||
The tag names the packages:
|
||||
|
||||
### Workflow Files
|
||||
| tag | publishes |
|
||||
|---|---|
|
||||
| `v3.2.13` | every workspace package |
|
||||
| `hanzoai-v3.2.13` | the root `hanzoai` client alone |
|
||||
| `hanzo-tools-0.3.5` | every `pkg/hanzo-tools-*` |
|
||||
| `hanzo-mcp-0.15.15` | the matching `pkg/<name>`, else `hanzo` |
|
||||
|
||||
- **`.github/workflows/publish-pypi.yml`**: Main publishing workflow
|
||||
- **`.github/workflows/hanzo-packages-ci.yml`**: CI with integrated publishing
|
||||
- **`.github/workflows/test.yml`**: Test suite that must pass
|
||||
A `v*` tag is not as broad as it looks: uploads pass `--skip-existing`, so a
|
||||
package whose version is already on PyPI is a no-op. What it does publish is
|
||||
every package whose in-tree version is new — which is the point, and also the
|
||||
reason to look before you tag.
|
||||
|
||||
## 📝 How to Release
|
||||
Watch the run under Actions on git.hanzo.ai. Versions are semver, and PyPI never
|
||||
lets a version be replaced: a bad release is fixed by a higher one.
|
||||
|
||||
### Option 1: Automatic (Easiest) 🎯
|
||||
## Before you tag
|
||||
|
||||
Simply update the version and push to main:
|
||||
Build what the lane will build and read the metadata you are about to publish —
|
||||
the registry page is the first thing a reader sees, and it is permanent.
|
||||
|
||||
```bash
|
||||
# Update version in package's pyproject.toml
|
||||
vim pkg/hanzo-mcp/pyproject.toml # Change version = "1.2.3"
|
||||
|
||||
# Commit and push
|
||||
git add -A
|
||||
git commit -m "Release hanzo-mcp v1.2.3"
|
||||
git push origin main
|
||||
|
||||
# CI will automatically detect and publish the new version!
|
||||
python -m build . --outdir dist # or pkg/<name>
|
||||
python -m twine check dist/*
|
||||
```
|
||||
|
||||
### Option 2: Using Tags
|
||||
`twine check` catches a README that will not render. It does not catch a missing
|
||||
one: a package with no `readme`, `license` or `authors` in its `pyproject.toml`
|
||||
uploads happily and lands as a blank page.
|
||||
|
||||
#### Release All Packages
|
||||
## If the lane fails
|
||||
|
||||
To release all packages with the same version:
|
||||
|
||||
```bash
|
||||
# Update version in all pyproject.toml files
|
||||
# Then create and push a tag
|
||||
git tag v1.2.3
|
||||
git push origin v1.2.3
|
||||
```
|
||||
|
||||
#### Release Individual Package
|
||||
|
||||
To release a specific package:
|
||||
|
||||
```bash
|
||||
# Update version in the specific package's pyproject.toml
|
||||
# Then create and push a package-specific tag
|
||||
git tag hanzo-mcp-1.2.3
|
||||
git push origin hanzo-mcp-1.2.3
|
||||
```
|
||||
|
||||
### Tag Naming Convention
|
||||
|
||||
- **`v*`** - Releases all packages (e.g., `v1.2.3`)
|
||||
- **`hanzo-*`** - Releases the main hanzo package (e.g., `hanzo-1.2.3`)
|
||||
- **`hanzo-network-*`** - Releases hanzo-network package
|
||||
- **`hanzo-mcp-*`** - Releases hanzo-mcp package
|
||||
- **`hanzo-agents-*`** - Releases hanzo-agents package
|
||||
- **`hanzo-memory-*`** - Releases hanzo-memory package
|
||||
- **`hanzo-aci-*`** - Releases hanzo-aci package
|
||||
- **`hanzo-repl-*`** - Releases hanzo-repl package
|
||||
|
||||
## 🔧 Manual Release (Emergency)
|
||||
|
||||
If automated release fails, you can manually publish:
|
||||
|
||||
```bash
|
||||
# Set PyPI token
|
||||
export PYPI_TOKEN=your_token_here
|
||||
|
||||
# Publish all packages
|
||||
./bin/publish-all-packages.sh
|
||||
|
||||
# Or publish specific package
|
||||
./bin/publish-all-packages.sh hanzo-mcp
|
||||
```
|
||||
|
||||
## ✅ Release Checklist
|
||||
|
||||
Before creating a release tag:
|
||||
|
||||
1. [ ] Update version in `pyproject.toml` file(s)
|
||||
2. [ ] Update CHANGELOG if applicable
|
||||
3. [ ] Ensure all tests pass locally
|
||||
4. [ ] Commit all changes
|
||||
5. [ ] Create and push tag
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
- PyPI tokens are stored as GitHub secrets
|
||||
- Use `HANZO_PYPI_TOKEN` or `PYPI_TOKEN` secret names
|
||||
- Tokens have package-specific or organization-wide permissions
|
||||
|
||||
## 📊 Release Status
|
||||
|
||||
You can monitor release status at:
|
||||
|
||||
- [GitHub Actions](https://github.com/hanzoai/python-sdk/actions)
|
||||
- [PyPI - hanzo](https://pypi.org/project/hanzo/)
|
||||
- [PyPI - hanzo-network](https://pypi.org/project/hanzo-network/)
|
||||
- [PyPI - hanzo-mcp](https://pypi.org/project/hanzo-mcp/)
|
||||
- [PyPI - hanzo-agents](https://pypi.org/project/hanzo-agents/)
|
||||
- [PyPI - hanzo-memory](https://pypi.org/project/hanzo-memory/)
|
||||
- [PyPI - hanzo-aci](https://pypi.org/project/hanzo-aci/)
|
||||
- [PyPI - hanzo-repl](https://pypi.org/project/hanzo-repl/)
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Tests Failing
|
||||
|
||||
If tests fail, the release will not proceed. Check:
|
||||
- Test logs in GitHub Actions
|
||||
- Local test execution with `pytest`
|
||||
|
||||
### PyPI Upload Fails
|
||||
|
||||
Common issues:
|
||||
- Version already exists on PyPI
|
||||
- Invalid PyPI token
|
||||
- Network issues
|
||||
|
||||
Solutions:
|
||||
- Bump version number
|
||||
- Check GitHub secrets configuration
|
||||
- Retry the workflow
|
||||
|
||||
### Package Not Publishing
|
||||
|
||||
Ensure:
|
||||
- Tag matches the naming convention
|
||||
- Package directory exists in `pkg/`
|
||||
- `pyproject.toml` is properly configured
|
||||
|
||||
## 📈 Version Management
|
||||
|
||||
We follow semantic versioning (MAJOR.MINOR.PATCH):
|
||||
|
||||
- **MAJOR**: Breaking API changes
|
||||
- **MINOR**: New features, backward compatible
|
||||
- **PATCH**: Bug fixes, backward compatible
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
When adding a new package:
|
||||
|
||||
1. Create package directory in `pkg/`
|
||||
2. Add `pyproject.toml` with proper configuration
|
||||
3. Update CI workflows to include the new package
|
||||
4. Add package to this documentation
|
||||
|
||||
---
|
||||
|
||||
For questions or issues, please open an issue on [GitHub](https://github.com/hanzoai/python-sdk/issues).
|
||||
Read the job log — every failure mode it knows about prints a line saying which
|
||||
credential or path it wanted. The KMS read is the one worth naming: KMS serves
|
||||
`GET /v1/kms/secrets/<path>/<name>?env=<env>` and takes the org from the caller's
|
||||
token, so a request in any other shape 404s exactly like an unseeded path. Prove
|
||||
a route against a secret you know exists before concluding a secret is missing.
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check package versions and publish to PyPI if newer version is available.
|
||||
This script is designed to run in CI on every push to main.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Tuple, Optional
|
||||
from pathlib import Path
|
||||
|
||||
# Colors for output
|
||||
RED = "\033[0;31m"
|
||||
GREEN = "\033[0;32m"
|
||||
YELLOW = "\033[1;33m"
|
||||
BLUE = "\033[0;34m"
|
||||
NC = "\033[0m" # No Color
|
||||
|
||||
|
||||
def print_info(msg: str):
|
||||
print(f"{GREEN}[INFO]{NC} {msg}")
|
||||
|
||||
|
||||
def print_warn(msg: str):
|
||||
print(f"{YELLOW}[WARN]{NC} {msg}")
|
||||
|
||||
|
||||
def print_error(msg: str):
|
||||
print(f"{RED}[ERROR]{NC} {msg}")
|
||||
|
||||
|
||||
def print_action(msg: str):
|
||||
print(f"{BLUE}[ACTION]{NC} {msg}")
|
||||
|
||||
|
||||
def get_local_version(package_dir: Path) -> Optional[str]:
|
||||
"""Extract version from pyproject.toml."""
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
if not pyproject_path.exists():
|
||||
return None
|
||||
|
||||
with open(pyproject_path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Match version = "x.y.z" or version = 'x.y.z'
|
||||
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def get_pypi_version(package_name: str) -> Optional[str]:
|
||||
"""Get the latest version from PyPI."""
|
||||
url = f"https://pypi.org/pypi/{package_name}/json"
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=30) as response: # noqa: S310
|
||||
data = json.loads(response.read())
|
||||
return data.get("info", {}).get("version")
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
# Package doesn't exist on PyPI yet
|
||||
return None
|
||||
raise
|
||||
except Exception as e:
|
||||
print_warn(f"Could not fetch PyPI version for {package_name}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_version(version: str) -> Tuple[int, ...]:
|
||||
"""Parse version string to tuple for comparison."""
|
||||
# Remove any pre-release or build metadata
|
||||
version = re.split(r"[-+]", version)[0]
|
||||
return tuple(int(x) for x in version.split("."))
|
||||
|
||||
|
||||
def is_newer_version(local_version: str, pypi_version: Optional[str]) -> bool:
|
||||
"""Check if local version is newer than PyPI version."""
|
||||
if pypi_version is None:
|
||||
# Package doesn't exist on PyPI
|
||||
return True
|
||||
|
||||
try:
|
||||
local_tuple = parse_version(local_version)
|
||||
pypi_tuple = parse_version(pypi_version)
|
||||
return local_tuple > pypi_tuple
|
||||
except (ValueError, AttributeError):
|
||||
# If we can't parse versions, compare as strings
|
||||
return local_version != pypi_version
|
||||
|
||||
|
||||
def build_package(package_dir: Path) -> bool:
|
||||
"""Build the Python package."""
|
||||
print_info(f"Building package in {package_dir}")
|
||||
|
||||
# Clean previous builds
|
||||
for dir_name in ["dist", "build"]:
|
||||
dir_path = package_dir / dir_name
|
||||
if dir_path.exists():
|
||||
subprocess.run(["rm", "-rf", str(dir_path)], check=True)
|
||||
|
||||
# Remove .egg-info directories
|
||||
for egg_info in package_dir.glob("*.egg-info"):
|
||||
subprocess.run(["rm", "-rf", str(egg_info)], check=True)
|
||||
|
||||
# Build the package
|
||||
result = subprocess.run(["python", "-m", "build"], cwd=package_dir, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
print_error(f"Build failed: {result.stderr}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def publish_package(package_dir: Path, package_name: str) -> bool:
|
||||
"""Publish package to PyPI."""
|
||||
print_action(f"Publishing {package_name} to PyPI")
|
||||
|
||||
# Check for PyPI token
|
||||
pypi_token = os.environ.get("PYPI_TOKEN") or os.environ.get("HANZO_PYPI_TOKEN")
|
||||
if not pypi_token:
|
||||
print_error("PYPI_TOKEN or HANZO_PYPI_TOKEN environment variable not set")
|
||||
return False
|
||||
|
||||
# Upload to PyPI
|
||||
result = subprocess.run(
|
||||
["python", "-m", "twine", "upload", "dist/*", "--skip-existing"],
|
||||
cwd=package_dir,
|
||||
env={**os.environ, "TWINE_USERNAME": "__token__", "TWINE_PASSWORD": pypi_token},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print_error(f"Upload failed: {result.stderr}")
|
||||
return False
|
||||
|
||||
print_info(f"✅ Successfully published {package_name}")
|
||||
return True
|
||||
|
||||
|
||||
def check_and_publish_package(package_name: str, package_dir: Path) -> bool:
|
||||
"""Check version and publish if newer."""
|
||||
print_info(f"Checking {package_name}...")
|
||||
|
||||
# Get local version
|
||||
local_version = get_local_version(package_dir)
|
||||
if not local_version:
|
||||
print_warn(f"Could not find version for {package_name}")
|
||||
return False
|
||||
|
||||
print_info(f" Local version: {local_version}")
|
||||
|
||||
# Get PyPI version
|
||||
pypi_version = get_pypi_version(package_name)
|
||||
if pypi_version:
|
||||
print_info(f" PyPI version: {pypi_version}")
|
||||
else:
|
||||
print_info(f" PyPI version: Not published yet")
|
||||
|
||||
# Check if we need to publish
|
||||
if is_newer_version(local_version, pypi_version):
|
||||
print_action(f"📦 New version detected for {package_name}: {local_version}")
|
||||
|
||||
# Build the package
|
||||
if not build_package(package_dir):
|
||||
return False
|
||||
|
||||
# Publish to PyPI
|
||||
return publish_package(package_dir, package_name)
|
||||
else:
|
||||
print_info(f" ✓ {package_name} is up to date")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to check and publish all packages."""
|
||||
# Define packages in dependency order
|
||||
packages = [
|
||||
"hanzo-network",
|
||||
"hanzo-memory",
|
||||
"hanzo-agents",
|
||||
"hanzo-aci",
|
||||
"hanzo-mcp",
|
||||
"hanzo-dev",
|
||||
"hanzo",
|
||||
]
|
||||
|
||||
# Get repository root
|
||||
script_dir = Path(__file__).parent
|
||||
repo_root = script_dir.parent
|
||||
pkg_dir = repo_root / "pkg"
|
||||
|
||||
# Install required tools
|
||||
print_info("Installing build tools...")
|
||||
try:
|
||||
# Try with pip first
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--quiet",
|
||||
"--upgrade",
|
||||
"pip",
|
||||
"build",
|
||||
"twine",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
# Fall back to installing without pip upgrade
|
||||
try:
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "--quiet", "build", "twine"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
except Exception:
|
||||
print_warn("Could not install build tools. Make sure pip, build, and twine are available.")
|
||||
print_info("You can install them with: pip install build twine")
|
||||
|
||||
# Track results
|
||||
published = []
|
||||
failed = []
|
||||
skipped = []
|
||||
|
||||
# Check and publish each package
|
||||
for package_name in packages:
|
||||
package_dir = pkg_dir / package_name
|
||||
|
||||
if not package_dir.exists():
|
||||
print_warn(f"Package directory {package_dir} does not exist")
|
||||
skipped.append(package_name)
|
||||
continue
|
||||
|
||||
try:
|
||||
local_version = get_local_version(package_dir)
|
||||
pypi_version = get_pypi_version(package_name)
|
||||
|
||||
if is_newer_version(local_version, pypi_version):
|
||||
if check_and_publish_package(package_name, package_dir):
|
||||
published.append(f"{package_name} ({local_version})")
|
||||
else:
|
||||
failed.append(package_name)
|
||||
else:
|
||||
skipped.append(f"{package_name} (already at {local_version})")
|
||||
except Exception as e:
|
||||
print_error(f"Error processing {package_name}: {e}")
|
||||
failed.append(package_name)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
if published:
|
||||
print(f"\n{GREEN}✅ Published ({len(published)}):{NC}")
|
||||
for pkg in published:
|
||||
print(f" • {pkg}")
|
||||
|
||||
if skipped:
|
||||
print(f"\n{BLUE}⏭️ Skipped ({len(skipped)}):{NC}")
|
||||
for pkg in skipped:
|
||||
print(f" • {pkg}")
|
||||
|
||||
if failed:
|
||||
print(f"\n{RED}❌ Failed ({len(failed)}):{NC}")
|
||||
for pkg in failed:
|
||||
print(f" • {pkg}")
|
||||
sys.exit(1)
|
||||
|
||||
# Set GitHub Actions output if running in CI
|
||||
if os.environ.get("GITHUB_ACTIONS"):
|
||||
if published:
|
||||
print(
|
||||
f"::notice title=Published Packages::Published {len(published)} packages to PyPI: {', '.join(published)}"
|
||||
)
|
||||
else:
|
||||
print("::notice title=No Updates::All packages are up to date on PyPI")
|
||||
|
||||
print(f"\n{GREEN}✨ All packages processed successfully!{NC}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,140 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Comprehensive script to publish all Python packages to PyPI
|
||||
# Usage: ./bin/publish-all-packages.sh [package-name]
|
||||
# If no package name is provided, all packages will be published
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if PYPI_TOKEN is set
|
||||
if [ -z "$PYPI_TOKEN" ]; then
|
||||
print_error "PYPI_TOKEN environment variable is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the package to publish (if specified)
|
||||
SPECIFIC_PACKAGE=$1
|
||||
|
||||
# List of all packages in order of dependency
|
||||
PACKAGES=(
|
||||
"hanzo-network"
|
||||
"hanzo-memory"
|
||||
"hanzo-agents"
|
||||
"hanzo-aci"
|
||||
"hanzo-mcp"
|
||||
"hanzo-dev"
|
||||
"hanzo"
|
||||
)
|
||||
|
||||
# Function to get package version from pyproject.toml
|
||||
get_version() {
|
||||
local package=$1
|
||||
local pyproject="pkg/$package/pyproject.toml"
|
||||
if [ -f "$pyproject" ]; then
|
||||
grep "^version = " "$pyproject" | sed 's/version = "\(.*\)"/\1/'
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check if package exists on PyPI
|
||||
check_pypi() {
|
||||
local package=$1
|
||||
local version=$2
|
||||
curl -s "https://pypi.org/pypi/$package/$version/json" > /dev/null 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Function to build and publish a package
|
||||
publish_package() {
|
||||
local package=$1
|
||||
local package_dir="pkg/$package"
|
||||
|
||||
if [ ! -d "$package_dir" ]; then
|
||||
print_error "Package directory $package_dir does not exist"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_info "Processing package: $package"
|
||||
|
||||
# Get version
|
||||
local version=$(get_version "$package")
|
||||
print_info "Package version: $version"
|
||||
|
||||
# Check if already published
|
||||
if check_pypi "$package" "$version"; then
|
||||
print_warn "Package $package version $version already exists on PyPI, skipping..."
|
||||
return 0
|
||||
fi
|
||||
|
||||
cd "$package_dir"
|
||||
|
||||
# Clean previous builds
|
||||
print_info "Cleaning previous builds..."
|
||||
rm -rf dist/ build/ *.egg-info
|
||||
|
||||
# Build the package
|
||||
print_info "Building package..."
|
||||
python -m build
|
||||
|
||||
# Upload to PyPI
|
||||
print_info "Uploading to PyPI..."
|
||||
python -m twine upload dist/* --skip-existing
|
||||
|
||||
print_info "✅ Successfully published $package version $version"
|
||||
|
||||
cd - > /dev/null
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
print_info "Starting package publication process..."
|
||||
|
||||
# Install required tools
|
||||
print_info "Installing build tools..."
|
||||
python -m pip install --quiet --upgrade pip build twine
|
||||
|
||||
# If specific package is provided, publish only that
|
||||
if [ -n "$SPECIFIC_PACKAGE" ]; then
|
||||
if [[ " ${PACKAGES[@]} " =~ " ${SPECIFIC_PACKAGE} " ]]; then
|
||||
publish_package "$SPECIFIC_PACKAGE"
|
||||
else
|
||||
print_error "Unknown package: $SPECIFIC_PACKAGE"
|
||||
print_info "Available packages: ${PACKAGES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Publish all packages
|
||||
print_info "Publishing all packages..."
|
||||
for package in "${PACKAGES[@]}"; do
|
||||
publish_package "$package" || {
|
||||
print_error "Failed to publish $package"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
fi
|
||||
|
||||
print_info "🎉 All packages published successfully!"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eux
|
||||
mkdir -p dist
|
||||
rye build --clean
|
||||
rye publish --yes --token=$PYPI_TOKEN
|
||||
@@ -151,7 +151,6 @@ include = [
|
||||
"/*.md",
|
||||
"/mypy.ini",
|
||||
"/noxfile.py",
|
||||
"bin/*",
|
||||
"examples/*",
|
||||
"pkg/hanzoai/*",
|
||||
"tests/*",
|
||||
@@ -542,7 +541,6 @@ hanzo-tools-s3 = { workspace = true }
|
||||
|
||||
[tool.ruff.lint.extend-per-file-ignores]
|
||||
# Allow print statements in scripts and examples
|
||||
"bin/**.py" = ["T201", "T203"]
|
||||
"scripts/**.py" = ["T201", "T203", "E722"]
|
||||
"tests/**.py" = ["T201", "T203", "F401", "F841"]
|
||||
"examples/**.py" = ["T201", "T203", "F841"]
|
||||
|
||||
Reference in New Issue
Block a user