استضافة وكيل مراجعة PR بالذكاء الاصطناعي على VPS
شغّل مراجع PR على VPS تملكه باستخدام مشغّل مستضاف ذاتياً، وقيّد الطلبات بالفرق والمسارات والحجم، وانشر تعليقات مضمنة، واحسب تكلفة كل PR بدقة.
ما يفعله وكيل مراجعة PR مستضاف ذاتياً
وكيل مراجعة PR مستضاف ذاتياً هو برنامج صغير يعمل على خادم تملكه. يقرأ الفرق في طلب السحب (PR)، ويرسل الأسطر التي تغيّرت فقط إلى نموذج. ثم ينشر المخرجات كتعليقات مراجعة مضمنة. لا ينفّذ أبداً checkout لفرعك، ولا يقرأ أي ملف لم يلمسه طلب السحب. بيانات الاعتماد الوحيدة التي يحتفظ بها هي مفتاح API واحد للنموذج ورمز مميز واحد يمكنه إضافة التعليقات ولا يملك أي صلاحية أخرى.
يستطيع النموذج قراءة الفرق. هذا الجزء محسوم. المهم هو وجهة الفرق والجهة التي تحتفظ بالمفتاح. يعني روبوت المراجعة المستضاف أن كل فرق من كل مستودع خاص يغادر شبكتك، ويصل إلى سجلات طرف ثالث، ويخضع لسياسة الاحتفاظ الخاصة به. على VPS تملكه، ينتقل الفرق من GitHub إلى خادمك ثم إلى API النموذج، ويمكنك قراءة الأسطر الأربعين من الشيفرة التي تحدد ما يُرسل.
ما تحتاج إليه قبل البدء
- خادم VPS يعمل بنظام Ubuntu 24.04، ويكون مشغّل GitHub Actions مستضافاً ذاتياً مسجلاً مسبقاً في المستودع. أضف إليه التصنيف
pr-reviewعند تسجيله، لأن سير العمل أدناه يختار المشغّل وفق هذا التصنيف. - مفتاح API من Anthropic صادر عن Claude Console.
- مستودع تتحكم فيمن يمكنه فتح طلب سحب. المستودع الخاص هو الحالة الأسهل. يوضّح قسم التفرعات أدناه الحالة العامة، لكن الإجابة فيها أقل اطمئناناً.
تثبيت 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 اعتباراً من August 2026. يدعم أي إصدار يبدأ من 2.20 راية --input المستخدمة أدناه. تعني رسالة Command 'gh' not found أن مكوّن universe غير مفعّل، لذلك شغّل sudo add-apt-repository universe ثم حاول مرة أخرى.
مكان حفظ المفتاح والرمز المميز
سرّان، ولكل منهما مدة صلاحية مختلفة. لا تضع أيّاً منهما في المستودع.
ANTHROPIC_API_KEY هو سرّ خاص بالمستودع. عيّنه ضمن Settings، ثم Secrets and variables، ثم Actions. يشفّره GitHub ويحقنه في بيئة الخطوة وقت التشغيل. لا يكون هذا السر ملفاً على القرص، ولا يظهر في سجل git.
يعمل GITHUB_TOKEN بطريقة مختلفة. تنشئ Actions رمزاً مميزاً جديداً لكل مهمة، وتتلفه عند انتهاء المهمة. تحدد كتلة permissions: في سير العمل ما يستطيع هذا الرمز المميز فعله. وهنا تُطبَّق فعلياً قاعدة أقل الصلاحيات:
permissions:
contents: read
pull-requests: writeيمكن لهذا الرمز المميز نشر مراجعة. لكنه لا يستطيع دفع commit، أو دمج فرع، أو تعديل ملف سير عمل، أو الوصول إلى مستودع آخر. الوكيل الذي يستطيع التعليق هو مراجع. أما الوكيل الذي يستطيع الدفع فهو منفّذ للـcommit، ولم يوافق أحد على ذلك. تعامل مع مفتاح النموذج بالعناية نفسها، لأنه ينفق أموالاً من حسابك. يشرح إبقاء الأسرار بعيداً عن متناول وكيل الذكاء الاصطناعي مزيداً من التفاصيل حول هذا النوع من المشكلات.
تستبدل Actions سلسلة السر نفسها تماماً بـ *** في سجلات المهام. وهي تطابق السلسلة نفسها فقط. لذلك يظهر المفتاح مكشوفاً إذا شفّرته باستخدام base64، أو قسمته على سطرين، أو طبعته حرفاً واحداً في كل مرة. لا تضف خطوة تصحيح تعرض محتويات البيئة.
لماذا لا يرى طلب السحب من مستودع متشعب مفتاح API الخاص بك
قاعدة GitHub بسيطة: باستثناء GITHUB_TOKEN، لا تُمرَّر الأسرار إلى المشغّل عندما يُشغَّل سير العمل من مستودع متشعب. لذلك يبدأ تشغيل pull_request من مستودع متشعب النص البرمجي من دون ANTHROPIC_API_KEY، ويفشل أول استدعاء لـAPI مع invalid x-api-key.
قد يبدو الحل السهل هو تغيير المشغّل إلى pull_request_target، إذ يُشغَّل هذا الخيار ضمن سياق المستودع الأساسي ويحصل فعلاً على الأسرار. لا تفعل ذلك هنا. تنص إرشادات الأمان الخاصة بـGitHub على أن عمليات سير العمل هذه «تتمتع بامتيازات، ما يعني أنها تشترك في ذاكرة التخزين المؤقت نفسها للفرع الرئيسي مع مشغّلات سير العمل الأخرى ذات الامتيازات، وقد تملك صلاحية الكتابة إلى المستودع والوصول إلى الأسرار المشار إليها»، وأن النتيجة «قد تُستغل للاستيلاء على مستودع».
وتتحدث الإرشادات نفسها بوضوح عن المشغّل: «ينبغي عدم استخدام المشغّلات المستضافة ذاتياً تقريباً أبداً مع المستودعات العامة على GitHub، لأن أي مستخدم يمكنه فتح طلبات سحب إلى المستودع واختراق البيئة».
يفرض ذلك خيارين في التصميم. تتضمن المهمة شرطاً حارساً، لذلك لا تعمل إلا على الفروع التي دُفعت إلى مستودعك الخاص. كما أن سير العمل لا يحتوي على خطوة actions/checkout على الإطلاق. لا يملك الوكيل الفرع على القرص، لذلك لا يكون طلب السحب الضار سوى نص يُرسل إلى نموذج. ولا يمكنه تشغيل نص برمجي للبناء على VPS الخاص بك، لأن أي شيء على VPS الخاص بك لا يشغّله مطلقاً. لكن النص ليس آمناً تلقائياً: فالفرق الذي كتبه شخص غريب هو إدخال غير موثوق يصل إلى نموذج، وهو حد الثقة نفسه الذي تواجهه عندما تمنح وكيلاً صلاحية البحث على الويب، والشيء الوحيد الذي يحصره هنا هو أن هذا الوكيل لا يستطيع سوى نشر تعليق.
اجلب الفرق، لا المستودع
يجلب طلب واحد الفرق بالكامل كنص عادي.
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 يصف طلب السحب إلى الفرق الموحّد نفسه، بينما يطبع gh api نص الاستجابة دون تغيير. يجب أن يبدأ السطر الأول الذي تراه بـ diff --git a/. تعني gh: Not Found (HTTP 404) أن الرمز المميز لا يستطيع الوصول إلى المستودع، وهذا يعني في معظم الحالات، عند استخدام رمز مميز شخصي دقيق الصلاحيات، أن إذن Pull requests لم يُفعّل.
رشِّح قبل أن تنفق رمزاً واحداً
هذا القسم هو ما يحدد الفرق بين bot يقرأه الناس وbot يكتمونه. يعمل كل مرشح أدناه قبل أن يرى النموذج بايتاً واحداً.
- مرشحات المسارات. استبعد ملفات القفل، والمجلدات التي تحتوي على تبعيات مضمّنة، والحزم المصغّرة، والشفرة المُولَّدة. إن وضع النموذج تعليقاً على
package-lock.jsonفسيكون ذلك ضجيجاً محضاً، وغالباً ما تحتوي هذه الملفات على معظم البايتات في diff. - حدّ للحجم. إذا تجاوز التغيير هذا الحد، فتخطَّ المراجعة وأنهِ التنفيذ بحالة نجاح. سيحصل 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تقسيم الفرق حسب الملف هو ما يتيح تصفية المسارات. وترقيم كل سطر هو ما يجعل تعليقات المراجعة تظهر في مواضعها. يقبل GitHub التعليق المضمّن فقط على سطر يكون جزءاً من الفرق، لذلك يجب أن يشير النموذج إلى رقم سطر حقيقي. إن تزويده بالأرقام يعني أنه يستطيع نسخ أحدها بدلاً من اختراع رقم.
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)يحمل رأس المقطع معلومات الترقيم. يوضح @@ -12,7 +12,9 @@ أن مقطع الملف الجديد يبدأ عند السطر 12، لذلك يبدأ العداد من هناك ويتقدم مع الأسطر المضافة وغير المتغيرة فقط. تمر الأسطر المحذوفة من دون ترقيم، لأنها غير موجودة في الملف الجديد. ويتجاوز الشرط الخاص بالأسطر التي تبدأ بشرطة مائلة عكسية العلامة التي يكتبها git عند نهاية الملف للإشارة إلى عدم وجود محرف سطر جديد، إذ كانت ستؤدي لولا ذلك إلى إزاحة كل رقم لاحق بمقدار واحد.
يستخدم كلا الخروجين الحالة 0، وليس 1. يجب أن يظهر طلب السحب الذي جرت تصفيته أو الذي يتجاوز الحجم Check أخضر. أما Check أحمر لا يستطيع الإنسان اتخاذ إجراء بشأنه فسيُتجاهل، وبمجرد تجاهل Check واحد، تُتجاهل كلها.
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 معالجة هذه الكتلة. وتُزال البادئة b/ من path، لأن هذه البادئة تأتي من رأس الفرق، بينما يريد 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 يعيّنه لكل مهمة بالفعل. وتهم مجموعة concurrency بالنسبة إلى التكلفة: من دونها، يؤدي دفع ثلاثة إصلاحات سريعة إلى فرع إلى تشغيل ثلاث عمليات مراجعة كاملة، وتدفع تكلفة العمليات الثلاث. أما معها، فلا يبقى سوى آخر عملية.
ينفّذ السطر if: مهمتين. يتجاهل النصف الأول طلبات السحب من المستودعات المتفرعة، لأنها ستفشل على أي حال من دون مفتاح. أما النصف الثاني فيوفّر لفريقك مفتاح إيقاف: أضف التصنيف no-ai-review إلى طلب سحب، ولن تُشغَّل المهمة.
افتح طلب سحب وراقب ما يحدث:
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 في السجل. هذه هي النتيجة المتوقعة في طلب سحب صغير ونظيف.
ما تكلفة مراجعة طلب السحب الآلية؟
يشكّل الـdiff معظم الإدخال تقريباً، لذلك يحدد حجم الـdiff السعر. فيما يلي diff مقاس من 500 سطر، بالإضافة إلى system prompt، وقد حُسب باستخدام نقطة نهاية عدّ الرموز بدلاً من التقدير.
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 محللاً رمزياً أحدث ينتج رموزاً أكثر بنحو 30% للإدخال نفسه، وتوثّق Anthropic ذلك في صفحة الأسعار. ضع هذا العامل في الحسبان عند مقارنة سعر نموذج أحدث بنموذج أقدم اعتماداً على سعر كل مليون رمز فقط.
الأسعار المعلنة اعتباراً من August 2026: يبلغ سعر Haiku 4.5 دولاراً واحداً لكل مليون رمز إدخال و5 دولارات لكل مليون رمز إخراج. ويبلغ سعر Sonnet 5 دولارين و10 دولارات وفق السعر التمهيدي الساري حتى 31 August 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 سنتاً لكل طلب سحب على Haiku 4.5، و9.1 سنتاً على Opus 5. يدفع فريق يدمج 200 طلب سحب شهرياً نحو $2.80 على Haiku 4.5، أو $7.28 على Sonnet 5، أو $18.20 على Opus 5. اعتباراً من 1 September 2026، اضرب قيمة Sonnet 5 في 1.5.
هناك عاملان يرفعان الفاتورة الفعلية فوق هذا التقدير. يراجع مشغّل synchronize كل push، لذلك تكلّف branch النشطة التي تحتوي على ثمانية pushes ثماني مراجعات، ولا تساعد قاعدة التزامن إلا عندما تصل pushes بفواصل زمنية قصيرة. تفترض الأرقام أيضاً أن path filters تعمل كما ينبغي؛ إذ يمكن لملف lock واحد غير مفلتر أن يضاعف الإدخال وحده.
لا تفيد prompt caching في هذه الحالة. يجب أن تكون البادئة المخزنة مؤقتاً مطابقة بايتاً لبايت بين الاستدعاءات، بينما يختلف الـdiff في كل مرة. وsystem prompt هو الجزء الثابت الوحيد، لكنه أقصر بكثير من الحد الأدنى القابل للتخزين المؤقت. للاطلاع على القاعدة العامة، راجع متى تصبح prompt caching مجدية من حيث التكلفة، ولاختيار النموذج المناسب من النماذج الثلاثة أعلاه، راجع أي نموذج Claude تستخدم لكل مهمة.
قِس الـdiffs الخاصة بك قبل تفعيلها
أضف هذا السطر بعد إنشاء payload، ثم شغّل السكربت يدوياً على عدة طلبات سحب من الشهر الماضي:
print(client.messages.count_tokens(
model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)لا تشغّل نقطة النهاية الخاصة بالعدّ النموذج، لذلك لا تستهلك رموز إدخال أو إخراج، وتستخدم المحلل الرمزي الخاص بالنموذج الذي تحدده. شغّلها على عشرة طلبات سحب فعلية من مستودعك، واستخدم الوسيط بدلاً من المتوسط، حتى لا تؤثر عملية ترحيل ضخمة واحدة في التقدير.
لماذا تُكتم روبوتات المراجعة، وكيف تتجنب ذلك
هناك سلوكان يدمران الثقة بهذه الروبوتات، ولكل منهما إصلاح في الشيفرة أعلاه.
مراجعة كل شيء دفعة واحدة. الروبوت الذي يترك أربعين تعليقاً لا يقرأ أحد تعليقاته. عتبة الخطورة وحدّ التعليقات العشرة ليسا مسألة لباقة، بل هما ما يُبقي النتائج الفعلية ظاهرة. يؤدي الفرز حسب الخطورة قبل الاقتطاع إلى إسقاط النتائج الأقل أهمية بدلاً من إسقاط عشرة نتائج عشوائية.
إضافة تعليق بثقة بشأن شيء لا يستطيع الروبوت التحقق منه. هذا هو السلوك الذي يدفع المهندسين إلى تعطيله نهائياً. قد يكتب النموذج، بعد عرض 200 سطر من قاعدة شيفرة تتكون من 40,000 سطر، العبارة: «هذا يعطّل إبطال ذاكرة التخزين المؤقت في redis_client.py» بشأن ملف لم يره قط. يدفعه توجيه النظام، بصياغة واضحة، إلى الإبلاغ عن العيوب الظاهرة في الأسطر المعروضة فقط، وإغفال أي شيء غير متأكد منه. إن تسمية الفشل مباشرةً أكثر فاعلية من طلب الدقة عموماً، كما أن إخبار النموذج بأن النتيجة الفارغة طبيعية يمنعه من اختلاق شيء يقوله عن تغيير مكوّن من سطرين.
انشر المراجعة بصفتها COMMENT، وليس بصفتها REQUEST_CHANGES. يجب ألا يتمكن رأي النموذج من منع الدمج، لأنه ما إن يتمكن من ذلك حتى يزيل شخص لديه موعد نهائي سير العمل بأكمله بدلاً من مجادلته.
أوضاع الفشل، مع السلاسل النصية التي ستظهر لك
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 من model API. تفشل الخطوة عند أول استدعاء messages.create. إما أن السر ANTHROPIC_API_KEY غير مضبوط في المستودع، أو أن pull request جاء من fork، ولذلك لم تمرر Actions أي أسرار. كان ينبغي لحاجز fork في السطر if: تخطي الخطوة، لذا تحقق من ذلك السطر أولاً.
gh: Resource not accessible by integration (HTTP 403). لا يستطيع رمز المهمة الكتابة إلى pull requests. أضف pull-requests: write إلى كتلة permissions:. إذا كان موجوداً بالفعل، فانتقل إلى Settings، ثم Actions، ثم General، حيث يمكن لسياسة المؤسسة تقييد ما يمكن لأي رمز workflow طلبه.
json.decoder.JSONDecodeError. لم يُرجع النموذج JSON قابلاً للتحليل. السبب الشائع هو أن الاستجابة بلغت حد الرموز وتوقفت في منتصف الكائن. يطبع سطر السجل 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 غير المتصل المهمة في قائمة الانتظار من دون أي رسالة خطأ في pull request.
تعود كل مراجعة فارغة. اضبط MIN_SEVERITY على low لتشغيل واحد. إذا ظهرت النتائج، فهذا يعني أن الحد يؤدي وظيفته. وإذا لم يظهر شيء، فاطبع payload وتأكد من أن عوامل التصفية لم تزل diff بالكامل.
تشغيله بالتوازي مع الوكلاء الآخرين
المراجع صغير، لذلك قد يكون من المغري وضعه على الخادم الذي يشغّل كل شيء آخر. افصله إذا كان المستودع مهماً. تحتفظ هذه العملية برمز مميز يمكنه التعليق على التعليمات البرمجية، وبمفتاح ينفق أموالك، كما أن self-hosted runner هو، بحكم تصميمه، مكاناً تُنفَّذ فيه تعليمات سير العمل. حساب مخصص غير مميّز لا يملك صلاحيات sudo، على مضيف لا يشغّل أي شيء آخر، هو الحد الأدنى المطلوب. وإذا كنت تشغّل أيضاً وكلاء تفاعليين يسحبون التعليمات البرمجية، فإن آلة افتراضية مؤقتة لكل وكيل هي النمط القابل للاستمرار، بينما يشرح تشغيل وكيل برمجة على VPS الإعداد العام. وإذا كانت Anthropic API جديدة عليك، فإن إنشاء أول تطبيق Claude API على VPS يمثل نقطة بداية أبسط من هذا.
FAQ
هل يحتاج وكيل مراجعة PR يعمل بالذكاء الاصطناعي إلى صلاحية الكتابة في مستودعي؟
لا. يحتاج إلى pull-requests: write لنشر مراجعة، وإلى contents: read لجلب الفرق. هذه هي الصلاحيات المطلوبة كلها. وتحددها في كتلة permissions: ضمن سير العمل، التي تقيّد ما يمكن لـGITHUB_TOKEN الخاص بكل مهمة تنفيذه. بهذين السطرين، يستطيع الوكيل التعليق على طلب سحب، لكنه لا يستطيع دفع commit أو دمج branch. انشر المراجعات باستخدام event: COMMENT بدلاً من REQUEST_CHANGES، حتى لا يستطيع الوكيل أيضاً حظر الدمج.
لماذا يفشل تعليقي على المراجعة مع HTTP 422؟
يقبل GitHub تعليق المراجعة المضمّن فقط على سطر موجود ضمن diff الخاص بطلب السحب، ويُرجع Pull request review thread line must be part of the diff إذا لم يكن السطر كذلك. تحقق من أن path نسبي إلى المستودع ولا يتضمن البادئة b/ من ترويسة diff، وأن رقم السطر يقع داخل hunk الخاص بذلك الملف. يجب أن تكون side هي RIGHT للسطر المضاف أو غير المتغير، وLEFT للسطر المحذوف. إن إضافة رقم سطر الملف الجديد إلى بداية كل سطر في diff قبل إرساله إلى النموذج تمنع النموذج من اختلاق الأرقام من الأساس.
هل يمكنني تشغيل هذا على مستودع عام يتضمن طلبات سحب من forks؟
ليس بهذا التصميم. لا يمرر GitHub الأسرار إلى سير عمل يُشغَّل من fork، لذلك يكون مفتاح النموذج مفقوداً ويفشل التشغيل. ويوضح GitHub أيضاً أنه «ينبغي عدم استخدام self-hosted runners تقريباً أبداً مع المستودعات العامة»، لأن أي شخص يمكنه فتح طلب سحب يتسبب في تشغيل تعليمات برمجية على جهازك. بالنسبة إلى مشروع عام، قيّد المراجع إما بالفروع المدفوعة إلى المستودع نفسه، وهو ما يفعله الحارس if:، أو انقل خطوة المراجعة إلى runner مستضاف من GitHub، مع قبول خروج diff من بنيتك التحتية.
ما النموذج الذي ينبغي أن أستخدمه لمراجعة طلبات السحب؟
ابدأ باستخدام Haiku 4.5. قراءة diff محدود مقابل قائمة ثابتة من أنواع العيوب ليست مسألة تحتاج إلى استدلال صعب، كما أن النموذج الأرخص يحافظ على الفاتورة الشهرية عند رقم لا يثير اعتراض أحد. انتقل إلى Sonnet 5 إذا وجدت أنه يفوّت أخطاء حقيقية في لغتك أو framework لديك، وقِس ذلك بدلاً من افتراضه. يُعد Opus 5 الأغلى بين النماذج الثلاثة بفارق كبير لكل طلب سحب، ولذلك يكون تبرير استخدامه أسهل على branch الإصدار منه عند كل push إلى كل branch للميزات.