1
1
import sys
2
- from pathlib import Path
3
-
4
- sys .path .append (str (Path (__file__ ).parent .parent .parent ))
5
-
6
- import g4f
7
2
import os
3
+ import time
8
4
import requests
5
+ from pathlib import Path
9
6
from github import Github
7
+ import g4f
10
8
11
- g4f .debug .logging = True
12
- g4f .debug .version_check = False
9
+ sys .path .append (str (Path (__file__ ).parent .parent .parent ))
13
10
14
- GITHUB_TOKEN = os .getenv ('GITHUB_TOKEN' )
15
- GITHUB_REPOSITORY = os .getenv ('GITHUB_REPOSITORY' )
16
- PR_NUMBER = os .getenv ('PR_NUMBER' )
11
+ # GitHub API credentials
12
+ GITHUB_TOKEN = os .getenv ("GITHUB_TOKEN" )
13
+ GITHUB_REPOSITORY = os .getenv ("GITHUB_REPOSITORY" )
14
+ PR_NUMBER = os .getenv ("PR_NUMBER" )
17
15
18
16
# Allowed domains after migration
19
17
ALLOWED_DOMAINS = {"is-epic.me" , "is-awsm.tech" }
20
18
21
- # README rules for validation
19
+ # PR Review Rules
22
20
README_RULES = """
23
21
1. Subdomains must be for personal sites, open-source projects, or legitimate services.
24
- 2. JSON structure must be valid and must contain 'domain', 'subdomain', 'owner', and 'records' fields .
25
- 3. Wildcard domains (e.g., *.example.is-epic.me) require a detailed reason .
22
+ 2. JSON must contain 'domain', 'subdomain', 'owner', and 'records'.
23
+ 3. Wildcard domains require justification .
26
24
4. Cloudflare (NS), Netlify, and Vercel are not supported.
27
- 5. Illegal or inappropriate domain use is strictly prohibited.
28
- 6. PR descriptions must be clear, and all required fields must be properly filled .
29
- 7. Only the new domains (is-epic.me and is-awsm.tech) are allowed. Old domains will be rejected .
25
+ 5. Illegal or inappropriate domain use is prohibited.
26
+ 6. PR descriptions must be clear.
27
+ 7. Only new domains (is-epic.me and is-awsm.tech) are allowed.
30
28
"""
31
29
32
30
def fetch_changed_files (pr ):
33
31
"""Fetches the list of files changed in the PR."""
34
32
return [file .filename for file in pr .get_files ()]
35
33
36
34
def fetch_file_content (repo , filename ):
37
- """Fetches the content of a given file in the PR."""
35
+ """Fetches file content from a PR."""
38
36
try :
39
37
file_content = repo .get_contents (filename , ref = pr .head .ref )
40
38
return file_content .decoded_content .decode ()
41
39
except Exception as e :
42
40
return f"Error fetching file content: { e } "
43
41
44
42
def ai_review_pr (pr_body , changed_files , file_contents ):
45
- """Uses AI to review the PR according to README rules."""
43
+ """Uses AI to review the PR based on the rules."""
46
44
review_prompt = f"""
47
- Review the following pull request based on these rules:
45
+ Review the PR based on these rules:
48
46
49
47
{ README_RULES }
50
48
51
49
PR Description: { pr_body }
52
50
Changed Files: { ', ' .join (changed_files )}
53
-
51
+
54
52
File Contents:
55
53
{ file_contents }
56
54
57
- Check if the PR follows the rules. Approve if everything is correct, or request changes if there are issues.
58
- Also, ensure that only the new domains (is-epic .me and is-awsm .tech) are used.
59
- If old domains (is-cool.me, is-app.tech) are present, reject the PR and request a domain update .
55
+ - Approve if all rules are met.
56
+ - Reject if old domains (` is-cool .me`, ` is-app .tech` ) are used.
57
+ - If the JSON structure is missing or incorrect, request changes .
60
58
"""
61
59
62
- response = g4f .ChatCompletion .create (model = g4f .models .gpt_4 , messages = [{"role" : "user" , "content" : review_prompt }])
63
-
64
- if isinstance (response , dict ):
65
- decision = response .get ("content" , "" ).strip ().lower ()
66
- else :
67
- print (f"Unexpected response format: { response } " )
68
- decision = response .strip ().lower ()
60
+ try :
61
+ response = g4f .ChatCompletion .create (
62
+ model = g4f .models .gpt_4 ,
63
+ messages = [{"role" : "user" , "content" : review_prompt }]
64
+ )
65
+
66
+ # Ensure response is a string
67
+ decision = response .get ("content" , "" ).strip ().lower () if isinstance (response , dict ) else response .strip ().lower ()
69
68
70
- return decision
69
+ # Log unexpected responses
70
+ if not decision :
71
+ print ("❌ AI response is empty or invalid. Defaulting to 'request changes'." )
72
+ return "request changes"
73
+
74
+ # Force rejection if old domains are detected
75
+ if "is-cool.me" in file_contents or "is-app.tech" in file_contents :
76
+ print ("❌ PR rejected due to use of old domains." )
77
+ return "reject"
78
+
79
+ return decision
80
+
81
+ except Exception as e :
82
+ print (f"❌ AI review failed: { e } " )
83
+ return "request changes"
71
84
72
85
def post_comment (pr , message ):
73
86
"""Posts a comment on the PR."""
@@ -84,99 +97,16 @@ def main():
84
97
decision = ai_review_pr (pr .body , changed_files , file_contents )
85
98
86
99
if "approve" in decision :
87
- pr .create_review (event = "APPROVE" , body = "AI Code Reviewer has approved this PR." )
88
- print ("PR Approved by AI" )
100
+ pr .create_review (event = "APPROVE" , body = "✅ AI Code Reviewer has approved this PR." )
101
+ print ("✅ PR Approved by AI" )
102
+ elif "reject" in decision :
103
+ pr .create_review (event = "REQUEST_CHANGES" , body = "❌ PR rejected due to rule violations." )
104
+ post_comment (pr , "❌ Your PR violates the domain rules (e.g., using `is-cool.me`). Please update and resubmit." )
105
+ print ("❌ PR Rejected" )
89
106
else :
90
- pr .create_review (event = "REQUEST_CHANGES" , body = "AI Code Reviewer suggests changes based on README rules ." )
91
- post_comment (pr , f" AI Code Reviewer suggests changes:\n \n { decision } " )
92
- print ("PR Needs Changes & Commented " )
107
+ pr .create_review (event = "REQUEST_CHANGES" , body = "⚠️ AI Code Reviewer suggests changes." )
108
+ post_comment (pr , "⚠️ AI Reviewer suggests changes:\n \n " + decision )
109
+ print ("⚠️ PR Needs Changes" )
93
110
94
111
if __name__ == "__main__" :
95
112
main ()
96
-
97
-
98
- def wait_for_workflows (repo , pr_number , max_wait = 1800 , interval = 30 ):
99
- """Waits for specified GitHub workflows to complete."""
100
- workflows = ["auto-merge.yml" , "save-pr-number.yml" , "validation.yml" ]
101
- start_time = time .time ()
102
-
103
- while time .time () - start_time < max_wait :
104
- runs = repo .get_workflow_runs (branch = f"refs/pull/{ pr_number } /merge" , status = "in_progress" )
105
- if all (run .name not in workflows for run in runs ):
106
- return True # All workflows completed
107
-
108
- time .sleep (interval )
109
-
110
- print ("❌ Workflows did not complete within the time limit." )
111
- return False
112
-
113
- import os
114
- import requests
115
-
116
- def approve_pull_request (repo , pr_number ):
117
- """Approves the pull request using GitHub token if available, otherwise fallback to standard API."""
118
- pr = repo .get_pull (pr_number )
119
-
120
- # Dismiss previous approvals first
121
- dismiss_previous_approvals (repo , pr_number )
122
-
123
- # Validate PR files and domains, collect feedback comments
124
- comments = []
125
-
126
- if not validate_pr_files (pr ):
127
- comments .append ({"file" : "unknown" , "line" : 0 , "comment" : "❌ PR contains invalid files. Please review file requirements." })
128
-
129
- if not validate_domains (pr ):
130
- comments .append ({"file" : "unknown" , "line" : 0 , "comment" : "❌ PR includes an invalid domain. Ensure compliance with domain policies." })
131
-
132
- # Wait for workflows to complete
133
- if not wait_for_workflows (repo , pr_number ):
134
- comments .append ({"file" : "unknown" , "line" : 0 , "comment" : "❌ Workflows did not pass successfully. Please check GitHub Actions." })
135
-
136
- # Post review comments if any issues are found
137
- if comments :
138
- post_review_comment (repo , pr_number , comments )
139
- return
140
-
141
- # Attempt to approve PR using GitHub Actions secret token
142
- github_token = os .getenv ("GITHUB_TOKEN" )
143
- if github_token :
144
- headers = {"Authorization" : f"token { github_token } " }
145
- approval_url = f"https://api.github.com/repos/{ repo .owner .login } /{ repo .name } /pulls/{ pr_number } /reviews"
146
- data = {"event" : "APPROVE" , "body" : "✅ PR approved automatically as it complies with rules and all workflow checks passed." }
147
-
148
- response = requests .post (approval_url , json = data , headers = headers )
149
- if response .status_code in [200 , 201 ]:
150
- print ("✅ PR approved successfully using GitHub token." )
151
- return
152
- else :
153
- print (f"⚠️ GitHub token approval failed: { response .json ()} " )
154
-
155
- # Fallback: Approve using repository API
156
- try :
157
- pr .create_review (event = "APPROVE" , body = "✅ PR approved automatically as it complies with rules and all workflow checks passed." )
158
- print ("✅ PR approved successfully using repository API." )
159
- except Exception as e :
160
- print (f"❌ Failed to approve PR using repository API: { e } " )
161
-
162
- def dismiss_previous_approvals (repo , pr_number ):
163
- """Dismisses previous approvals for the PR."""
164
- pr = repo .get_pull (pr_number )
165
- reviews = pr .get_reviews ()
166
-
167
- for review in reviews :
168
- if review .state == "APPROVED" :
169
- repo ._requester .requestJson ("PUT" , f"{ pr .url } /reviews/{ review .id } /dismissals" ,
170
- input = {"message" : "Approval dismissed due to new commit." })
171
- print (f"⚠️ Previous approval by { review .user .login } dismissed." )
172
-
173
- def post_review_comment (repo , pr_number , comments ):
174
- """Posts review comments on the PR instead of rejecting it outright."""
175
- pr = repo .get_pull (pr_number )
176
-
177
- review_comments = [{"path" : c ["file" ], "position" : c ["line" ], "body" : c ["comment" ]} for c in comments ]
178
- if review_comments :
179
- pr .create_review (event = "REQUEST_CHANGES" , comments = review_comments )
180
- print ("⚠️ Review comments posted for required changes." )
181
- else :
182
- print ("✅ No issues found." )
0 commit comments