22import argparse
33import io
44import json
5+ import logging
56import os
67import re
8+ import sys
79import tomllib
810from collections import defaultdict
911from datetime import datetime
1921from github .GithubException import BadCredentialsException
2022from github .PullRequest import PullRequest
2123
24+ log = logging .getLogger (__name__ )
25+
26+
27+ class ColorFormatter (logging .Formatter ):
28+ colors = {
29+ logging .CRITICAL : '\033 [31m' ,
30+ logging .ERROR : '\033 [31m' ,
31+ logging .WARNING : '\033 [33m' ,
32+ logging .DEBUG : '\033 [34m' ,
33+ }
34+
35+ def __init__ (self , use_color : bool ) -> None :
36+ super ().__init__ (fmt = '{message}' , style = '{' )
37+ self .use_color = use_color
38+
39+ def format (self , record : logging .LogRecord ) -> str :
40+ message = super ().format (record )
41+ if record .levelno == logging .INFO :
42+ return message
43+
44+ prefix = f'{ record .levelname .lower ()} : '
45+ if color := self .colors .get (record .levelno ) if self .use_color else None :
46+ prefix = f'{ color } { prefix } \033 [0m'
47+ return '\n ' .join (f'{ prefix } { line } ' for line in message .splitlines () or ['' ])
48+
2249
2350with open (os .path .join (os .path .dirname (__file__ ), 'missing_sources.toml' ), 'rb' ) as missing_sources_file :
2451 MISSING_SOURCES = tomllib .load (missing_sources_file )
@@ -254,6 +281,21 @@ def find_released_build(build_tag, package_name):
254281 return get_koji_build (max (tagged , key = lambda t : t ['build_id' ])['build_id' ])
255282 return None
256283
284+ def cached (key ):
285+ """Return the value cached for that key, or None when it must be fetched again."""
286+ if args .re_cache :
287+ log .debug ("cache ignored because of --re-cache: %s" , key )
288+ return None
289+ if key in CACHE :
290+ log .debug ("cache hit: %s" , key )
291+ return CACHE [key ]
292+ log .debug ("cache miss: %s" , key )
293+ return None
294+
295+ def cache (key , value ):
296+ log .debug ("caching: %s" , key )
297+ CACHE .set (key , value , expire = RETENTION_TIME )
298+
257299def find_commits (gh , repo , start_sha , end_sha ) -> list [Commit ]:
258300 """
259301 List the commits in the range [start_sha,end_sha[.
@@ -262,24 +304,24 @@ def find_commits(gh, repo, start_sha, end_sha) -> list[Commit]:
262304 A commit older that the end_sha commit and added by a merge commit won't appear in this list.
263305 """
264306 cache_key = f'commits-2-{ start_sha } -{ end_sha } '
265- if not args . re_cache and cache_key in CACHE :
266- return cast ( list [ Commit ], CACHE [ cache_key ])
307+ if ( commits := cast ( list [ Commit ], cached ( cache_key ))) is not None :
308+ return commits
267309 commits = []
268310 if gh :
269311 for commit in gh .get_repo (repo ).get_commits (start_sha ):
270312 if commit .sha == end_sha :
271313 break
272314 commits .append (commit )
273- CACHE . set (cache_key , commits , expire = RETENTION_TIME )
315+ cache (cache_key , commits )
274316 return commits
275317
276318def find_pull_requests (repo , start_sha , end_sha ):
277319 """Find the pull requests for the commits in the [start_sha,end_sha[ range."""
278320 prs = set ()
279321 for commit in find_commits (GITHUB , repo , start_sha , end_sha ):
280322 cache_key = f'commit-prs-6-{ commit .sha } '
281- if not args . re_cache and cache_key in CACHE :
282- prs .update (cast ( list [ PullRequest ], CACHE [ cache_key ]) )
323+ if ( commit_prs := cast ( list [ PullRequest ], cached ( cache_key ))) is not None :
324+ prs .update (commit_prs )
283325 elif GITHUB :
284326 commit_prs = list (commit .get_pulls ())
285327 if not commit_prs :
@@ -295,17 +337,17 @@ def find_pull_requests(repo, start_sha, end_sha):
295337 # commit. The latter is needed for the PRs merged by rebase or squash, whose merge
296338 # commit on the base branch is not one of the PR commits.
297339 commit_prs = [pr for pr in commit_prs if commit in pr .get_commits () or commit .sha == pr .merge_commit_sha ]
298- CACHE . set (cache_key , commit_prs , expire = RETENTION_TIME )
340+ cache (cache_key , commit_prs )
299341 prs .update (commit_prs )
300342 return sorted (prs , key = lambda p : p .number , reverse = True )
301343
302344def get_koji_build (build_id ) -> dict :
303345 cache_key = f'koji-build-{ build_id } '
304- if not args . re_cache and cache_key in CACHE :
305- return cast ( dict , CACHE [ cache_key ])
346+ if ( build := cast ( dict , cached ( cache_key ))) is not None :
347+ return build
306348 else :
307349 build = KOJI .getBuild (build_id )
308- CACHE . set (cache_key , build , expire = RETENTION_TIME )
350+ cache (cache_key , build )
309351 return build
310352
311353def get_plane_issues (plane_token ):
@@ -409,8 +451,15 @@ def get_plane_issues_with_milestones(plane_token):
409451)
410452parser .add_argument ('--re-cache' , help = "Refresh the cache" , action = 'store_true' )
411453parser .add_argument ('--json-output' , help = "Also write a machine readable report in json format to this path" )
454+ parser .add_argument ('--debug' , help = "Log more details on stderr" , action = 'store_true' )
412455args = parser .parse_args ()
413456
457+ handler = logging .StreamHandler ()
458+ handler .setFormatter (ColorFormatter (
459+ not os .environ .get ('NO_COLOR' ) and (bool (os .environ .get ('FORCE_COLOR' )) or sys .stderr .isatty ()),
460+ ))
461+ logging .basicConfig (level = logging .DEBUG if args .debug else logging .INFO , handlers = [handler ])
462+
414463CACHE = diskcache .Cache (args .cache )
415464RETENTION_TIME = 24 * 60 * 60 # 24 hours
416465
@@ -426,9 +475,9 @@ def get_plane_issues_with_milestones(plane_token):
426475# load the issues from plane, so we can search for the plane card related to a build
427476try :
428477 issues = get_plane_issues_with_milestones (args .plane_token )
429- except Exception :
478+ except Exception as e :
430479 issues = []
431- raise
480+ log . warning ( "failed to load the tickets from plane: %s" , e )
432481
433482# connect to github
434483GITHUB = None
@@ -438,6 +487,12 @@ def get_plane_issues_with_milestones(plane_token):
438487 GITHUB .get_repo ('xcp-ng/xcp' ) # check that the token is valid
439488 except BadCredentialsException :
440489 GITHUB = None
490+ log .warning ("the github token is invalid" )
491+ except github .GithubException as e :
492+ GITHUB = None
493+ log .warning ("failed to connect to github: %s" , e )
494+ else :
495+ log .warning ("no github token, the pull requests come from the cache" )
441496
442497# load the packages maintainers
443498with urlopen ('https://github.com/xcp-ng/xcp/raw/refs/heads/master/scripts/rpm_owners/packages.json' ) as f :
@@ -464,6 +519,7 @@ def get_plane_issues_with_milestones(plane_token):
464519 KOJI = koji .ClientSession ('https://kojihub.xcp-ng.org' , config )
465520 KOJI .ssl_login (config ['cert' ], None , config ['serverca' ])
466521 for tag in tags :
522+ log .info ('processing tag %s' , tag )
467523 tag_data = {'tag' : tag , 'builds' : []}
468524 report_data ['tags' ].append (tag_data )
469525 tag_history = dict (
@@ -475,6 +531,8 @@ def get_plane_issues_with_milestones(plane_token):
475531 taggeds = (t for t in taggeds if t ['package_name' ] in args .packages or args .packages == [])
476532 taggeds = sorted (taggeds , key = lambda t : (tag_history [t ['build_id' ]], t ['build_id' ]), reverse = True )
477533 for tagged in taggeds :
534+ build_url = f'https://koji.xcp-ng.org/buildinfo?buildID={ tagged ["build_id" ]} '
535+ log .info (' processing build %s (%s)' , tagged ['nvr' ], build_url )
478536 build = get_koji_build (tagged ['build_id' ])
479537 prs : list [PullRequest ] = []
480538 maintained_by = None
@@ -484,7 +542,6 @@ def get_plane_issues_with_milestones(plane_token):
484542 (repo , sha ) = parse_source (build ['source' ])
485543 prs = find_pull_requests (repo , sha , previous_build_sha )
486544 maintained_by = PACKAGES .get (tagged ['package_name' ], {}).get ('maintainer' )
487- build_url = f'https://koji.xcp-ng.org/buildinfo?buildID={ tagged ["build_id" ]} '
488545 build_issues = filter_issues (issues , [build_url ] + [pr .html_url for pr in prs ])
489546 print_table_line (
490547 temp_out , tagged ['nvr' ], build_url , build_issues , tagged ['owner_name' ], prs , maintained_by
0 commit comments