imbi-plugin-github 2.29.4__tar.gz → 2.30.0__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.29.4 → imbi_plugin_github-2.30.0}/PKG-INFO +2 -2
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/pyproject.toml +2 -2
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/commits.py +107 -16
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/tests/test_commits.py +156 -3
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/.gitignore +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/README.md +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/moon.yml +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/README.md +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/__init__.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/_app_auth.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/_hosts.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/_repos.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/deployment.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/doctor.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/identity.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/lifecycle.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/plugin.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/pull_requests.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/py.typed +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/tests/__init__.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/tests/test_deployment.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/tests/test_doctor.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/tests/test_hosts.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/tests/test_identity.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/tests/test_lifecycle.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/tests/test_pull_requests.py +0 -0
- {imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/tests/test_repos.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: imbi-plugin-github
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.30.0
|
|
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-Expression: BSD-3-Clause
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
13
13
|
Requires-Python: >=3.14
|
|
14
14
|
Requires-Dist: httpx>=0.27
|
|
15
|
-
Requires-Dist: imbi-common[databases]==2.
|
|
15
|
+
Requires-Dist: imbi-common[databases]==2.30.0
|
|
16
16
|
Requires-Dist: pydantic>=2
|
|
17
17
|
Requires-Dist: pyjwt[crypto]>=2.8
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "imbi-plugin-github"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.30.0"
|
|
4
4
|
description = "GitHub identity plugin for Imbi (github.com / GHEC / GHES)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
|
18
18
|
]
|
|
19
19
|
dependencies = [
|
|
20
20
|
"httpx>=0.27",
|
|
21
|
-
"imbi-common[databases]==2.
|
|
21
|
+
"imbi-common[databases]==2.30.0",
|
|
22
22
|
"pydantic>=2",
|
|
23
23
|
"pyjwt[crypto]>=2.8",
|
|
24
24
|
]
|
|
@@ -85,11 +85,24 @@ _MAX_HISTORY_PAGES = 100
|
|
|
85
85
|
# ``_MAX_HISTORY_PAGES``: this path exists to pick up the handful of
|
|
86
86
|
# commits a release build pushed, not to stand in for a full backfill.
|
|
87
87
|
_MAX_SINCE_PAGES = 10
|
|
88
|
-
# CI status is hydrated with one ``/check-runs`` call per commit
|
|
89
|
-
# prohibitively expensive across a full-history
|
|
90
|
-
# most-recent commits
|
|
91
|
-
#
|
|
92
|
-
|
|
88
|
+
# CI status is hydrated with one ``/check-runs`` call per commit (more
|
|
89
|
+
# when it pages), which is prohibitively expensive across a full-history
|
|
90
|
+
# backfill. Bound it to the most-recent commits; older commits keep the
|
|
91
|
+
# ``'unknown'`` default.
|
|
92
|
+
#
|
|
93
|
+
# 200 is where the gap actually closes -- the ``recent-commits`` endpoint
|
|
94
|
+
# caps ``limit`` at 200 and the deployments tab asks for exactly that
|
|
95
|
+
# (``COMMIT_WINDOW``), so hydrating past 200 cannot change what a reader
|
|
96
|
+
# sees. This deliberately stops at half of it: backfills are already
|
|
97
|
+
# slow, and every commit here costs at least one ``/check-runs`` call, so
|
|
98
|
+
# 25 -> 200 would have octupled the request count in one step. 100 buys
|
|
99
|
+
# most of the visible window at 4x rather than 8x; rows 100-199 keep the
|
|
100
|
+
# ``'unknown'`` default until this is raised or made configurable.
|
|
101
|
+
_BACKFILL_CI_LIMIT = 100
|
|
102
|
+
# Pages of ``/check-runs`` to walk per commit before giving up. 100 per
|
|
103
|
+
# page * 5 = 500 runs, well past the busiest observed commit (50), so the
|
|
104
|
+
# cap only bounds a pathological repo rather than shaping normal reads.
|
|
105
|
+
_MAX_CHECK_RUN_PAGES = 5
|
|
93
106
|
# Page size for ``sync_new_commits``' last-resort walk, used when nothing
|
|
94
107
|
# is stored *and* no ``since`` was supplied. Sized to cover a release
|
|
95
108
|
# build's handful of pushed commits, not to approximate a backfill.
|
|
@@ -577,6 +590,29 @@ async def _fetch_recent_commits(
|
|
|
577
590
|
return typing.cast('list[dict[str, typing.Any]]', resp.json())
|
|
578
591
|
|
|
579
592
|
|
|
593
|
+
async def _fetch_single_commit(
|
|
594
|
+
client: httpx.AsyncClient, sha: str, *, max_wait: float
|
|
595
|
+
) -> list[dict[str, typing.Any]]:
|
|
596
|
+
"""``GET /commits/{sha}`` as a one-item list, ``[]`` when absent.
|
|
597
|
+
|
|
598
|
+
For a delivery that names a head commit but carries no previous
|
|
599
|
+
head -- a ``workflow_run`` / ``check_suite`` style event rather than
|
|
600
|
+
a ``push`` -- the only commit the payload attests to is *sha*
|
|
601
|
+
itself. The response shape matches a ``commits[]`` entry from the
|
|
602
|
+
compare API, so :func:`_commit_record` maps it unchanged and the
|
|
603
|
+
caller's CI hydration and author resolution work on the list as-is.
|
|
604
|
+
"""
|
|
605
|
+
resp = await _request(client, 'GET', f'/commits/{sha}', max_wait=max_wait)
|
|
606
|
+
if resp.status_code == 404:
|
|
607
|
+
LOGGER.warning(
|
|
608
|
+
'github-commit-sync: commit %s not found; nothing to sync',
|
|
609
|
+
_short_sha(sha),
|
|
610
|
+
)
|
|
611
|
+
return []
|
|
612
|
+
resp.raise_for_status()
|
|
613
|
+
return [typing.cast('dict[str, typing.Any]', resp.json())]
|
|
614
|
+
|
|
615
|
+
|
|
580
616
|
async def _ci_status(
|
|
581
617
|
client: httpx.AsyncClient,
|
|
582
618
|
sha: str,
|
|
@@ -595,22 +631,54 @@ async def _ci_status(
|
|
|
595
631
|
non-200, a parse error, a network error, or a rate-limit pause -- CI
|
|
596
632
|
status is best-effort metadata and must never fail the sync, mirroring
|
|
597
633
|
the swallow-and-continue contract the rest of this module follows.
|
|
634
|
+
|
|
635
|
+
The whole run set has to be in hand before rolling up, so this pages
|
|
636
|
+
to the end rather than reading the first page: the endpoint defaults
|
|
637
|
+
to 30 per page and a busy commit carries far more (one observed
|
|
638
|
+
commit reports 50), which on a partial read drops whichever runs fall
|
|
639
|
+
past the cut -- silently turning a ``fail`` into a ``pass`` when the
|
|
640
|
+
failing run is the one left off.
|
|
598
641
|
"""
|
|
599
642
|
if _checks_disabled(credentials, base, owner, repo):
|
|
600
643
|
return 'unknown'
|
|
644
|
+
runs: list[dict[str, typing.Any]] = []
|
|
645
|
+
params: dict[str, str] = {'per_page': '100'}
|
|
601
646
|
try:
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
647
|
+
for page in range(1, _MAX_CHECK_RUN_PAGES + 1):
|
|
648
|
+
resp = await _request(
|
|
649
|
+
client,
|
|
650
|
+
'GET',
|
|
651
|
+
f'/commits/{sha}/check-runs',
|
|
652
|
+
params=params,
|
|
653
|
+
max_wait=max_wait,
|
|
654
|
+
)
|
|
655
|
+
if resp.status_code == 403:
|
|
656
|
+
_record_checks_disabled(credentials, base, owner, repo)
|
|
657
|
+
return 'unknown'
|
|
658
|
+
if resp.status_code != 200:
|
|
659
|
+
return 'unknown'
|
|
660
|
+
payload = typing.cast('dict[str, typing.Any]', resp.json())
|
|
661
|
+
runs.extend(payload.get('check_runs') or [])
|
|
662
|
+
next_url = _next_page_url(resp.headers.get('link'))
|
|
663
|
+
next_page = (
|
|
664
|
+
_query_param(next_url, 'page')
|
|
665
|
+
if next_url is not None
|
|
666
|
+
else None
|
|
667
|
+
)
|
|
668
|
+
if next_page is None:
|
|
669
|
+
break
|
|
670
|
+
params['page'] = next_page
|
|
671
|
+
if page == _MAX_CHECK_RUN_PAGES:
|
|
672
|
+
LOGGER.warning(
|
|
673
|
+
'github-commit-sync truncated check-runs for %s at %d '
|
|
674
|
+
'pages (%d runs); ci_status may be incomplete',
|
|
675
|
+
_short_sha(sha),
|
|
676
|
+
_MAX_CHECK_RUN_PAGES,
|
|
677
|
+
len(runs),
|
|
678
|
+
)
|
|
611
679
|
except Exception: # noqa: BLE001 - CI status is best-effort metadata
|
|
612
680
|
return 'unknown'
|
|
613
|
-
return _check_runs_to_status(
|
|
681
|
+
return _check_runs_to_status({'check_runs': runs})
|
|
614
682
|
|
|
615
683
|
|
|
616
684
|
async def _hydrate_ci(
|
|
@@ -826,6 +894,11 @@ class SyncCommitsConfig(pydantic.BaseModel):
|
|
|
826
894
|
|
|
827
895
|
Selectors resolve against the event context, so the push body lives
|
|
828
896
|
under ``/payload`` (e.g. ``/payload/after``).
|
|
897
|
+
|
|
898
|
+
A rule bound to a non-push event points ``after_selector`` at that
|
|
899
|
+
body's head SHA and leaves ``before_selector`` alone: the default
|
|
900
|
+
``/payload/before`` does not resolve there, which is what selects
|
|
901
|
+
:func:`sync_commits`' single-commit path.
|
|
829
902
|
"""
|
|
830
903
|
|
|
831
904
|
before_selector: JsonPointer = pydantic.Field(
|
|
@@ -886,7 +959,21 @@ async def sync_commits(
|
|
|
886
959
|
action_config: SyncCommitsConfig,
|
|
887
960
|
event: object,
|
|
888
961
|
) -> None:
|
|
889
|
-
"""Sync the commits
|
|
962
|
+
"""Sync the commits a delivery attests to into the ``commits`` table.
|
|
963
|
+
|
|
964
|
+
The range synced is chosen from what ``before_selector`` resolves to,
|
|
965
|
+
which separates three cases that must not be collapsed:
|
|
966
|
+
|
|
967
|
+
* **a usable SHA** -- an ordinary push; sync ``before...after``.
|
|
968
|
+
* **nothing at all** (the pointer does not resolve) -- the delivery
|
|
969
|
+
has no notion of a previous head, so the only commit it attests to
|
|
970
|
+
is ``after``; sync that one commit. This is the ``workflow_run``
|
|
971
|
+
shape, where falling through to the ``_last_known_sha`` compare
|
|
972
|
+
below yields an empty ``head...head`` range and silently syncs
|
|
973
|
+
nothing.
|
|
974
|
+
* **the zero SHA** -- a push that *does* carry a before, naming no
|
|
975
|
+
prior commit (a new branch). Heal from the last commit stored for
|
|
976
|
+
the project, else fall back to a bounded recent-history walk.
|
|
890
977
|
|
|
891
978
|
Branch gating is the rule's CEL ``filter_expression``'s job; this
|
|
892
979
|
action does not filter refs itself.
|
|
@@ -911,6 +998,10 @@ async def sync_commits(
|
|
|
911
998
|
raw = await _fetch_compare_commits(
|
|
912
999
|
client, before, after, max_wait=_WEBHOOK_MAX_WAIT_SECONDS
|
|
913
1000
|
)
|
|
1001
|
+
elif before is None:
|
|
1002
|
+
raw = await _fetch_single_commit(
|
|
1003
|
+
client, after, max_wait=_WEBHOOK_MAX_WAIT_SECONDS
|
|
1004
|
+
)
|
|
914
1005
|
else:
|
|
915
1006
|
last = await _last_known_sha(ctx.project_id)
|
|
916
1007
|
if last:
|
|
@@ -87,6 +87,27 @@ def _push(
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
def _workflow_run(
|
|
91
|
+
*,
|
|
92
|
+
head: str = 'b' * 40,
|
|
93
|
+
branch: str = 'main',
|
|
94
|
+
full_name: str = 'octo/demo',
|
|
95
|
+
repo_url: str = 'https://api.github.com/repos/octo/demo',
|
|
96
|
+
) -> dict[str, object]:
|
|
97
|
+
"""A ``workflow_run`` body: a head SHA, and no ``before`` anywhere."""
|
|
98
|
+
return {
|
|
99
|
+
'action': 'completed',
|
|
100
|
+
'workflow': {'name': 'CI/CD'},
|
|
101
|
+
'workflow_run': {
|
|
102
|
+
'head_sha': head,
|
|
103
|
+
'head_branch': branch,
|
|
104
|
+
'event': 'push',
|
|
105
|
+
'conclusion': 'success',
|
|
106
|
+
},
|
|
107
|
+
'repository': {'full_name': full_name, 'url': repo_url},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
90
111
|
def _event(body: dict[str, typing.Any]) -> dict[str, typing.Any]:
|
|
91
112
|
"""Wrap a push body in the event context handlers now receive."""
|
|
92
113
|
return {
|
|
@@ -132,6 +153,10 @@ def _check_runs_url(sha: str) -> str:
|
|
|
132
153
|
return f'https://api.github.com/repos/octo/demo/commits/{sha}/check-runs'
|
|
133
154
|
|
|
134
155
|
|
|
156
|
+
# Any commit's ``/check-runs``, with or without the paging query string.
|
|
157
|
+
_CHECK_RUNS_RE = r'.+/commits/.+/check-runs(\?.*)?$'
|
|
158
|
+
|
|
159
|
+
|
|
135
160
|
class ConfigTestCase(unittest.TestCase):
|
|
136
161
|
def test_sync_commits_defaults(self) -> None:
|
|
137
162
|
cfg = commits.SyncCommitsConfig()
|
|
@@ -223,7 +248,20 @@ class ApiBaseResolutionTestCase(unittest.TestCase):
|
|
|
223
248
|
self.assertIsNone(self._base(ctx=_ctx(), repo_url='not-a-repo-url'))
|
|
224
249
|
|
|
225
250
|
|
|
251
|
+
_WORKFLOW_RUN_CONFIG = commits.SyncCommitsConfig(
|
|
252
|
+
after_selector='/payload/workflow_run/head_sha',
|
|
253
|
+
ref_selector='/payload/workflow_run/head_branch',
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
226
257
|
class SyncCommitsTestCase(unittest.IsolatedAsyncioTestCase):
|
|
258
|
+
def setUp(self) -> None:
|
|
259
|
+
# Same process-wide /check-runs 403 cache SyncCommitsCiStatus
|
|
260
|
+
# clears: without this a 403 recorded by another test suppresses
|
|
261
|
+
# hydration here and the ci_status assertions below read
|
|
262
|
+
# 'unknown' depending on execution order.
|
|
263
|
+
deployment._CHECKS_DISABLED_TOKENS.clear()
|
|
264
|
+
|
|
227
265
|
@respx.mock
|
|
228
266
|
async def test_normal_compare_inserts_records(self) -> None:
|
|
229
267
|
base, head = 'a' * 40, 'b' * 40
|
|
@@ -326,6 +364,119 @@ class SyncCommitsTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
326
364
|
self.assertTrue(route.called)
|
|
327
365
|
insert.assert_awaited_once()
|
|
328
366
|
|
|
367
|
+
@respx.mock
|
|
368
|
+
async def test_absent_before_syncs_the_single_head_commit(self) -> None:
|
|
369
|
+
head = 'b' * 40
|
|
370
|
+
route = respx.get(
|
|
371
|
+
f'https://api.github.com/repos/octo/demo/commits/{head}'
|
|
372
|
+
).mock(return_value=httpx.Response(200, json=_commit(head)))
|
|
373
|
+
respx.get(_check_runs_url(head)).mock(
|
|
374
|
+
return_value=httpx.Response(200, json=_check_runs('success'))
|
|
375
|
+
)
|
|
376
|
+
with mock.patch(_INSERT, new=mock.AsyncMock()) as insert:
|
|
377
|
+
await commits.sync_commits(
|
|
378
|
+
ctx=_ctx(),
|
|
379
|
+
credentials=_CREDS,
|
|
380
|
+
external_identifier='',
|
|
381
|
+
action_config=_WORKFLOW_RUN_CONFIG,
|
|
382
|
+
event=_event(_workflow_run(head=head)),
|
|
383
|
+
)
|
|
384
|
+
self.assertTrue(route.called)
|
|
385
|
+
insert.assert_awaited_once()
|
|
386
|
+
table, records = _await_args(insert)
|
|
387
|
+
self.assertEqual('commits', table)
|
|
388
|
+
self.assertEqual(1, len(records))
|
|
389
|
+
self.assertEqual(head, records[0].sha)
|
|
390
|
+
self.assertEqual('main', records[0].ref)
|
|
391
|
+
self.assertEqual('pass', records[0].ci_status)
|
|
392
|
+
|
|
393
|
+
@respx.mock
|
|
394
|
+
async def test_absent_before_never_consults_last_known_sha(self) -> None:
|
|
395
|
+
"""The single-commit path must not fall through to a compare.
|
|
396
|
+
|
|
397
|
+
Regression guard: resolving ``_last_known_sha`` yields the head
|
|
398
|
+
itself once a prior delivery stored it, and ``head...head`` is an
|
|
399
|
+
empty range -- the commit is then silently never refreshed.
|
|
400
|
+
"""
|
|
401
|
+
head = 'b' * 40
|
|
402
|
+
respx.get(
|
|
403
|
+
f'https://api.github.com/repos/octo/demo/commits/{head}'
|
|
404
|
+
).mock(return_value=httpx.Response(200, json=_commit(head)))
|
|
405
|
+
respx.get(_check_runs_url(head)).mock(
|
|
406
|
+
return_value=httpx.Response(200, json=_check_runs('success'))
|
|
407
|
+
)
|
|
408
|
+
with (
|
|
409
|
+
mock.patch(_QUERY, new=mock.AsyncMock()) as query,
|
|
410
|
+
mock.patch(_INSERT, new=mock.AsyncMock()) as insert,
|
|
411
|
+
):
|
|
412
|
+
await commits.sync_commits(
|
|
413
|
+
ctx=_ctx(),
|
|
414
|
+
credentials=_CREDS,
|
|
415
|
+
external_identifier='',
|
|
416
|
+
action_config=_WORKFLOW_RUN_CONFIG,
|
|
417
|
+
event=_event(_workflow_run(head=head)),
|
|
418
|
+
)
|
|
419
|
+
query.assert_not_awaited()
|
|
420
|
+
insert.assert_awaited_once()
|
|
421
|
+
|
|
422
|
+
@respx.mock
|
|
423
|
+
async def test_ci_status_pages_past_the_first_check_runs_page(
|
|
424
|
+
self,
|
|
425
|
+
) -> None:
|
|
426
|
+
"""A failure past page 1 must still roll the commit up to ``fail``.
|
|
427
|
+
|
|
428
|
+
The endpoint defaults to 30 runs per page; reading only the first
|
|
429
|
+
page turns a failing commit into a passing one whenever the
|
|
430
|
+
failing run sorts past the cut.
|
|
431
|
+
"""
|
|
432
|
+
head = 'b' * 40
|
|
433
|
+
url = _check_runs_url(head)
|
|
434
|
+
respx.get(
|
|
435
|
+
f'https://api.github.com/repos/octo/demo/commits/{head}'
|
|
436
|
+
).mock(return_value=httpx.Response(200, json=_commit(head)))
|
|
437
|
+
route = respx.get(url).mock(
|
|
438
|
+
side_effect=[
|
|
439
|
+
httpx.Response(
|
|
440
|
+
200,
|
|
441
|
+
json=_check_runs('success'),
|
|
442
|
+
headers={'link': f'<{url}?page=2>; rel="next"'},
|
|
443
|
+
),
|
|
444
|
+
httpx.Response(200, json=_check_runs('failure')),
|
|
445
|
+
]
|
|
446
|
+
)
|
|
447
|
+
with mock.patch(_INSERT, new=mock.AsyncMock()) as insert:
|
|
448
|
+
await commits.sync_commits(
|
|
449
|
+
ctx=_ctx(),
|
|
450
|
+
credentials=_CREDS,
|
|
451
|
+
external_identifier='',
|
|
452
|
+
action_config=_WORKFLOW_RUN_CONFIG,
|
|
453
|
+
event=_event(_workflow_run(head=head)),
|
|
454
|
+
)
|
|
455
|
+
self.assertEqual(2, route.call_count)
|
|
456
|
+
self.assertEqual(
|
|
457
|
+
'100', route.calls[0].request.url.params.get('per_page')
|
|
458
|
+
)
|
|
459
|
+
insert.assert_awaited_once()
|
|
460
|
+
_, records = _await_args(insert)
|
|
461
|
+
self.assertEqual('fail', records[0].ci_status)
|
|
462
|
+
|
|
463
|
+
@respx.mock
|
|
464
|
+
async def test_absent_before_unknown_commit_inserts_nothing(self) -> None:
|
|
465
|
+
head = 'b' * 40
|
|
466
|
+
respx.get(
|
|
467
|
+
f'https://api.github.com/repos/octo/demo/commits/{head}'
|
|
468
|
+
).mock(return_value=httpx.Response(404, json={}))
|
|
469
|
+
with mock.patch(_INSERT, new=mock.AsyncMock()) as insert:
|
|
470
|
+
with self.assertLogs(commits.LOGGER, level='WARNING'):
|
|
471
|
+
await commits.sync_commits(
|
|
472
|
+
ctx=_ctx(),
|
|
473
|
+
credentials=_CREDS,
|
|
474
|
+
external_identifier='',
|
|
475
|
+
action_config=_WORKFLOW_RUN_CONFIG,
|
|
476
|
+
event=_event(_workflow_run(head=head)),
|
|
477
|
+
)
|
|
478
|
+
insert.assert_not_awaited()
|
|
479
|
+
|
|
329
480
|
@respx.mock
|
|
330
481
|
async def test_compare_pagination_collects_all_pages(self) -> None:
|
|
331
482
|
base, head = 'a' * 40, 'b' * 40
|
|
@@ -484,7 +635,7 @@ class SyncCommitsCiStatusTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
484
635
|
@respx.mock
|
|
485
636
|
async def test_403_degrades_and_caches(self) -> None:
|
|
486
637
|
self._mock_compare('c' * 40, 'd' * 40)
|
|
487
|
-
route = respx.get(url__regex=
|
|
638
|
+
route = respx.get(url__regex=_CHECK_RUNS_RE).mock(
|
|
488
639
|
return_value=httpx.Response(403)
|
|
489
640
|
)
|
|
490
641
|
records = await self._run()
|
|
@@ -1307,7 +1458,7 @@ class SyncAllHistoryTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
1307
1458
|
respx.get(f'{self._REPO}/git/matching-refs/tags').mock(
|
|
1308
1459
|
return_value=httpx.Response(200, json=[])
|
|
1309
1460
|
)
|
|
1310
|
-
ci_route = respx.get(url__regex=
|
|
1461
|
+
ci_route = respx.get(url__regex=_CHECK_RUNS_RE).mock(
|
|
1311
1462
|
return_value=httpx.Response(200, json=_check_runs('success'))
|
|
1312
1463
|
)
|
|
1313
1464
|
with mock.patch.object(commits, '_BACKFILL_CI_LIMIT', 1):
|
|
@@ -1894,7 +2045,9 @@ class SyncNewCommitsTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
1894
2045
|
self.assertEqual(1, written)
|
|
1895
2046
|
params = history.calls.last.request.url.params
|
|
1896
2047
|
self.assertNotIn('since', params)
|
|
1897
|
-
self.assertEqual(
|
|
2048
|
+
self.assertEqual(
|
|
2049
|
+
str(commits._RECENT_FALLBACK_COMMITS), params['per_page']
|
|
2050
|
+
)
|
|
1898
2051
|
|
|
1899
2052
|
@respx.mock
|
|
1900
2053
|
async def test_uncomparable_stored_sha_falls_back(self) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/_app_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/deployment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/lifecycle.py
RENAMED
|
File without changes
|
|
File without changes
|
{imbi_plugin_github-2.29.4 → imbi_plugin_github-2.30.0}/src/imbi/plugins/github/pull_requests.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
|