imbi-plugin-github 2.11.0__tar.gz → 2.11.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/PKG-INFO +2 -2
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/pyproject.toml +4 -3
- imbi_plugin_github-2.11.3/src/imbi_plugin_github/pull_requests.py +364 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/uv.lock +6 -6
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/.github/workflows/publish.yml +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/.github/workflows/test.yml +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/.gitignore +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/.pre-commit-config.yaml +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/CLAUDE.md +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/LICENSE +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/README.md +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/justfile +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/src/imbi_plugin_github/__init__.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/src/imbi_plugin_github/_app_auth.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/src/imbi_plugin_github/_hosts.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/src/imbi_plugin_github/_repos.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/src/imbi_plugin_github/commits.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/src/imbi_plugin_github/deployment.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/src/imbi_plugin_github/identity.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/src/imbi_plugin_github/lifecycle.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/tests/__init__.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/tests/test_commits.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/tests/test_deployment.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/tests/test_hosts.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/tests/test_identity.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/tests/test_lifecycle.py +0 -0
- {imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/tests/test_repos.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imbi-plugin-github
|
|
3
|
-
Version: 2.11.
|
|
3
|
+
Version: 2.11.3
|
|
4
4
|
Summary: GitHub identity plugin for Imbi (github.com / GHEC / GHES)
|
|
5
5
|
Author-email: "Gavin M. Roy" <gavinr@aweber.com>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
14
|
Requires-Python: >=3.14
|
|
15
15
|
Requires-Dist: httpx>=0.27
|
|
16
|
-
Requires-Dist: imbi-common[databases]==2.11.
|
|
16
|
+
Requires-Dist: imbi-common[databases]==2.11.3
|
|
17
17
|
Requires-Dist: pydantic>=2
|
|
18
18
|
Requires-Dist: pyjwt[crypto]>=2.8
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "imbi-plugin-github"
|
|
3
|
-
version = "2.11.
|
|
3
|
+
version = "2.11.3"
|
|
4
4
|
description = "GitHub identity plugin for Imbi (github.com / GHEC / GHES)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -16,7 +16,7 @@ classifiers = [
|
|
|
16
16
|
]
|
|
17
17
|
dependencies = [
|
|
18
18
|
"httpx>=0.27",
|
|
19
|
-
"imbi-common[databases]==2.11.
|
|
19
|
+
"imbi-common[databases]==2.11.3",
|
|
20
20
|
"pydantic>=2",
|
|
21
21
|
"pyjwt[crypto]>=2.8",
|
|
22
22
|
]
|
|
@@ -32,12 +32,13 @@ github-lifecycle = "imbi_plugin_github.lifecycle:GitHubLifecyclePlugin"
|
|
|
32
32
|
github-lifecycle-ec = "imbi_plugin_github.lifecycle:GitHubEnterpriseCloudLifecyclePlugin"
|
|
33
33
|
github-lifecycle-es = "imbi_plugin_github.lifecycle:GitHubEnterpriseServerLifecyclePlugin"
|
|
34
34
|
github-commit-sync = "imbi_plugin_github.commits:GitHubCommitSyncPlugin"
|
|
35
|
+
github-pr-sync = "imbi_plugin_github.pull_requests:GitHubPRSyncPlugin"
|
|
35
36
|
|
|
36
37
|
[dependency-groups]
|
|
37
38
|
dev = [
|
|
38
39
|
"basedpyright",
|
|
39
40
|
"coverage[toml]",
|
|
40
|
-
"imbi-common[server,databases]==2.11.
|
|
41
|
+
"imbi-common[server,databases]==2.11.3",
|
|
41
42
|
"pre-commit",
|
|
42
43
|
"pytest",
|
|
43
44
|
"pytest-asyncio",
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""GitHub pull-request history sync (webhook action plugin).
|
|
2
|
+
|
|
3
|
+
:class:`GitHubPRSyncPlugin` exposes one webhook action --
|
|
4
|
+
``sync_pull_requests`` -- dispatched by ``imbi-gateway`` on
|
|
5
|
+
``pull_request`` deliveries. It writes directly to the ClickHouse
|
|
6
|
+
``pull_requests`` table rather than relying on the materialized view
|
|
7
|
+
that previously extracted PR data from ``imbi.events``.
|
|
8
|
+
|
|
9
|
+
The plugin also exposes :meth:`GitHubPRSyncPlugin.sync_all_history`
|
|
10
|
+
for an on-demand, host-invoked backfill: it walks
|
|
11
|
+
``GET /repos/{owner}/{repo}/pulls?state=all`` and records every PR.
|
|
12
|
+
GitHub's list-PRs API omits per-file diff stats
|
|
13
|
+
(``additions``/``deletions``/``changed_files``), so backfill rows
|
|
14
|
+
store ``0`` for those fields; they are accurate on webhook-driven rows.
|
|
15
|
+
|
|
16
|
+
PR rows are written to the shared ClickHouse ``pull_requests`` table via
|
|
17
|
+
:func:`imbi_common.clickhouse.insert`. Writes are best-effort: a storage
|
|
18
|
+
failure is logged and swallowed so an analytics hiccup never 5xxs the
|
|
19
|
+
webhook, exactly as the gateway's own event recording behaves.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import datetime
|
|
25
|
+
import logging
|
|
26
|
+
import typing
|
|
27
|
+
|
|
28
|
+
import httpx
|
|
29
|
+
import jsonpointer
|
|
30
|
+
import pydantic
|
|
31
|
+
from imbi_common import clickhouse
|
|
32
|
+
from imbi_common.json_pointer import JsonPointer
|
|
33
|
+
from imbi_common.models import PullRequestRecord
|
|
34
|
+
from imbi_common.plugins.base import (
|
|
35
|
+
ActionDescriptor,
|
|
36
|
+
CredentialField,
|
|
37
|
+
PluginContext,
|
|
38
|
+
PluginManifest,
|
|
39
|
+
WebhookActionPlugin,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
from imbi_plugin_github._hosts import host_to_api_base
|
|
43
|
+
from imbi_plugin_github._repos import resolve_owner_repo
|
|
44
|
+
from imbi_plugin_github.commits import (
|
|
45
|
+
_BACKFILL_MAX_WAIT_SECONDS, # pyright: ignore[reportPrivateUsage]
|
|
46
|
+
_client, # pyright: ignore[reportPrivateUsage]
|
|
47
|
+
_insert_best_effort, # pyright: ignore[reportPrivateUsage]
|
|
48
|
+
_request, # pyright: ignore[reportPrivateUsage]
|
|
49
|
+
_resolve, # pyright: ignore[reportPrivateUsage]
|
|
50
|
+
_resolve_bearer, # pyright: ignore[reportPrivateUsage]
|
|
51
|
+
_resolve_host_for_context, # pyright: ignore[reportPrivateUsage]
|
|
52
|
+
)
|
|
53
|
+
from imbi_plugin_github.deployment import (
|
|
54
|
+
_next_page_url, # pyright: ignore[reportPrivateUsage]
|
|
55
|
+
_parse_iso, # pyright: ignore[reportPrivateUsage]
|
|
56
|
+
_query_param, # pyright: ignore[reportPrivateUsage]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
LOGGER = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
_SELF_SLUG = 'github-pr-sync'
|
|
62
|
+
# GitHub's pulls list is 100 per page; cap at 100 pages = 10k PRs so a
|
|
63
|
+
# very large repo can't pin a one-shot backfill indefinitely.
|
|
64
|
+
_MAX_HISTORY_PAGES = 100
|
|
65
|
+
# Webhook actions handle these PR lifecycle transitions; synchronize
|
|
66
|
+
# fires when new commits are pushed to the PR branch (title/state may
|
|
67
|
+
# also change).
|
|
68
|
+
_SYNC_ACTIONS = frozenset({'opened', 'closed', 'reopened', 'synchronize'})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _parse_pr_datetime(value: object) -> datetime.datetime | None:
|
|
72
|
+
"""Parse a GitHub ISO timestamp or return ``None`` for null/missing."""
|
|
73
|
+
if not isinstance(value, str) or not value:
|
|
74
|
+
return None
|
|
75
|
+
try:
|
|
76
|
+
return _parse_iso(value)
|
|
77
|
+
except Exception: # noqa: BLE001
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _pr_record(
|
|
82
|
+
pr: dict[str, typing.Any],
|
|
83
|
+
*,
|
|
84
|
+
project_id: str,
|
|
85
|
+
) -> PullRequestRecord | None:
|
|
86
|
+
"""Map a GitHub PR object onto a :class:`PullRequestRecord`.
|
|
87
|
+
|
|
88
|
+
Returns ``None`` if the PR object is missing required fields (id,
|
|
89
|
+
number, created_at) so malformed payloads are silently skipped.
|
|
90
|
+
"""
|
|
91
|
+
pr_id = pr.get('id')
|
|
92
|
+
pr_number = pr.get('number')
|
|
93
|
+
created_at_raw = pr.get('created_at')
|
|
94
|
+
if not pr_id or not pr_number or not created_at_raw:
|
|
95
|
+
return None
|
|
96
|
+
created_at = _parse_pr_datetime(created_at_raw)
|
|
97
|
+
if created_at is None:
|
|
98
|
+
return None
|
|
99
|
+
updated_at = _parse_pr_datetime(pr.get('updated_at')) or created_at
|
|
100
|
+
user: dict[str, typing.Any] = pr.get('user') or {}
|
|
101
|
+
return PullRequestRecord(
|
|
102
|
+
project_id=project_id,
|
|
103
|
+
pr_id=str(pr_id),
|
|
104
|
+
pr_number=int(pr_number),
|
|
105
|
+
title=str(pr.get('title') or ''),
|
|
106
|
+
url=str(pr.get('html_url') or ''),
|
|
107
|
+
state=str(pr.get('state') or 'open'),
|
|
108
|
+
author=str(user.get('login') or ''),
|
|
109
|
+
draft=bool(pr.get('draft', False)),
|
|
110
|
+
merged=bool(pr.get('merged', False)),
|
|
111
|
+
created_at=created_at,
|
|
112
|
+
updated_at=updated_at,
|
|
113
|
+
merged_at=_parse_pr_datetime(pr.get('merged_at')),
|
|
114
|
+
additions=int(pr.get('additions') or 0),
|
|
115
|
+
deletions=int(pr.get('deletions') or 0),
|
|
116
|
+
changed_files=int(pr.get('changed_files') or 0),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def _fetch_all_prs(
|
|
121
|
+
client: httpx.AsyncClient,
|
|
122
|
+
*,
|
|
123
|
+
max_wait: float,
|
|
124
|
+
) -> list[dict[str, typing.Any]]:
|
|
125
|
+
"""Walk ``GET /pulls?state=all`` and return every PR object.
|
|
126
|
+
|
|
127
|
+
GitHub's list-PRs endpoint omits ``additions``, ``deletions``, and
|
|
128
|
+
``changed_files``; backfill rows store ``0`` for those fields.
|
|
129
|
+
Caps at ``_MAX_HISTORY_PAGES`` pages (10k PRs) so a repo with a very
|
|
130
|
+
large PR history can't pin the worker indefinitely.
|
|
131
|
+
"""
|
|
132
|
+
out: list[dict[str, typing.Any]] = []
|
|
133
|
+
params: dict[str, str] = {'state': 'all', 'per_page': '100'}
|
|
134
|
+
for _ in range(_MAX_HISTORY_PAGES):
|
|
135
|
+
resp = await _request(
|
|
136
|
+
client, 'GET', '/pulls', params=params, max_wait=max_wait
|
|
137
|
+
)
|
|
138
|
+
resp.raise_for_status()
|
|
139
|
+
page: list[dict[str, typing.Any]] = resp.json()
|
|
140
|
+
if not page:
|
|
141
|
+
break
|
|
142
|
+
out.extend(page)
|
|
143
|
+
next_url = _next_page_url(resp.headers.get('link'))
|
|
144
|
+
if next_url is None:
|
|
145
|
+
break
|
|
146
|
+
next_page = _query_param(next_url, 'page')
|
|
147
|
+
if next_page is None:
|
|
148
|
+
break
|
|
149
|
+
params['page'] = next_page
|
|
150
|
+
else:
|
|
151
|
+
LOGGER.warning(
|
|
152
|
+
'github-pr-sync: reached %d-page cap fetching PRs; '
|
|
153
|
+
'history may be truncated',
|
|
154
|
+
_MAX_HISTORY_PAGES,
|
|
155
|
+
)
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class SyncPRsConfig(pydantic.BaseModel):
|
|
160
|
+
"""``WebhookRule.handler_config`` for ``sync_pull_requests``.
|
|
161
|
+
|
|
162
|
+
Selectors resolve against the event context; the PR body lives
|
|
163
|
+
under ``/payload`` (e.g. ``/payload/action``).
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
action_selector: JsonPointer = pydantic.Field(
|
|
167
|
+
default_factory=lambda: jsonpointer.JsonPointer('/payload/action')
|
|
168
|
+
)
|
|
169
|
+
pr_selector: JsonPointer = pydantic.Field(
|
|
170
|
+
default_factory=lambda: jsonpointer.JsonPointer(
|
|
171
|
+
'/payload/pull_request'
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
repository_selector: JsonPointer = pydantic.Field(
|
|
175
|
+
default_factory=lambda: jsonpointer.JsonPointer(
|
|
176
|
+
'/payload/repository/full_name'
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
api_base_url: str | None = None
|
|
180
|
+
repo_api_url_selector: JsonPointer = pydantic.Field(
|
|
181
|
+
default_factory=lambda: jsonpointer.JsonPointer(
|
|
182
|
+
'/payload/repository/url'
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def sync_pull_requests(
|
|
188
|
+
*,
|
|
189
|
+
ctx: PluginContext,
|
|
190
|
+
credentials: dict[str, str],
|
|
191
|
+
external_identifier: str,
|
|
192
|
+
action_config: SyncPRsConfig,
|
|
193
|
+
event: object,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Sync a pull_request event delivery into the ``pull_requests`` table.
|
|
196
|
+
|
|
197
|
+
Handles ``opened``, ``closed``, ``reopened``, and ``synchronize``
|
|
198
|
+
actions. Other actions (e.g. ``labeled``, ``review_requested``) are
|
|
199
|
+
silently ignored -- the rule's CEL filter can pre-screen them, but
|
|
200
|
+
this action is defensive too.
|
|
201
|
+
|
|
202
|
+
The full PR object is in the webhook payload so no extra GitHub API
|
|
203
|
+
call is needed.
|
|
204
|
+
"""
|
|
205
|
+
del external_identifier
|
|
206
|
+
action = _resolve(action_config.action_selector, event)
|
|
207
|
+
if not isinstance(action, str) or action not in _SYNC_ACTIONS:
|
|
208
|
+
return
|
|
209
|
+
pr_raw = _resolve(action_config.pr_selector, event)
|
|
210
|
+
if not isinstance(pr_raw, dict):
|
|
211
|
+
return
|
|
212
|
+
pr_obj = typing.cast('dict[str, typing.Any]', pr_raw)
|
|
213
|
+
record = _pr_record(pr_obj, project_id=ctx.project_id)
|
|
214
|
+
if record is None:
|
|
215
|
+
LOGGER.warning(
|
|
216
|
+
'github-pr-sync: skipping malformed PR payload for project %s '
|
|
217
|
+
'(action=%s, missing required fields)',
|
|
218
|
+
ctx.project_id,
|
|
219
|
+
action,
|
|
220
|
+
)
|
|
221
|
+
return
|
|
222
|
+
try:
|
|
223
|
+
await clickhouse.insert('pull_requests', [record])
|
|
224
|
+
except Exception:
|
|
225
|
+
LOGGER.exception(
|
|
226
|
+
'github-pr-sync: failed to record PR #%d for project %s',
|
|
227
|
+
record.pr_number,
|
|
228
|
+
ctx.project_id,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
sync_pull_requests_descriptor = ActionDescriptor(
|
|
233
|
+
name='sync_pull_requests',
|
|
234
|
+
label='Sync Pull Request History',
|
|
235
|
+
description=(
|
|
236
|
+
'Record pull_request webhook events (opened, closed, reopened, '
|
|
237
|
+
'synchronize) directly into the ClickHouse pull_requests table.'
|
|
238
|
+
),
|
|
239
|
+
callable=typing.cast(
|
|
240
|
+
'typing.Any',
|
|
241
|
+
'imbi_plugin_github.pull_requests:sync_pull_requests',
|
|
242
|
+
),
|
|
243
|
+
config_model=typing.cast(
|
|
244
|
+
'typing.Any',
|
|
245
|
+
'imbi_plugin_github.pull_requests:SyncPRsConfig',
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class GitHubPRSyncPlugin(WebhookActionPlugin):
|
|
251
|
+
"""Webhook-action plugin syncing GitHub pull-request history.
|
|
252
|
+
|
|
253
|
+
Carries its own service credential (PAT or GitHub App) -- it is not
|
|
254
|
+
folded into the identity / deployment / lifecycle plugins, which run
|
|
255
|
+
as the acting user. Auth is resolved identically to
|
|
256
|
+
:class:`~imbi_plugin_github.commits.GitHubCommitSyncPlugin`.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
manifest = PluginManifest(
|
|
260
|
+
slug='github-pr-sync',
|
|
261
|
+
name='GitHub Pull Request History Sync',
|
|
262
|
+
description=(
|
|
263
|
+
'Syncs pull request history from GitHub webhooks into '
|
|
264
|
+
'ClickHouse for analytics. Also supports on-demand backfill '
|
|
265
|
+
'of the full PR history via sync_all_history.'
|
|
266
|
+
),
|
|
267
|
+
plugin_type='webhook',
|
|
268
|
+
credentials=[
|
|
269
|
+
CredentialField(
|
|
270
|
+
name='access_token',
|
|
271
|
+
label='GitHub Token (PAT)',
|
|
272
|
+
description=(
|
|
273
|
+
'Static personal/service access token. Use this *or* '
|
|
274
|
+
'the GitHub App fields below.'
|
|
275
|
+
),
|
|
276
|
+
required=False,
|
|
277
|
+
),
|
|
278
|
+
CredentialField(
|
|
279
|
+
name='app_id',
|
|
280
|
+
label='GitHub App ID',
|
|
281
|
+
description=(
|
|
282
|
+
'GitHub App identifier; with a private key the plugin '
|
|
283
|
+
'mints short-lived installation tokens.'
|
|
284
|
+
),
|
|
285
|
+
required=False,
|
|
286
|
+
),
|
|
287
|
+
CredentialField(
|
|
288
|
+
name='private_key',
|
|
289
|
+
label='GitHub App Private Key',
|
|
290
|
+
description=(
|
|
291
|
+
'App private key, raw PEM or base64-encoded PEM.'
|
|
292
|
+
),
|
|
293
|
+
required=False,
|
|
294
|
+
),
|
|
295
|
+
CredentialField(
|
|
296
|
+
name='installation_id',
|
|
297
|
+
label='GitHub App Installation ID',
|
|
298
|
+
description=(
|
|
299
|
+
'Optional. When unset, the installation is discovered '
|
|
300
|
+
'from the repository.'
|
|
301
|
+
),
|
|
302
|
+
required=False,
|
|
303
|
+
),
|
|
304
|
+
],
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
@classmethod
|
|
308
|
+
def actions(cls) -> list[ActionDescriptor]:
|
|
309
|
+
return [sync_pull_requests_descriptor]
|
|
310
|
+
|
|
311
|
+
async def sync_all_history(
|
|
312
|
+
self,
|
|
313
|
+
*,
|
|
314
|
+
ctx: PluginContext,
|
|
315
|
+
credentials: dict[str, str],
|
|
316
|
+
) -> int:
|
|
317
|
+
"""Record the project's full pull request history.
|
|
318
|
+
|
|
319
|
+
Host-invoked (no webhook payload): the host instantiates the
|
|
320
|
+
plugin, builds a :class:`PluginContext` carrying the project's
|
|
321
|
+
links and the connected ``service_plugins``, resolves this
|
|
322
|
+
plugin's service ``credentials``, and awaits this method. The
|
|
323
|
+
GitHub host/flavor is read from ``service_plugins``, the
|
|
324
|
+
``(owner, repo)`` from the project links, and the bearer token
|
|
325
|
+
from the same PAT-or-App resolution the webhook actions use.
|
|
326
|
+
|
|
327
|
+
Walks every PR in the repo (``state=all``, paginated) and upserts
|
|
328
|
+
into the ClickHouse ``pull_requests`` table.
|
|
329
|
+
``ReplacingMergeTree`` dedupes against rows the webhook already
|
|
330
|
+
recorded, so re-running is safe.
|
|
331
|
+
|
|
332
|
+
``additions``, ``deletions``, and ``changed_files`` are ``0`` for
|
|
333
|
+
backfill rows (GitHub's list-PRs API omits them); they are
|
|
334
|
+
accurate for any future webhook-driven rows.
|
|
335
|
+
|
|
336
|
+
Returns the number of PRs recorded. Raises :class:`ValueError`
|
|
337
|
+
only when the host or repository can't be resolved; ClickHouse
|
|
338
|
+
failures are swallowed (the count reflects what was written).
|
|
339
|
+
Propagates :class:`PluginRateLimited` when a GitHub rate-limit
|
|
340
|
+
reset is further out than ``_BACKFILL_MAX_WAIT_SECONDS`` so the
|
|
341
|
+
host can pause the worker and keep the job queued until GitHub
|
|
342
|
+
resumes rather than fail it.
|
|
343
|
+
"""
|
|
344
|
+
host = _resolve_host_for_context(ctx)
|
|
345
|
+
if host is None:
|
|
346
|
+
raise ValueError(
|
|
347
|
+
'github-pr-sync could not resolve a GitHub host for an '
|
|
348
|
+
'on-demand sync: connect a GitHub plugin to the service'
|
|
349
|
+
)
|
|
350
|
+
base = host_to_api_base(host)
|
|
351
|
+
owner, repo = resolve_owner_repo(ctx, host, _SELF_SLUG)
|
|
352
|
+
token = await _resolve_bearer(credentials, base, owner, repo)
|
|
353
|
+
async with _client(base, owner, repo, token) as client:
|
|
354
|
+
raw_prs = await _fetch_all_prs(
|
|
355
|
+
client, max_wait=_BACKFILL_MAX_WAIT_SECONDS
|
|
356
|
+
)
|
|
357
|
+
records: list[pydantic.BaseModel] = []
|
|
358
|
+
for pr in raw_prs:
|
|
359
|
+
record = _pr_record(pr, project_id=ctx.project_id)
|
|
360
|
+
if record is not None:
|
|
361
|
+
records.append(record)
|
|
362
|
+
return await _insert_best_effort(
|
|
363
|
+
'pull_requests', records, ctx.project_id
|
|
364
|
+
)
|
|
@@ -667,7 +667,7 @@ wheels = [
|
|
|
667
667
|
|
|
668
668
|
[[package]]
|
|
669
669
|
name = "imbi-common"
|
|
670
|
-
version = "2.11.
|
|
670
|
+
version = "2.11.3"
|
|
671
671
|
source = { registry = "https://pypi.org/simple" }
|
|
672
672
|
dependencies = [
|
|
673
673
|
{ name = "cryptography" },
|
|
@@ -683,9 +683,9 @@ dependencies = [
|
|
|
683
683
|
{ name = "python-slugify" },
|
|
684
684
|
{ name = "typer" },
|
|
685
685
|
]
|
|
686
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
686
|
+
sdist = { url = "https://files.pythonhosted.org/packages/89/ab/e60f6d52bccfdf557de08169014e898c5f5b6e5d55633d77bf1386f7a110/imbi_common-2.11.3.tar.gz", hash = "sha256:b6e10de8c97726e74b4b779558dc8e228f76fb447adce231aeaeb96d07d95e7a", size = 349524, upload-time = "2026-06-12T00:35:18.234Z" }
|
|
687
687
|
wheels = [
|
|
688
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
688
|
+
{ url = "https://files.pythonhosted.org/packages/28/6d/00a22fde24df9f13b23edb6688f1875b308db82dc9d3ed48cd6d70f955bb/imbi_common-2.11.3-py3-none-any.whl", hash = "sha256:f788e5e966b5fd55030b3af32915fbfed16474ad55c9a86257ef2fd7fcb3663e", size = 113174, upload-time = "2026-06-12T00:35:16.674Z" },
|
|
689
689
|
]
|
|
690
690
|
|
|
691
691
|
[package.optional-dependencies]
|
|
@@ -703,7 +703,7 @@ server = [
|
|
|
703
703
|
|
|
704
704
|
[[package]]
|
|
705
705
|
name = "imbi-plugin-github"
|
|
706
|
-
version = "2.11.
|
|
706
|
+
version = "2.11.3"
|
|
707
707
|
source = { editable = "." }
|
|
708
708
|
dependencies = [
|
|
709
709
|
{ name = "httpx" },
|
|
@@ -732,7 +732,7 @@ dist = [
|
|
|
732
732
|
[package.metadata]
|
|
733
733
|
requires-dist = [
|
|
734
734
|
{ name = "httpx", specifier = ">=0.27" },
|
|
735
|
-
{ name = "imbi-common", extras = ["databases"], specifier = "==2.11.
|
|
735
|
+
{ name = "imbi-common", extras = ["databases"], specifier = "==2.11.3" },
|
|
736
736
|
{ name = "pydantic", specifier = ">=2" },
|
|
737
737
|
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.8" },
|
|
738
738
|
]
|
|
@@ -741,7 +741,7 @@ requires-dist = [
|
|
|
741
741
|
dev = [
|
|
742
742
|
{ name = "basedpyright" },
|
|
743
743
|
{ name = "coverage", extras = ["toml"] },
|
|
744
|
-
{ name = "imbi-common", extras = ["server", "databases"], specifier = "==2.11.
|
|
744
|
+
{ name = "imbi-common", extras = ["server", "databases"], specifier = "==2.11.3" },
|
|
745
745
|
{ name = "pre-commit" },
|
|
746
746
|
{ name = "pytest" },
|
|
747
747
|
{ name = "pytest-asyncio" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{imbi_plugin_github-2.11.0 → imbi_plugin_github-2.11.3}/src/imbi_plugin_github/deployment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|