77from pathlib import Path
88
99
10- def run_gh_command (cmd , capture_json = True ):
10+ def run_gh_command (cmd : str , capture_json : bool = True ) -> str | dict | None :
11+ """Run a GitHub CLI command and return the output as JSON or plain text."""
1112 try :
1213 result = subprocess .run (shlex .split (cmd ), capture_output = True , check = True , text = True )
1314 return json .loads (result .stdout ) if capture_json else result .stdout .strip ()
@@ -18,12 +19,32 @@ def run_gh_command(cmd, capture_json=True):
1819 return None
1920
2021
21- def check_rate_limit (min_remaining = 100 ):
22+ def run_gh_command_json (cmd : str ) -> dict | None :
23+ """Run a GitHub CLI command and return the output as JSON."""
24+ output = run_gh_command (cmd , capture_json = True )
25+ if output is None :
26+ return None
27+
28+ return output if isinstance (output , dict ) else json .loads (output )
29+
30+
31+ def run_gh_command_text (cmd : str ) -> str | None :
32+ """Run a GitHub CLI command and return the output as plain text."""
33+ output = run_gh_command (cmd , capture_json = False )
34+ if output is None :
35+ return None
36+
37+ return output if isinstance (output , str ) else json .dumps (output , indent = 2 )
38+
39+
40+ def check_rate_limit (min_remaining : int = 100 ) -> bool :
41+ """Check GitHub API rate limit and return True if sufficient calls remaining."""
2242 print ("⏳ Checking GitHub API rate limit..." )
23- remaining = run_gh_command ('gh api rate_limit --jq ".rate.remaining"' , capture_json = False )
43+ remaining = run_gh_command_text ('gh api rate_limit --jq ".rate.remaining"' )
2444 if remaining is None :
2545 print ("⚠️ Could not determine API rate limit. Proceeding with caution." )
2646 return True
47+
2748 try :
2849 remaining_int = int (remaining )
2950 print (f"🔢 API calls remaining: { remaining_int } " )
@@ -36,13 +57,17 @@ def check_rate_limit(min_remaining=100):
3657 return True
3758
3859
39- def ensure_label (repo , label , color , description , dry_run = False ):
40- existing = run_gh_command (f"gh label list --repo { repo } --limit 100" , capture_json = False )
60+ def ensure_label (
61+ repo : str , label : str , color : str , description : str , dry_run : bool = False
62+ ) -> None :
63+ """Ensure a label exists in the specified repository."""
64+ existing = run_gh_command_text (f"gh label list --repo { repo } --limit 100" )
4165 if not existing or not any (line .startswith (label ) for line in existing .splitlines ()):
4266 action = "Would create" if dry_run else "Creating"
4367 print (f"🛠️ { action } label: { label } in { repo } " )
4468 if dry_run :
4569 return
70+
4671 try :
4772 subprocess .run (
4873 [
@@ -63,7 +88,10 @@ def ensure_label(repo, label, color, description, dry_run=False):
6388 print (f"⚠️ Failed to create label: { label } in { repo } " )
6489
6590
66- def create_issue (repo , title , body , dry_run = False , labels = None ):
91+ def create_issue (
92+ repo : str , title : str , body : str , dry_run : bool = False , labels : list [str ] | None = None
93+ ) -> None :
94+ """Create a new issue in the specified repository."""
6795 labels = labels or ["security" , "dependabot" ]
6896
6997 if dry_run :
@@ -83,22 +111,22 @@ def create_issue(repo, title, body, dry_run=False, labels=None):
83111 print (f"❌ Failed to create issue in { repo } : { title } " )
84112
85113
86- def get_open_issue_titles (repo ):
114+ def get_open_issue_titles (repo : str ) -> set [str ]:
115+ """Get titles of all open issues in the specified repository."""
87116 # Get all open issue titles for repo in one go (up to 100)
88- output = run_gh_command (
89- f"gh issue list --repo { repo } --state open --json title" , capture_json = True
90- )
117+ output = run_gh_command_json (f"gh issue list --repo { repo } --state open --json title" )
91118 if output is None :
92119 return set ()
120+
93121 return set (issue ["title" ] for issue in output )
94122
95123
96- def process_repo (repo , dry_run = False ):
124+ def process_repo (repo : str , dry_run : bool = False ) -> None :
125+ """Process a single repository to check for Dependabot alerts and create issues."""
97126 print (f"🔍 Checking alerts for: { repo } " )
98127
99- alerts = run_gh_command (
128+ alerts = run_gh_command_json (
100129 f'gh api -X GET "/repos/{ repo } /dependabot/alerts?per_page=100" --paginate' ,
101- capture_json = True ,
102130 )
103131 if not alerts :
104132 print (f"✅ No open dependabot alerts found for { repo } ." )
@@ -145,19 +173,37 @@ def process_repo(repo, dry_run=False):
145173 print (f"⚠️ Issue already exists in { repo } : '{ title } '. Skipping..." )
146174 continue
147175
148- ensure_label (repo , "security" , "d73a4a" , "Security-related issues" , dry_run )
149- ensure_label (repo , "dependabot" , "0366d6" , "Dependabot alerts" , dry_run )
176+ labels = [
177+ {
178+ "name" : "security" ,
179+ "color" : "d73a4a" ,
180+ "description" : "Security-related issues" ,
181+ },
182+ {
183+ "name" : "dependabot" ,
184+ "color" : "0366d6" ,
185+ "description" : "Dependabot alerts" ,
186+ },
187+ ]
150188
151- labels = ["security" , "dependabot" ]
152189 if fpv is None :
153- ensure_label (repo , "no-patch" , "ededed" , "No patched version available" , dry_run )
154- labels .append ("no-patch" )
190+ labels .append (
191+ {
192+ "name" : "no-patch" ,
193+ "color" : "ededed" ,
194+ "description" : "No patched version available" ,
195+ }
196+ )
197+
198+ for label in labels :
199+ ensure_label (repo , label ["name" ], label ["color" ], label ["description" ], dry_run )
155200
156- create_issue (repo , title , body , dry_run = dry_run , labels = labels )
201+ create_issue (repo , title , body , dry_run = dry_run , labels = [ label [ "name" ] for label in labels ] )
157202
158203
159- def load_repos (path ):
160- with open (path ) as f :
204+ def load_repos (path : Path ) -> list [str ]:
205+ """Load repository names from a file, ignoring comments and empty lines."""
206+ with open (path , "r" , encoding = "utf-8" ) as f :
161207 return [
162208 line .split ("#" )[0 ].strip ()
163209 for line in f
0 commit comments