2
2
import os
3
3
import json
4
4
import requests
5
- import time
6
5
from pathlib import Path
7
6
from github import Github
8
- import g4f # Assuming you're using g4f for AI responses
7
+ import g4f
9
8
10
9
sys .path .append (str (Path (__file__ ).parent .parent .parent ))
11
10
12
- # 🔒 GitHub API Credentials
13
- GITHUB_TOKEN = os .getenv ("GITHUB_TOKEN" ) # Used for commenting & requesting changes
11
+ # GitHub API credentials
12
+ GITHUB_TOKEN = os .getenv ("GITHUB_TOKEN" ) # Used for commenting and requesting changes
14
13
BOT_GITHUB_TOKEN = os .getenv ("BOT" ) # Used for approvals
15
14
GITHUB_REPOSITORY = os .getenv ("GITHUB_REPOSITORY" )
16
15
PR_NUMBER = os .getenv ("PR_NUMBER" )
17
16
18
- # 🚫 Disallowed Domains
19
- DISALLOWED_DOMAINS = {"is-cool.me" , "is-app.tech" }
20
- # ✅ Allowed Domains
17
+ # Allowed and Forbidden domains
21
18
ALLOWED_DOMAINS = {"is-epic.me" , "is-awsm.tech" }
19
+ FORBIDDEN_DOMAINS = {"is-cool.me" , "is-app.tech" }
22
20
23
- # 📌 PR Review Rules
21
+ # PR Review Guidelines
24
22
README_RULES = """
25
- ### PR Review Guidelines
26
- 1. **Valid Subdomains**: Only for personal sites, open-source projects, or legitimate services.
27
- 2. **Correct JSON Structure**: Must include `domain`, `subdomain`, `owner`, and `records` fields.
28
- 3. **No Wildcard Abuse**: `*.example.com` requires proper justification.
29
- 4. **Disallowed DNS Providers**: Cloudflare (NS), Netlify, and Vercel are **not allowed**.
30
- 5. **Legal & Appropriate Use**: Domains must not be used for illegal or inappropriate purposes.
31
- 6. **Clear PR Descriptions**: Should explain why the subdomain is needed.
32
- 7. **Domain Restrictions**: Only `is-epic.me` and `is-awsm.tech` are allowed.
23
+ ### PR Review Guidelines:
24
+ 1️⃣ **Allowed domains:** Only `is-epic.me` and `is-awsm.tech` are allowed.
25
+ 2️⃣ **JSON Structure:** Every file must have `domain`, `subdomain`, `owner`, and `records`.
26
+ 3️⃣ **No Wildcard Abuse:** Wildcard (`*.example.com`) requires proper justification.
27
+ 4️⃣ **DNS Providers:** Cloudflare (NS), Netlify, and Vercel are **not allowed**.
28
+ 5️⃣ **Legal & Appropriate Usage:** Domains must not be used for illegal purposes.
33
29
"""
34
30
35
- # 📌 Function: Fetch Changed Files
31
+ def fetch_pr (repo ):
32
+ """Fetches the PR object."""
33
+ return repo .get_pull (int (PR_NUMBER ))
34
+
36
35
def fetch_changed_files (pr ):
37
- """Fetches the list of changed files in the PR."""
36
+ """Gets the list of changed files in the PR."""
38
37
return [file .filename for file in pr .get_files ()]
39
38
40
- # 📌 Function: Fetch File Content
41
39
def fetch_file_content (repo , filename , pr ):
42
40
"""Fetches file content from a PR."""
43
41
try :
44
42
file_content = repo .get_contents (filename , ref = pr .head .ref )
45
43
return file_content .decoded_content .decode ()
46
- except Exception as e :
47
- return f"Error fetching file content: { e } "
44
+ except Exception :
45
+ return " "
48
46
49
- # 📌 Function: Check JSON Syntax
50
47
def check_json_syntax (file_contents ):
51
- """Validates JSON format ."""
48
+ """Validates JSON syntax ."""
52
49
try :
53
50
json .loads (file_contents )
54
- return True , None # Valid JSON
51
+ return None # No syntax errors
55
52
except json .JSONDecodeError as e :
56
- return False , str (e ) # Return syntax error
53
+ return str (e ) # Return error message
57
54
58
- # 📌 Function: AI PR Review
59
- def ai_review_pr (pr_body , changed_files , file_contents ):
60
- """Uses AI to review the PR based on the guidelines."""
55
+ def analyze_file_contents (file_contents ):
56
+ """Analyzes the file and finds exact line numbers for issues."""
57
+ issues = []
58
+ lines = file_contents .split ("\n " )
61
59
62
- # 🚨 **HARD-CODED CHECK FOR OLD DOMAINS**
63
- if any (domain in pr_body or domain in file_contents for domain in DISALLOWED_DOMAINS ):
64
- return "request changes" , ["🚫 This PR contains a **forbidden domain** (`is-cool.me` or `is-app.tech`). Only `is-epic.me` or `is-awsm.tech` are allowed." ]
60
+ for i , line in enumerate (lines , start = 1 ):
61
+ for domain in FORBIDDEN_DOMAINS :
62
+ if domain in line :
63
+ issues .append ({
64
+ "line" : i ,
65
+ "issue" : f"Forbidden domain `{ domain } ` found." ,
66
+ "fix" : f"Replace `{ domain } ` with an allowed domain like `is-epic.me` or `is-awsm.tech`."
67
+ })
65
68
66
- # 🚨 **CHECK JSON SYNTAX**
67
- is_valid_json , json_error = check_json_syntax (file_contents )
68
- if not is_valid_json :
69
- return "request changes" , [f"⚠️ JSON Syntax Error: { json_error } . Please fix and resubmit." ]
69
+ return issues
70
70
71
+ def ai_review_pr (pr_body , changed_files , file_contents ):
72
+ """Uses AI to review the PR based on rules."""
71
73
review_prompt = f"""
72
74
**Task:** Review this Pull Request based on the following rules:
73
75
@@ -90,8 +92,8 @@ def ai_review_pr(pr_body, changed_files, file_contents):
90
92
- If issues exist, respond with:
91
93
- **Structured comments per issue.**
92
94
- **GitHub Actions-style comments**, e.g.:
93
- - 'Consider handling session failures ...'
94
- - 'Avoid using a generic exception handler ...'
95
+ - 'Forbidden domain found on line X ...'
96
+ - 'Ensure all fields are present in JSON ...'
95
97
96
98
**DO NOT** just say "Request changes"—explain why!
97
99
"""
@@ -104,58 +106,74 @@ def ai_review_pr(pr_body, changed_files, file_contents):
104
106
105
107
decision = response .get ("content" , "" ).strip () if isinstance (response , dict ) else response .strip ()
106
108
107
- # 🚨 Safety Check: If AI fails, request changes
109
+ # If AI fails or response is empty , request changes automatically
108
110
if not decision :
111
+ print ("❌ AI response is empty or invalid. Defaulting to 'request changes'." )
109
112
return "request changes" , ["AI review failed. Please manually check." ]
110
113
111
- # 🚨 If AI finds issues, reject PR
114
+ # If AI finds issues, extract structured comments
112
115
if "consider" in decision .lower () or "avoid" in decision .lower ():
113
116
return "request changes" , decision .split ("\n " )
114
117
115
118
return "approve" , []
116
119
117
120
except Exception as e :
118
- return "request changes" , [f"AI review failed: { e } . Please manually check." ]
121
+ print (f"❌ AI review failed: { e } " )
122
+ return "request changes" , ["AI review failed. Please manually check." ]
119
123
120
- # 📌 Function: Post Comment on PR
121
124
def post_comment (pr , message ):
122
125
"""Posts a comment on the PR."""
123
- pr .create_issue_comment (message )
126
+ existing_comments = [comment .body for comment in pr .get_issue_comments ()]
127
+ if message not in existing_comments :
128
+ pr .create_issue_comment (message )
129
+
130
+ def request_changes (pr , issues , filename ):
131
+ """Requests changes on the PR and comments on how to fix them."""
132
+ formatted_issues = "\n \n " .join ([f"- **Line { issue ['line' ]} :** { issue ['issue' ]} \n - **Suggested Fix:** { issue ['fix' ]} " for issue in issues ])
124
133
125
- # 📌 Function: Approve PR
126
- def approve_pr ():
127
- """Approves the PR using the bot's GitHub token."""
134
+ pr .create_review (event = "REQUEST_CHANGES" , body = f"⚠️ AI Review found issues in `{ filename } `. See comments for fixes." )
135
+ post_comment (pr , f"⚠️ **AI Review suggests changes for `{ filename } `:**\n \n { formatted_issues } " )
136
+
137
+ def approve_pr (pr ):
138
+ """Approves the PR using the bot's token."""
128
139
bot_github = Github (BOT_GITHUB_TOKEN )
129
140
bot_repo = bot_github .get_repo (GITHUB_REPOSITORY )
130
141
bot_pr = bot_repo .get_pull (int (PR_NUMBER ))
131
142
132
143
bot_pr .create_review (event = "APPROVE" , body = "✅ AI Code Reviewer (Bot) has approved this PR." )
133
144
print ("✅ PR Approved by AI (Using Bot Token)" )
134
145
135
- # 📌 Function: Request Changes on PR
136
- def request_changes (pr , comments ):
137
- """Requests changes using the default GitHub Actions token."""
138
- formatted_comments = "\n \n " .join ([f"⚠️ **{ comment } **" for comment in comments ])
139
- pr .create_review (event = "REQUEST_CHANGES" , body = f"⚠️ AI Review suggests changes:\n \n { formatted_comments } " )
140
- post_comment (pr , f"⚠️ AI Review:\n \n { formatted_comments } " )
141
- print ("⚠️ PR Needs Changes" )
142
-
143
- # 📌 Main Function
144
146
def main ():
145
147
github = Github (GITHUB_TOKEN )
146
148
repo = github .get_repo (GITHUB_REPOSITORY )
147
- pr = repo . get_pull ( int ( PR_NUMBER ) )
149
+ pr = fetch_pr ( repo )
148
150
149
151
changed_files = fetch_changed_files (pr )
150
- file_contents = "\n \n " .join ([f"### { file } \n { fetch_file_content (repo , file , pr )} " for file in changed_files ])
151
-
152
- # 🚀 Run AI Review
153
- decision , comments = ai_review_pr (pr .body , changed_files , file_contents )
154
-
155
- if decision == "approve" :
156
- approve_pr () # Uses bot token for approval
157
- elif decision == "request changes" :
158
- request_changes (pr , comments ) # Uses GitHub Actions token for requests
152
+ all_issues = []
153
+
154
+ for file in changed_files :
155
+ file_contents = fetch_file_content (repo , file , pr )
156
+
157
+ # Check for syntax errors in JSON files
158
+ if file .endswith (".json" ):
159
+ syntax_error = check_json_syntax (file_contents )
160
+ if syntax_error :
161
+ all_issues .append ({"line" : "N/A" , "issue" : f"Invalid JSON syntax: { syntax_error } " , "fix" : "Fix the JSON structure." })
162
+
163
+ # Domain validation and other checks
164
+ issues = analyze_file_contents (file_contents )
165
+ if issues :
166
+ all_issues .extend (issues )
167
+
168
+ # AI Review for extra validation
169
+ ai_decision , ai_comments = ai_review_pr (pr .body , changed_files , file_contents )
170
+
171
+ # Request changes if issues exist
172
+ if all_issues or ai_decision == "request changes" :
173
+ request_changes (pr , all_issues , "Multiple Files" )
174
+ post_comment (pr , "\n " .join (ai_comments ))
175
+ else :
176
+ approve_pr (pr )
159
177
160
178
if __name__ == "__main__" :
161
179
main ()
0 commit comments