feat(ai-review): automate Claude and ChatGPT review generation
New steps before validation: - 'Generate Claude review': calls Anthropic API (claude-opus-4-6), posts formatted comment with required DoD/Blocker/Major structure - 'Generate ChatGPT review': calls OpenAI API (gpt-4o), same format - Both steps skip gracefully if API key secret is not set - Idempotent: skips generation if review comment already exists - Validation step remains unchanged as final gate Required secrets in consumer repo: ANTHROPIC_API_KEY, OPENAI_API_KEY Permission updated: pull-requests/issues write (needed to post comments)
This commit is contained in:
parent
ec14acb46e
commit
967788e045
1 changed files with 187 additions and 2 deletions
189
.github/workflows/repo-pipeline.yml
vendored
189
.github/workflows/repo-pipeline.yml
vendored
|
|
@ -181,9 +181,194 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: read
|
pull-requests: write
|
||||||
issues: read
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
|
- name: Generate Claude review
|
||||||
|
env:
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
|
||||||
|
echo "::notice::ANTHROPIC_API_KEY not set – Claude review skipped"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import os, json, urllib.request
|
||||||
|
|
||||||
|
token = os.environ["GITHUB_TOKEN"]
|
||||||
|
repo = os.environ["REPO"]
|
||||||
|
pr_num = os.environ["PR_NUMBER"]
|
||||||
|
headers_gh = {"Authorization": f"Bearer {token}", "User-Agent": "vanity-dev-engine",
|
||||||
|
"Accept": "application/vnd.github.v3+json"}
|
||||||
|
|
||||||
|
# Skip if Claude review already exists
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"https://api.github.com/repos/{repo}/issues/{pr_num}/comments?per_page=100",
|
||||||
|
headers=headers_gh)
|
||||||
|
with urllib.request.urlopen(req) as r:
|
||||||
|
comments = json.loads(r.read())
|
||||||
|
if any("### Claude" in (c.get("body") or "") for c in comments):
|
||||||
|
print("Claude review already present – skipping generation.")
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
# Fetch PR diff (truncated to 12 000 chars to stay within token limit)
|
||||||
|
req_diff = urllib.request.Request(
|
||||||
|
f"https://api.github.com/repos/{repo}/pulls/{pr_num}",
|
||||||
|
headers={**headers_gh, "Accept": "application/vnd.github.v3.diff"})
|
||||||
|
with urllib.request.urlopen(req_diff) as r:
|
||||||
|
diff = r.read().decode("utf-8", errors="replace")[:12000]
|
||||||
|
|
||||||
|
# Fetch PR body
|
||||||
|
req_pr = urllib.request.Request(
|
||||||
|
f"https://api.github.com/repos/{repo}/pulls/{pr_num}", headers=headers_gh)
|
||||||
|
with urllib.request.urlopen(req_pr) as r:
|
||||||
|
pr_data = json.loads(r.read())
|
||||||
|
pr_body = (pr_data.get("body") or "")[:800]
|
||||||
|
|
||||||
|
prompt = f"""You are a senior iOS Swift developer reviewing a pull request.
|
||||||
|
Analyse the changes carefully and write a concise code review.
|
||||||
|
|
||||||
|
PR title: {os.environ["PR_TITLE"]}
|
||||||
|
PR description: {pr_body}
|
||||||
|
|
||||||
|
Git diff (may be truncated):
|
||||||
|
{diff}
|
||||||
|
|
||||||
|
Reply with EXACTLY this structure – no deviations:
|
||||||
|
|
||||||
|
### Claude
|
||||||
|
|
||||||
|
DoD status: PASS
|
||||||
|
Blocker: 0
|
||||||
|
Major: 0
|
||||||
|
|
||||||
|
<your review here – cover code quality, correctness, Swift best practices,
|
||||||
|
potential bugs, and suggestions. Be specific and constructive.>
|
||||||
|
|
||||||
|
Only set DoD status to FAIL or raise Blocker/Major above 0 when you find
|
||||||
|
real defects that must be fixed before merging."""
|
||||||
|
|
||||||
|
payload = json.dumps({
|
||||||
|
"model": "claude-opus-4-6",
|
||||||
|
"max_tokens": 1500,
|
||||||
|
"messages": [{"role": "user", "content": prompt}]
|
||||||
|
}).encode()
|
||||||
|
req_ai = urllib.request.Request(
|
||||||
|
"https://api.anthropic.com/v1/messages", data=payload,
|
||||||
|
headers={"x-api-key": os.environ["ANTHROPIC_API_KEY"],
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
"content-type": "application/json"})
|
||||||
|
with urllib.request.urlopen(req_ai) as r:
|
||||||
|
review = json.loads(r.read())["content"][0]["text"]
|
||||||
|
|
||||||
|
# Post comment
|
||||||
|
body_payload = json.dumps({"body": review}).encode()
|
||||||
|
req_post = urllib.request.Request(
|
||||||
|
f"https://api.github.com/repos/{repo}/issues/{pr_num}/comments",
|
||||||
|
data=body_payload,
|
||||||
|
headers={**headers_gh, "Content-Type": "application/json"})
|
||||||
|
with urllib.request.urlopen(req_post) as r:
|
||||||
|
result = json.loads(r.read())
|
||||||
|
print(f"Claude review posted: {result['html_url']}")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
- name: Generate ChatGPT review
|
||||||
|
env:
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${OPENAI_API_KEY:-}" ]; then
|
||||||
|
echo "::notice::OPENAI_API_KEY not set – ChatGPT review skipped"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import os, json, urllib.request
|
||||||
|
|
||||||
|
token = os.environ["GITHUB_TOKEN"]
|
||||||
|
repo = os.environ["REPO"]
|
||||||
|
pr_num = os.environ["PR_NUMBER"]
|
||||||
|
headers_gh = {"Authorization": f"Bearer {token}", "User-Agent": "vanity-dev-engine",
|
||||||
|
"Accept": "application/vnd.github.v3+json"}
|
||||||
|
|
||||||
|
# Skip if ChatGPT review already exists
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"https://api.github.com/repos/{repo}/issues/{pr_num}/comments?per_page=100",
|
||||||
|
headers=headers_gh)
|
||||||
|
with urllib.request.urlopen(req) as r:
|
||||||
|
comments = json.loads(r.read())
|
||||||
|
if any("### ChatGPT" in (c.get("body") or "") for c in comments):
|
||||||
|
print("ChatGPT review already present – skipping generation.")
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
# Fetch PR diff
|
||||||
|
req_diff = urllib.request.Request(
|
||||||
|
f"https://api.github.com/repos/{repo}/pulls/{pr_num}",
|
||||||
|
headers={**headers_gh, "Accept": "application/vnd.github.v3.diff"})
|
||||||
|
with urllib.request.urlopen(req_diff) as r:
|
||||||
|
diff = r.read().decode("utf-8", errors="replace")[:12000]
|
||||||
|
|
||||||
|
# Fetch PR body
|
||||||
|
req_pr = urllib.request.Request(
|
||||||
|
f"https://api.github.com/repos/{repo}/pulls/{pr_num}", headers=headers_gh)
|
||||||
|
with urllib.request.urlopen(req_pr) as r:
|
||||||
|
pr_data = json.loads(r.read())
|
||||||
|
pr_body = (pr_data.get("body") or "")[:800]
|
||||||
|
|
||||||
|
prompt = f"""You are a senior iOS Swift developer reviewing a pull request.
|
||||||
|
Analyse the changes carefully and write a concise code review.
|
||||||
|
|
||||||
|
PR title: {os.environ["PR_TITLE"]}
|
||||||
|
PR description: {pr_body}
|
||||||
|
|
||||||
|
Git diff (may be truncated):
|
||||||
|
{diff}
|
||||||
|
|
||||||
|
Reply with EXACTLY this structure – no deviations:
|
||||||
|
|
||||||
|
### ChatGPT
|
||||||
|
|
||||||
|
DoD status: PASS
|
||||||
|
Blocker: 0
|
||||||
|
Major: 0
|
||||||
|
|
||||||
|
<your review here – cover code quality, correctness, Swift best practices,
|
||||||
|
potential bugs, and suggestions. Be specific and constructive.>
|
||||||
|
|
||||||
|
Only set DoD status to FAIL or raise Blocker/Major above 0 when you find
|
||||||
|
real defects that must be fixed before merging."""
|
||||||
|
|
||||||
|
payload = json.dumps({
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"max_tokens": 1500,
|
||||||
|
"messages": [{"role": "user", "content": prompt}]
|
||||||
|
}).encode()
|
||||||
|
req_ai = urllib.request.Request(
|
||||||
|
"https://api.openai.com/v1/chat/completions", data=payload,
|
||||||
|
headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}",
|
||||||
|
"content-type": "application/json"})
|
||||||
|
with urllib.request.urlopen(req_ai) as r:
|
||||||
|
review = json.loads(r.read())["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
# Post comment
|
||||||
|
body_payload = json.dumps({"body": review}).encode()
|
||||||
|
req_post = urllib.request.Request(
|
||||||
|
f"https://api.github.com/repos/{repo}/issues/{pr_num}/comments",
|
||||||
|
data=body_payload,
|
||||||
|
headers={**headers_gh, "Content-Type": "application/json"})
|
||||||
|
with urllib.request.urlopen(req_post) as r:
|
||||||
|
result = json.loads(r.read())
|
||||||
|
print(f"ChatGPT review posted: {result['html_url']}")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
- name: Validate ChatGPT and Claude review status
|
- name: Validate ChatGPT and Claude review status
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue