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
209 lines
6.3 KiB
Python
Executable File
209 lines
6.3 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 = "codex-rs/Cargo.toml"
|
|
|
|
|
|
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Create a tagged Codex release.")
|
|
parser.add_argument(
|
|
"version",
|
|
help="Version string used for Cargo.toml and the Git tag (e.g. 0.1.0-alpha.4).",
|
|
)
|
|
return parser.parse_args(argv[1:])
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
args = parse_args(argv)
|
|
try:
|
|
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
|
|
|
|
|
|
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},
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|