Skip to content

Commit b1d5f7c

Browse files
authored
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
1 parent 066c6cc commit b1d5f7c

File tree

1 file changed

+166
-63
lines changed

1 file changed

+166
-63
lines changed

codex-rs/scripts/create_github_release

Lines changed: 166 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
#!/usr/bin/env python3
22

33
import argparse
4+
import base64
5+
import json
46
import re
57
import subprocess
68
import sys
7-
import tempfile
8-
from pathlib import Path
9+
10+
11+
REPO = "openai/codex"
12+
BRANCH_REF = "heads/main"
13+
CARGO_TOML_PATH = "codex-rs/Cargo.toml"
914

1015

1116
def parse_args(argv: list[str]) -> argparse.Namespace:
@@ -20,85 +25,183 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
2025
def main(argv: list[str]) -> int:
2126
args = parse_args(argv)
2227
try:
23-
with tempfile.TemporaryDirectory() as temp_dir:
24-
repo_dir = Path(temp_dir) / "codex"
25-
clone_repository(repo_dir)
26-
branch = current_branch(repo_dir)
27-
create_release(args.version, branch, repo_dir)
28+
print("Fetching branch head...")
29+
base_commit = get_branch_head()
30+
print(f"Base commit: {base_commit}")
31+
print("Fetching commit tree...")
32+
base_tree = get_commit_tree(base_commit)
33+
print(f"Base tree: {base_tree}")
34+
print("Fetching Cargo.toml...")
35+
current_contents = fetch_file_contents(base_commit)
36+
print("Updating version...")
37+
updated_contents = replace_version(current_contents, args.version)
38+
print("Creating blob...")
39+
blob_sha = create_blob(updated_contents)
40+
print(f"Blob SHA: {blob_sha}")
41+
print("Creating tree...")
42+
tree_sha = create_tree(base_tree, blob_sha)
43+
print(f"Tree SHA: {tree_sha}")
44+
print("Creating commit...")
45+
commit_sha = create_commit(args.version, tree_sha, base_commit)
46+
print(f"Commit SHA: {commit_sha}")
47+
print("Creating tag...")
48+
tag_sha = create_tag(args.version, commit_sha)
49+
print(f"Tag SHA: {tag_sha}")
50+
print("Creating tag ref...")
51+
create_tag_ref(args.version, tag_sha)
52+
print("Done.")
2853
except ReleaseError as error:
2954
print(f"ERROR: {error}", file=sys.stderr)
3055
return 1
3156
return 0
3257

3358

34-
def current_branch(repo_dir: Path) -> str:
35-
result = run_git(
36-
repo_dir,
37-
["symbolic-ref", "--short", "-q", "HEAD"],
38-
capture_output=True,
39-
check=False,
40-
)
41-
branch = result.stdout.strip()
42-
if result.returncode != 0 or not branch:
43-
raise ReleaseError("Could not determine the current branch (detached HEAD?).")
44-
return branch
59+
class ReleaseError(RuntimeError):
60+
pass
61+
62+
def run_gh_api(endpoint: str, *, method: str = "GET", payload: dict | None = None) -> dict:
63+
print(f"Running gh api {method} {endpoint}")
64+
command = [
65+
"gh",
66+
"api",
67+
endpoint,
68+
"--method",
69+
method,
70+
"-H",
71+
"Accept: application/vnd.github+json",
72+
]
73+
json_payload = None
74+
if payload is not None:
75+
json_payload = json.dumps(payload)
76+
print(f"Payload: {json_payload}")
77+
command.extend(["-H", "Content-Type: application/json", "--input", "-"])
78+
result = subprocess.run(command, text=True, capture_output=True, input=json_payload)
79+
if result.returncode != 0:
80+
message = result.stderr.strip() or result.stdout.strip() or "gh api call failed"
81+
raise ReleaseError(message)
82+
try:
83+
return json.loads(result.stdout or "{}")
84+
except json.JSONDecodeError as error:
85+
raise ReleaseError("Failed to parse response from gh api.") from error
4586

4687

47-
def update_version(version: str, cargo_toml: Path) -> None:
48-
content = cargo_toml.read_text(encoding="utf-8")
49-
new_content, matches = re.subn(
50-
r'^version = "[^"]+"', f'version = "{version}"', content, count=1, flags=re.MULTILINE
88+
def get_branch_head() -> str:
89+
response = run_gh_api(f"/repos/{REPO}/git/refs/{BRANCH_REF}")
90+
try:
91+
return response["object"]["sha"]
92+
except KeyError as error:
93+
raise ReleaseError("Unable to determine branch head.") from error
94+
95+
96+
def get_commit_tree(commit_sha: str) -> str:
97+
response = run_gh_api(f"/repos/{REPO}/git/commits/{commit_sha}")
98+
try:
99+
return response["tree"]["sha"]
100+
except KeyError as error:
101+
raise ReleaseError("Commit response missing tree SHA.") from error
102+
103+
104+
def fetch_file_contents(ref_sha: str) -> str:
105+
response = run_gh_api(f"/repos/{REPO}/contents/{CARGO_TOML_PATH}?ref={ref_sha}")
106+
try:
107+
encoded_content = response["content"].replace("\n", "")
108+
encoding = response.get("encoding", "")
109+
except KeyError as error:
110+
raise ReleaseError("Failed to fetch Cargo.toml contents.") from error
111+
112+
if encoding != "base64":
113+
raise ReleaseError(f"Unexpected Cargo.toml encoding: {encoding}")
114+
115+
try:
116+
return base64.b64decode(encoded_content).decode("utf-8")
117+
except (ValueError, UnicodeDecodeError) as error:
118+
raise ReleaseError("Failed to decode Cargo.toml contents.") from error
119+
120+
121+
def replace_version(contents: str, version: str) -> str:
122+
updated, matches = re.subn(
123+
r'^version = "[^"]+"', f'version = "{version}"', contents, count=1, flags=re.MULTILINE
51124
)
52125
if matches != 1:
53126
raise ReleaseError("Unable to update version in Cargo.toml.")
54-
cargo_toml.write_text(new_content, encoding="utf-8")
127+
return updated
55128

56129

57-
def create_release(version: str, branch: str, repo_dir: Path) -> None:
58-
tag = f"rust-v{version}"
59-
run_git(repo_dir, ["checkout", "-b", tag])
130+
def create_blob(content: str) -> str:
131+
response = run_gh_api(
132+
f"/repos/{REPO}/git/blobs",
133+
method="POST",
134+
payload={"content": content, "encoding": "utf-8"},
135+
)
60136
try:
61-
update_version(version, repo_dir / "codex-rs" / "Cargo.toml")
62-
run_git(repo_dir, ["add", "codex-rs/Cargo.toml"])
63-
run_git(repo_dir, ["commit", "-m", f"Release {version}"])
64-
run_git(repo_dir, ["tag", "-a", tag, "-m", f"Release {version}"])
65-
run_git(repo_dir, ["push", "origin", f"refs/tags/{tag}"])
66-
finally:
67-
run_git(repo_dir, ["checkout", branch])
68-
69-
70-
def clone_repository(destination: Path) -> None:
71-
result = subprocess.run(
72-
["gh", "repo", "clone", "openai/codex", str(destination), "--", "--depth", "1"],
73-
text=True,
137+
return response["sha"]
138+
except KeyError as error:
139+
raise ReleaseError("Blob creation response missing SHA.") from error
140+
141+
142+
def create_tree(base_tree_sha: str, blob_sha: str) -> str:
143+
response = run_gh_api(
144+
f"/repos/{REPO}/git/trees",
145+
method="POST",
146+
payload={
147+
"base_tree": base_tree_sha,
148+
"tree": [
149+
{
150+
"path": CARGO_TOML_PATH,
151+
"mode": "100644",
152+
"type": "blob",
153+
"sha": blob_sha,
154+
}
155+
],
156+
},
74157
)
75-
if result.returncode != 0:
76-
raise ReleaseError("Failed to clone openai/codex using gh.")
77-
78-
79-
class ReleaseError(RuntimeError):
80-
pass
158+
try:
159+
return response["sha"]
160+
except KeyError as error:
161+
raise ReleaseError("Tree creation response missing SHA.") from error
162+
163+
164+
def create_commit(version: str, tree_sha: str, parent_sha: str) -> str:
165+
response = run_gh_api(
166+
f"/repos/{REPO}/git/commits",
167+
method="POST",
168+
payload={
169+
"message": f"Release {version}",
170+
"tree": tree_sha,
171+
"parents": [parent_sha],
172+
},
173+
)
174+
try:
175+
return response["sha"]
176+
except KeyError as error:
177+
raise ReleaseError("Commit creation response missing SHA.") from error
178+
179+
180+
def create_tag(version: str, commit_sha: str) -> str:
181+
tag_name = f"rust-v{version}"
182+
response = run_gh_api(
183+
f"/repos/{REPO}/git/tags",
184+
method="POST",
185+
payload={
186+
"tag": tag_name,
187+
"message": f"Release {version}",
188+
"object": commit_sha,
189+
"type": "commit",
190+
},
191+
)
192+
try:
193+
return response["sha"]
194+
except KeyError as error:
195+
raise ReleaseError("Tag creation response missing SHA.") from error
81196

82197

83-
def run_git(
84-
repo_dir: Path,
85-
args: list[str],
86-
*,
87-
capture_output: bool = False,
88-
check: bool = True,
89-
) -> subprocess.CompletedProcess:
90-
result = subprocess.run(
91-
["git", *args],
92-
cwd=repo_dir,
93-
text=True,
94-
capture_output=capture_output,
198+
def create_tag_ref(version: str, tag_sha: str) -> None:
199+
tag_ref = f"refs/tags/rust-v{version}"
200+
run_gh_api(
201+
f"/repos/{REPO}/git/refs",
202+
method="POST",
203+
payload={"ref": tag_ref, "sha": tag_sha},
95204
)
96-
if check and result.returncode != 0:
97-
stderr = result.stderr.strip() if result.stderr else ""
98-
stdout = result.stdout.strip() if result.stdout else ""
99-
message = stderr if stderr else stdout
100-
raise ReleaseError(message or f"git {' '.join(args)} failed")
101-
return result
102205

103206

104207
if __name__ == "__main__":

0 commit comments

Comments
 (0)