|
97 | 97 |
|
98 | 98 | ## Getting Started
|
99 | 99 |
|
| 100 | +django-github-app provides a router-based system for handling GitHub webhook events, built on top of [gidgethub](https://github.com/gidgethub/gidgethub). The router matches incoming webhooks to your handler functions based on the event type and optional action. |
| 101 | +
|
| 102 | +Each handler receives two key arguments: |
| 103 | +
|
| 104 | +- `event`: A `gidgethub.sansio.Event` containing the webhook payload |
| 105 | +- `gh`: A `gidgethub.abc.GitHubAPI` instance for making API calls |
| 106 | + |
| 107 | +<add note about `django_github_app.github.AsyncGitHubAPI` which is what actually is passed as `gh`. it's an opinionated implementation of the abstract GitHubAPI provided by gidgethub and uses httpx as it's client> |
| 108 | +
|
| 109 | +Here's an example: |
| 110 | + |
| 111 | +```python |
| 112 | +from django_github_app.routing import Router |
| 113 | +
|
| 114 | +gh = Router() |
| 115 | +
|
| 116 | +# Handle any issue event |
| 117 | +@gh.event("issues") |
| 118 | +async def handle_issue(event, gh, *args, **kwargs): |
| 119 | + issue = event.data["issue"] |
| 120 | + labels = [] |
| 121 | + |
| 122 | + # Add labels based on issue title |
| 123 | + title = issue["title"].lower() |
| 124 | + if "bug" in title: |
| 125 | + labels.append("bug") |
| 126 | + if "feature" in title: |
| 127 | + labels.append("enhancement") |
| 128 | + |
| 129 | + if labels: |
| 130 | + await gh.post( |
| 131 | + issue["labels_url"], |
| 132 | + data=labels |
| 133 | + ) |
| 134 | +
|
| 135 | +# Handle specific issue actions |
| 136 | +@gh.event("issues", action="opened") |
| 137 | +async def welcome_new_issue(event, gh, *args, **kwargs): |
| 138 | + """Post a comment when a new issue is opened""" |
| 139 | + url = event.data["issue"]["comments_url"] |
| 140 | + await gh.post(url, data={ |
| 141 | + "body": "Thanks for opening an issue! We'll take a look soon." |
| 142 | + }) |
| 143 | +``` |
| 144 | +
|
| 145 | +In this example, we automatically label issues based on their title and post a welcome comment on newly opened issues. The router ensures each webhook is directed to the appropriate handler based on the event type and action. |
| 146 | +
|
| 147 | +> [!NOTE] |
| 148 | +> Handlers must be async functions as django-github-app uses gidgethub for webhook event routing which only supports async operations. Sync support is planned to better integrate with Django projects that don't use async. |
| 149 | +
|
| 150 | +For more information about GitHub webhook events and payloads, see: |
| 151 | +
|
| 152 | +- [Webhook events and payloads](https://docs.github.com/en/webhooks/webhook-events-and-payloads) |
| 153 | +- [About webhooks](https://docs.github.com/en/webhooks/about-webhooks) |
| 154 | +
|
| 155 | +## Features |
| 156 | +
|
| 157 | +### GitHub API Client |
| 158 | +
|
| 159 | +The library provides `AsyncGitHubAPI`, an implementation of gidgethub's abstract `GitHubAPI` class that handles authentication and uses [httpx](https://github.com/encode/httpx) as its HTTP client. While it's automatically provided in webhook handlers, you can also use it directly in your code: |
| 160 | +
|
| 161 | +```python |
| 162 | +from django_github_app.github import AsyncGitHubAPI |
| 163 | +from django_github_app.models import Installation |
| 164 | +
|
| 165 | +# Access public endpoints without authentication |
| 166 | +async def get_public_repo(): |
| 167 | + async with AsyncGitHubAPI() as gh: |
| 168 | + return await gh.getitem("/repos/django/django") |
| 169 | +
|
| 170 | +# Interact as the GitHub App installation |
| 171 | +async def create_comment(repo_full_name: str): |
| 172 | + # Get the installation for the repository |
| 173 | + installation = await Installation.objects.aget(repositories__full_name=repo_full_name) |
| 174 | + |
| 175 | + async with AsyncGitHubAPI(installation_id=installation.installation_id) as gh: |
| 176 | + await gh.post( |
| 177 | + f"/repos/{repo_full_name}/issues/1/comments", |
| 178 | + data={"body": "Hello!"} |
| 179 | + ) |
| 180 | +``` |
| 181 | +
|
| 182 | +The client automatically handles authentication and token refresh when an installation ID is provided. The installation ID is GitHub's identifier for where your app is installed, which you can get from the `installation_id` field on the `Installation` model. |
| 183 | +
|
| 184 | +### Models |
| 185 | +
|
| 186 | +#### `EventLog` |
| 187 | +
|
| 188 | +Stores incoming webhook events with their payload and timestamp. Includes automatic cleanup of old events based on the `DAYS_TO_KEEP_EVENTS` setting via a `EventLog.objects.acleanup_events` manager method. |
| 189 | +
|
| 190 | +#### `Installation` |
| 191 | +
|
| 192 | +Represents where your GitHub App is installed. Stores the installation ID and metadata from GitHub, and provides methods for authentication: |
| 193 | +
|
| 194 | +```python |
| 195 | +from django_github_app.models import Installation |
| 196 | +
|
| 197 | +# Get an installation and its access token |
| 198 | +installation = await Installation.objects.aget(repositories__full_name="owner/repo") |
| 199 | +async with AsyncGitHubAPI(installation_id=installation.installation_id) as gh: |
| 200 | + # Do something as the installation |
| 201 | +``` |
| 202 | +
|
| 203 | +#### `Repository` |
| 204 | +
|
| 205 | +Represents repositories where your app is installed. Provides convenience methods for common GitHub operations: |
| 206 | +
|
| 207 | +```python |
| 208 | +from django_github_app.models import Repository |
| 209 | +
|
| 210 | +# Get issues for a repository |
| 211 | +repo = await Repository.objects.aget(full_name="owner/repo") |
| 212 | +issues = await repo.aget_issues() |
| 213 | +``` |
| 214 | +
|
| 215 | +All models provide both async and sync versions of their methods, though async is recommended for better performance. |
| 216 | +
|
100 | 217 | ## Configuration
|
101 | 218 |
|
| 219 | +Configuration of django-github-app is done through a `GITHUB_APP` dictionary in your Django project's `DJANGO_SETTINGS_MODULE`. |
| 220 | +
|
| 221 | +Here is an example configuration with the default values shown: |
| 222 | +
|
| 223 | +```python |
| 224 | +GITHUB_APP = { |
| 225 | + "APP_ID": "", |
| 226 | + "AUTO_CLEANUP_EVENTS": True, |
| 227 | + "CLIENT_ID": "", |
| 228 | + "DAYS_TO_KEEP_EVENTS": 7, |
| 229 | + "NAME": "", |
| 230 | + "PRIVATE_KEY": "", |
| 231 | + "WEBHOOK_SECRET": "", |
| 232 | +} |
| 233 | +``` |
| 234 | +
|
| 235 | +The following settings are required: |
| 236 | +
|
| 237 | +- `APP_ID` |
| 238 | +- `CLIENT_ID` |
| 239 | +- `NAME` |
| 240 | +- `PRIVATE_KEY` |
| 241 | +- `WEBHOOK_SECRET` |
| 242 | +
|
| 243 | +### `APP_ID` |
| 244 | +
|
| 245 | +> ❗ **Required** | `str` |
| 246 | +
|
| 247 | +The GitHub App's unique identifier. Obtained when registering your GitHub App. |
| 248 | +
|
| 249 | +### `AUTO_CLEANUP_EVENTS` |
| 250 | +
|
| 251 | +> **Optional** | `bool` | Default: `True` |
| 252 | +
|
| 253 | +Boolean flag to enable automatic cleanup of old webhook events. |
| 254 | +
|
| 255 | +### `CLIENT_ID` |
| 256 | +
|
| 257 | +> ❗ **Required** | `str` |
| 258 | +
|
| 259 | +The GitHub App's client ID. Obtained when registering your GitHub App. |
| 260 | +
|
| 261 | +### `DAYS_TO_KEEP_EVENTS` |
| 262 | +
|
| 263 | +> **Optional** | `int` | Default: `7` |
| 264 | +
|
| 265 | +Number of days to retain webhook events before cleanup. |
| 266 | +
|
| 267 | +### `NAME` |
| 268 | +
|
| 269 | +> ❗ **Required** | `str` |
| 270 | +
|
| 271 | +The GitHub App's name as registered on GitHub. |
| 272 | +
|
| 273 | +### `PRIVATE_KEY` |
| 274 | +
|
| 275 | +> ❗ **Required** | `str` |
| 276 | +
|
| 277 | +The GitHub App's private key for authentication. Can be provided as: |
| 278 | +
|
| 279 | +- Raw key contents in environment variable |
| 280 | +- File contents read from disk: `Path("path/to/key.pem").read_text()` |
| 281 | +
|
| 282 | +### `WEBHOOK_SECRET` |
| 283 | +
|
| 284 | +> **Required** | `str` |
| 285 | +
|
| 286 | +Secret used to verify webhook payloads from GitHub. |
| 287 | +
|
102 | 288 | ## License
|
103 | 289 |
|
104 | 290 | django-github-app is licensed under the MIT license. See the [`LICENSE`](LICENSE) file for more information.
|
0 commit comments