11import os
22
3- from github import Github
3+ from github import Github , GithubException
4+ from utils import extract_all_linked_issues
45
56MAINTAINER_ROLES = frozenset ({"OWNER" , "MEMBER" , "COLLABORATOR" })
67DEFAULT_EXCLUDE_PR_AUTHORS = "dependabot[bot]"
@@ -22,7 +23,6 @@ def __init__(self):
2223 self .bot_login = (
2324 bot_username if bot_username .endswith ("[bot]" ) else f"{ bot_username } [bot]"
2425 )
25-
2626 if self .github_token and self .repository_name :
2727 try :
2828 self .github = Github (self .github_token )
@@ -52,69 +52,87 @@ def _project_title_key(cls, title: str) -> str:
5252
5353 def get_issue_projects (self , owner , repo_name , issue_number ):
5454 query = """
55- query($owner: String!, $repo: String!, $issueNumber: Int!) {
55+ query($owner: String!, $repo: String!, $issueNumber: Int!, $cursor: String ) {
5656 repository(owner: $owner, name: $repo) {
5757 issue(number: $issueNumber) {
58- projectItems(first: 10 ) {
58+ projectItems(first: 100, after: $cursor ) {
5959 nodes {
6060 project {
6161 title
6262 }
6363 }
64+ pageInfo {
65+ hasNextPage
66+ endCursor
67+ }
6468 }
6569 }
6670 }
6771 }
6872 """
69- variables = {"owner" : owner , "repo" : repo_name , "issueNumber" : issue_number }
70- headers , result = self .github .requester .graphql_query (query , variables )
71- print (f"DEBUG: Raw GraphQL response: { result } " )
73+ projects = []
74+ has_next_page = True
75+ cursor = None
76+
77+ while has_next_page :
78+ variables = {
79+ "owner" : owner ,
80+ "repo" : repo_name ,
81+ "issueNumber" : issue_number ,
82+ "cursor" : cursor ,
83+ }
84+ headers , result = self .github .requester .graphql_query (query , variables )
7285
73- if "errors" in result :
74- raise ValueError (f"GraphQL API Permission Error: { result ['errors' ]} " )
86+ if "errors" in result :
87+ raise ValueError (f"GraphQL API Permission Error: { result ['errors' ]} " )
88+
89+ repo_data = result .get ("data" , {}).get ("repository" )
90+ if not repo_data :
91+ raise ValueError (
92+ f"GraphQL could not access repository { owner } /{ repo_name } ; "
93+ "possible GitHub API or permission error"
94+ )
95+
96+ issue_node = repo_data .get ("issue" )
97+ if issue_node is None :
98+ raise ValueError (
99+ f"GraphQL could not access issue { owner } /{ repo_name } #{ issue_number } ; "
100+ "possible GitHub API or permission error"
101+ )
102+
103+ project_items = issue_node .get ("projectItems" )
104+ if project_items is None :
105+ raise ValueError (
106+ f"GraphQL could not read project assignments for "
107+ f"{ owner } /{ repo_name } #{ issue_number } ; "
108+ "possible GitHub API or permission error"
109+ )
110+
111+ nodes = project_items .get ("nodes" ) or []
112+ for node in nodes :
113+ if not node :
114+ continue
115+ project = node .get ("project" ) or {}
116+ title = project .get ("title" )
117+ if title :
118+ projects .append (self ._normalize_project_title (title ))
119+
120+ page_info = project_items .get ("pageInfo" ) or {}
121+ has_next_page = page_info .get ("hasNextPage" , False )
122+ cursor = page_info .get ("endCursor" )
75123
76- repo_data = result .get ("data" , {}).get ("repository" )
77- if not repo_data :
78- raise ValueError (
79- f"GraphQL could not access repository { owner } /{ repo_name } ; "
80- "possible GitHub API or permission error"
81- )
82- issue_node = repo_data .get ("issue" )
83- if issue_node is None :
84- raise ValueError (
85- f"GraphQL could not access issue { owner } /{ repo_name } #{ issue_number } ; "
86- "possible GitHub API or permission error"
87- )
88- project_items = issue_node .get ("projectItems" )
89- if project_items is None :
90- raise ValueError (
91- f"GraphQL could not read project assignments for "
92- f"{ owner } /{ repo_name } #{ issue_number } ; "
93- "possible GitHub API or permission error"
94- )
95- nodes = project_items .get ("nodes" ) or []
96- projects = []
97- for node in nodes :
98- if not node :
99- continue
100- project = node .get ("project" ) or {}
101- title = project .get ("title" )
102- if title :
103- projects .append (self ._normalize_project_title (title ))
104124 return projects
105125
106126 def validate_pr_issues (self , pr ):
107127 """Validate if a pull request is from an exempt user or references a validated issue."""
108128 if not self .github or not self .repository_name :
109129 print ("GitHub client or repository name not initialized" )
110130 return False
111-
112131 pr_author = (
113132 pr .user .login
114133 if pr .user and isinstance (getattr (pr .user , "login" , None ), str )
115134 else ""
116135 )
117-
118136 exclude_authors_env = os .environ .get (
119137 "EXCLUDE_PR_AUTHORS" , DEFAULT_EXCLUDE_PR_AUTHORS
120138 )
@@ -124,39 +142,30 @@ def validate_pr_issues(self, pr):
124142 if pr_author in excluded_authors :
125143 print (f"Author { pr_author } is in the exclude list. Proceeding." )
126144 return True
127-
128145 author_association = str (getattr (pr , "author_association" , "" ) or "" )
129146 if author_association in MAINTAINER_ROLES :
130147 print (
131148 f"Author { pr_author } is exempt due to association: "
132149 f"{ author_association } . Proceeding."
133150 )
134151 return True
135-
136- from utils import extract_all_linked_issues
137-
138152 pr_body = pr .body if isinstance (pr .body , str ) else ""
139153 linked_issues = extract_all_linked_issues (pr_body , self .repository_name )
140154 if not linked_issues :
141155 print ("No linked issues found in PR body for external contributor." )
142156 return False
143-
144157 current_org = self .repository_name .split ("/" )[0 ].lower ()
145158 required_projects = {
146159 self ._project_title_key (p ) for p in REQUIRED_CONTRIBUTOR_PROJECTS
147160 }
148-
149161 for owner , repo_name , issue_number in linked_issues :
150162 if owner .lower () != current_org :
151163 print (
152164 f"Issue { owner } /{ repo_name } #{ issue_number } does not belong "
153165 f"to organization { current_org } , skipping validation."
154166 )
155167 continue
156-
157168 try :
158- from github import GithubException
159-
160169 target_repo = self .github .get_repo (f"{ owner } /{ repo_name } " )
161170 issue = target_repo .get_issue (issue_number )
162171 except Exception as e :
@@ -167,19 +176,16 @@ def validate_pr_issues(self, pr):
167176 continue
168177 print (f"Error fetching issue { owner } /{ repo_name } #{ issue_number } : { e } " )
169178 raise
170-
171179 if issue .pull_request :
172180 print (
173181 f"Reference { owner } /{ repo_name } #{ issue_number } is a pull request, skipping validation."
174182 )
175183 continue
176-
177184 if issue .state != "open" :
178185 print (
179186 f"Issue { owner } /{ repo_name } #{ issue_number } is not open, skipping validation."
180187 )
181188 continue
182-
183189 issue_labels = [label .name .lower () for label in issue .labels ]
184190 valid_labels = [
185191 lbl for lbl in issue_labels if lbl not in INVALID_ISSUE_LABELS
@@ -195,22 +201,14 @@ def validate_pr_issues(self, pr):
195201 "invalid/wontfix label, skipping validation."
196202 )
197203 continue
198-
199204 try :
200205 projects = self .get_issue_projects (owner , repo_name , issue_number )
201206 except Exception as e :
202207 print (
203208 f"Error fetching projects for issue { owner } /{ repo_name } #{ issue_number } : { e } "
204209 )
205210 raise
206-
207- print (
208- f"DEBUG: Raw projects for { owner } /{ repo_name } #{ issue_number } : { projects } "
209- )
210211 project_keys = [self ._project_title_key (p ) for p in projects ]
211- print (f"DEBUG: Normalized project keys: { project_keys } " )
212- print (f"DEBUG: Required project keys: { required_projects } " )
213-
214212 has_valid_project = any (key in required_projects for key in project_keys )
215213 if has_valid_project :
216214 print (
@@ -223,12 +221,10 @@ def validate_pr_issues(self, pr):
223221 f"to any required project (found: { projects or 'none' } ), "
224222 "skipping validation."
225223 )
226-
227224 return False
228225
229226 def get_bot_comment (self , pr , comment_type , after_date = None , issue_comments = None ):
230227 """Get the comment of this bot with the given marker if it exists.
231-
232228 If ``after_date`` is provided, only considers comments posted after that date.
233229 """
234230 try :
@@ -251,7 +247,6 @@ def get_bot_comment(self, pr, comment_type, after_date=None, issue_comments=None
251247
252248 def has_bot_comment (self , pr , comment_type , after_date = None , issue_comments = None ):
253249 """Check if PR already has a specific type of bot comment.
254-
255250 Uses HTML markers. If ``after_date`` is provided,
256251 only considers comments posted after that date.
257252 """
@@ -277,11 +272,14 @@ def get_invalid_unvalidated_issue_comment(self, pr_author):
277272 "request.\n \n "
278273 "An issue is considered validated when it is open, has an appropriate "
279274 "label other than `invalid` or `wontfix`, and is assigned to one of "
280- "the OpenWISP contributor project boards mentioned in the "
281- "contributing guidelines.\n \n "
282- "Please see the OpenWISP policy on unsolicited and AI-assisted "
283- "contributions:\n "
284- "https://openwisp.io/docs/dev/general/code-of-conduct.html\n \n "
275+ "the project boards mentioned in the "
276+ "[OpenWISP Contributing Guidelines]"
277+ "(https://openwisp.io/docs/dev/developer/contributing.html).\n \n "
278+ "Please see the [OpenWISP Anti AI Spam Policy]"
279+ "(https://openwisp.io/docs/dev/general/code-of-conduct.html).\n \n "
280+ "Feel free to join the [OpenWISP dev chatroom]"
281+ "(https://matrix.to/#/#openwisp_development:gitter.im) "
282+ "to coordinate with the development team.\n \n "
285283 "If this is not resolved within 24 hours, this pull request "
286284 "will be closed automatically. "
287285 "Thank you for your understanding."
0 commit comments