chore: use gh instead of git to do work to avoid overhead of a local clone (#3230)

The advantage of this implementation is that it can be run from
"anywhere" so long as the user has `gh` installed with the appropriate
credentials to write to the `openai/codex` repo. Unlike the previous
implementation, it avoids the overhead of creating a local clone of the
repo.

Ran:

```
./codex-rs/scripts/create_github_release 0.31.0-alpha.2
```

which appeared to work as expected:

- workflow https://github.com/openai/codex/actions/runs/17508564352
- release
https://github.com/openai/codex/releases/tag/rust-v0.31.0-alpha.2

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/3230).
* #3231
* __->__ #3230
* #3228
* #3226
This commit is contained in:
Michael Bolin
2025-09-05 21:58:42 -07:00
committed by GitHub
parent 066c6cce02
commit b1d5f7c0bd

View File

@@ -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__":