8 בניית יכולות

GitHub API ו-Webhooks — גישה תכנותית

בפרק הזה תלמדו לגשת ל-GitHub בצורה תכנותית — קריאות REST ו-GraphQL עם אימות נכון, הגדרת Webhooks שמגיבים לאירועים בזמן אמת, ובניית GitHub App בסיסית. תבינו את ההבדלים בין שיטות האימות, תנהלו rate limits, ותבנו אוטומציות שעובדות מעבר לממשק הגרפי. אלה הכלים שמפתחים מנוסים משתמשים בהם כדי להפוך GitHub ממקום לשמור קוד לפלטפורמת אוטומציה מלאה.

מה יהיה לך בסוף הפרק הזה
מה תלמד בפרק הזה
דרישות קדם
הפרויקט שלך

בפרק 2 למדנו לגשת ל-API דרך gh api — פקודות בסיסיות מהטרמינל. בפרק הזה אנחנו צוללים לעומק: REST עם pagination וסינון, GraphQL עם nested queries, Webhooks שמגיבים לאירועים בזמן אמת, ובניית GitHub App בסיסית. זו השכבה שמאפשרת אוטומציות שלא ניתן לבנות רק עם Actions — בוטים שמגיבים לאירועים, dashboards שמרכזים מידע ממספר ריפוזיטורים, ואינטגרציות עם מערכות חיצוניות. בפרק 9 נשתמש בכל מה שלמדנו כאן כדי לבנות אוטומציות ניהוליות ברמת ארגון.

מילון מונחים
REST API
ממשק תכנותי מבוסס HTTP עם endpoints נפרדים לכל משאב — הסטנדרט של GitHub
GraphQL
שפת שאילתות שמאפשרת לבקש בדיוק את השדות שצריכים בקריאה אחת
Endpoint
כתובת URL ספציפית ב-API שמייצגת משאב — למשל /repos/{owner}/{repo}/issues
Query
בקשת קריאה ב-GraphQL — שליפת נתונים בלי לשנות אותם
Mutation
בקשת כתיבה ב-GraphQL — יצירה, עדכון או מחיקה של נתונים
PAT
Personal Access Token — מפתח אישי לאימות מול ה-API
Fine-grained PAT
PAT חדש עם הרשאות מדויקות לכל ריפוזיטורי — מחליף את ה-classic PAT
GitHub App
אינטגרציה עצמאית שפועלת בזהות משלה, עם הרשאות מדויקות ו-tokens קצרי חיים
OAuth App
אפליקציה שמאמתת משתמשים דרך GitHub ופועלת בשמם
Installation Token
טוקן זמני (שעה) ש-GitHub App מקבלת עבור ריפוזיטורי ספציפי
Webhook
מנגנון שבו GitHub שולח HTTP POST לשרת שלכם כשקורה אירוע
Payload
גוף ה-JSON שנשלח עם כל webhook — מכיל את כל הפרטים על האירוע
HMAC-SHA256
אלגוריתם חתימה קריפטוגרפית לאימות שה-webhook באמת הגיע מ-GitHub
Rate Limit
מגבלת בקשות לשעה — 5,000 ל-PAT, עד 15,000 ל-GitHub App
Conditional Request
בקשה עם ETag/If-None-Match שמחזירה 304 אם לא השתנה כלום — לא נספרת ב-rate limit
Octokit
ה-SDK הרשמי של GitHub ל-JavaScript, Ruby ו-.NET — מעטפת נוחה מעל ה-API

8.1 נוף ה-API של GitHub — שלוש דרכי גישה

כל דבר שעושים בממשק הגרפי של GitHub — פתיחת issue, מיזוג PR, יצירת release — אפשר לעשות דרך ה-API. ולא רק אפשר — חובה, אם רוצים אוטומציות אמיתיות. הממשק הגרפי מצוין לפעולות בודדות, אבל כשצריכים לעשות משהו על 50 ריפוזיטורים, לבנות dashboard שמרכז מידע, או להגיב אוטומטית לאירועים — ה-API הוא הכלי.

ל-GitHub יש שלוש דרכי גישה תכנותית:

  1. REST API — ממשק HTTP קלאסי עם endpoints נפרדים לכל משאב. הבסיס. הכי מתועד. מתאים ל-CRUD פשוט ולרוב המשימות היומיומיות.
  2. GraphQL API — שפת שאילתות מתקדמת. מבקשים בדיוק את השדות שצריכים, בקריאה אחת. מושלם לשאילתות מורכבות שמערבות nested data.
  3. gh api — הפקודה מ-GitHub CLI (פרק 2) שנותנת גישה מיידית לשניהם, בלי להתעסק עם tokens ידנית. הדרך המהירה ביותר לבדוק ולנסות.

מתי צריכים API? כל פעם שה-UI לא מספיק:

גרסת API נוכחית: ב-מרץ 2026 שוחררה גרסה 2026-03-10, אבל ברירת המחדל נשארת 2022-11-28 לתאימות אחורית. כדי להשתמש בגרסה החדשה, מוסיפים header:

X-GitHub-Api-Version: 2026-03-10

שינויים שבורים בגרסה החדשה כוללים, בין היתר: submodules מסומנים כ-type: "submodule" במקום "file", הסרת use_squash_pr_title_as_default, ועוד. אם אתם לא צריכים את השינויים — עדיף להישאר עם ברירת המחדל עד שהכלים שלכם מוכנים.

עשו עכשיו 2 דקות

בדקו כמה בקשות API נשארו לכם השעה הזו. פתחו טרמינל והריצו:

gh api /rate_limit --jq '.resources.core | "Limit: \(.limit), Remaining: \(.remaining), Reset: \(.reset)"'

אם ה-remaining קרוב ל-limit — עדיין לא השתמשתם ב-API היום. בסוף הפרק זה ישתנה.

8.2 REST API — הבסיס של הכל

ה-REST API של GitHub הוא ממשק HTTP קלאסי: לכל משאב (ריפוזיטורי, issue, PR, user) יש endpoint עם כתובת URL ברורה. הבסיס: https://api.github.com.

מבנה Endpoint:

# רשימת issues בריפוזיטורי
GET /repos/{owner}/{repo}/issues

# issue ספציפי
GET /repos/{owner}/{repo}/issues/{issue_number}

# יצירת issue חדש
POST /repos/{owner}/{repo}/issues

# עדכון issue
PATCH /repos/{owner}/{repo}/issues/{issue_number}

# מחיקת תגובה
DELETE /repos/{owner}/{repo}/issues/comments/{comment_id}

ארבע פעולות HTTP מכסות את הכל:

עשו עכשיו 3 דקות

שלפו את רשימת ה-issues בריפוזיטורי שלכם (או בריפו פתוח). הריצו:

# שליפת 5 issues אחרונים עם שם ומצב
gh api repos/{owner}/{repo}/issues \
  -q '.[0:5] | .[] | "\(.number) \(.title) [\(.state)]"'

החליפו {owner}/{repo} בשם הריפו שלכם, או נסו על cli/cli.

Pagination — עמוד אחרי עמוד:

רוב ה-endpoints מחזירים מקסימום 30 פריטים (ברירת מחדל). אפשר לבקש עד 100 עם per_page:

# 100 issues בעמוד, עמוד 2
gh api repos/cli/cli/issues?per_page=100&page=2 --jq 'length'

# כל העמודים אוטומטית (gh api מטפל ב-pagination)
gh api repos/cli/cli/issues --paginate --jq '.[].title'

ב-REST, ה-pagination עובד עם Link header — GitHub שולח בתשובה את הקישורים לעמוד הבא, הקודם, הראשון והאחרון. הפקודה gh api --paginate עוקבת אחרי ה-links אוטומטית.

סינון ומיון — שולטים במה שחוזר:

# רק issues פתוחים, ממוינים לפי תגובות (הכי מדוברים קודם)
gh api repos/{owner}/{repo}/issues?state=open&sort=comments&direction=desc \
  --jq '.[] | "\(.number) \(.title) — \(.comments) comments"'

# issues שעודכנו בשבוע האחרון
gh api "repos/{owner}/{repo}/issues?state=all&since=$(date -d '7 days ago' -u +%Y-%m-%dT%H:%M:%SZ)" \
  --jq '.[] | "\(.number) \(.title) [\(.state)]"'

# labels ספציפיים
gh api repos/{owner}/{repo}/issues?labels=bug,priority-high \
  --jq '.[] | "#\(.number) \(.title)"'

פרמטרי סינון נפוצים:

Response Headers שכדאי להכיר:

הקוד שלכם Client / Script GET /repos/.../issues Authorization: Bearer token GitHub API api.github.com אימות → הרשאות → נתונים 200 OK X-RateLimit-Remaining: 4,987 JSON Response [{title, state, ...}] זרימת REST API — בקשה ותגובה
עשו עכשיו 5 דקות

צרו issue חדש דרך ה-API, עדכנו אותו, ומחקו אותו. שלושה שלבים — שלוש פעולות HTTP:

# 1. POST — יצירת issue
gh api repos/{owner}/{repo}/issues \
  -f title="Test Issue from API" \
  -f body="This issue was created via the GitHub REST API" \
  -f labels[]="documentation" \
  --jq '.number'

# 2. PATCH — עדכון (השתמשו במספר שקיבלתם)
gh api repos/{owner}/{repo}/issues/{NUMBER} -X PATCH \
  -f title="Updated: Test Issue from API" \
  -f state=closed

# 3. GET — אימות שזה עבד
gh api repos/{owner}/{repo}/issues/{NUMBER} \
  --jq '"\(.title) [\(.state)]"'

פתחו את הריפו בדפדפן ותראו את ה-issue שנוצר ונסגר — הכל דרך ה-API.

דוגמה מתקדמת — סקריפט שמנקה issues ישנים:

הנה דוגמה מעשית שמשלבת pagination, סינון, ועדכון. הסקריפט מוצא את כל ה-issues שלא עודכנו 90 יום ומוסיף להם label:

# מצאו issues ישנים וסמנו אותם
CUTOFF=$(date -d '90 days ago' -u +%Y-%m-%dT%H:%M:%SZ)
gh api repos/{owner}/{repo}/issues?state=open\&since=2020-01-01T00:00:00Z\&per_page=100 \
  --paginate --jq ".[] | select(.updated_at < \"$CUTOFF\") | .number" | \
  while read NUM; do
    gh api repos/{owner}/{repo}/issues/$NUM -X PATCH -f labels[]="stale"
    echo "Labeled #$NUM as stale"
  done

עוד REST endpoints שימושיים שכדאי להכיר:

עשו עכשיו 3 דקות

חפשו את כל ה-issues הפתוחים שלכם בכל הריפוזיטורים — לא רק ריפו אחד:

# חיפוש חוצה ריפוזיטורים: כל ה-issues שמוקצים לכם
gh api "/search/issues?q=assignee:$(gh api /user --jq '.login')+state:open+type:issue" \
  --jq '.items[] | "\(.repository_url | split("/") | .[-1]) #\(.number) \(.title)"'

זה מראה את העוצמה של ה-Search API — שליפה חוצת ריפוזיטורים שלא ניתנת ב-UI.

Status Codes שחשוב להכיר:

כשעובדים עם ה-API, קודי התגובה מספרים את כל הסיפור:

8.3 GraphQL API — שאילתות חכמות

ב-REST, כל endpoint מחזיר מבנה קבוע — גם אם צריכים רק שדה אחד. ב-GraphQL, אתם קובעים בדיוק מה לקבל. Endpoint אחד: https://api.github.com/graphql. Method אחד: POST. השאלה היחידה היא מה שואלים.

Query — שליפת נתונים:

# שליפת שם המשתמש ו-bio + 5 ריפוזיטורים אחרונים
gh api graphql -f query='
{
  viewer {
    login
    bio
    repositories(first: 5, orderBy: {field: UPDATED_AT, direction: DESC}) {
      nodes {
        name
        stargazerCount
        primaryLanguage { name }
      }
    }
  }
}'

מה שקורה כאן: בקריאה אחת מקבלים את פרטי המשתמש + 5 ריפוזיטורים אחרונים + כוכבים + שפה ראשית. ב-REST הייתם צריכים שלוש קריאות נפרדות: אחת ל-user, אחת ל-repos, ואחת לכל repo בשביל שפות.

Variables — פרמטרים דינמיים:

במקום להטמיע ערכים ישירות ב-query, משתמשים ב-variables. זה מאפשר לשלוח את אותו query עם פרמטרים שונים, ומונע בעיות injection:

gh api graphql -F owner='{owner}' -F name='{repo}' -f query='
query($owner: String!, $name: String!) {
  repository(owner: $owner, name: $name) {
    pullRequests(first: 3, states: OPEN) {
      nodes {
        number
        title
        author { login }
        reviews(first: 5) {
          nodes { state }
        }
        files(first: 10) {
          nodes { path additions deletions }
        }
      }
    }
  }
}'

שימו לב: PR + author + reviews + files changed — הכל בבקשה אחת. ב-REST הייתם צריכים 4 קריאות (PR list, כל PR, reviews, files) שכופלות ב-3 PRs = 12 קריאות vs אחת.

Fragments — שימוש חוזר בשדות:

כשיש שדות שחוזרים על עצמם בכמה מקומות ב-query, אפשר להגדיר fragment פעם אחת ולהשתמש בו:

gh api graphql -f query='
fragment RepoInfo on Repository {
  name
  stargazerCount
  isArchived
  primaryLanguage { name }
  defaultBranchRef { name }
}

{
  viewer {
    repositories(first: 3, orderBy: {field: STARGAZERS, direction: DESC}) {
      nodes { ...RepoInfo }
    }
    repositoriesContributedTo(first: 3, contributionTypes: [COMMIT]) {
      nodes { ...RepoInfo }
    }
  }
}'

ה-fragment RepoInfo מגדיר פעם אחת אילו שדות לשלוף מכל repository, ומשמש גם ל-repos שלכם וגם ל-repos שתרמתם אליהם.

Mutation — שינוי נתונים:

# הוספת תגובה ל-issue (צריכים Node ID, לא מספר רגיל)
gh api graphql -f query='
mutation {
  addComment(input: {
    subjectId: "ISSUE_NODE_ID"
    body: "Comment added via GraphQL API"
  }) {
    commentEdge {
      node { body createdAt }
    }
  }
}'

טיפ: ה-subjectId ב-GraphQL הוא Node ID ולא מספר רגיל. Node IDs הם מזהים ייחודיים גלובליים ב-GitHub שמתחילים ב-prefix שמזהה את הסוג — I_ ל-Issue, PR_ ל-Pull Request, R_ ל-Repository. אפשר לשלוף אותם עם REST:

# שליפת Node ID של issue
gh api repos/{owner}/{repo}/issues/{n} --jq .node_id

# שליפת Node ID של ריפוזיטורי
gh api repos/{owner}/{repo} --jq .node_id

חשוב: אם משתמשים ב-mutations (כתיבה) ב-GraphQL, חייבים Node IDs. אם רק קוראים (queries), אפשר להשתמש ב-owner/name הרגילים.

Aliases — שאילתות מקבילות באותו query:

מה אם צריכים לקרוא לאותו field פעמיים עם פרמטרים שונים? ב-GraphQL, משתמשים ב-aliases:

gh api graphql -F owner='{owner}' -F name='{repo}' -f query='
query($owner: String!, $name: String!) {
  repository(owner: $owner, name: $name) {
    openPRs: pullRequests(states: OPEN) { totalCount }
    mergedPRs: pullRequests(states: MERGED) { totalCount }
    openIssues: issues(states: OPEN) { totalCount }
    closedIssues: issues(states: CLOSED) { totalCount }
  }
}' --jq '.data.repository | "Open PRs: \(.openPRs.totalCount), Merged: \(.mergedPRs.totalCount), Open Issues: \(.openIssues.totalCount), Closed: \(.closedIssues.totalCount)"'

ארבעה מספרים בקריאה אחת. ב-REST הייתם צריכים 4 קריאות נפרדות עם פרמטרי state שונים.

Connections ו-Pagination ב-GraphQL:

ב-GraphQL, pagination עובדת עם cursors, לא עמודים. זה יותר מדויק מ-offset-based pagination ומונע דילוג על פריטים כשנתונים משתנים בזמן ה-pagination:

# שליפת 100 issues עם cursor-based pagination
gh api graphql -f query='
query($cursor: String) {
  repository(owner: "facebook", name: "react") {
    issues(first: 100, after: $cursor, states: OPEN) {
      pageInfo {
        hasNextPage
        endCursor
      }
      nodes {
        number
        title
      }
    }
  }
}'

כדי לעבור לעמוד הבא, שולחים את endCursor כ-variable $cursor בקריאה הבאה. כשה-hasNextPage מחזיר false — סיימתם. היתרון על offset-based pagination (REST): אם נוצרו issues חדשים בזמן שעברתם עמודים, cursor pagination לא תדלג על פריטים ולא תראה כפילויות.

עשו עכשיו 3 דקות

הריצו את ה-query הראשון — שלפו את 5 הריפוזיטורים האחרונים שלכם:

gh api graphql -f query='{
  viewer {
    repositories(first: 5, orderBy: {field: UPDATED_AT, direction: DESC}) {
      nodes { name stargazerCount updatedAt }
    }
  }
}' --jq '.data.viewer.repositories.nodes[] | "\(.name) — \(.stargazerCount) stars"'

דוגמה מעשית — dashboard query:

הנה query שמרכז מידע שימושי לסקירת פרויקט — PRs פתוחים עם סטטוס review, issues קריטיים, ו-releases אחרונים. הכל בקריאה אחת:

gh api graphql -F owner='{owner}' -F name='{repo}' -f query='
query($owner: String!, $name: String!) {
  repository(owner: $owner, name: $name) {
    openPRs: pullRequests(first: 5, states: OPEN, orderBy: {field: UPDATED_AT, direction: DESC}) {
      totalCount
      nodes {
        number
        title
        author { login }
        reviewDecision
        createdAt
      }
    }
    criticalIssues: issues(first: 5, states: OPEN, labels: ["bug", "critical"]) {
      totalCount
      nodes {
        number
        title
        assignees(first: 1) { nodes { login } }
      }
    }
    latestRelease: releases(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) {
      nodes {
        tagName
        publishedAt
        author { login }
      }
    }
  }
}' --jq '.data.repository'

שימו לב ל-aliases (openPRs, criticalIssues, latestRelease) — הם מאפשרים לקרוא לאותו type כמה פעמים עם פילטרים שונים בקריאה אחת. בלי aliases, GraphQL לא היה מאפשר שני שדות pullRequests באותו query.

ה-query הזה מחליף מה שב-REST היה דורש לפחות 3 קריאות (PRs, issues, releases), ואם הייתם רוצים גם reviewDecision ו-assignees — הייתם צריכים עוד 5+ קריאות. ב-GraphQL: קריאה אחת. 0.2 שניות. בקשת API אחת מתוך 5,000.

תרגיל 1: REST vs GraphQL — השוואה מעשית 20 דקות

בחרו ריפוזיטורי (שלכם או ציבורי) ושלפו את אותו מידע בשתי דרכים:

משימה: קבלו את 3 ה-PRs האחרונים הפתוחים עם שם, מחבר, ומספר קבצים שהשתנו.

חלק א — REST (3+ קריאות):

# 1. שלפו PRs
gh api repos/{owner}/{repo}/pulls?state=open&per_page=3

# 2. לכל PR — שלפו files
gh api repos/{owner}/{repo}/pulls/{NUMBER}/files --jq 'length'

חלק ב — GraphQL (קריאה אחת):

gh api graphql -F owner='{owner}' -F name='{repo}' -f query='
query($owner: String!, $name: String!) {
  repository(owner: $owner, name: $name) {
    pullRequests(first: 3, states: OPEN) {
      nodes {
        title
        author { login }
        changedFiles
      }
    }
  }
}'

השוו: כמה קריאות API נדרשו בכל שיטה? כמה נתונים מיותרים הגיעו ב-REST? איזו שיטה הייתה יותר נוחה?

תוצר: רשמו את ההשוואה — מספר קריאות, גודל response, נוחות כתיבה.

8.4 REST vs GraphQL — מתי מה

שתי הגישות חיות יחד — השאלה היא לא "מה יותר טוב" אלא "מה מתאים למקרה הספציפי".

מסגרת החלטה: REST vs GraphQL
קריטריוןREST APIGraphQL API
מתי עדיףפעולות CRUD פשוטות, webhooks, Actionsשאילתות מורכבות, dashboards, ריבוי קשרים
מספר קריאותקריאה לכל endpoint בנפרדקריאה אחת עם nested fields
Over-fetchingמקבלים הכל, גם מה שלא צריכיםמבקשים רק את השדות הרלוונטיים
PaginationLink header + page/per_pageConnections עם cursor-based pagination
CachingHTTP caching מובנה (ETag, 304)אין caching HTTP native — צריך לנהל בקוד
תיעודעשיר מאוד — דוגמאות לכל endpointSchema explorer + introspection
Error handlingHTTP status codes (404, 403, 422)תמיד 200 — שגיאות בתוך ה-JSON
עקומת למידהנמוכה — HTTP סטנדרטיבינונית — צריך ללמוד syntax
מתי REST ומתי GraphQL? מה סוג הפעולה? CRUD פשוט (create/update/delete) REST API שליפה מורכבת / nested data GraphQL Webhook / Actions integration REST API טיפ: בפועל, רוב המפתחים משלבים את שתי הגישות לפי הצורך

כלל אצבע: אם אתם כותבים סקריפט CLI מהיר — REST עם gh api. אם אתם בונים dashboard או צריכים nested data — GraphQL. אם אתם לא בטוחים — התחילו עם REST. תמיד אפשר לעבור ל-GraphQL אחר כך.

טיפ

בפועל, רוב הצוותים משלבים את שניהם. REST ל-webhooks ול-Actions workflows (כי הם REST-native), ו-GraphQL ל-dashboards ולדוחות שצריכים nested data. אל תנסו לבחור רק אחד — השתמשו בכל אחד למה שהוא טוב בו.

8.5 אימות — PAT, OAuth ו-GitHub Apps

כל קריאה ל-API צריכה אימות. בלעדיו — מגבלת 60 בקשות לשעה בלבד. עם אימות — 5,000+. אבל יש כמה דרכים, ולא כולן שוות. בואו נכיר את כולן ונבין מתי להשתמש בכל אחת.

1. Fine-grained PAT — הבחירה הנכונה לסקריפטים אישיים:

עשו עכשיו 5 דקות

צרו fine-grained PAT: לכו ל-Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token. בחרו ריפו ספציפי, תנו הרשאות Issues: Read & Write בלבד, הגדירו תפוגה של 30 יום. שמרו את ה-token — הוא לא יוצג שוב.

דוגמה — שימוש ב-fine-grained PAT עם curl:

# שליפת issues עם fine-grained PAT (כשלא משתמשים ב-gh)
curl -s -H "Authorization: Bearer ghp_xxxxxxxxxxxx" \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  "https://api.github.com/repos/{owner}/{repo}/issues?state=open&per_page=5" | \
  jq '.[].title'

2. Classic PAT — הימנעו:

ה-Classic PAT משתמש ב-scopes רחבים — repo נותן גישה מלאה לכל הריפוזיטורים שלכם, ציבוריים ופרטיים. אין סיבה להשתמש בו ב-2026. GitHub ממליץ במפורש על fine-grained. Classic PAT לא מגביל לריפוזיטורים ספציפיים, לא דורש תאריך תפוגה (אם כי מומלץ), ומהווה סיכון אבטחה רציני אם דולף — התוקף מקבל גישה לכל הקוד שלכם.

טעות נפוצה: שימוש ב-Classic PAT לאוטומציות

ה-scope repo נותן read/write לכל הריפוזיטורים — אם ה-token דולף, כל הקוד שלכם חשוף. Fine-grained PAT מגביל לריפוזיטורים ספציפיים עם הרשאות מדויקות. מה לעשות במקום: צרו fine-grained PAT עם הרשאות מינימליות לריפו ספציפי, והגדירו תפוגה של 30-90 יום.

3. OAuth Apps — למי שצריך אימות משתמשים:

4. GitHub Apps — ההמלצה לאינטגרציות:

5. GITHUB_TOKEN ב-Actions:

מסגרת החלטה: בחירת שיטת אימות
סיטואציהשיטת אימותלמה
סקריפט אישי חד-פעמיFine-grained PATמהיר להגדרה, הרשאות מוגבלות
Actions workflowGITHUB_TOKENמובנה, אוטומטי, מאובטח
בוט/אינטגרציהGitHub Appזהות עצמאית, tokens קצרי חיים, rate limit גבוה
אפליקציית ווב למשתמשיםOAuth App או GitHub AppUser authorization flow עם PKCE
CI/CD בין ריפוזיטוריםGitHub Appגישה למספר ריפוזיטורים בלי PAT אישי

כלל אצבע: אם זה רק בשבילכם → fine-grained PAT. אם זה ב-Actions → GITHUB_TOKEN. אם זה בשביל אנשים אחרים או production → GitHub App.

8.6 Webhooks — GitHub מדבר אליכם

עד עכשיו אתם שלחתם בקשות ל-GitHub (pull). עם Webhooks, GitHub שולח הודעות אליכם (push). כל אירוע בריפוזיטורי — push, PR נפתח, issue נסגר, release חדש — יכול לשלוח HTTP POST לשרת שלכם. זה מה שמאפשר לבנות בוטים, התראות, ואוטומציות שמגיבות בזמן אמת.

איך זה עובד:

  1. אתם מגדירים URL שלכם ב-Settings → Webhooks
  2. אתם בוחרים אילו אירועים מעניינים אתכם
  3. GitHub שולח POST עם JSON payload לכל אירוע שקורה
  4. השרת שלכם מקבל, מאמת את החתימה, ומגיב תוך 10 שניות
זרימת Webhook — מאירוע לתגובה 1. אירוע PR נפתח 2. GitHub HTTP POST + Payload 3. השרת שלכם אימות חתימה 4. עיבוד לוגיקה שלכם 5. תגובה 200 OK ה-Payload כולל: Headers: X-GitHub-Event: pull_request | X-Hub-Signature-256: sha256=... | X-GitHub-Delivery: guid Body: { action: "opened", pull_request: { title, body, user, ... }, repository: { ... }, sender: { ... } } חובה: להגיב 2XX תוך 10 שניות | מגבלת payload: 25MB | ההמלצה: עיבוד async בתור (queue)

הגדרת Webhook:

# הגדרה דרך ה-UI:
# Settings → Webhooks → Add webhook
# Payload URL: https://your-server.com/webhook
# Content type: application/json
# Secret: YOUR_STRONG_RANDOM_SECRET
# Events: "Let me select individual events" → Pull requests, Issues

# או דרך ה-API:
gh api repos/{owner}/{repo}/hooks -f name=web \
  -f config[url]="https://your-server.com/webhook" \
  -f config[content_type]=json \
  -f config[secret]="YOUR_SECRET" \
  -f events[]=pull_request \
  -f events[]=issues

Events נפוצים:

כל event כולל שדה action שמפרט מה בדיוק קרה. למשל, pull_request עם action: "opened", "closed", "synchronize" (קוד חדש נדחף ל-PR קיים), "review_requested". זה חשוב כי בדרך כלל לא רוצים לטפל בכל ה-actions — רק בספציפיים. למשל, בוט שמוסיף labels צריך רק opened, לא synchronize.

מבנה ה-Payload — מה מגיע בפועל:

כל payload כולל שלושה חלקים עיקריים:

ה-payload עשוי להיות גדול — עד 25MB. לכן חשוב לפרסר רק את השדות שצריכים ולא לנסות לשמור את כל ה-payload.

עשו עכשיו 5 דקות

בואו נראה webhook בפעולה בלי להקים שרת. לכו ל-webhook.site — תקבלו URL ייחודי. עכשיו:

  1. לכו ל-Settings → Webhooks → Add webhook בריפו שלכם
  2. הדביקו את ה-URL מ-webhook.site ב-Payload URL
  3. בחרו Content type: application/json
  4. בחרו "Send me everything" (לבדיקה)
  5. לחצו Add webhook
  6. עכשיו פתחו issue בריפו — וחזרו ל-webhook.site לראות את ה-payload
עשו עכשיו 3 דקות

חפשו ב-payload שהגיע ל-webhook.site את השדות: action, sender.login, repository.full_name. זה המידע שהשרת שלכם ישתמש בו כדי להחליט מה לעשות.

דוגמאות מייצגות — תרחישי Webhook בסטארטאפים ישראליים:

הדוגמאות הבאות מבוססות על תבניות שימוש נפוצות ומייצגות סיטואציות טיפוסיות.

Webhooks הם כלי קריטי בסטארטאפים ישראליים שצריכים לנוע מהר עם צוותים קטנים. הנה חמישה תרחישים טיפוסיים:

תרחיש 1 — התראות Slack אוטומטיות (צוות של 15 מפתחים):

סטארטאפ B2B SaaS מגדיר webhook על pull_request.opened ו-pull_request.review_requested. כל PR חדש שולח הודעה מפורמטת ל-Slack channel של הצוות עם שם ה-PR, מי פתח, כמה קבצים שונו, וקישור ישיר ל-review. כש-review מתבקש, ה-reviewer מקבל DM אישי. התוצאה: זמן ה-review ירד מ-24 שעות ל-4 שעות כי המידע מגיע מיידית ולא צריך לבדוק GitHub באופן ידני.

תרחיש 2 — סנכרון GitHub-Jira דו-כיווני:

חברת cybersecurity ישראלית עם צוותי פיתוח ו-product שמשתמשים בכלים שונים. Webhook על event issues מאפשר: כל issue שנסגר ב-GitHub מעדכן אוטומטית את הכרטיס המתאים ב-Jira ל-"Done". כשמוסיפים label priority:critical ב-GitHub, ה-Jira ticket עולה ל-P1. חיסכון: 30 דקות ביום של עדכונים ידניים, ואין סיכון ששני הכלים לא מסונכרנים.

תרחיש 3 — בוט Quality Gate עם auto-labeling:

צוות DevOps בסטארטאפ fintech מגדיר webhook על check_suite.completed. כש-CI נכשל, הבוט מוסיף label ci-failed ל-PR, שולח DM ל-author ב-Slack, ומסיר את ה-PR מ-"Ready for Review" ב-Project board. כש-CI עובר, הבוט מסיר ci-failed, מוסיף ready-for-review, ומקצה reviewer אוטומטית על בסיס CODEOWNERS.

תרחיש 4 — Release Notes אוטומטיים:

כל release.published מפעיל webhook שלוקח את ה-release notes מ-GitHub, מפרמט אותם ל-HTML, שולח email ל-רשימת לקוחות, ומפרסם לדף עדכונים פנימי. ה-PM לא צריך לכתוב changelog ידנית — הכל מגיע אוטומטית מ-GitHub releases.

תרחיש 5 — Deployment Tracking ו-Compliance:

סטארטאפ healthtech שכפוף לרגולציה מגדיר webhook על deployment ו-deployment_status. כל deployment נרשם למערכת audit פנימית עם: מי ביצע, מה השתנה (commit SHA), מתי, והאם ה-deployment הצליח. זה מספק את דרישות ה-compliance של SOC2 ו-HIPAA בלי עבודה ידנית.

כל התרחישים האלה משותפים בדבר אחד: הם הופכים תהליך ידני לאוטומטי. במקום שמישהו יבדוק, יעדכן, וישלח — webhook מפעיל תהליך מיידי. בסטארטאפ ישראלי ממוצע, אוטומציה אחת כזו חוסכת 2-5 שעות שבועיות של עבודה ידנית לצוות.

טעות נפוצה: Subscribe ל-All Events

מפתחים רבים בוחרים "Send me everything" ומקבלים עשרות webhooks על כל פעולה. זה יוצר עומס על השרת, מסבך את הלוגיקה, ומקשה על debugging. מה לעשות במקום: בחרו רק את ה-events שבאמת צריכים. אם צריכים רק PR notifications — בחרו רק pull_request. תמיד אפשר להוסיף events אחר כך.

8.7 אבטחת Webhooks — אימות חתימות

הבעיה: כל מי שיודע את ה-URL שלכם יכול לשלוח POST מזויף שנראה כמו GitHub. בלי אימות, אי אפשר לדעת שה-webhook באמת הגיע מ-GitHub. תוקף יכול לגרום לבוט שלכם למזג PRs, למחוק branches, או לשלוח הודעות בשם הארגון.

הפתרון: HMAC-SHA256 signature verification. כשמגדירים webhook, בוחרים secret. GitHub משתמש ב-secret כדי ליצור חתימה על כל payload, ושולח אותה ב-header X-Hub-Signature-256. השרת שלכם מחשב את אותה חתימה ומשווה.

טעות נפוצה: דילוג על אימות חתימת Webhook

מפתחים רבים מדלגים על אימות החתימה "כי זה עובד גם בלי". זו פרצת אבטחה קריטית — כל אחד יכול לשלוח requests מזויפים ל-endpoint שלכם. מה לעשות במקום: תמיד אמתו חתימה עם X-Hub-Signature-256 ו-crypto.timingSafeEqual(). זה 10 שורות קוד שמגנות על כל המערכת.

קוד אימות ב-Node.js:

import crypto from 'crypto';

function verifyWebhookSignature(payload, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf-8')
    .digest('hex');

  // CRITICAL: use timingSafeEqual to prevent timing attacks
  // NEVER use === for signature comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your Express handler:
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-hub-signature-256'];
  const event = req.headers['x-github-event'];
  const deliveryId = req.headers['x-github-delivery'];

  if (!verifyWebhookSignature(req.rawBody, signature, process.env.WEBHOOK_SECRET)) {
    console.warn(`Rejected webhook ${deliveryId}: invalid signature`);
    return res.status(401).send('Invalid signature');
  }

  // Signature verified — safe to process
  console.log(`Event: ${event}, Action: ${req.body.action}, Delivery: ${deliveryId}`);

  // Respond immediately, process async
  res.status(200).send('OK');

  // Queue async processing (don't block the response)
  processWebhookAsync(event, req.body);
});

נקודות קריטיות:

IP Allowlisting — שכבת הגנה נוספת:

מעבר לחתימה, אפשר להגביל את ה-webhook endpoint לקבל בקשות רק מכתובות IP של GitHub:

# שליפת רשימת כתובות IP של GitHub
gh api /meta --jq '.hooks'
# ["192.30.252.0/22", "185.199.108.0/22", ...]

הוסיפו את הרשימה ל-firewall או ל-reverse proxy (Nginx, Cloudflare, AWS ALB). שימו לב: הרשימה עלולה להשתנות כש-GitHub מוסיפה data centers, אז כדאי ליצור סקריפט שמושך את הרשימה אחת ליום ומעדכן את ה-allowlist אוטומטית. שילוב של IP allowlisting עם HMAC signature verification נותן שתי שכבות הגנה — גם אם אחת נכשלת, השנייה מגנה.

תרגיל 2: Webhook שמגיב ל-PR events 25 דקות

בנו שרת Node.js מינימלי שמקבל webhooks ומגיב:

// server.js — minimal webhook handler
import http from 'http';
import crypto from 'crypto';

const SECRET = process.env.WEBHOOK_SECRET || 'dev-secret';
const PORT = 3000;
const processed = new Set(); // idempotency guard

function verify(payload, sig) {
  const expected = 'sha256=' + crypto.createHmac('sha256', SECRET)
    .update(payload).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

http.createServer((req, res) => {
  if (req.method !== 'POST') { res.end('OK'); return; }

  let body = '';
  req.on('data', chunk => body += chunk);
  req.on('end', () => {
    const sig = req.headers['x-hub-signature-256'];
    const deliveryId = req.headers['x-github-delivery'];

    if (!sig || !verify(body, sig)) {
      res.writeHead(401); res.end('Bad signature');
      return;
    }

    // Idempotency check
    if (processed.has(deliveryId)) {
      res.writeHead(200); res.end('Already processed');
      return;
    }
    processed.add(deliveryId);

    const event = req.headers['x-github-event'];
    const data = JSON.parse(body);
    console.log(`[${event}] ${data.action} by ${data.sender?.login}`);

    if (event === 'pull_request' && data.action === 'opened') {
      console.log(`New PR: #${data.pull_request.number} "${data.pull_request.title}"`);
      console.log(`Author: ${data.pull_request.user.login}`);
      console.log(`Files changed: ${data.pull_request.changed_files}`);
      // Here you would call the API to add a comment, label, etc.
    }

    if (event === 'issues' && data.action === 'opened') {
      console.log(`New Issue: #${data.issue.number} "${data.issue.title}"`);
    }

    res.writeHead(200); res.end('OK');
  });
}).listen(PORT, () => console.log(`Webhook server on port ${PORT}`));

הפעלה:

  1. שמרו את הקובץ כ-server.js
  2. הריצו: WEBHOOK_SECRET=mysecret node server.js
  3. חשפו את הפורט עם כלי כמו ngrok: ngrok http 3000
  4. הגדירו webhook בריפו עם ה-URL מ-ngrok + secret mysecret
  5. פתחו PR — ותראו את ה-log בטרמינל

תוצר: שרת webhook פעיל שמקבל events, מאמת חתימות, ומדפיס מידע על PRs ו-issues חדשים.

8.8 GitHub Apps — האינטגרציה המתקדמת

PATs הם סבבה לסקריפטים אישיים. אבל מה אם בונים בוט שצריך לעבוד על ריפוזיטורים של אנשים אחרים? או אינטגרציה שלא תלויה במשתמש ספציפי? GitHub Apps הם התשובה.

למה GitHub App ולא PAT:

זרימת אימות GitHub App — שלושה שלבים 1. Private Key PEM file + App ID → יוצרים JWT (RS256) 2. JWT Token תוקף: 10 דקות → POST /app/installations/{id}/access_tokens 3. Installation Token תוקף: שעה אחת → משתמשים ב-API כ-App GitHub API 5,000-15,000 req/hr למה שלושה שלבים? Private Key נשמר אצלכם בלבד | JWT חי 10 דקות — מספיק ליצור token Installation Token חי שעה — אם דולף, הנזק מוגבל | Octokit מנהל את הכל אוטומטית

יצירת GitHub App:

  1. Settings → Developer settings → GitHub Apps → New GitHub App
  2. הגדירו שם, Homepage URL, ו-Webhook URL (אם רוצים events)
  3. בחרו Permissions — מה ה-App צריכה לעשות
  4. בחרו Events — אילו webhooks לקבל
  5. צרו private key — קובץ PEM שישמש לאימות

זרימת אימות — שלושה שלבים בקוד:

// Step 1: Generate JWT from private key
import jwt from 'jsonwebtoken';
import fs from 'fs';

const privateKey = fs.readFileSync('private-key.pem');
const payload = {
  iat: Math.floor(Date.now() / 1000) - 60,   // issued at (60s ago for clock drift)
  exp: Math.floor(Date.now() / 1000) + 600,   // expires in 10 minutes
  iss: APP_ID                                   // your GitHub App ID
};
const jwtToken = jwt.sign(payload, privateKey, { algorithm: 'RS256' });

// Step 2: Get installation ID
// gh api /app/installations --jq '.[0].id' -H "Authorization: Bearer JWT"

// Step 3: Exchange JWT for installation access token
const response = await fetch(
  `https://api.github.com/app/installations/${installationId}/access_tokens`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${jwtToken}`,
      Accept: 'application/vnd.github+json'
    }
  }
);
const { token } = await response.json();
// token is valid for 1 hour — use it for API calls

דרך קלה יותר — Manifest Flow:

במקום למלא טפסים ידנית, אפשר ליצור App מ-manifest JSON שמגדיר את כל ההרשאות, events, ו-webhook URL. ה-manifest flow הוא handshake דמוי OAuth שיוצר אוטומטית את ה-App registration, כולל webhook secret, private key, client secret, וה-App ID. שימושי לאוטומציה של יצירת Apps, ליצירת templates שצוותים שונים יכולים להשתמש בהם, וליצירת Apps דרך CI/CD.

מתי PAT ומתי GitHub App — סיכום מהיר:

Probot — framework לפיתוח GitHub Apps ב-Node.js:

אם אתם בונים App ב-Node.js, Probot חוסך הרבה עבודה. במקום לנהל ידנית JWT generation, installation tokens, webhook verification, ו-event routing — Probot מטפל בכל זה אוטומטית. מתקינים, כותבים event handlers, ו-Probot עושה את השאר. זה ה-framework שמאחורי הרבה מה-GitHub Apps הפופולריות כמו Stale, Lock Threads, ו-Renovate:

// Using Probot framework
export default (app) => {
  // Respond to new PRs
  app.on('pull_request.opened', async (context) => {
    // context.octokit is already authenticated!
    const { data: files } = await context.octokit.pulls.listFiles(
      context.pullRequest({ per_page: 100 })
    );

    // Check if PR modifies sensitive files
    const sensitiveFiles = files.filter(f =>
      f.filename.includes('.env') || f.filename.includes('secrets')
    );

    if (sensitiveFiles.length > 0) {
      await context.octokit.issues.createComment(
        context.issue({
          body: '⚠️ This PR modifies sensitive files. Extra review required.'
        })
      );
      await context.octokit.issues.addLabels(
        context.issue({ labels: ['security-review'] })
      );
    } else {
      await context.octokit.issues.createComment(
        context.issue({ body: 'Thanks for the PR!' })
      );
    }
  });
};
עשו עכשיו 5 דקות

צרו GitHub App בסיסית: לכו ל-Settings → Developer settings → GitHub Apps → New GitHub App. הגדירו שם (למשל my-test-bot), Homepage URL (הריפו שלכם), השביתו Webhook (Active = unchecked). תנו Permissions: Issues → Read & Write. צרו את ה-App ואז צרו private key. שמרו את ה-PEM file ואת ה-App ID.

תרגיל 3: התקנת GitHub App על ריפו 15 דקות

קחו את ה-App שיצרתם בסעיף הקודם:

  1. בדף ה-App, לחצו Install App → בחרו את החשבון שלכם
  2. בחרו "Only select repositories" ובחרו ריפו אחד
  3. לחצו Install
  4. עכשיו שלפו את ה-installation ID:
# Replace YOUR_USERNAME with your GitHub username
gh api /users/{YOUR_USERNAME}/installation --jq '.id'

ברגע שיש לכם App ID, private key, ו-installation ID — אתם יכולים ליצור installation tokens ולקרוא ל-API בשם ה-App. ה-Octokit SDK (סעיף הבא) מפשט את זה מאוד.

תוצר: GitHub App מותקנת על ריפו עם installation ID ששמור אצלכם.

8.9 Octokit ו-gh api — כלי עבודה יומיים

אפשר לכתוב fetch() ידנית לכל קריאת API — אבל למה? Octokit הוא ה-SDK הרשמי של GitHub שמעטפת את כל ה-API עם TypeScript, auto-pagination, ניהול tokens, ועוד. זה מה שמפתחים מנוסים משתמשים ב-production.

התקנה ושימוש בסיסי:

npm install octokit
import { Octokit } from 'octokit';

// Authenticate with PAT
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });

// REST: List issues
const { data: issues } = await octokit.rest.issues.listForRepo({
  owner: 'your-org',
  repo: 'your-repo',
  state: 'open',
  per_page: 10
});
console.log(issues.map(i => `#${i.number} ${i.title}`));

// GraphQL: Same but with nested data
const { repository } = await octokit.graphql(`{
  repository(owner: "your-org", name: "your-repo") {
    issues(first: 10, states: OPEN) {
      nodes {
        number
        title
        labels(first: 5) { nodes { name } }
      }
    }
  }
}`);
console.log(repository.issues.nodes);

Octokit ל-GitHub Apps:

import { App } from 'octokit';

const app = new App({
  appId: process.env.APP_ID,
  privateKey: process.env.PRIVATE_KEY,
});

// Get authenticated octokit for a specific installation
const octokit = await app.getInstallationOctokit(installationId);

// Now use it — token management is automatic!
// JWT generation, installation token exchange, token refresh — all handled
const { data } = await octokit.rest.pulls.list({
  owner: 'your-org',
  repo: 'your-repo',
  state: 'open'
});

Auto-pagination — בלי לנהל עמודים:

אחד הפיצ'רים החזקים ביותר של Octokit. במקום לנהל ידנית Link headers, עמודים, ולולאות — octokit.paginate() עושה את הכל:

// Instead of manually following Link headers...
const allIssues = await octokit.paginate(
  octokit.rest.issues.listForRepo,
  { owner: 'your-org', repo: 'your-repo', state: 'all', per_page: 100 }
);
console.log(`Total issues: ${allIssues.length}`);
// Octokit follows all pages automatically

// You can also use a map function to transform while paginating:
const titles = await octokit.paginate(
  octokit.rest.issues.listForRepo,
  { owner: 'your-org', repo: 'your-repo', per_page: 100 },
  (response) => response.data.map(issue => issue.title)
);
// titles is a flat array of all issue titles across all pages

חבילות Octokit עיקריות:

Webhook Verification עם Octokit:

החבילה @octokit/webhooks מפשטת את כל תהליך האימות והטיפול ב-webhooks:

import { Webhooks } from '@octokit/webhooks';

const webhooks = new Webhooks({
  secret: process.env.WEBHOOK_SECRET
});

// Type-safe event handlers
webhooks.on('pull_request.opened', async ({ payload }) => {
  console.log(`PR #${payload.pull_request.number}: ${payload.pull_request.title}`);
  console.log(`By: ${payload.pull_request.user.login}`);
  console.log(`Changed files: ${payload.pull_request.changed_files}`);
});

webhooks.on('issues.labeled', async ({ payload }) => {
  if (payload.label.name === 'urgent') {
    console.log(`Issue #${payload.issue.number} marked as urgent!`);
  }
});

// In Express:
app.post('/webhook', async (req, res) => {
  await webhooks.verifyAndReceive({
    id: req.headers['x-github-delivery'],
    name: req.headers['x-github-event'],
    signature: req.headers['x-hub-signature-256'],
    payload: JSON.stringify(req.body),
  });
  res.status(200).send('OK');
});

מתי Octokit, מתי gh api, מתי curl:

עשו עכשיו 5 דקות

התקינו Octokit וכתבו סקריפט ראשון:

mkdir gh-api-test && cd gh-api-test
npm init -y && npm install octokit

# Create script.js:
cat > script.js << 'EOF'
import { Octokit } from 'octokit';
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const { data: user } = await octokit.rest.users.getAuthenticated();
console.log(`Hello ${user.login}! You have ${user.public_repos} public repos.`);
EOF

# Add "type": "module" to package.json, then:
GITHUB_TOKEN=$(gh auth token) node script.js
תרגיל 4: סקריפט אוטומציה מלא — סקירת ריפוזיטורים 20 דקות

בנו סקריפט שמרכז מידע מכל הריפוזיטורים שלכם ומזהה בעיות:

import { Octokit } from 'octokit';

const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });

// Get all your repos (auto-pagination!)
const repos = await octokit.paginate(octokit.rest.repos.listForAuthenticatedUser, {
  sort: 'updated',
  per_page: 100
});

// Summary: name, stars, language, open issues
console.log(`Total repos: ${repos.length}\n`);
console.log('--- Top 20 Most Recently Updated ---');
repos.slice(0, 20).forEach(r => {
  console.log(`${r.name.padEnd(30)} | ${r.language || 'N/A'} | ${r.open_issues_count} issues | ${r.stargazers_count} stars`);
});

// Find repos with problems
const noDesc = repos.filter(r => !r.description);
const noLicense = repos.filter(r => !r.license && r.visibility === 'public');
const archived = repos.filter(r => r.archived);

console.log(`\n--- Health Check ---`);
if (noDesc.length > 0) {
  console.log(`${noDesc.length} repos without description:`);
  noDesc.forEach(r => console.log(`  - ${r.name}`));
}
if (noLicense.length > 0) {
  console.log(`${noLicense.length} public repos without license:`);
  noLicense.forEach(r => console.log(`  - ${r.name}`));
}
console.log(`${archived.length} archived repos`);
console.log(`${repos.filter(r => r.open_issues_count > 10).length} repos with 10+ open issues`);

הריצו את הסקריפט. כמה ריפוזיטורים בלי description יש לכם? כמה public repos בלי license? תקנו אותם.

תוצר: סקריפט אוטומציה שנותן תמונת מצב של כל הריפוזיטורים שלכם ומזהה בעיות.

8.10 Rate Limits — הגבולות שחייבים להכיר

ל-GitHub API יש מגבלות. התעלמו מהן — ותקבלו 403 Forbidden עם ההודעה API rate limit exceeded. בואו נכיר את כל המספרים ואת האסטרטגיות להתמודד איתם.

Primary Rate Limits (בקשות/שעה):

GitHub App scaling: אם ל-App יש יותר מ-20 ריפוזיטורים או 20 משתמשים, היא מקבלת 50 בקשות נוספות/שעה לכל ריפו/משתמש, עד תקרה של 12,500 בקשות נוספות. בפועל, App גדולה עם 200 ריפוזיטורים ב-Enterprise Cloud מקבלת: 15,000 (base) + (200-20) * 50 = 24,000 בקשות/שעה. זה הרבה מאוד — מספיק לרוב האוטומציות.

מה קורה כשנגמר ה-rate limit?

כשמגיעים ל-primary rate limit, ה-API מחזיר 403 Forbidden עם header X-RateLimit-Remaining: 0. ה-header X-RateLimit-Reset מכיל Unix timestamp שאומר מתי ה-limit יתאפס (בדרך כלל תוך פחות מ-60 דקות). כש-secondary rate limit מכה, מקבלים 429 Too Many Requests עם header Retry-After שאומר כמה שניות לחכות.

Secondary Rate Limits — מגבלות נוספות מעבר ל-primary:

טעות נפוצה: התעלמות מ-Secondary Rate Limits

גם עם 4,000 בקשות נשארות ב-primary limit, אם שולחים 100 בקשות מקביליות — תחסמו. Secondary limits הם הסיבה ש-80% מה-rate limit errors מגיעים, לא primary limits. מה לעשות: הגבילו concurrency ל-10-20 בקשות מקביליות, והוסיפו 100ms delay בין קריאות ברצף.

Response Headers — איך לדעת איפה אתם עומדים:

# בדיקת rate limit headers מכל response
gh api repos/{owner}/{repo}/issues -i 2>&1 | grep -i "x-ratelimit"
# X-RateLimit-Limit: 5000
# X-RateLimit-Remaining: 4985
# X-RateLimit-Reset: 1712163600
# X-RateLimit-Used: 15
# X-RateLimit-Resource: core

Conditional Requests — הנשק הסודי:

כל response כולל header ETag. שמרו אותו. בקריאה הבאה, שלחו אותו ב-If-None-Match:

# First request — save the ETag
ETAG=$(gh api repos/{owner}/{repo}/issues -i 2>&1 | grep -i "^etag:" | awk '{print $2}' | tr -d '\r')

# Subsequent requests — use conditional
gh api repos/{owner}/{repo}/issues -H "If-None-Match: ${ETAG}" -i 2>&1 | head -1
# If nothing changed: HTTP/2 304 (0 bytes, does NOT count against rate limit!)
# If changed: HTTP/2 200 with new data + new ETag

למה זה חשוב? כי 304 responses לא נספרות כלל ב-rate limit. אם עושים polling כל 30 שניות ורוב הזמן אין שינוי — conditional requests חוסכים 90%+ מהקריאות.

אסטרטגיות לניהול rate limits:

Exponential Backoff בקוד — חובה לכל אוטומציה:

Exponential backoff אומר: קיבלתם 429 (Too Many Requests)? חכו שנייה. עדיין 429? חכו שתיים. עדיין? ארבע. הזמן מכפיל את עצמו. זה מונע "thundering herd" — מצב שבו כל הלקוחות מנסים שוב באותו רגע ומעמיסים על ה-API עוד יותר.

async function apiCallWithRetry(fn, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (error.status === 429 || error.status === 403) {
        const retryAfter = error.response?.headers['retry-after'];
        const waitTime = retryAfter
          ? parseInt(retryAfter) * 1000
          : Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
        console.log(`Rate limited. Waiting ${waitTime}ms...`);
        await new Promise(r => setTimeout(r, waitTime));
      } else {
        throw error; // Non-rate-limit error
      }
    }
  }
  throw new Error('Max retries exceeded');
}

כמה rate limit עולה לכם כל שיטה?

בואו נשווה את העלות בבקשות של אותה פעולה בשלוש גישות:

פעולהRESTGraphQLחיסכון
שליפת 10 PRs עם reviewers11 בקשות (1 list + 10 reviews)1 בקשה91%
סטטיסטיקות ריפו (issues, PRs, releases)3 בקשות1 בקשה67%
dashboard של 5 ריפוזיטורים15-25 בקשות1 בקשה (aliases)96%
Polling (כל 30 שניות, conditional)1 בקשה (0 if 304)1 בקשה (נספרת תמיד)REST wins!

המסקנה: GraphQL חוסך rate limit משמעותי על שליפות מורכבות עם nested data, אבל ל-REST יש יתרון ברור ב-polling ובמעקב שוטף בזכות conditional requests (304 לא נספר ב-rate limit כלל).

עשו עכשיו 3 דקות

בדקו את מצב ה-rate limit המלא שלכם:

gh api /rate_limit --jq '.resources | to_entries[] | "\(.key): \(.value.remaining)/\(.value.limit)"'

תראו את כל הקטגוריות: core, search, graphql, code_search, ועוד. שימו לב שכל אחת נפרדת — שימוש כבד ב-REST לא משפיע על GraphQL ולהפך.

8.11 סיכום ומבט קדימה

סיכום הפרק

בפרק הזה למדנו לגשת ל-GitHub בצורה תכנותית — השכבה שמאפשרת אוטומציות, אינטגרציות, ו-dashboards שלא ניתן לבנות דרך ה-UI. התחלנו מ-REST API — ממשק HTTP קלאסי שמתאים לרוב המשימות היומיומיות, עם endpoints ברורים, pagination, וסינון. עברנו ל-GraphQL — שפת שאילתות שחוסכת עשרות קריאות ע"י שליפת nested data בבקשה אחת.

למדנו לבחור שיטת אימות: fine-grained PAT לסקריפטים אישיים, GITHUB_TOKEN ב-Actions, ו-GitHub App לאינטגרציות production. צללנו לעולם ה-Webhooks — מנגנון push שבו GitHub מודיע לשרת שלכם על אירועים בזמן אמת, כולל אבטחת חתימות עם HMAC-SHA256. בנינו GitHub App בסיסית עם הרשאות, tokens קצרי חיים, ו-Probot framework. סיימנו עם rate limits — המגבלות שחייבים להכיר ואסטרטגיות (conditional requests, caching, backoff) שמאפשרות לעבוד בהן בצורה חכמה.

בפרק הבא: נעבור מ-API לניהול — ארגונים וצוותות. תלמדו להגדיר teams, CODEOWNERS, repository rulesets, ו-audit log. הכלים שלמדנו בפרק הזה (API, webhooks, Apps) הם הבסיס שעליו נבנה אוטומציות ניהוליות בפרק 9.

בדוק את עצמך — האם עברת את הפרק?
  1. מה היתרון העיקרי של GraphQL על REST כשצריכים nested data — ותנו דוגמה מהפרק? (רמז: סעיף 8.3, השוואת קריאות PR)
  2. למה fine-grained PAT עדיף על classic PAT — ומה שני ההבדלים העיקריים? (רמז: סעיף 8.5)
  3. מה קורה אם לא מאמתים חתימת webhook — ואיך מאמתים נכון (לא ===)? (רמז: סעיף 8.7, timing attacks)
  4. למה GitHub App עדיף על PAT לאינטגרציות production — תנו 3 סיבות? (רמז: סעיף 8.8)
  5. מה זה conditional request ולמה הוא חוסך rate limit? (רמז: סעיף 8.10, ETag ו-304)

אם ענית על 4 מתוך 5 — עברת!

שגרת עבודה

יומי (1-2 משימות)

  • השתמשו ב-gh api לבדיקות מהירות במקום לגלוש ל-UI — חוסך 5 דקות ביום
  • לפני כל סקריפט חדש — בדקו rate limit עם gh api /rate_limit

שבועי (3-4 משימות)

  • בדקו rate limit usage — האם מתקרבים לגבול? שקלו GraphQL או conditional requests
  • סקרו webhook delivery logs (Settings → Webhooks → Recent Deliveries) — האם יש כשלונות?
  • הריצו את סקריפט הסקירה מתרגיל 4 — בדקו repos בלי description או license

חודשי (2-3 משימות)

  • סקרו PATs — מחקו לא נחוצים, חדשו כאלה שקרובים לתפוגה
  • סובבו secrets של webhooks ושל GitHub Apps
  • בדקו permissions של Apps — הסירו הרשאות שלא נחוצות יותר (least privilege)
אם אתה עושה רק דבר אחד מהפרק הזה 5 דקות

צרו fine-grained PAT אם עדיין אין לכם. לכו ל-Settings → Developer settings → Personal access tokens → Fine-grained tokens. תנו גישה לריפו אחד, הרשאות מינימליות (Issues: Read), תפוגה של 30 יום. אחר כך הריצו:

gh api /user --jq '.login'

אם קיבלתם את שם המשתמש שלכם — אתם מוכנים להשתמש ב-API. זה הצעד הכי חשוב: ברגע שיש לכם PAT מוגדר, כל שאר הפרק (REST, GraphQL, webhooks) נפתח בפניכם.

צ'קליסט
  • ☐ ביצעתי קריאת REST API עם gh api
  • ☐ יצרתי issue ועדכנתי אותו דרך ה-API
  • ☐ כתבתי GraphQL query עם nested fields
  • ☐ השוויתי REST מול GraphQL על אותו מידע (תרגיל 1)
  • ☐ יצרתי fine-grained PAT עם הרשאות מדויקות
  • ☐ הגדרתי webhook ובדקתי payloads ב-webhook.site
  • ☐ כתבתי קוד אימות חתימה עם HMAC-SHA256
  • ☐ יצרתי GitHub App בסיסית
  • ☐ התקנתי App על ריפוזיטורי
  • ☐ השתמשתי ב-Octokit בסקריפט Node.js
  • ☐ בדקתי rate limit ויודע/ת מה המספרים
  • ☐ קראתי את כל 16 מונחי המילון