Skip to content

Commit cf868b9

Browse files
committed
feat: add new agentic workflows and scripts
1 parent 1c6c541 commit cf868b9

6 files changed

Lines changed: 677 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: PR Quality Check
2+
on:
3+
pull_request:
4+
types: [opened, reopened]
5+
6+
jobs:
7+
pr_quality_check:
8+
runs-on: ubuntu-latest
9+
permissions:
10+
pull-requests: write
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-python@v5
14+
with:
15+
python-version: '3.12'
16+
- run: pip install anthropic PyGithub
17+
- name: Run PR quality check agent
18+
env:
19+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
20+
AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }}
21+
AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }}
22+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23+
PR_NUMBER: ${{ github.event.pull_request.number }}
24+
REPO_NAME: ${{ github.repository }}
25+
PR_TITLE: ${{ github.event.pull_request.title }}
26+
PR_BODY: ${{ github.event.pull_request.body }}
27+
run: python scripts/pr_checker_agent.py
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Security Review
2+
3+
on:
4+
pull_request:
5+
types: [opened, reopened]
6+
issue_comment:
7+
types: [created]
8+
9+
jobs:
10+
security-review:
11+
runs-on: ubuntu-latest
12+
13+
# Always runs on PR creation
14+
# Also runs if comment on PR contains "/security-review"
15+
if: >
16+
github.event_name == 'pull_request' ||
17+
(
18+
github.event_name == 'issue_comment' &&
19+
github.event.issue.pull_request != null &&
20+
contains(github.event.comment.body, '/security-review')
21+
)
22+
23+
permissions:
24+
issues: write
25+
pull-requests: write
26+
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- uses: actions/setup-python@v5
31+
with:
32+
python-version: '3.12'
33+
34+
- run: pip install anthropic "PyGithub>=2.0"
35+
36+
- name: Run security review agent
37+
env:
38+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
39+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40+
REPO_NAME: ${{ github.repository }}
41+
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
42+
TRIGGER: ${{ github.event_name }}
43+
run: python scripts/security_review_agent.py

.github/workflows/triage.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Issue Triage
2+
on:
3+
issues:
4+
types: [opened, reopened]
5+
6+
jobs:
7+
triage:
8+
runs-on: ubuntu-latest
9+
permissions:
10+
issues: write
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-python@v5
14+
with:
15+
python-version: '3.12'
16+
- run: pip install anthropic PyGithub
17+
- name: Run triage agent
18+
env:
19+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
20+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21+
ISSUE_NUMBER: ${{ github.event.issue.number }}
22+
REPO_NAME: ${{ github.repository }}
23+
ISSUE_TITLE: ${{ github.event.issue.title }}
24+
ISSUE_BODY: ${{ github.event.issue.body }}
25+
run: python scripts/triage_agent.py

scripts/pr_checker_agent.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import os
2+
import anthropic
3+
from github import Github, Auth
4+
5+
# Setup
6+
7+
gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"]))
8+
repo = gh.get_repo(os.environ["REPO_NAME"])
9+
pr = repo.get_pull(int(os.environ["PR_NUMBER"]))
10+
author = os.environ["AUTHOR_USERNAME"]
11+
client = anthropic.Anthropic()
12+
13+
# Tools
14+
15+
TOOLS = [
16+
{
17+
"name": "post_comment",
18+
"description": (
19+
"Post a comment on the PR. Use this to welcome a first-time contributor, "
20+
"ask for a clearer description, request an issue link, or flag non-compliance "
21+
"with CONTRIBUTING.md. Combine multiple concerns into a single comment where "
22+
"possible rather than posting several separate ones."
23+
),
24+
"input_schema": {
25+
"type": "object",
26+
"properties": {
27+
"body": {"type": "string", "description": "The comment text (markdown supported)."}
28+
},
29+
"required": ["body"],
30+
},
31+
},
32+
]
33+
34+
# System prompt
35+
36+
SYSTEM_PROMPT = """You are a PR review assistant for an open-source GitHub repository.
37+
Given a newly opened PR, its author's contribution history, and the repository's CONTRIBUTING.md,
38+
you must check the following — in this order:
39+
40+
1. FIRST CONTRIBUTION: If this is the author's first contribution to the repo, welcome them warmly.
41+
Acknowledge their effort and point them to any relevant getting-started resources in CONTRIBUTING.md.
42+
43+
2. DESCRIPTION CLARITY: If the PR description is missing, too vague, or doesn't explain what
44+
the change does and why, ask for a clearer description.
45+
46+
3. LINKED ISSUE: Check whether the description contains a linked issue using keywords like
47+
"Fixes #N", "Closes #N", "Resolves #N", or "Related to #N". If no issue is linked,
48+
ask the author to either link an existing issue or create a new one.
49+
50+
4. CONTRIBUTING.md COMPLIANCE: Check whether the PR description follows the structure or
51+
requirements defined in CONTRIBUTING.md. If it doesn't comply, quote the relevant section
52+
and point out specifically what needs to change.
53+
54+
Important rules:
55+
- If multiple concerns apply, combine them into a single comment — never post more than one.
56+
- If everything looks good, stay silent. Do not post a comment just to say things look fine.
57+
- Be warm and constructive, never demanding. Remember this may be someone's first open-source contribution.
58+
- When referencing CONTRIBUTING.md requirements, be specific — quote or paraphrase the rule,
59+
don't just say "please read the contributing guide".
60+
- Most importantly, be as succint as possible."""
61+
62+
# GitHub helpers
63+
64+
def get_contributing_md() -> str:
65+
"""Fetches CONTRIBUTING.md from the repo root, or returns a notice if absent."""
66+
try:
67+
contents = repo.get_contents("CONTRIBUTING.md")
68+
return contents.decoded_content.decode("utf-8")
69+
except Exception:
70+
return "(No CONTRIBUTING.md found in this repository.)"
71+
72+
73+
def is_first_contribution() -> bool:
74+
"""Returns True if the author has no previously merged PRs in this repo."""
75+
first_contribution_list = ['FIRST_TIMER', 'FIRST_TIME_CONTRIBUTOR', 'NONE']
76+
if os.environ["AUTHOR_ASSOCIATION"] in first_contribution_list:
77+
return True
78+
return False
79+
80+
81+
def post_comment(body: str) -> str:
82+
pr.create_issue_comment(body)
83+
return "Comment posted."
84+
85+
# Tool dispatch
86+
87+
def handle_tool_call(name: str, inputs: dict) -> str:
88+
if name == "post_comment":
89+
result = post_comment(inputs["body"])
90+
else:
91+
result = f"Unknown tool: {name}"
92+
93+
print(f"[tool] {name}: {result}")
94+
return result
95+
96+
# Agentic loop
97+
98+
def build_initial_message() -> str:
99+
first_contribution = is_first_contribution()
100+
contributing_md = get_contributing_md()
101+
102+
return (
103+
f"Please review this newly opened PR:\n\n"
104+
f"Title: {os.environ['PR_TITLE']}\n"
105+
f"Author: {author} ({'first-time contributor' if first_contribution else 'returning contributor'})\n"
106+
f"Description:\n{os.environ.get('PR_BODY') or '(no description provided)'}\n\n"
107+
f"---\n"
108+
f"CONTRIBUTING.md contents:\n\n"
109+
f"{contributing_md}"
110+
)
111+
112+
113+
def run_pr_review_agent():
114+
messages = [{"role": "user", "content": build_initial_message()}]
115+
116+
while True:
117+
response = client.messages.create(
118+
model="claude-sonnet-4-20250514",
119+
max_tokens=1024,
120+
system=SYSTEM_PROMPT,
121+
tools=TOOLS,
122+
messages=messages,
123+
)
124+
125+
for block in response.content:
126+
if block.type == "text" and block.text:
127+
print(f"[agent] {block.text}")
128+
129+
messages.append({"role": "assistant", "content": response.content})
130+
131+
if response.stop_reason == "end_turn":
132+
break
133+
134+
tool_results = []
135+
for block in response.content:
136+
if block.type != "tool_use":
137+
continue
138+
result = handle_tool_call(block.name, block.input)
139+
tool_results.append({
140+
"type": "tool_result",
141+
"tool_use_id": block.id,
142+
"content": result,
143+
})
144+
145+
messages.append({"role": "user", "content": tool_results})
146+
147+
148+
if __name__ == "__main__":
149+
run_pr_review_agent()

0 commit comments

Comments
 (0)