From 967788e045a64c2bf5ee6be870de5386d8e49499 Mon Sep 17 00:00:00 2001 From: OliverGiertz Date: Wed, 11 Mar 2026 08:30:26 +0000 Subject: [PATCH] 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) --- .github/workflows/repo-pipeline.yml | 189 +++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 2 deletions(-) diff --git a/.github/workflows/repo-pipeline.yml b/.github/workflows/repo-pipeline.yml index dc19bab..c3a5170 100644 --- a/.github/workflows/repo-pipeline.yml +++ b/.github/workflows/repo-pipeline.yml @@ -181,9 +181,194 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - pull-requests: read - issues: read + pull-requests: write + issues: write 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 + + + + 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 + + + + 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 uses: actions/github-script@v7 with: