Files
dev/hanzo-dev/scripts/create_github_release
T
zeekay dae0dcad42 feat: rename code-rs to hanzo-dev and prepare for publication
🎯 Major directory restructure and publication preparation:

**Directory Rename:**
- Renamed code-rs/ → hanzo-dev/ for better branding alignment
- Updated all build scripts, GitHub workflows, and documentation
- Fixed all path references across the codebase

**Publication Setup:**
- Updated workspace version to 0.6.0 (production ready)
- Configured main CLI crate as 'hanzo-dev' (available on crates.io)
- Set publish = true for core crates (hanzo-core, hanzo-tui)
- Added proper package metadata (description, license, repository, etc.)
- Maintained internal crate names to minimize dependency changes

**Updated Components:**
- package.json build scripts now reference hanzo-dev/
- GitHub Actions workflows updated for new directory structure
- build-fast.sh and related scripts updated
- Documentation and README references updated
- All Cargo.toml manifests updated with publication metadata

**Publishing Strategy:**
- Main binary: hanzo-dev (new, available for publication)
- Core library: hanzo-core (existing name, ready to publish)
- TUI library: hanzo-tui (existing name, ready to publish)
- Avoids conflict with existing Hanzo AI infrastructure crates

This prepares the codebase for official publication to crates.io while
maintaining full compatibility with the existing build system and
providing clear branding as 'Hanzo Dev' CLI tool.
2026-01-14 16:49:45 -08:00

314 lines
9.5 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import base64
import json
import re
import subprocess
import sys
REPO = "openai/codex"
BRANCH_REF = "heads/main"
CARGO_TOML_PATH = "code-rs/Cargo.toml"
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Publish a tagged Codex release.")
parser.add_argument(
"-n",
"--dry-run",
action="store_true",
help="Print the version that would be used and exit before making changes.",
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--publish-alpha",
action="store_true",
help="Publish the next alpha release for the upcoming minor version.",
)
group.add_argument(
"--publish-release",
action="store_true",
help="Publish the next stable release by bumping the minor version.",
)
parser.add_argument(
"--emergency-version-override",
help="Publish a specific version because tag was created for the previous release but it never succeeded. Value should be semver, e.g., `0.43.0-alpha.9`.",
)
args = parser.parse_args(argv[1:])
if not (
args.publish_alpha
or args.publish_release
or args.emergency_version_override
):
parser.error(
"Must specify --publish-alpha, --publish-release, or --emergency-version-override."
)
return args
def main(argv: list[str]) -> int:
args = parse_args(argv)
try:
if args.emergency_version_override:
version = args.emergency_version_override
else:
version = determine_version(args)
print(f"Publishing version {version}")
if args.dry_run:
return 0
print("Fetching branch head...")
base_commit = get_branch_head()
print(f"Base commit: {base_commit}")
print("Fetching commit tree...")
base_tree = get_commit_tree(base_commit)
print(f"Base tree: {base_tree}")
print("Fetching Cargo.toml...")
current_contents = fetch_file_contents(base_commit)
print("Updating version...")
updated_contents = replace_version(current_contents, version)
print("Creating blob...")
blob_sha = create_blob(updated_contents)
print(f"Blob SHA: {blob_sha}")
print("Creating tree...")
tree_sha = create_tree(base_tree, blob_sha)
print(f"Tree SHA: {tree_sha}")
print("Creating commit...")
commit_sha = create_commit(version, tree_sha, base_commit)
print(f"Commit SHA: {commit_sha}")
print("Creating tag...")
tag_sha = create_tag(version, commit_sha)
print(f"Tag SHA: {tag_sha}")
print("Creating tag ref...")
create_tag_ref(version, tag_sha)
print("Done.")
except ReleaseError as error:
print(f"ERROR: {error}", file=sys.stderr)
return 1
return 0
class ReleaseError(RuntimeError):
pass
def run_gh_api(endpoint: str, *, method: str = "GET", payload: dict | None = None) -> dict:
print(f"Running gh api {method} {endpoint}")
command = [
"gh",
"api",
endpoint,
"--method",
method,
"-H",
"Accept: application/vnd.github+json",
]
json_payload = None
if payload is not None:
json_payload = json.dumps(payload)
print(f"Payload: {json_payload}")
command.extend(["-H", "Content-Type: application/json", "--input", "-"])
result = subprocess.run(command, text=True, capture_output=True, input=json_payload)
if result.returncode != 0:
message = result.stderr.strip() or result.stdout.strip() or "gh api call failed"
raise ReleaseError(message)
try:
return json.loads(result.stdout or "{}")
except json.JSONDecodeError as error:
raise ReleaseError("Failed to parse response from gh api.") from error
def get_branch_head() -> str:
response = run_gh_api(f"/repos/{REPO}/git/refs/{BRANCH_REF}")
try:
return response["object"]["sha"]
except KeyError as error:
raise ReleaseError("Unable to determine branch head.") from error
def get_commit_tree(commit_sha: str) -> str:
response = run_gh_api(f"/repos/{REPO}/git/commits/{commit_sha}")
try:
return response["tree"]["sha"]
except KeyError as error:
raise ReleaseError("Commit response missing tree SHA.") from error
def fetch_file_contents(ref_sha: str) -> str:
response = run_gh_api(f"/repos/{REPO}/contents/{CARGO_TOML_PATH}?ref={ref_sha}")
try:
encoded_content = response["content"].replace("\n", "")
encoding = response.get("encoding", "")
except KeyError as error:
raise ReleaseError("Failed to fetch Cargo.toml contents.") from error
if encoding != "base64":
raise ReleaseError(f"Unexpected Cargo.toml encoding: {encoding}")
try:
return base64.b64decode(encoded_content).decode("utf-8")
except (ValueError, UnicodeDecodeError) as error:
raise ReleaseError("Failed to decode Cargo.toml contents.") from error
def replace_version(contents: str, version: str) -> str:
updated, matches = re.subn(
r'^version = "[^"]+"', f'version = "{version}"', contents, count=1, flags=re.MULTILINE
)
if matches != 1:
raise ReleaseError("Unable to update version in Cargo.toml.")
return updated
def create_blob(content: str) -> str:
response = run_gh_api(
f"/repos/{REPO}/git/blobs",
method="POST",
payload={"content": content, "encoding": "utf-8"},
)
try:
return response["sha"]
except KeyError as error:
raise ReleaseError("Blob creation response missing SHA.") from error
def create_tree(base_tree_sha: str, blob_sha: str) -> str:
response = run_gh_api(
f"/repos/{REPO}/git/trees",
method="POST",
payload={
"base_tree": base_tree_sha,
"tree": [
{
"path": CARGO_TOML_PATH,
"mode": "100644",
"type": "blob",
"sha": blob_sha,
}
],
},
)
try:
return response["sha"]
except KeyError as error:
raise ReleaseError("Tree creation response missing SHA.") from error
def create_commit(version: str, tree_sha: str, parent_sha: str) -> str:
response = run_gh_api(
f"/repos/{REPO}/git/commits",
method="POST",
payload={
"message": f"Release {version}",
"tree": tree_sha,
"parents": [parent_sha],
},
)
try:
return response["sha"]
except KeyError as error:
raise ReleaseError("Commit creation response missing SHA.") from error
def create_tag(version: str, commit_sha: str) -> str:
tag_name = f"rust-v{version}"
response = run_gh_api(
f"/repos/{REPO}/git/tags",
method="POST",
payload={
"tag": tag_name,
"message": f"Release {version}",
"object": commit_sha,
"type": "commit",
},
)
try:
return response["sha"]
except KeyError as error:
raise ReleaseError("Tag creation response missing SHA.") from error
def create_tag_ref(version: str, tag_sha: str) -> None:
tag_ref = f"refs/tags/rust-v{version}"
run_gh_api(
f"/repos/{REPO}/git/refs",
method="POST",
payload={"ref": tag_ref, "sha": tag_sha},
)
def determine_version(args: argparse.Namespace) -> str:
latest_version = get_latest_release_version()
major, minor, patch = parse_semver(latest_version)
next_minor_version = format_version(major, minor + 1, patch)
if args.publish_release:
return next_minor_version
alpha_prefix = f"{next_minor_version}-alpha."
releases = list_releases()
highest_alpha = 0
found_alpha = False
for release in releases:
tag = release.get("tag_name", "")
candidate = strip_tag_prefix(tag)
if candidate and candidate.startswith(alpha_prefix):
suffix = candidate[len(alpha_prefix) :]
try:
alpha_number = int(suffix)
except ValueError:
continue
highest_alpha = max(highest_alpha, alpha_number)
found_alpha = True
if found_alpha:
return f"{alpha_prefix}{highest_alpha + 1}"
return f"{alpha_prefix}1"
def get_latest_release_version() -> str:
response = run_gh_api(f"/repos/{REPO}/releases/latest")
tag = response.get("tag_name")
version = strip_tag_prefix(tag)
if not version:
raise ReleaseError("Latest release tag has unexpected format.")
return version
def list_releases() -> list[dict]:
response = run_gh_api(f"/repos/{REPO}/releases?per_page=100")
if not isinstance(response, list):
raise ReleaseError("Unexpected response when listing releases.")
return response
def strip_tag_prefix(tag: str | None) -> str | None:
if not tag:
return None
prefix = "rust-v"
if not tag.startswith(prefix):
return None
return tag[len(prefix) :]
def parse_semver(version: str) -> tuple[int, int, int]:
parts = version.split(".")
if len(parts) != 3:
raise ReleaseError(f"Unexpected version format: {version}")
try:
return int(parts[0]), int(parts[1]), int(parts[2])
except ValueError as error:
raise ReleaseError(f"Version components must be integers: {version}") from error
def format_version(major: int, minor: int, patch: int) -> str:
return f"{major}.{minor}.{patch}"
if __name__ == "__main__":
sys.exit(main(sys.argv))