Skip to content

Commit 62fb1be

Browse files
Merge branch 'main' into main
2 parents e3abfad + d0fa875 commit 62fb1be

File tree

1 file changed

+82
-64
lines changed

1 file changed

+82
-64
lines changed

etc/tool/copilot.py

+82-64
Original file line numberDiff line numberDiff line change
@@ -2,72 +2,74 @@
22
import os
33
import json
44
import requests
5-
import time
65
from pathlib import Path
76
from github import Github
8-
import g4f # Assuming you're using g4f for AI responses
7+
import g4f
98

109
sys.path.append(str(Path(__file__).parent.parent.parent))
1110

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
1413
BOT_GITHUB_TOKEN = os.getenv("BOT") # Used for approvals
1514
GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY")
1615
PR_NUMBER = os.getenv("PR_NUMBER")
1716

18-
# 🚫 Disallowed Domains
19-
DISALLOWED_DOMAINS = {"is-cool.me", "is-app.tech"}
20-
# ✅ Allowed Domains
17+
# Allowed and Forbidden domains
2118
ALLOWED_DOMAINS = {"is-epic.me", "is-awsm.tech"}
19+
FORBIDDEN_DOMAINS = {"is-cool.me", "is-app.tech"}
2220

23-
# 📌 PR Review Rules
21+
# PR Review Guidelines
2422
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.
3329
"""
3430

35-
# 📌 Function: Fetch Changed Files
31+
def fetch_pr(repo):
32+
"""Fetches the PR object."""
33+
return repo.get_pull(int(PR_NUMBER))
34+
3635
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."""
3837
return [file.filename for file in pr.get_files()]
3938

40-
# 📌 Function: Fetch File Content
4139
def fetch_file_content(repo, filename, pr):
4240
"""Fetches file content from a PR."""
4341
try:
4442
file_content = repo.get_contents(filename, ref=pr.head.ref)
4543
return file_content.decoded_content.decode()
46-
except Exception as e:
47-
return f"Error fetching file content: {e}"
44+
except Exception:
45+
return ""
4846

49-
# 📌 Function: Check JSON Syntax
5047
def check_json_syntax(file_contents):
51-
"""Validates JSON format."""
48+
"""Validates JSON syntax."""
5249
try:
5350
json.loads(file_contents)
54-
return True, None # Valid JSON
51+
return None # No syntax errors
5552
except json.JSONDecodeError as e:
56-
return False, str(e) # Return syntax error
53+
return str(e) # Return error message
5754

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")
6159

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+
})
6568

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
7070

71+
def ai_review_pr(pr_body, changed_files, file_contents):
72+
"""Uses AI to review the PR based on rules."""
7173
review_prompt = f"""
7274
**Task:** Review this Pull Request based on the following rules:
7375
@@ -90,8 +92,8 @@ def ai_review_pr(pr_body, changed_files, file_contents):
9092
- If issues exist, respond with:
9193
- **Structured comments per issue.**
9294
- **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...'
9597
9698
**DO NOT** just say "Request changes"—explain why!
9799
"""
@@ -104,58 +106,74 @@ def ai_review_pr(pr_body, changed_files, file_contents):
104106

105107
decision = response.get("content", "").strip() if isinstance(response, dict) else response.strip()
106108

107-
# 🚨 Safety Check: If AI fails, request changes
109+
# If AI fails or response is empty, request changes automatically
108110
if not decision:
111+
print("❌ AI response is empty or invalid. Defaulting to 'request changes'.")
109112
return "request changes", ["AI review failed. Please manually check."]
110113

111-
# 🚨 If AI finds issues, reject PR
114+
# If AI finds issues, extract structured comments
112115
if "consider" in decision.lower() or "avoid" in decision.lower():
113116
return "request changes", decision.split("\n")
114117

115118
return "approve", []
116119

117120
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."]
119123

120-
# 📌 Function: Post Comment on PR
121124
def post_comment(pr, message):
122125
"""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])
124133

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."""
128139
bot_github = Github(BOT_GITHUB_TOKEN)
129140
bot_repo = bot_github.get_repo(GITHUB_REPOSITORY)
130141
bot_pr = bot_repo.get_pull(int(PR_NUMBER))
131142

132143
bot_pr.create_review(event="APPROVE", body="✅ AI Code Reviewer (Bot) has approved this PR.")
133144
print("✅ PR Approved by AI (Using Bot Token)")
134145

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
144146
def main():
145147
github = Github(GITHUB_TOKEN)
146148
repo = github.get_repo(GITHUB_REPOSITORY)
147-
pr = repo.get_pull(int(PR_NUMBER))
149+
pr = fetch_pr(repo)
148150

149151
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)
159177

160178
if __name__ == "__main__":
161179
main()

0 commit comments

Comments
 (0)