1
1
import enum
2
+ import logging
2
3
import os
3
4
import re
4
5
import subprocess
@@ -55,6 +56,7 @@ class PullDetails(typing.TypedDict):
55
56
56
57
def check_env ():
57
58
"""Check if the required environment variables are set."""
59
+ logging .debug ("Checking environment variables..." )
58
60
required_vars = [
59
61
"GITHUB_TOKEN" ,
60
62
"TARGET_REPO" ,
@@ -67,9 +69,12 @@ def check_env():
67
69
required_vars .append ("OPENAI_API_KEY" )
68
70
missing_vars = [var for var in required_vars if not os .getenv (var )]
69
71
if missing_vars :
72
+ logging .error ("Missing required environment variables: %s" , ", " .join (missing_vars ))
70
73
raise EnvironmentError (f"Missing required environment variables: { ', ' .join (missing_vars )} " )
71
74
72
75
76
+ logging .basicConfig (level = os .getenv ("LOG_LEVEL" , "INFO" ).upper ())
77
+
73
78
# Environment variables
74
79
TRANSLATE_CHANGES = os .getenv ("TRANSLATE_CHANGES" , "False" ).lower () in ("true" , "yes" , "1" )
75
80
CHANGELOG_AUTHOR = os .getenv ("CHANGELOG_AUTHOR" , "" )
@@ -86,73 +91,82 @@ def check_env():
86
91
87
92
def run_command (command : str ) -> str :
88
93
"""Run a shell command and return its output."""
94
+ logging .debug ("Running command: %s" , command )
89
95
try :
90
96
result : CompletedProcess [str ] = subprocess .run (command , shell = True , capture_output = True , text = True )
91
97
result .check_returncode ()
98
+ logging .debug ("Command output: %s" , result .stdout .strip ())
92
99
return result .stdout .strip ()
93
100
except subprocess .CalledProcessError as e :
94
- print (f"Error executing command: { command } \n Exit code: { e .returncode } \n Output: { e .output } \n Error: { e .stderr } " )
101
+ logging .error ("Error executing command: %s" , command )
102
+ logging .error ("Exit code: %d, Output: %s, Error: %s" , e .returncode , e .output , e .stderr )
95
103
raise
96
104
97
105
98
106
def setup_repo ():
99
107
"""Clone the repository and set up the upstream remote."""
100
- print ( f "Cloning repository: { TARGET_REPO } " )
108
+ logging . info ( "Cloning repository: %s" , TARGET_REPO )
101
109
run_command (f"git clone https://x-access-token:{ GITHUB_TOKEN } @github.com/{ TARGET_REPO } .git repo" )
102
110
os .chdir ("repo" )
103
111
run_command (f"git remote add upstream https://x-access-token:{ GITHUB_TOKEN } @github.com/{ UPSTREAM_REPO } .git" )
104
- print ( run_command (f"git remote -v" ))
112
+ logging . info ( "Git remotes set up: %s" , run_command (f"git remote -v" ))
105
113
106
114
107
115
def update_merge_branch ():
108
116
"""Update the merge branch with the latest changes from upstream."""
109
- print ( f "Fetching branch { UPSTREAM_BRANCH } from upstream..." )
117
+ logging . info ( "Fetching branch %s from upstream..." , UPSTREAM_BRANCH )
110
118
run_command (f"git fetch upstream { UPSTREAM_BRANCH } " )
111
119
run_command (f"git fetch origin" )
112
120
all_branches : list [str ] = run_command ("git branch -a" ).split ()
121
+ logging .debug ("Fetched branches: %s" , all_branches )
113
122
114
123
if f"remotes/origin/{ MERGE_BRANCH } " not in all_branches :
115
- print ( f "Branch '{ MERGE_BRANCH } ' does not exist. Creating it from upstream/{ UPSTREAM_BRANCH } ..." )
124
+ logging . info ( "Branch '%s ' does not exist. Creating it from upstream/%s ..." , MERGE_BRANCH , UPSTREAM_BRANCH )
116
125
run_command (f"git checkout -b { MERGE_BRANCH } upstream/{ UPSTREAM_BRANCH } " )
117
126
run_command (f"git push -u origin { MERGE_BRANCH } " )
118
127
return
119
128
120
- print ( f "Resetting { MERGE_BRANCH } onto upstream/{ UPSTREAM_BRANCH } ..." )
129
+ logging . info ( "Resetting '%s' onto upstream/%s ..." , MERGE_BRANCH , UPSTREAM_BRANCH )
121
130
run_command (f"git checkout { MERGE_BRANCH } " )
122
131
run_command (f"git reset --hard upstream/{ UPSTREAM_BRANCH } " )
123
-
124
- print ("Pushing changes to origin..." )
132
+ logging .info ("Pushing changes to origin..." )
125
133
run_command (f"git push origin { MERGE_BRANCH } --force" )
126
134
127
135
128
136
def detect_commits () -> list [str ]:
129
137
"""Detect commits from upstream not yet in downstream."""
130
- print ("Detecting new commits from upstream..." )
138
+ logging . info ("Detecting new commits from upstream..." )
131
139
commit_log : list [str ] = run_command (f"git log { TARGET_BRANCH } ..{ MERGE_BRANCH } --pretty=format:'%h %s'" ).split ("\n " )
132
140
commit_log .reverse ()
141
+ logging .debug ("Detected commits: %s" , commit_log )
133
142
return commit_log
134
143
135
144
136
145
def fetch_pull (github : Github , pull_number : int ) -> PullRequest | None :
137
146
"""Fetch the pull request from GitHub."""
147
+ logging .debug ("Fetching pull request #%d" , pull_number )
138
148
upstream_repo : Repository = github .get_repo (UPSTREAM_REPO )
139
149
140
150
max_retries = 3
141
151
for attempt in range (max_retries ):
142
152
try :
143
- return upstream_repo .get_pull (int (pull_number ))
153
+ pull = upstream_repo .get_pull (int (pull_number ))
154
+ logging .debug ("Successfully fetched PR #%d: %s" , pull_number , pull .title )
155
+ return pull
144
156
except Exception as e :
145
- print ( f "Error fetching PR #{ pull_number } : { e } " )
157
+ logging . error ( "Error fetching PR #%d: %s" , pull_number , e )
146
158
if attempt + 1 < max_retries :
159
+ logging .warning ("Retrying fetch for PR #%d (attempt %d/%d)" , pull_number , attempt + 1 , max_retries )
147
160
time .sleep (2 )
148
161
else :
162
+ logging .error ("Failed to fetch PR #%d after %d attempts" , pull_number , max_retries )
149
163
return None
150
164
151
165
152
166
def build_details (github : Github , commit_log : list [str ],
153
167
translate : typing .Optional [typing .Callable [[typing .Dict [int , list [Change ]]], None ]]) -> PullDetails :
154
168
"""Generate data from parsed commits."""
155
- print ("Building details..." )
169
+ logging . info ("Building pull request details from commit log ..." )
156
170
pull_number_pattern : Pattern [str ] = re .compile ("#(?P<id>\\ d+)" )
157
171
details = PullDetails (
158
172
changelog = {},
@@ -168,55 +182,63 @@ def build_details(github: Github, commit_log: list[str],
168
182
for commit in commit_log :
169
183
match = re .search (pull_number_pattern , commit )
170
184
if not match :
171
- print ( f "Skipping { commit } " )
185
+ logging . debug ( "Skipping commit without pull request reference: %s" , commit )
172
186
continue
173
187
174
188
pull_number = int (match .group ("id" ))
175
189
176
190
if pull_number in pull_cache :
177
- print (
178
- f"WARNING: pull duplicate found.\n "
179
- f"1: { pull_cache [pull_number ]} \n "
180
- f"2: { commit } "
191
+ logging .warning (
192
+ "Duplicate pull request detected for #%d\n "
193
+ "Existing: %s\n "
194
+ "New: %s" ,
195
+ pull_number , pull_cache [pull_number ], commit
181
196
)
182
- print (f"Skipping { commit } " )
183
197
continue
184
198
185
199
pull_cache [pull_number ] = commit
186
200
futures [executor .submit (fetch_pull , github , pull_number )] = pull_number
187
201
188
202
for future in as_completed (futures ):
189
203
pull_number = futures [future ]
190
- pull : PullRequest | None = future .result ()
191
-
192
- if not pull :
193
- print (f"Pull { pull_number } was not fetched. Skipping." )
194
- continue
195
-
196
- process_pull (details , pull )
204
+ try :
205
+ pull : PullRequest | None = future .result ()
206
+ if not pull :
207
+ logging .warning ("Failed to fetch pull request #%d. Skipping." , pull_number )
208
+ continue
209
+ process_pull (details , pull )
210
+ except Exception as e :
211
+ logging .error ("Error processing pull request #%d: %s" , pull_number , e )
197
212
198
213
if translate :
199
214
translate (details ["changelog" ])
200
215
216
+ logging .info ("Details building complete. Processed %d pull requests." , len (details ["merge_order" ]))
201
217
return details
202
218
203
219
204
220
def process_pull (details : PullDetails , pull : PullRequest ):
205
221
"""Handle fetched pull request data during details building."""
222
+ logging .debug ("Processing pull request #%d: %s" , pull .number , pull .title )
206
223
pull_number : int = pull .number
207
224
labels : list [str ] = [label .name for label in pull .get_labels ()]
208
225
pull_changes : list [Change ] = []
226
+
209
227
try :
210
228
for label in labels :
211
229
if label == UpstreamLabel .CONFIG_CHANGE .value :
212
230
details ["config_changes" ].append (pull )
231
+ logging .debug ("Detected CONFIG_CHANGE label for PR #%d" , pull_number )
213
232
elif label == UpstreamLabel .SQL_CHANGE .value :
214
233
details ["sql_changes" ].append (pull )
234
+ logging .debug ("Detected SQL_CHANGE label for PR #%d" , pull_number )
215
235
elif label == UpstreamLabel .WIKI_CHANGE .value :
216
236
details ["wiki_changes" ].append (pull )
237
+ logging .debug ("Detected WIKI_CHANGE label for PR #%d" , pull_number )
217
238
218
239
parsed = changelog_utils .parse_changelog (pull .body )
219
240
if parsed and parsed ["changes" ]:
241
+ logging .debug ("Parsed changelog for PR #%d: %s" , pull_number , parsed ["changes" ])
220
242
for change in parsed ["changes" ]:
221
243
pull_changes .append (Change (
222
244
tag = change ["tag" ],
@@ -226,28 +248,34 @@ def process_pull(details: PullDetails, pull: PullRequest):
226
248
227
249
if pull_changes :
228
250
details ["changelog" ][pull_number ] = pull_changes
251
+ logging .debug ("Added %d changes for PR #%d" , len (pull_changes ), pull_number )
229
252
except Exception as e :
230
- print (
231
- f"An error occurred while processing { pull .html_url } \n "
232
- f"Body: { pull .body } "
253
+ logging .error (
254
+ "An error occurred while processing PR #%d: %s\n "
255
+ "Body: %s" ,
256
+ pull .number , e , pull .body
233
257
)
234
- raise e
258
+ raise
235
259
236
260
237
261
def translate_changelog (changelog : typing .Dict [int , list [Change ]]):
238
262
"""Translate changelog using OpenAI API."""
239
- print ("Translating changelog..." )
263
+ logging . info ("Translating changelog..." )
240
264
if not changelog :
265
+ logging .warning ("No changelog entries to translate." )
241
266
return
242
267
243
268
changes : list [Change ] = [change for changes in changelog .values () for change in changes ]
244
269
if not changes :
270
+ logging .warning ("No changes found in the changelog to translate." )
245
271
return
246
272
273
+ logging .debug ("Preparing text for translation: %d changes" , len (changes ))
274
+ text = "\n " .join ([change ["message" ] for change in changes ])
275
+ logging .debug (text )
247
276
script_dir = Path (__file__ ).resolve ().parent
248
277
with open (script_dir .joinpath ("translation_context.txt" ), encoding = "utf-8" ) as f :
249
278
context = "\n " .join (f .readlines ()).strip ()
250
- text = "\n " .join ([change ["message" ] for change in changes ])
251
279
252
280
client = OpenAI (
253
281
base_url = "https://models.inference.ai.azure.com" ,
@@ -265,12 +293,13 @@ def translate_changelog(changelog: typing.Dict[int, list[Change]]):
265
293
translated_text : str | None = response .choices [0 ].message .content
266
294
267
295
if not translated_text :
268
- print ( "WARNING: changelog translation failed!" )
269
- print ( response )
296
+ logging . warning ( "Changelog translation failed!" )
297
+ logging . debug ( "Translation API response: %s" , response )
270
298
return
271
299
272
300
for change , translated_message in zip (changes , translated_text .split ("\n " ), strict = True ):
273
301
change ["translated_message" ] = translated_message
302
+ logging .debug ("Translated: %s -> %s" , change ["message" ], translated_message )
274
303
275
304
276
305
def silence_pull_url (pull_url : str ) -> str :
@@ -280,35 +309,44 @@ def silence_pull_url(pull_url: str) -> str:
280
309
281
310
def prepare_pull_body (details : PullDetails ) -> str :
282
311
"""Build new pull request body from the generated changelog."""
312
+ logging .info ("Preparing pull request body..." )
283
313
pull_body : str = (
284
314
f"This pull request merges upstream/{ UPSTREAM_BRANCH } . "
285
315
f"Resolve possible conflicts manually and make sure all the changes are applied correctly.\n "
286
316
)
287
317
288
318
if not details :
319
+ logging .warning ("No pull details provided. Using default body." )
289
320
return pull_body
290
321
291
322
label_to_pulls : dict [UpstreamLabel , list [PullRequest ]] = {
292
323
UpstreamLabel .CONFIG_CHANGE : details ["config_changes" ],
293
324
UpstreamLabel .SQL_CHANGE : details ["sql_changes" ],
294
325
UpstreamLabel .WIKI_CHANGE : details ["wiki_changes" ]
295
326
}
327
+
296
328
for label , fetched_pulls in label_to_pulls .items ():
297
329
if not fetched_pulls :
330
+ logging .debug ("No pulls found for label '%s'" , label .value )
298
331
continue
299
332
300
333
pull_body += (
301
334
f"\n > [!{ LABEL_BLOCK_STYLE [label ]} ]\n "
302
335
f"> { label .value } :\n "
303
336
)
304
337
for fetched_pull in fetched_pulls :
305
- pull_body += f"> { silence_pull_url (fetched_pull .html_url )} \n "
338
+ silenced_url = silence_pull_url (fetched_pull .html_url )
339
+ logging .debug ("Adding pull #%d to body: %s" , fetched_pull .number , silenced_url )
340
+ pull_body += f"> { silenced_url } \n "
306
341
307
342
if not details ["changelog" ]:
343
+ logging .info ("No changelog entries found." )
308
344
return pull_body
309
345
346
+ logging .info ("Adding changelog entries to pull request body." )
310
347
pull_body += f"\n ## Changelog\n "
311
348
pull_body += f":cl: { CHANGELOG_AUTHOR } \n " if CHANGELOG_AUTHOR else ":cl:\n "
349
+
312
350
for pull_number in details ["merge_order" ]:
313
351
if pull_number not in details ["changelog" ]:
314
352
continue
@@ -319,44 +357,50 @@ def prepare_pull_body(details: PullDetails) -> str:
319
357
pull_url : str = silence_pull_url (change ["pull" ].html_url )
320
358
if translated_message :
321
359
pull_body += f"{ tag } : { translated_message } <!-- { message } ({ pull_url } ) -->\n "
360
+ logging .debug ("Added translated change for PR #%d: %s" , pull_number , translated_message )
322
361
else :
323
362
pull_body += f"{ tag } : { message } <!-- ({ pull_url } ) -->\n "
363
+ logging .debug ("Added original change for PR #%d: %s" , pull_number , message )
324
364
pull_body += "/:cl:\n "
325
365
366
+ logging .info ("Pull request body prepared successfully." )
326
367
return pull_body
327
368
328
369
329
370
def create_pr (repo : Repository , details : PullDetails ):
330
371
"""Create a pull request with the processed changelog."""
372
+ logging .info ("Creating pull request..." )
331
373
pull_body : str = prepare_pull_body (details )
332
- print ("Creating pull request..." )
333
-
334
- # Create the pull request
335
- pull : PullRequest = repo .create_pull (
336
- title = f"Merge Upstream { datetime .today ().strftime ('%d.%m.%Y' )} " ,
337
- body = pull_body ,
338
- head = MERGE_BRANCH ,
339
- base = TARGET_BRANCH
340
- )
341
374
342
- if details ["wiki_changes" ]:
343
- pull .add_to_labels (DownstreamLabel .WIKI_CHANGE )
375
+ try :
376
+ # Create the pull request
377
+ pull : PullRequest = repo .create_pull (
378
+ title = f"Merge Upstream { datetime .today ().strftime ('%d.%m.%Y' )} " ,
379
+ body = pull_body ,
380
+ head = MERGE_BRANCH ,
381
+ base = TARGET_BRANCH
382
+ )
383
+ logging .info ("Pull request created: %s" , pull .html_url )
344
384
345
- print ("Pull request created successfully." )
385
+ if details ["wiki_changes" ]:
386
+ pull .add_to_labels (DownstreamLabel .WIKI_CHANGE )
387
+ logging .debug ("Added WIKI_CHANGE label to pull request." )
388
+ except Exception as e :
389
+ logging .error ("Failed to create pull request: %s" , e )
390
+ raise
346
391
347
392
348
393
def check_pull_exists (target_repo : Repository , base : str , head : str ):
349
- """Check if the merge pull request already exist. In this case, fail the action ."""
350
- print ("Checking on existing pull request..." )
394
+ """Check if the merge pull request already exists ."""
395
+ logging . info ("Checking if pull request already exists between '%s' and '%s' ..." , base , head )
351
396
owner : str = target_repo .owner .login
352
397
base_strict = f"{ owner } :{ base } "
353
398
head_strict = f"{ owner } :{ head } "
354
399
existing_pulls : PaginatedList [PullRequest ] = target_repo .get_pulls (state = "open" , base = base_strict , head = head_strict )
355
- for pull in existing_pulls :
356
- print (f"Pull request already exists. { pull .html_url } " )
357
-
358
- if existing_pulls .totalCount :
400
+ if existing_pulls .totalCount > 0 :
401
+ logging .error ("Pull request already exists: %s" , ", " .join (pull .html_url for pull in existing_pulls ))
359
402
exit (1 )
403
+ logging .debug ("No existing pull requests found." )
360
404
361
405
if __name__ == "__main__" :
362
406
github = Github (GITHUB_TOKEN )
@@ -372,4 +416,4 @@ def check_pull_exists(target_repo: Repository, base: str, head: str):
372
416
details : PullDetails = build_details (github , commit_log , translate_changelog if TRANSLATE_CHANGES else None )
373
417
create_pr (target_repo , details )
374
418
else :
375
- print ( f "No changes detected from { UPSTREAM_REPO } / { UPSTREAM_BRANCH } . Skipping pull request creation." )
419
+ logging . info ( "No changes detected from %s/%s . Skipping pull request creation." , UPSTREAM_REPO , UPSTREAM_BRANCH )
0 commit comments