1
1
#!/usr/bin/env python3
2
2
3
3
import argparse
4
+ import base64
5
+ import json
4
6
import re
5
7
import subprocess
6
8
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"
9
14
10
15
11
16
def parse_args (argv : list [str ]) -> argparse .Namespace :
@@ -20,85 +25,183 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
20
25
def main (argv : list [str ]) -> int :
21
26
args = parse_args (argv )
22
27
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." )
28
53
except ReleaseError as error :
29
54
print (f"ERROR: { error } " , file = sys .stderr )
30
55
return 1
31
56
return 0
32
57
33
58
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
45
86
46
87
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
51
124
)
52
125
if matches != 1 :
53
126
raise ReleaseError ("Unable to update version in Cargo.toml." )
54
- cargo_toml . write_text ( new_content , encoding = "utf-8" )
127
+ return updated
55
128
56
129
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
+ )
60
136
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
+ },
74
157
)
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
81
196
82
197
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 },
95
204
)
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
102
205
103
206
104
207
if __name__ == "__main__" :
0 commit comments