איך להריץ סוכן AI לסקירת PR על שרת VPS פרטי
למדו איך להגדיר סוכן לסקירת קוד ב-GitHub שרץ על שרת VPS שבבעלותכם. המדריך כולל הגדרת GitHub Actions runner, סינון לפי גודל קבצים, והטמעת הערות inline בטוחות ללא צד שלישי.
מה עושה סוכן לסקירת PR באירוח עצמי
סוכן לסקירת PR באירוח עצמי הוא תוכנית קטנה הפועלת על שרת שבבעלותך. הסוכן קורא את ה-diff של ה-pull request (PR) ושולח רק את השורות ששונו למודל. התוצאה שחוזרת מהמודל מפורסמת כהערות סקירה מוטמעות (inline). הסוכן לעולם אינו מבצע checkout לענף (branch) שלך, והוא לעולם אינו קורא קובץ שה-pull request לא נגע בו. פרטי ההזדהות היחידים שהוא מחזיק הם מפתח API אחד של המודל ו-token אחד שמאפשר רק להוסיף הערות ולא שום פעולה אחרת.
מודל מסוגל לקרוא diff. החלק הזה פתור. מה שחשוב הוא לאן ה-diff מגיע ומי מחזיק במפתח. בוט סקירה מנוהל (hosted) משמעו שכל diff מכל מאגר פרטי עוזב את הרשת שלך, מגיע ללוגים של צד שלישי, ונשמר תחת מדיניות שמירת הנתונים שלהם. ב-VPS (virtual private server) שבבעלותך, ה-diff עובר מ-GitHub לשרת שלך ומשם ל-API של המודל, ואתה יכול לקרוא את ארבעים שורות הקוד שמחליטות מה נשלח.
דרישות קדם
- שרת VPS המריץ Ubuntu 24.04 עם GitHub Actions runner בהתקנה עצמית שכבר רשום למאגר. בעת הרישום, הוסיפו לו את התווית
pr-review, כיוון שזרימת העבודה (workflow) להלן מבצעת בחירה על סמך תווית זו. - מפתח API של Anthropic מתוך ה-Claude Console.
- מאגר שבו יש לכם שליטה על מי מורשה לפתוח בקשת משיכה (pull request). מאגר פרטי הוא המקרה הפשוט. סעיף ה-fork להלן מכסה את המקרה הציבורי, והפתרון שם נוח פחות.
התקנת ה-reviewer על ה-VPS
שירות ה-runner פועל תחת חשבון המשתמש ללא הרשאות מיוחדות שיצרת בעת הרצת ./svc.sh install. התקן את ה-reviewer תחת אותו חשבון משתמש כדי שהמשימה תוכל להריץ אותו ללא צורך ב-sudo. החלף את runner להלן בשם החשבון שלך.
sudo apt update && sudo apt install -y gh python3-venv
sudo install -d -m 755 -o runner -g runner /opt/pr-review
sudo -u runner python3 -m venv /opt/pr-review/venv
sudo -u runner /opt/pr-review/venv/bin/pip install anthropic
gh --versionהפקודה gh --version מציגה gh version 2.45.0 ב-Ubuntu 24.04 נכון לאוגוסט 2026. כל גרסה מ-2.20 ומעלה כוללת את הדגל --input שבו נעשה שימוש להלן. הודעה מסוג Command 'gh' not found משמעותה שרכיב ה-universe אינו מופעל, לכן הרץ את sudo add-apt-repository universe ונסה שוב.
היכן נשמרים המפתח והאסימון
שני סודות, שני מחזורי חיים שונים. אף אחד מהם אינו נשמר במאגר הקוד (repository).
ANTHROPIC_API_KEY הוא סוד של מאגר הקוד, המוגדר תחת Settings, לאחר מכן Secrets and variables, ולבסוף Actions. GitHub מצפינה אותו ומזריקה אותו לסביבת הריצה של השלב בזמן הביצוע. הוא לעולם אינו קובץ על הדיסק ולעולם אינו מופיע בהיסטוריית ה-git.
GITHUB_TOKEN פועל בצורה שונה. Actions מנפיקה אסימון חדש עבור כל משימה (job) ומשמידה אותו עם סיום המשימה. מה שהאסימון רשאי לעשות נקבע על ידי בלוק ה-permissions: בתוך ה-workflow, ולכן כאן בא לידי ביטוי עקרון הסמכויות המצומצמות (least privilege):
permissions:
contents: read
pull-requests: writeאסימון זה יכול לפרסם ביקורת (review). הוא אינו יכול לבצע push ל-commit, למזג ענף (branch), לערוך קובץ workflow או לגעת במאגר קוד אחר. סוכן שיכול להוסיף תגובה הוא מבקר (reviewer). סוכן שיכול לבצע push הוא בעל הרשאת כתיבה (committer), ואף אחד לא הסכים לכך. התייחסו למפתח המודל באותה רמת זהירות, כיוון שהוא מוציא כספים מחשבונכם. מידע נוסף על סוג זה של בעיות ניתן למצוא ב-שמירה על סודות מחוץ להישג ידם של סוכני AI.
Actions מחליפה את מחרוזת הסוד המדויקת ב-*** בתוך לוגים של משימות. ההחלפה מתבצעת עבור המחרוזת המדויקת בלבד, לכן מפתח שעבר קידוד base64, פוצל לשתי שורות, או הודפס תו אחר תו, יופיע בטקסט גלוי. אל תוסיפו שלב ניפוי שגיאות (debug) שמדפיס את כל משתני הסביבה.
מדוע Pull Request מתוך Fork לעולם לא יראה את מפתח ה-API שלך
הכלל של GitHub הוא פשוט: למעט GITHUB_TOKEN, סודות (secrets) אינם מועברים ל-runner כאשר Workflow מופעל מתוך מאגר (repository) מפוצל (fork). לכן, הרצה של pull_request מתוך fork מתחילה את הסקריפט שלך ללא ANTHROPIC_API_KEY, וקריאת ה-API הראשונה נכשלת עם invalid x-api-key.
הפתרון המפתה הוא לשנות את הטריגר ל-pull_request_target, שרץ בהקשר של המאגר המקורי (base repository) ואכן מקבל את הסודות. אל תעשה זאת כאן. הנחיות האבטחה של GitHub קובעות כי Workflow כאלו "הם בעלי הרשאות גבוהות, מה שאומר שהם חולקים את אותו מטמון (cache) של הענף הראשי עם Workflow אחרים בעלי הרשאות, ועשויים להיות בעלי גישת כתיבה למאגר וגישה לסודות המוגדרים בו", וכי ניתן "לנצל זאת כדי להשתלט על מאגר".
אותן הנחיות מבהירות לגבי ה-runner: "אין להשתמש כמעט לעולם ב-Self-hosted runners עבור מאגרים ציבוריים ב-GitHub, מכיוון שכל משתמש יכול לפתוח Pull Request מול המאגר ולפרוץ את הסביבה".
עובדה זו מכתיבה שתי בחירות תכנוניות. העבודה (job) כוללת מנגנון הגנה כך שהיא רצה רק על ענפים (branches) שנדחפו למאגר שלך. כמו כן, ל-Workflow אין שלב actions/checkout כלל. הסוכן (agent) לעולם לא מחזיק את הענף על הדיסק, כך ש-Pull Request עוין הוא רק טקסט שנשלח למודל. הוא אינו יכול להריץ סקריפט בנייה על ה-VPS שלך, מכיוון ששום דבר ב-VPS שלך לא מריץ אותו. עם זאת, טקסט אינו בהכרח מזיק: Diff שנכתב על ידי זר הוא קלט לא מהימן המגיע למודל, זהו אותו גבול אמון שאתה פוגש כאשר אתה נותן לסוכן לבצע חיפוש באינטרנט, והדבר היחיד שמכיל אותו כאן הוא שהסוכן הזה אינו יכול לעשות דבר מלבד לפרסם תגובה.
משיכת ה-diff במקום המאגר
בקשה אחת מחזירה את ה-diff המלא כטקסט פשוט.
export GH_TOKEN=your_token # in the workflow this comes from secrets.GITHUB_TOKEN
gh api /repos/OWNER/REPO/pulls/42 -H "Accept: application/vnd.github.diff"סוג המדיה Accept: application/vnd.github.diff הוא זה שהופך את התגובה מאובייקט JSON המתאר את ה-pull request ל-unified diff עצמו, ו-gh api מדפיס את גוף התגובה ללא שינוי. השורה הראשונה שתראו אמורה להתחיל ב-diff --git a/. השגיאה gh: Not Found (HTTP 404) מציינת שה-token אינו יכול לראות את המאגר, מה שבמקרה של fine-grained personal token אומר כמעט תמיד שההרשאה ל-Pull requests לא הוגדרה.
סינון לפני ניצול token
סעיף זה הוא ההבדל בין בוט שאנשים קוראים לבין בוט שאנשים משתיקים. כל מסנן להלן מופעל לפני שהמודל רואה ולו בייט אחד.
- מסנני נתיבים (Path filters). נעלו קבצים, ספריות צד-שלישי (vendored directories), חבילות שעברו minification וקוד שנוצר אוטומטית. הערה של מודל על
package-lock.jsonהיא רעש טהור, וקבצים אלו מהווים לרוב את רוב הבייטים ב-diff. - מכסת גודל (Size cap). מעבר למכסה, דלגו על הסקירה וצאו בסטטוס תקין. refactor של 4,000 שורות יקבל שורה אחת כנה המציינת שהקוד גדול מכדי להיסקר אוטומטית, במקום שישים ניחושים.
- סף חומרה ומכסת הערות. דווחו על ממצאים ברמת חומרה גבוהה ובינונית, עד עשרה ממצאים, לפי סדר חומרה יורד. איש אינו קורא את ההערה האחת-עשרה.
הסקריפט
שמרו זאת כ-/opt/pr-review/review.py. הסקריפט קורא את התצורה שלו מתוך משתני הסביבה, כך שניתן להחליף מודלים בתהליך העבודה ללא צורך בשינוי קוד.
#!/usr/bin/env python3
"""Review only the changed lines of one pull request."""
import json
import os
import subprocess
import sys
import anthropic
REPO = os.environ["GITHUB_REPOSITORY"]
PR = os.environ["PR_NUMBER"]
MODEL = os.environ.get("REVIEW_MODEL", "claude-haiku-4-5-20251001")
MAX_DIFF_BYTES = int(os.environ.get("MAX_DIFF_BYTES", "120000"))
MIN_SEVERITY = os.environ.get("MIN_SEVERITY", "medium")
MAX_COMMENTS = 10
RANK = {"low": 0, "medium": 1, "high": 2}
SKIP = ("package-lock.json", "poetry.lock", "/vendor/", "/node_modules/", ".min.js")
raw_diff = subprocess.run(
["gh", "api", f"/repos/{REPO}/pulls/{PR}",
"-H", "Accept: application/vnd.github.diff"],
check=True, capture_output=True, text=True,
).stdoutפיצול ה-diff לפי קובץ הוא מה שמאפשר סינון נתיבים. מספור כל שורה הוא מה שמאפשר להערות הביקורת להופיע במקומן. GitHub מקבלת הערה מוטמעת רק בשורה שהיא חלק מה-diff, לכן על המודל לציין מספר שורה אמיתי. העברת המספרים למודל מאפשרת לו להעתיק מספר קיים במקום להמציא אחד.
def per_file(diff_text):
"""Split a unified diff into one string per file."""
sections, current = [], []
for line in diff_text.splitlines():
if line.startswith("diff --git ") and current:
sections.append("\n".join(current))
current = []
current.append(line)
if current:
sections.append("\n".join(current))
return sections
def annotate(section):
"""Prefix every line that exists in the new file with its line number."""
out, n, in_hunk = [], 0, False
for line in section.splitlines():
if line.startswith("@@"):
n = int(line.split("+")[1].split(",")[0].split(" ")[0])
in_hunk = True
out.append(line)
elif not in_hunk or line.startswith(("-", "\\")):
out.append(line)
else:
out.append(f"{n}\t{line}")
n += 1
return "\n".join(out)
kept = [s for s in per_file(raw_diff)
if not any(p in s.split("\n", 1)[0] for p in SKIP)]
payload = "\n".join(annotate(s) for s in kept)
if not payload.strip():
print("every changed file was filtered out")
raise SystemExit(0)
if len(payload) > MAX_DIFF_BYTES:
print(f"diff is {len(payload)} bytes, over the {MAX_DIFF_BYTES} cap")
raise SystemExit(0)כותרת ה-hunk נושאת את המספור. @@ -12,7 +12,9 @@ מציין שה-hunk של הקובץ החדש מתחיל בשורה 12, לכן המונה מתחיל שם ומתקדם רק בשורות שנוספו או שלא השתנו. שורות שהוסרו עוברות ללא מספור, כיוון שהן אינן קיימות בקובץ החדש. הבדיקה על שורות המתחילות בלוכסן אחורי מדלגת על סמן ה-no-newline ש-git כותב בסוף קובץ, אשר ללא סינון זה היה מסיט כל מספר עוקב ב-1.
שתי היציאות משתמשות בסטטוס 0, ולא ב-1. בקשת משיכה (pull request) שסוננה או שהיא גדולה מדי צריכה להציג סימון ירוק. סימון אדום שאדם אינו יכול לפעול לפיו זוכה להתעלמות, וברגע שסימון אחד זוכה להתעלמות, כך קורה לכולם.
SYSTEM = (
"You review one pull request diff. Every line that exists in the new file is "
"prefixed with its line number and a tab character. "
"Report only defects you can see in the lines shown: a crash, a resource leak, "
"a security mistake, a wrong boundary condition, a broken contract with code "
"that is visible in this diff. Do not comment on style, naming or formatting. "
"Do not guess about code you cannot see. Leave out anything you are not "
"certain about. An empty findings list is a normal and common answer. "
'Reply with JSON only, in this shape: {"findings": [{"path": "src/app.py", '
'"line": 42, "severity": "high", "comment": "what is wrong, then why"}]} '
"Every line number must be one you can see in the left column of that file."
)
client = anthropic.Anthropic()
message = client.messages.create(
model=MODEL,
max_tokens=2000,
system=SYSTEM,
messages=[{"role": "user", "content": payload}],
)
print(f"stop={message.stop_reason} in={message.usage.input_tokens} "
f"out={message.usage.output_tokens}", file=sys.stderr)
text = message.content[0].text
findings = json.loads(text[text.find("{"):text.rfind("}") + 1])["findings"]
findings = [f for f in findings if RANK.get(f["severity"], 0) >= RANK[MIN_SEVERITY]]
findings.sort(key=lambda f: -RANK.get(f["severity"], 0))
del findings[MAX_COMMENTS:]
if not findings:
print("nothing above the severity threshold; posting no comment")
raise SystemExit(0)
review = {
"event": "COMMENT",
"body": f"Automated review of the changed lines. {len(findings)} finding(s).",
"comments": [
{"path": f["path"].removeprefix("b/"), "line": f["line"], "side": "RIGHT",
"body": f"**{f['severity']}** {f['comment']}"}
for f in findings
],
}
subprocess.run(
["gh", "api", "-X", "POST", f"/repos/{REPO}/pulls/{PR}/reviews", "--input", "-"],
input=json.dumps(review), text=True, check=True,
)שלושה פרטים בבלוק זה הם קריטיים לפעולה. ה-JSON נחתך בין ה-{ הראשון ל-} האחרון, כיוון שמודל לעיתים עוטף את תשובתו בתוך בלוק קוד, ו-json.loads נכשל מול בלוק כזה. ה-path עובר הסרה של ה-b/ המוביל, כיוון שקידומת זו מגיעה מכותרת ה-diff ו-GitHub דורשת נתיב יחסי למאגר. ו---input - שולח את כל הביקורת כקריאת API אחת, כך שעשרה ממצאים מגיעים כהתראה אחת במקום כעשר.
כאשר אין מה לדווח, הסקריפט אינו מפרסם דבר. בוט שכותב "לא נמצאו בעיות" בכל בקשת משיכה מרגיל אנשים לדלג עליו, ואז הם ידלגו גם על ההודעה החשובה באמת.
שילוב בתהליך העבודה
שמרו זאת כ-.github/workflows/pr-review.yml:
name: pr-review
on:
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- '**.md'
- 'docs/**'
permissions:
contents: read
pull-requests: write
concurrency:
group: pr-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
if: github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'no-ai-review')
runs-on: [self-hosted, linux, pr-review]
steps:
- name: Review the changed lines
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REVIEW_MODEL: claude-haiku-4-5-20251001
MIN_SEVERITY: medium
run: /opt/pr-review/venv/bin/python /opt/pr-review/review.pyהמשתנה GITHUB_REPOSITORY אינו מופיע בבלוק ה-env: הזה, כיוון ש-Actions מגדיר אותו עבור כל job באופן אוטומטי. לקבוצת ה-concurrency יש חשיבות לעלויות: ללא הגדרה זו, דחיפה של שלושה תיקונים מהירים לאותו branch תריץ שלוש בדיקות מלאות ותחויבו על כולן, בעוד שעם הגדרה זו רק האחרונה תתבצע.
השורה if: מבצעת שתי פעולות. החלק הראשון מדלג על pull requests שמגיעים מ-forks, שבין כה וכה היו נכשלים בהיעדר מפתח. החלק השני מעניק לצוות שלכם אפשרות השבתה: הוסיפו את התווית no-ai-review ל-pull request וה-job לא ירוץ.
פתחו pull request וצפו בתוצאות:
gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --commentsהרצה שמסתיימת תוך שניות ספורות עם nothing above the severity threshold; posting no comment בלוג מעידה על פעולה תקינה. ב-pull request קטן ונקי, זו התוצאה המצופה.
מהי העלות של סקירת Pull Request אוטומטית?
ה־diff מהווה כמעט את כל הקלט, לכן גודל ה־diff הוא שקובע את המחיר. להלן מדידה של diff באורך 500 שורות בתוספת ה-system prompt, שחושבו באמצעות ה-endpoint לספירת טוקנים ולא על בסיס הערכה.
The data behind this chart
[
{
"label": "Haiku 4.5",
"input_tokens": "8,000",
"output_tokens": "1,200"
},
{
"label": "Sonnet 5",
"input_tokens": "10,400",
"output_tokens": "1,560"
},
{
"label": "Opus 5",
"input_tokens": "10,400",
"output_tokens": "1,560"
}
]ה־diff הזה הגיע ל-8,000 טוקני קלט ב-Haiku 4.5 ו-10,400 ב-Sonnet 5. אותו טקסט, ספירה שונה. מודלי Claude מגרסה 4.7 ואילך משתמשים ב-tokenizer חדש יותר שמייצר כ-30% יותר טוקנים עבור אותו קלט, כפי שמתועד בדף התמחור של Anthropic. קחו זאת בחשבון בכל פעם שאתם משווים מודל חדש מול מודל ישן על בסיס מחיר למיליון טוקנים בלבד.
מחירון נכון לאוגוסט 2026: Haiku 4.5 עולה $1 למיליון טוקני קלט ו-$5 למיליון טוקני פלט. Sonnet 5 עולה $2 ו-$10 במסגרת תמחור היכרות התקף עד ה-31 באוגוסט 2026, ולאחר מכן $3 ו-$15. Opus 5 עולה $5 ו-$25.
The data behind this chart
[
{
"label": "Haiku 4.5",
"cost_per_pr_cents": 1.4,
"cost_200_prs_usd": "2.80"
},
{
"label": "Sonnet 5",
"cost_per_pr_cents": 3.64,
"cost_200_prs_usd": "7.28"
},
{
"label": "Opus 5",
"cost_per_pr_cents": 9.1,
"cost_200_prs_usd": "18.20"
}
]מדובר ב-1.4 סנט לכל Pull Request ב-Haiku 4.5 וב-9.1 סנט ב-Opus 5. צוות שממזג 200 בקשות Pull Request בחודש ישלם כ-$2.80 ב-Haiku 4.5, $7.28 ב-Sonnet 5, או $18.20 ב-Opus 5. החל מה-1 בספטמבר 2026, יש להכפיל את השורה של Sonnet 5 ב-1.5.
שני גורמים עשויים להעלות את החשבונית בפועל מעבר להערכה זו. ה-synchronize מפעיל סקירות בכל push, כך שענף (branch) פעיל עם שמונה פעולות push יעלה כשמונה סקירות, וכלל ה-concurrency עוזר רק כאשר ה-pushes מתבצעים בסמיכות זמנים. הנתונים גם מניחים שמסנני הנתיבים (path filters) עובדים: קובץ lock אחד ללא סינון יכול להכפיל את הקלט בעצמו.
Prompt caching לא עוזר כאן. ה-prefix השמור חייב להיות זהה ברמת הבייט בין קריאה לקריאה, וה-diff משתנה בכל פעם. ה-system prompt הוא החלק היציב היחיד, והוא קטן בהרבה מהאורך המינימלי הנדרש ל-caching. לכלל הכללי, ראו מתי prompt caching משתלם, ולבחירה בין שלושת המודלים לעיל, ראו באיזה מודל Claude להשתמש לכל משימה.
מדדו את ה-diffs שלכם לפני ההפעלה
הוסיפו את השורה הזו לאחר ש-payload נבנה, ולאחר מכן הריצו את הסקריפט ידנית על כמה מה-Pull Requests של החודש שעבר:
print(client.messages.count_tokens(
model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)ה-endpoint לספירה אינו מריץ את המודל, לכן הוא אינו צורך טוקני קלט או פלט, והוא משתמש ב-tokenizer השייך למודל שציינתם. הריצו אותו על עשרה Pull Requests אמיתיים מהמאגר שלכם וקחו את החציון ולא את הממוצע, כדי שמיגרציה אחת ענקית לא תעוות את ההערכה.
מדוע בוטים לסקירת קוד מושתקים, וכיצד להימנע מכך
שתי התנהגויות פוגעות באמון בבוטים אלו, ולשתיהן יש פתרון בקוד שלעיל.
סקירה של הכל בבת אחת. בוט שמשאיר ארבעים הערות לא יזכה לכך שאף אחת מהן תיקרא. סף החומרה ומכסת עשר ההערות אינם עניין של נימוס, אלא האמצעי ששומר על הממצאים האמיתיים גלויים. מיון לפי חומרה לפני הקיטום מבטיח שהמכסה תסיר את הממצאים הפחות חשובים במקום עשרה ממצאים אקראיים.
מתן הערות בביטחון על נושאים שהבוט אינו יכול לבדוק. זו ההתנהגות שגורמת למהנדסים לכבות את הבוט לצמיתות. מודל שמוצגות לו 200 שורות מתוך בסיס קוד של 40,000 שורות עדיין יכתוב "זה שובר את מנגנון ה-cache invalidation ב-redis_client.py" על קובץ שהוא מעולם לא ראה. ה-system prompt מציב גבול ברור בשפה פשוטה: דווח רק על פגמים הנראים לעין בשורות המוצגות, והימנע מכל דבר שאינך בטוח לגביו. ציון הכשל באופן ישיר עובד טוב יותר מאשר בקשה כללית לדיוק, והבהרה למודל שתוצאה ריקה היא מצב תקין היא שמונעת ממנו להמציא דברים לומר על שינוי של שתי שורות.
פרסמו את הסקירה כ-COMMENT, לעולם לא כ-REQUEST_CHANGES. דעתו של מודל לא אמורה להיות מסוגלת לחסום מיזוג (merge), וברגע שהיא תוכל, מישהו שנמצא תחת לחץ של דדליין יסיר את כל ה-workflow במקום להתווכח איתו.
מצבי כשל והודעות שגיאה נפוצות
שגיאת HTTP 422 בעת שליחת הביקורת. gh מדפיס gh: Unprocessable Entity (HTTP 422) וגוף התגובה מציין את השדה הבעייתי: Pull request review thread line must be part of the diff. GitHub אינו יכול לעגן את ההערה. הסיבות הנפוצות הן מספר שורה שהמודל המציא, path שעדיין נושא את הקידומת b/, או הערה על שורה שהוסרה, מה שמחייב הגדרת side ל-LEFT במקום RIGHT. הדפיסו את ה-JSON של הביקורת לפני השליחה ובדקו הערה אחת ידנית מול ה-diff.
invalid x-api-key מה-API של המודל. השלב נכשל בקריאת ה-messages.create הראשונה. או שהסוד ANTHROPIC_API_KEY לא הוגדר במאגר, או שה-pull request הגיע מ-fork, ולכן Actions לא העביר סודות כלל. מנגנון ההגנה מפני fork בשורה if: אמור היה לדלג על כך, לכן בדקו שורה זו תחילה.
gh: Resource not accessible by integration (HTTP 403). ה-token של ה-job אינו מורשה לכתוב ל-pull requests. הוסיפו את pull-requests: write לבלוק ה-permissions:. אם ההגדרה כבר קיימת, בדקו ב-Settings, לאחר מכן ב-Actions, ולבסוף ב-General; ייתכן שמדיניות הארגון מגבילה את ההרשאות שניתן לבקש עבור כל workflow token.
json.decoder.JSONDecodeError. המודל לא החזיר JSON תקין. הסיבה הנפוצה היא תגובה שהגיעה למכסת ה-tokens ונקטעה באמצע אובייקט. שורת הלוג מדפיסה stop_reason בדיוק עבור מקרה זה: ערך של max_tokens משמעו שיש להעלות את max_tokens או להוריד את MAX_COMMENTS.
ה-workflow אינו רץ כלל. gh run list אינו מציג דבר עבור ה-pull request. ודאו ש-paths-ignore לא סינן את כל הקבצים ששונו, בדקו את מנגנון ההגנה על ה-fork וה-label, ולאחר מכן ודאו שה-runner פעיל באמצעות sudo systemctl status 'actions.runner.*' ב-VPS. runner לא מקוון משאיר את ה-job במצב המתנה (queued) ללא הודעת שגיאה ב-pull request.
כל ביקורת חוזרת ריקה. הגדירו את MIN_SEVERITY ל-low עבור הרצה אחת. אם ממצאים מופיעים, סף הסינון פועל כשורה. אם לא מופיע דבר, הדפיסו את payload וודאו שהפילטרים לא הסירו את כל ה-diff.
הרצת הסוכן לצד סוכנים אחרים
ה־reviewer הוא קטן, ולכן מפתה להריץ אותו על השרת שכבר מריץ את כל השאר. עם זאת, יש להפריד אותו אם המאגר (repository) הוא בעל חשיבות. תהליך זה מחזיק אסימון (token) שיכול להגיב על הקוד שלכם ומפתח (key) שיכול להוציא כספים, וסוכן שמתארח באופן עצמי (self-hosted runner) הוא מטבעו מקום שבו מורץ קוד של תהליכי עבודה (workflow). חשבון ייעודי ללא הרשאות מיוחדות ללא זכויות sudo, על מארח שלא מריץ שום דבר אחר, הוא קו הבסיס. אם אתם מריצים גם סוכנים אינטראקטיביים שמבצעים checkout לקוד, מכונה וירטואלית חד-פעמית לכל סוכן היא התבנית המומלצת, ו-הרצת סוכן כתיבת קוד על VPS מכסה את התצורה הכללית. אם ה-API של Anthropic חדש לכם, אפליקציית Claude API ראשונה על VPS היא נקודת התחלה קטנה יותר מאשר זו.
FAQ
האם סוכן ביקורת קוד מבוסס AI זקוק להרשאות כתיבה למאגר שלי?
לא. הוא זקוק ל-pull-requests: write כדי לפרסם ביקורת ול-contents: read כדי למשוך את ה-diff. זוהי הרשימה המלאה, ואתה מגדיר אותה בבלוק ה-permissions: של ה-workflow, אשר מגביל את מה ש-GITHUB_TOKEN של כל job יכול לבצע. עם שתי שורות אלו, הסוכן יכול להגיב על pull request אך אינו יכול לבצע push ל-commit או למזג (merge) ענף. פרסם ביקורות באמצעות event: COMMENT במקום REQUEST_CHANGES, כך שהוא גם לא יוכל לחסום מיזוג.
מדוע תגובת הביקורת שלי נכשלת עם שגיאת HTTP 422?
GitHub מקבלת תגובת ביקורת מוטמעת (inline) רק בשורה שהיא חלק מה-diff של ה-pull request, ומחזירה Pull request review thread line must be part of the diff כאשר זה אינו המצב. ודא ש-path הוא יחסי למאגר ללא קידומת b/ מתוך כותרת ה-diff, ושהשורה מופיעה בתוך מקטע (hunk) של אותו קובץ. ה-side חייב להיות RIGHT עבור שורה שנוספה או שלא השתנתה, ו-LEFT עבור שורה שהוסרה. הוספת מספר השורה מהקובץ החדש לכל שורה ב-diff לפני השליחה למודל היא הפעולה שמונעת מהמודל להמציא מספרים מלכתחילה.
האם ניתן להריץ זאת על מאגר ציבורי עם pull requests מ-forks?
לא בתצורה זו. GitHub אינה מעבירה secrets ל-workflow שמופעל מ-fork, לכן מפתח המודל חסר וההרצה נכשלת. GitHub גם מציינת שאין להשתמש ב-self-hosted runners עבור מאגרים ציבוריים, כיוון שכל אחד יכול לפתוח pull request שיגרום להרצת קוד על המכונה שלך. עבור פרויקט ציבורי, הגבל את המבקר לענפים שנדחפו (pushed) למאגר עצמו, מה ש-if: מבצע, או העבר את שלב הביקורת ל-GitHub-hosted runner וקבל את העובדה שה-diff עוזב את התשתית שלך.
באיזה מודל כדאי להשתמש לביקורת pull request?
התחל עם Haiku 4.5. קריאת diff מוגבל בגודלו מול רשימה קבועה של סוגי פגמים אינה בעיית הסקה מורכבת, והמודל הזול ביותר שומר על החשבונית החודשית במספר שאיש לא יתווכח עליו. עברו ל-Sonnet 5 אם אתם מגלים שהוא מפספס באגים אמיתיים בשפה או ב-framework שלכם, ומדדו זאת במקום להניח זאת מראש. Opus 5 הוא היקר מבין השלושה בפער ניכר לכל pull request, מה שקל יותר להצדיק בענף release מאשר בכל push לכל ענף feature.