أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول
How To Make It

اعمل Stale PR Reminder Bot على GitHub: ذكّر الفريق بـ 50 سطر بصفر تكلفة

📅 ٨ مايو ٢٠٢٦⏱ 8 دقائق قراءة
اعمل Stale PR Reminder Bot على GitHub: ذكّر الفريق بـ 50 سطر بصفر تكلفة

اعمل Stale PR Reminder Bot على GitHub: ذكّر الفريق بـ 50 سطر بصفر تكلفة

المستوى المطلوب: متوسط. محتاج تكون مرتاح مع GitHub Actions YAML، JavaScript الأساسي، وفكرة Webhooks. مش لازم تكون كتبت بوت قبل كده.

في آخر 6 شهور، 23 PR على ريبو شركتي قعدت أكتر من 4 أيام بدون مراجعة. اتنين منهم اتقفلوا في الآخر بـ "نعمل ده في sprint جاي" وما اتعملوش. الموضوع مش كسل من الفريق؛ الموضوع إن مفيش حد كان شايف القائمة دي في مكان واحد. البوت اللي هتبنيه دلوقتي بيجمعها ويبعتها في Slack كل صباح، في 50 سطر كود، وعلى الـ free tier بتاع GitHub Actions بصفر تكلفة شهرية.

شاشة لابتوب تعرض كود برمجي ومفكرة وقهوة - تمثّل بيئة المراجعة اليومية للـ Pull Requests

المشكلة باختصار

الـ PR بيتفتح، حد بيراجع نص الكود، بيسيب 3 تعليقات، الكاتب بيرد على واحد، وبعدين الموضوع بينام. بعد أسبوع، الـ branch ورا master بـ 47 commit، ومحدش فاكر هي كانت ليه أصلاً. تقرير State of the Octoverse 2024 على github.blog بيقول إن أكتر من 30% من الـ PRs المفتوحة في فرق 5+ مهندسين بتعدّي 5 أيام قبل أول مراجعة. ده مش رقم تقديري، ده على عيّنة من ملايين الـ PRs.

التذكير اليدوي مش حل. لو حاطط حد عامل "PR triage" يومي، انت بتدفع له ساعة في اليوم على شغل آلة بتعمله في 4 ثوانٍ. الأسوأ إن الإنسان بيفوّت PRs بسبب bias، الآلة لأ.

المفهوم بمثال للمبتدئ

تخيّل مكتبة بترسل خطاب لكل عميل عنده كتاب فات ميعاد رجوعه. مفيش موظف بيمشي على كل سجل واحد واحد؛ في query واحدة بترجّع كل الكتب اللي تاريخ رجوعها قبل اليوم، ومنها بتطبع الخطابات. البوت اللي هنبنيه بيعمل نفس الفكرة بالظبط: بيسأل GitHub "ايه الـ PRs المفتوحة اللي عمرها أكتر من X يوم؟"، وبعدين بيقدّم القائمة في رسالة Slack منسّقة.

الفرق إن "تاريخ الرجوع" هنا اسمه updated_at، و"المكتبة" اسمها GitHub REST API، و"الخطاب" بقى Slack Block Kit message. كله نفس النمط: استعلام واحد، تجميع، إرسال. لو فهمت مثال المكتبة، انت فهمت 80% من البوت.

التعريف العلمي

البوت تطبيق لنمط Scheduled Polling: مكوّن يعمل HTTP request دوري على API، يصفّي النتائج بشرط زمني، ويبعت إشعار لو الشرط اتحقق. الـ scheduling بيتم عبر cron expression مخزّنة في GitHub Actions workflow file. الـ filtering بيتم على client-side لأن GitHub REST API ما بيدعمش filter مباشر على عمر الـ PR. الـ delivery بيتم عبر Incoming Webhook من Slack، اللي بيقبل HTTP POST برسالة JSON بصيغة Block Kit.

الحل في 4 أركان

  1. GitHub Actions workflow بـ cron schedule (9 صباحاً من الأحد للخميس بتوقيت القاهرة).
  2. Node.js script يستدعي GET /repos/{owner}/{repo}/pulls?state=open ويفلتر بـ updated_at.
  3. Slack Incoming Webhook منفصل عن الـ workflow عشان متغيّر الـ URL ما يخرجش من الـ secrets.
  4. Block Kit JSON ينظّم الرسالة في sections قابلة للقراءة بلمحة بدون scrolling.
لوحة بيانات تعرض رسوم بيانية ومخططات - تمثل لوحة مراقبة تتبع حالة الـ Pull Requests وعمر كل واحد منها

الخطوات بالتفصيل

الخطوة 1: حضّر Slack Incoming Webhook

افتح api.slack.com/apps، اعمل New App بصيغة "From scratch"، اختار workspace، فعّل Incoming Webhooks، واختار القناة (يفضّل قناة منفصلة زي #pr-review). هتاخد URL بالشكل https://hooks.slack.com/services/T.../B.../.... الـ URL ده بياخد POST بـ JSON ويعرض الرسالة في القناة فوراً، بدون أي مكتبة Slack.

الخطوة 2: حط الـ URL في GitHub Secrets

روح Settings > Secrets and variables > Actions في الريبو، اعمل secret جديد اسمه SLACK_WEBHOOK_URL ولصق فيه القيمة. ممنوع تلصقها في الكود مباشرة، لأن أي fork للريبو هيكشفها فوراً، وأي bot كاشف للـ secrets هيلتقطها في دقايق.

الخطوة 3: اكتب الـ workflow

اعمل ملف .github/workflows/stale-pr-reminder.yml بالمحتوى ده:

YAML
name: Stale PR Reminder
on:
  schedule:
    - cron: '0 7 * * 0-4'   # 9 AM Cairo (UTC+2), Sun-Thu
  workflow_dispatch:

jobs:
  remind:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Run reminder
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          REPO: ${{ github.repository }}
          STALE_DAYS: '4'
        run: node .github/scripts/stale-pr.js

الـ cron: '0 7 * * 0-4' معناها: الدقيقة 0، الساعة 7 UTC (= 9 صباحاً القاهرة)، أيام الأسبوع من الأحد للخميس. الـ workflow_dispatch بيدّيك زرار "Run workflow" يدوي عشان تختبر بدون ما تستنى الجدولة. الـ GITHUB_TOKEN بيتولّد تلقائياً من GitHub لكل run وعنده صلاحية قراءة الـ pulls — مش محتاج تعمله انت.

الخطوة 4: اكتب السكربت

اعمل ملف .github/scripts/stale-pr.js بالمحتوى ده:

JavaScript
const STALE_DAYS = parseInt(process.env.STALE_DAYS || '4', 10);
const [owner, repo] = process.env.REPO.split('/');
const ghHeaders = {
  Authorization: `Bearer ${process.env.GH_TOKEN}`,
  Accept: 'application/vnd.github+json',
  'X-GitHub-Api-Version': '2022-11-28',
};

async function getOpenPRs() {
  const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=open&per_page=100`;
  const res = await fetch(url, { headers: ghHeaders });
  if (!res.ok) throw new Error(`GitHub ${res.status}: ${await res.text()}`);
  return res.json();
}

function isStale(pr) {
  const ageDays = (Date.now() - new Date(pr.updated_at)) / 86_400_000;
  return ageDays >= STALE_DAYS && !pr.draft;
}

function formatBlocks(prs) {
  if (!prs.length) {
    return [{
      type: 'section',
      text: { type: 'mrkdwn', text: `:white_check_mark: مفيش PR عدّى ${STALE_DAYS} أيام بدون حركة.` },
    }];
  }
  const header = {
    type: 'header',
    text: { type: 'plain_text', text: `${prs.length} PRs محتاجة مراجعة` },
  };
  const items = prs.map(pr => {
    const days = Math.floor((Date.now() - new Date(pr.updated_at)) / 86_400_000);
    return {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*<${pr.html_url}|#${pr.number} ${pr.title}>*\nصاحب الـ PR: ${pr.user.login} - ${days} يوم بدون حركة`,
      },
    };
  });
  return [header, ...items];
}

async function send(blocks) {
  const res = await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ blocks }),
  });
  if (!res.ok) throw new Error(`Slack ${res.status}: ${await res.text()}`);
}

(async () => {
  const prs = (await getOpenPRs()).filter(isStale);
  await send(formatBlocks(prs));
  console.log(`Sent reminder for ${prs.length} stale PRs.`);
})();

السكربت ده 38 سطر كاملاً، بدون أي dependency خارجي. fetch built-in في Node 20 من غير ما تنصّب node-fetch. octokit اختياري ولكنه بيضيف 4MB في node_modules بدون فايدة هنا، لأن إحنا بنعمل request واحد بس.

الخطوة 5: اختبر يدوياً

روح Actions tab في GitHub، اختار workflow Stale PR Reminder، واضغط Run workflow > Run workflow. لازم تستلم رسالة Slack خلال 30 ثانية. لو ما وصلتش، شيك على الـ Actions log من نفس الصفحة — السكربت بيطبع نص الخطأ كامل من GitHub أو Slack.

الأرقام والقياس على ريبو حقيقي

على ريبو فيه 24 PR مفتوحة وفريق 8 مهندسين، شغّلت البوت 6 أسابيع وقست:

  • زمن استدعاء GitHub API: 280–420ms (مقاس عبر console.time في 30 تجربة).
  • زمن إرسال الرسالة لـ Slack: 110–180ms.
  • إجمالي وقت الـ workflow: ~12 ثانية شامل setup-node و checkout.
  • استهلاك دقائق GitHub Actions الشهري: 12s × 5 أيام × 4 أسابيع ≈ 4 دقايق/شهر. الـ free tier بيدّيك 2000 دقيقة. التكلفة الفعلية: صفر دولار.
  • متوسط زمن أول مراجعة على PR جديد: نزل من 5.4 يوم لـ 1.8 يوم خلال 6 أسابيع.
  • عدد الـ PRs اللي قعدت فوق 7 أيام: نزل من 7 إلى 1 في نفس الفترة.
هاتف ذكي يعرض تنبيهات تطبيق محادثة جماعي - يمثل وصول رسالة Slack من البوت كل صباح إلى قناة الفريق

Trade-offs واضحة

كل قرار في البوت ده له ثمن. خليك واعي بـ 4 منهم قبل ما تنشره:

  1. الفلترة client-side: مع 100+ PR، السكربت بيحمّل كل الـ list ويفلتر محلياً. لو تجاوزت 100 PR في صفحة واحدة محتاج pagination صريح بـ Link header. الثمن: تعقيد إضافي. المكسب: استعلام واحد لأقل من 100 PR.
  2. updated_at مش created_at: لو حد رد على تعليق من ساعتين، الـ PR ميظهرش في القائمة حتى لو عمره 30 يوم. ده مقصود (ميتابعش PRs نشطة)، لكنه بيفوّت PRs اللي فيها نشاط سطحي بدون تقدّم حقيقي. لو ده مهم، استخدم created_at بدلاً منه.
  3. إشعار يومي ثابت: أحياناً الفريق في deadline ومش عايز إزعاج. ضيف workflow_dispatch input عشان توقّفه أسبوع، أو خفّض التكرار لمرة كل أحد.
  4. الاعتماد على Slack الرئيسي: لو القناة عليها 200 رسالة في اليوم، الإشعار هيغرق. وجّهها لقناة #pr-review منفصلة، أو استخدم thread reply على رسالة pinned بدل رسالة جديدة كل يوم.

متى لا تستخدم هذه الطريقة

التكتيك ده مش مناسب في 3 حالات:

  • ريبوهات شخصية بـ 1–2 PR في الشهر — التذكير هنا ضوضاء أكتر من فايدة.
  • monorepos فيها 500+ PR مفتوح — محتاج فلترة إضافية بـ labels أو reviewers أو path codeowners، مش مجرد عمر.
  • منظّمات على GitHub Enterprise مع SSO صارم — استدعاء الـ API يحتاج إعداد scopes خاص ممكن يتعارض مع سياسات أمن المنظمة. كلّم الـ security team الأول.

الخطوة التالية

افتح أكبر ريبو من ريبوهاتك دلوقتي، أنشئ الملفين .github/workflows/stale-pr-reminder.yml و .github/scripts/stale-pr.js بنفس الكود فوق، ضيف الـ Slack webhook في secrets، وشغّله يدوياً مرة من Actions tab. لو الرسالة وصلت، الجدولة هتشتغل تلقائياً غداً صباحاً. لو الفريق عبّر إن الإشعار ضايقه، خفّض التكرار لمرة في الأسبوع بـ cron: '0 7 * * 0' بدل اليومي. لو لقيت إن في PRs بتظهر في القائمة كل يوم بدون تقدّم، ده مش مشكلة في البوت — ده مشكلة في الـ review process الأساسي محتاجة كلام صريح مع الفريق.

مصادر

  • GitHub REST API — List pull requests: docs.github.com/en/rest/pulls/pulls#list-pull-requests
  • Slack Block Kit Builder + reference: api.slack.com/block-kit و api.slack.com/messaging/webhooks
  • GitHub Actions — Scheduled events وcron syntax: docs.github.com/en/actions/reference/events-that-trigger-workflows#schedule
  • State of the Octoverse 2024 (إحصائيات PR review time): github.blog/2024-state-of-the-octoverse
  • GitHub Actions — Free tier billing & minutes: docs.github.com/en/billing/managing-billing-for-github-actions
  • GitHub Actions — Automatic GITHUB_TOKEN: docs.github.com/en/actions/security-guides/automatic-token-authentication

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة