diff --git a/codex-rs/scripts/create_github_release b/codex-rs/scripts/create_github_release index 59a2adcd..08f0b11f 100755 --- a/codex-rs/scripts/create_github_release +++ b/codex-rs/scripts/create_github_release @@ -1,11 +1,16 @@ #!/usr/bin/env python3 import argparse +import base64 +import json import re import subprocess import sys -import tempfile -from pathlib import Path + + +REPO = "openai/codex" +BRANCH_REF = "heads/main" +CARGO_TOML_PATH = "codex-rs/Cargo.toml" def parse_args(argv: list[str]) -> argparse.Namespace: @@ -20,85 +25,183 @@ def parse_args(argv: list[str]) -> argparse.Namespace: def main(argv: list[str]) -> int: args = parse_args(argv) try: - with tempfile.TemporaryDirectory() as temp_dir: - repo_dir = Path(temp_dir) / "codex" - clone_repository(repo_dir) - branch = current_branch(repo_dir) - create_release(args.version, branch, repo_dir) + 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, args.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(args.version, tree_sha, base_commit) + print(f"Commit SHA: {commit_sha}") + print("Creating tag...") + tag_sha = create_tag(args.version, commit_sha) + print(f"Tag SHA: {tag_sha}") + print("Creating tag ref...") + create_tag_ref(args.version, tag_sha) + print("Done.") except ReleaseError as error: print(f"ERROR: {error}", file=sys.stderr) return 1 return 0 -def current_branch(repo_dir: Path) -> str: - result = run_git( - repo_dir, - ["symbolic-ref", "--short", "-q", "HEAD"], - capture_output=True, - check=False, - ) - branch = result.stdout.strip() - if result.returncode != 0 or not branch: - raise ReleaseError("Could not determine the current branch (detached HEAD?).") - return branch - - -def update_version(version: str, cargo_toml: Path) -> None: - content = cargo_toml.read_text(encoding="utf-8") - new_content, matches = re.subn( - r'^version = "[^"]+"', f'version = "{version}"', content, count=1, flags=re.MULTILINE - ) - if matches != 1: - raise ReleaseError("Unable to update version in Cargo.toml.") - cargo_toml.write_text(new_content, encoding="utf-8") - - -def create_release(version: str, branch: str, repo_dir: Path) -> None: - tag = f"rust-v{version}" - run_git(repo_dir, ["checkout", "-b", tag]) - try: - update_version(version, repo_dir / "codex-rs" / "Cargo.toml") - run_git(repo_dir, ["add", "codex-rs/Cargo.toml"]) - run_git(repo_dir, ["commit", "-m", f"Release {version}"]) - run_git(repo_dir, ["tag", "-a", tag, "-m", f"Release {version}"]) - run_git(repo_dir, ["push", "origin", f"refs/tags/{tag}"]) - finally: - run_git(repo_dir, ["checkout", branch]) - - -def clone_repository(destination: Path) -> None: - result = subprocess.run( - ["gh", "repo", "clone", "openai/codex", str(destination), "--", "--depth", "1"], - text=True, - ) - if result.returncode != 0: - raise ReleaseError("Failed to clone openai/codex using gh.") - - 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 run_git( - repo_dir: Path, - args: list[str], - *, - capture_output: bool = False, - check: bool = True, -) -> subprocess.CompletedProcess: - result = subprocess.run( - ["git", *args], - cwd=repo_dir, - text=True, - capture_output=capture_output, + +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}, ) - if check and result.returncode != 0: - stderr = result.stderr.strip() if result.stderr else "" - stdout = result.stdout.strip() if result.stdout else "" - message = stderr if stderr else stdout - raise ReleaseError(message or f"git {' '.join(args)} failed") - return result if __name__ == "__main__":