imbi-plugin-github 2.11.5__tar.gz → 2.12.1__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.5 → imbi_plugin_github-2.12.1}/CLAUDE.md +31 -21
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/PKG-INFO +2 -2
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/pyproject.toml +3 -16
- imbi_plugin_github-2.12.1/src/imbi_plugin_github/__init__.py +13 -0
- imbi_plugin_github-2.12.1/src/imbi_plugin_github/_hosts.py +113 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/_repos.py +2 -2
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/commits.py +59 -154
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/deployment.py +25 -173
- imbi_plugin_github-2.12.1/src/imbi_plugin_github/doctor.py +676 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/identity.py +31 -192
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/lifecycle.py +34 -200
- imbi_plugin_github-2.12.1/src/imbi_plugin_github/plugin.py +335 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/pull_requests.py +32 -67
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/tests/test_commits.py +67 -105
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/tests/test_deployment.py +137 -124
- imbi_plugin_github-2.12.1/tests/test_doctor.py +531 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/tests/test_hosts.py +28 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/tests/test_identity.py +96 -74
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/tests/test_lifecycle.py +113 -107
- imbi_plugin_github-2.12.1/tests/test_pull_requests.py +184 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/uv.lock +6 -6
- imbi_plugin_github-2.11.5/src/imbi_plugin_github/__init__.py +0 -13
- imbi_plugin_github-2.11.5/src/imbi_plugin_github/_hosts.py +0 -67
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/.github/workflows/publish.yml +0 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/.github/workflows/test.yml +0 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/.gitignore +0 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/.pre-commit-config.yaml +0 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/LICENSE +0 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/README.md +0 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/justfile +0 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/_app_auth.py +0 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/tests/__init__.py +0 -0
- {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.12.1}/tests/test_repos.py +0 -0
|
@@ -5,10 +5,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
5
5
|
## What This Is
|
|
6
6
|
|
|
7
7
|
A set of GitHub plugins for the Imbi platform, distributed as a single
|
|
8
|
-
Python package (`imbi_plugin_github`). It ships **
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
Python package (`imbi_plugin_github`). It ships **one connection plugin**
|
|
9
|
+
plus **one host-agnostic plugin per behavior** — `github-connection`,
|
|
10
|
+
`github-identity`, `github-deployment`, `github-lifecycle`,
|
|
11
|
+
`github-commit-sync`, `github-pr-sync` (six total). There are no longer
|
|
12
|
+
per-flavor (github.com / GHEC / GHES) variants: the operator picks the
|
|
13
|
+
flavor + host once on the connection plugin, and every behavioral plugin
|
|
14
|
+
reads it from there. The Imbi host discovers them through the
|
|
12
15
|
`imbi.plugins` entry points declared in `pyproject.toml`; that table is
|
|
13
16
|
the registration surface — adding a plugin class means adding an entry
|
|
14
17
|
point there.
|
|
@@ -38,21 +41,25 @@ over `src`. Python is pinned to **3.14**; ruff uses single quotes and a
|
|
|
38
41
|
|
|
39
42
|
## Architecture
|
|
40
43
|
|
|
41
|
-
### The
|
|
44
|
+
### The connection-plugin / host-agnostic pattern
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`
|
|
46
|
+
A single `github-connection` plugin (a `ConnectionPlugin` from
|
|
47
|
+
`imbi_common`, with no behavior) carries the `flavor` option
|
|
48
|
+
(`github.com` / `ghec` / `ghes`), the `host` option, and the shared
|
|
49
|
+
App/PAT credentials for the service. Every behavioral plugin is
|
|
50
|
+
host-agnostic and resolves its host from that connection plugin via the
|
|
51
|
+
`github-connection` entry on `ctx.service_plugins`:
|
|
46
52
|
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
- identity / deployment / lifecycle call `_resolve_host(ctx)` →
|
|
54
|
+
`resolve_connection_host(ctx.service_plugins, label)`.
|
|
55
|
+
- commit-sync / pr-sync resolve it through `_connection_host` /
|
|
56
|
+
`_resolve_api_base` (the webhook path also falls back to the explicit
|
|
57
|
+
`api_base_url`, the service endpoint, and the push payload's
|
|
58
|
+
`repository.url`, in that order).
|
|
51
59
|
|
|
52
|
-
`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
(`identity._endpoints`, `deployment._api_base`, `lifecycle._api_base`):
|
|
60
|
+
`resolve_connection_host` returns a bare host; `host_to_api_base` maps it
|
|
61
|
+
to the REST API base. Routing (the single source of truth in
|
|
62
|
+
`host_to_api_base`, with OAuth URLs built in `identity._endpoints`):
|
|
56
63
|
|
|
57
64
|
| Host | REST API base | OAuth base (identity only) |
|
|
58
65
|
| ------------------- | ------------------------------ | --------------------------------- |
|
|
@@ -60,14 +67,17 @@ consistent and is **duplicated** across the three modules
|
|
|
60
67
|
| `<tenant>.ghe.com` | `https://api.<tenant>.ghe.com` | `https://<tenant>.ghe.com/login/oauth` |
|
|
61
68
|
| GHES `<host>` | `https://<host>/api/v3` | `https://<host>/login/oauth` |
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
The connection plugin must be populated onto `ctx.service_plugins` by the
|
|
71
|
+
host: imbi-gateway already surfaces every service plugin for the webhook
|
|
72
|
+
path; imbi-api populates it for identity/deployment/lifecycle calls.
|
|
65
73
|
|
|
66
74
|
### Shared helpers (single sources of truth)
|
|
67
75
|
|
|
68
|
-
- `_hosts.py` — `normalize_host`
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
- `_hosts.py` — `normalize_host` / `require_ghec_tenant_host` (host-option
|
|
77
|
+
validation), `flavor_host` (validate a connection plugin's
|
|
78
|
+
`flavor`+`host` to a bare host), `find_connection` /
|
|
79
|
+
`resolve_connection_host` (locate the `github-connection` sibling on
|
|
80
|
+
`service_plugins`), and `host_to_api_base` (bare host → REST API base).
|
|
71
81
|
- `_repos.py` — `resolve_owner_repo(ctx, host, label)` derives the target
|
|
72
82
|
`(owner, repo)` for deployment and lifecycle calls: it scans
|
|
73
83
|
`ctx.project_links` (preferring the `github-repository` key, skipping
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imbi-plugin-github
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.12.1
|
|
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.
|
|
16
|
+
Requires-Dist: imbi-common[databases]==2.12.1
|
|
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.
|
|
3
|
+
version = "2.12.1"
|
|
4
4
|
description = "GitHub identity plugin for Imbi (github.com / GHEC / GHES)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -16,29 +16,16 @@ classifiers = [
|
|
|
16
16
|
]
|
|
17
17
|
dependencies = [
|
|
18
18
|
"httpx>=0.27",
|
|
19
|
-
"imbi-common[databases]==2.
|
|
19
|
+
"imbi-common[databases]==2.12.1",
|
|
20
20
|
"pydantic>=2",
|
|
21
21
|
"pyjwt[crypto]>=2.8",
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
-
[project.entry-points."imbi.plugins"]
|
|
25
|
-
github = "imbi_plugin_github.identity:GitHubPlugin"
|
|
26
|
-
github-enterprise-cloud = "imbi_plugin_github.identity:GitHubEnterpriseCloudPlugin"
|
|
27
|
-
github-enterprise-server = "imbi_plugin_github.identity:GitHubEnterpriseServerPlugin"
|
|
28
|
-
github-deployment = "imbi_plugin_github.deployment:GitHubDeploymentPlugin"
|
|
29
|
-
github-deployment-ec = "imbi_plugin_github.deployment:GitHubEnterpriseCloudDeploymentPlugin"
|
|
30
|
-
github-deployment-es = "imbi_plugin_github.deployment:GitHubEnterpriseServerDeploymentPlugin"
|
|
31
|
-
github-lifecycle = "imbi_plugin_github.lifecycle:GitHubLifecyclePlugin"
|
|
32
|
-
github-lifecycle-ec = "imbi_plugin_github.lifecycle:GitHubEnterpriseCloudLifecyclePlugin"
|
|
33
|
-
github-lifecycle-es = "imbi_plugin_github.lifecycle:GitHubEnterpriseServerLifecyclePlugin"
|
|
34
|
-
github-commit-sync = "imbi_plugin_github.commits:GitHubCommitSyncPlugin"
|
|
35
|
-
github-pr-sync = "imbi_plugin_github.pull_requests:GitHubPRSyncPlugin"
|
|
36
|
-
|
|
37
24
|
[dependency-groups]
|
|
38
25
|
dev = [
|
|
39
26
|
"basedpyright",
|
|
40
27
|
"coverage[toml]",
|
|
41
|
-
"imbi-common[server,databases]==2.
|
|
28
|
+
"imbi-common[server,databases]==2.12.1",
|
|
42
29
|
"pre-commit",
|
|
43
30
|
"pytest",
|
|
44
31
|
"pytest-asyncio",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Imbi GitHub plugin (Architecture v3).
|
|
2
|
+
|
|
3
|
+
One :class:`~imbi_plugin_github.plugin.GitHubPlugin` backs every GitHub
|
|
4
|
+
Integration. The registry discovers this package by its ``imbi_plugin_*``
|
|
5
|
+
name and reads the module-level :data:`PLUGIN` attribute.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from imbi_plugin_github.plugin import GitHubPlugin
|
|
9
|
+
|
|
10
|
+
#: The package's single plugin, discovered by the registry convention scan.
|
|
11
|
+
PLUGIN = GitHubPlugin
|
|
12
|
+
|
|
13
|
+
__all__ = ['PLUGIN', 'GitHubPlugin']
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Shared host resolution utilities for the GitHub plugin.
|
|
2
|
+
|
|
3
|
+
Every capability resolves the GitHub host (github.com, a GHEC
|
|
4
|
+
``*.ghe.com`` tenant, or a GHES appliance) from the Integration's
|
|
5
|
+
``flavor`` + ``host`` option values, surfaced on
|
|
6
|
+
``PluginContext.integration_options``. This module is the single source
|
|
7
|
+
of truth for validating those options and mapping the resolved host to
|
|
8
|
+
the REST API base.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import typing
|
|
15
|
+
import urllib.parse
|
|
16
|
+
|
|
17
|
+
LOGGER = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def normalize_host(raw: typing.Any, label: str) -> str:
|
|
21
|
+
"""Validate and normalize an integration ``host`` value.
|
|
22
|
+
|
|
23
|
+
Strips whitespace, accepts an optional scheme, and rejects values
|
|
24
|
+
with paths / queries / fragments so callers can compose URLs from
|
|
25
|
+
the result without producing malformed endpoints.
|
|
26
|
+
"""
|
|
27
|
+
host = str(raw or '').strip()
|
|
28
|
+
if not host:
|
|
29
|
+
raise ValueError(f'{label} requires the "host" option')
|
|
30
|
+
parsed = urllib.parse.urlsplit(
|
|
31
|
+
host if '://' in host else f'https://{host}'
|
|
32
|
+
)
|
|
33
|
+
if (
|
|
34
|
+
not parsed.hostname
|
|
35
|
+
or parsed.port is not None
|
|
36
|
+
or parsed.path not in ('', '/')
|
|
37
|
+
or parsed.query
|
|
38
|
+
or parsed.fragment
|
|
39
|
+
):
|
|
40
|
+
raise ValueError(f'{label} got invalid host value: {host!r}')
|
|
41
|
+
return parsed.hostname
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def require_ghec_tenant_host(host: str, label: str) -> str:
|
|
45
|
+
"""Refuse anything that isn't a ``*.ghe.com`` tenant host."""
|
|
46
|
+
if (
|
|
47
|
+
not host.endswith('.ghe.com')
|
|
48
|
+
or host == '.ghe.com'
|
|
49
|
+
or host.startswith('api.')
|
|
50
|
+
):
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f'{label} requires a tenant host like "tenant.ghe.com"; '
|
|
53
|
+
f'got {host!r}'
|
|
54
|
+
)
|
|
55
|
+
return host
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def host_to_api_base(host: str) -> str:
|
|
59
|
+
"""Map a resolved GitHub host to its REST API base.
|
|
60
|
+
|
|
61
|
+
The single source of truth for GitHub's flavor routing:
|
|
62
|
+
``github.com`` -> ``api.github.com``, a ``*.ghe.com`` tenant ->
|
|
63
|
+
``api.<tenant>.ghe.com``, and a GHES appliance -> ``<host>/api/v3``.
|
|
64
|
+
"""
|
|
65
|
+
if host == 'github.com':
|
|
66
|
+
return 'https://api.github.com'
|
|
67
|
+
if host.endswith('.ghe.com'):
|
|
68
|
+
return f'https://api.{host}'
|
|
69
|
+
return f'https://{host}/api/v3'
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def flavor_host(options: dict[str, typing.Any], label: str) -> str:
|
|
73
|
+
"""Validate the Integration's ``flavor`` + ``host`` to a bare host.
|
|
74
|
+
|
|
75
|
+
The operator picks an explicit ``flavor`` (``github`` / ``ghec`` /
|
|
76
|
+
``ghes``); the ``host`` is required for the two enterprise flavors
|
|
77
|
+
and ignored for ``github``. Returns the bare hostname the rest of the
|
|
78
|
+
plugin composes URLs against (``github.com``, the validated
|
|
79
|
+
``*.ghe.com`` tenant, or the normalized GHES appliance host).
|
|
80
|
+
"""
|
|
81
|
+
flavor = str(options.get('flavor') or '').strip()
|
|
82
|
+
if flavor == 'github':
|
|
83
|
+
return 'github.com'
|
|
84
|
+
if flavor == 'ghec':
|
|
85
|
+
host = normalize_host(options.get('host'), label)
|
|
86
|
+
# Accept a bare tenant slug (e.g. ``aweber``) and compute the full
|
|
87
|
+
# tenant host; GHEC tenants always live under ``.ghe.com``.
|
|
88
|
+
if '.' not in host:
|
|
89
|
+
host = f'{host}.ghe.com'
|
|
90
|
+
return require_ghec_tenant_host(host, label)
|
|
91
|
+
if flavor == 'ghes':
|
|
92
|
+
return normalize_host(options.get('host'), label)
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f'{label} got invalid integration flavor {flavor!r}; expected one '
|
|
95
|
+
f'of "github", "ghec", or "ghes"'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def resolve_host(
|
|
100
|
+
integration_options: dict[str, typing.Any], label: str
|
|
101
|
+
) -> str | None:
|
|
102
|
+
"""Resolve the GitHub host from the Integration options, or ``None``.
|
|
103
|
+
|
|
104
|
+
Returns ``None`` (after logging) when the flavor/host is missing or
|
|
105
|
+
unusable so callers on the webhook path can fall through to another
|
|
106
|
+
resolution source rather than failing the delivery. Callers that
|
|
107
|
+
require a host raise on the ``None``.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
return flavor_host(integration_options, label)
|
|
111
|
+
except ValueError as exc:
|
|
112
|
+
LOGGER.warning('%s: unusable integration flavor/host: %s', label, exc)
|
|
113
|
+
return None
|
|
@@ -74,7 +74,7 @@ def derive_owner_repo_from_links(
|
|
|
74
74
|
"""Find a project link pointing at ``host`` and parse owner/repo.
|
|
75
75
|
|
|
76
76
|
Prefers the dashboard link keyed by ``preferred_key`` (the bound
|
|
77
|
-
|
|
77
|
+
Integration slug) when present and pointing at ``host``,
|
|
78
78
|
then the legacy ``github-repository`` link key, then scans the
|
|
79
79
|
remaining same-host links and returns the first usable one.
|
|
80
80
|
Returns ``None`` when nothing matches.
|
|
@@ -122,7 +122,7 @@ def resolve_owner_repo(
|
|
|
122
122
|
derived = derive_owner_repo_from_links(
|
|
123
123
|
ctx.project_links,
|
|
124
124
|
host,
|
|
125
|
-
preferred_key=ctx.
|
|
125
|
+
preferred_key=ctx.integration_slug,
|
|
126
126
|
)
|
|
127
127
|
if derived is not None:
|
|
128
128
|
return derived
|
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
"""GitHub commit / tag history sync (webhook action plugin).
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
``
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*runtime* data on the webhook path rather than a class attribute, so one
|
|
8
|
-
callable serves all three flavors. The API base is resolved per call, in
|
|
9
|
-
order:
|
|
3
|
+
The module exposes two webhook actions -- ``sync_commits`` and
|
|
4
|
+
``sync_tags`` -- dispatched by ``imbi-gateway`` on ``push`` deliveries
|
|
5
|
+
(catalogued by the plugin's ``webhook-actions`` capability). The API base
|
|
6
|
+
is resolved per call, in order:
|
|
10
7
|
|
|
11
8
|
1. ``api_base_url`` from the rule's ``handler_config`` (explicit
|
|
12
9
|
override), else
|
|
13
|
-
2. the
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
3. the ``ThirdPartyService.api_endpoint``
|
|
17
|
-
(``ctx.assignment_options['service_endpoint']``), else
|
|
18
|
-
4. the push payload's ``repository.url`` (already the flavor-correct API
|
|
10
|
+
2. the Integration's ``flavor`` + ``host`` options on
|
|
11
|
+
``ctx.integration_options``, else
|
|
12
|
+
3. the push payload's ``repository.url`` (already the flavor-correct API
|
|
19
13
|
URL) as a last resort.
|
|
20
14
|
|
|
21
|
-
The
|
|
22
|
-
for an on-demand, host-invoked
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
The :class:`GitHubCommitSync` capability handler exposes
|
|
16
|
+
:meth:`~GitHubCommitSync.sync_all_history` for an on-demand, host-invoked
|
|
17
|
+
backfill: there is no push payload, so the GitHub host is read from
|
|
18
|
+
``ctx.integration_options`` and ``(owner, repo)`` from the project links;
|
|
19
|
+
it walks the full default-branch history and the complete tag list rather
|
|
20
|
+
than a single push delta.
|
|
26
21
|
|
|
27
22
|
Commit / tag rows are written to the shared ClickHouse ``commits`` /
|
|
28
23
|
``tags`` tables via :func:`imbi_common.clickhouse.insert`. Writes are
|
|
@@ -51,20 +46,13 @@ from imbi_common.models import CommitRecord, TagRecord
|
|
|
51
46
|
from imbi_common.plugins.base import (
|
|
52
47
|
ActionDescriptor,
|
|
53
48
|
CheckStatus,
|
|
54
|
-
|
|
49
|
+
CommitSyncCapability,
|
|
55
50
|
PluginContext,
|
|
56
|
-
PluginManifest,
|
|
57
|
-
ServicePlugin,
|
|
58
|
-
WebhookActionPlugin,
|
|
59
51
|
)
|
|
60
52
|
from imbi_common.plugins.errors import PluginRateLimited
|
|
61
53
|
|
|
62
54
|
from imbi_plugin_github import _app_auth
|
|
63
|
-
from imbi_plugin_github._hosts import
|
|
64
|
-
host_to_api_base,
|
|
65
|
-
normalize_host,
|
|
66
|
-
require_ghec_tenant_host,
|
|
67
|
-
)
|
|
55
|
+
from imbi_plugin_github._hosts import host_to_api_base, resolve_host
|
|
68
56
|
from imbi_plugin_github._repos import resolve_owner_repo
|
|
69
57
|
from imbi_plugin_github.deployment import (
|
|
70
58
|
_auth_headers, # pyright: ignore[reportPrivateUsage]
|
|
@@ -82,10 +70,6 @@ LOGGER = logging.getLogger(__name__)
|
|
|
82
70
|
|
|
83
71
|
_HTTP_TIMEOUT_SECONDS = 10.0
|
|
84
72
|
_ZERO_SHA = '0' * 40
|
|
85
|
-
# This plugin's own slug; skipped when reading the GitHub host/flavor
|
|
86
|
-
# from connected ``service_plugins`` so the commit-sync entry can't
|
|
87
|
-
# masquerade as a github.com host on a GHEC/GHES service.
|
|
88
|
-
_SELF_SLUG = 'github-commit-sync'
|
|
89
73
|
# GitHub's compare endpoint caps ``commits[]`` at 250 per page and
|
|
90
74
|
# paginates the rest; bound the walk so a pathological single push
|
|
91
75
|
# (force-push of thousands of commits) can't pin us on one endpoint.
|
|
@@ -309,34 +293,6 @@ def _branch_short_name(ref: str) -> str:
|
|
|
309
293
|
return ref.removeprefix(prefix)
|
|
310
294
|
|
|
311
295
|
|
|
312
|
-
def _github_plugin_host(plugin: ServicePlugin) -> str | None:
|
|
313
|
-
"""Resolve the GitHub host from a connected plugin's slug + options.
|
|
314
|
-
|
|
315
|
-
Returns ``None`` for non-GitHub plugins and for GitHub plugins whose
|
|
316
|
-
required ``host`` option is missing or invalid (logged) so the caller
|
|
317
|
-
can fall through to the next resolution source.
|
|
318
|
-
"""
|
|
319
|
-
slug = plugin.slug
|
|
320
|
-
if not slug.startswith('github'):
|
|
321
|
-
return None
|
|
322
|
-
label = f'github-commit-sync (via {slug})'
|
|
323
|
-
try:
|
|
324
|
-
if slug.endswith('-ec') or slug == 'github-enterprise-cloud':
|
|
325
|
-
return require_ghec_tenant_host(
|
|
326
|
-
normalize_host(plugin.options.get('host'), label), label
|
|
327
|
-
)
|
|
328
|
-
if slug.endswith('-es') or slug == 'github-enterprise-server':
|
|
329
|
-
return normalize_host(plugin.options.get('host'), label)
|
|
330
|
-
except ValueError as exc:
|
|
331
|
-
LOGGER.warning(
|
|
332
|
-
'Connected GitHub plugin %r has an unusable host option: %s',
|
|
333
|
-
slug,
|
|
334
|
-
exc,
|
|
335
|
-
)
|
|
336
|
-
return None
|
|
337
|
-
return 'github.com'
|
|
338
|
-
|
|
339
|
-
|
|
340
296
|
def _api_base_from_repo_url(repo_url: object) -> str | None:
|
|
341
297
|
"""Derive the API base from the push payload's ``repository.url``.
|
|
342
298
|
|
|
@@ -363,24 +319,15 @@ def _resolve_api_base(
|
|
|
363
319
|
"""Pick the GitHub API base for this call (see module docstring)."""
|
|
364
320
|
if explicit:
|
|
365
321
|
return explicit.rstrip('/')
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
# github.com on a GHEC/GHES service.
|
|
370
|
-
if plugin.slug == _SELF_SLUG:
|
|
371
|
-
continue
|
|
372
|
-
host = _github_plugin_host(plugin)
|
|
373
|
-
if host:
|
|
374
|
-
return host_to_api_base(host)
|
|
375
|
-
endpoint = ctx.assignment_options.get('service_endpoint')
|
|
376
|
-
if isinstance(endpoint, str) and endpoint:
|
|
377
|
-
return endpoint.rstrip('/')
|
|
322
|
+
host = resolve_host(ctx.integration_options, 'github-commit-sync')
|
|
323
|
+
if host:
|
|
324
|
+
return host_to_api_base(host)
|
|
378
325
|
base = _api_base_from_repo_url(_resolve(repo_url_pointer, event))
|
|
379
326
|
if base:
|
|
380
327
|
LOGGER.info(
|
|
381
328
|
"github-commit-sync falling back to the event's repository.url "
|
|
382
|
-
'for the API base; no api_base_url
|
|
383
|
-
'
|
|
329
|
+
'for the API base; no api_base_url or resolvable Integration '
|
|
330
|
+
'flavor/host was available'
|
|
384
331
|
)
|
|
385
332
|
return base
|
|
386
333
|
return None
|
|
@@ -732,20 +679,13 @@ async def _hydrate_ci(
|
|
|
732
679
|
def _resolve_host_for_context(ctx: PluginContext) -> str | None:
|
|
733
680
|
"""Resolve the GitHub web host for an on-demand sync (no payload).
|
|
734
681
|
|
|
735
|
-
|
|
736
|
-
returns the
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
connected GitHub plugin yields ``None``.
|
|
682
|
+
Reads the Integration's ``flavor`` + ``host`` options from
|
|
683
|
+
``ctx.integration_options`` and returns the resolved host (github.com,
|
|
684
|
+
a GHEC tenant, or a GHES appliance). Unlike the webhook path there is
|
|
685
|
+
no push payload to fall back to, so a missing/unusable flavor/host
|
|
686
|
+
yields ``None``.
|
|
741
687
|
"""
|
|
742
|
-
|
|
743
|
-
if plugin.slug == _SELF_SLUG:
|
|
744
|
-
continue
|
|
745
|
-
host = _github_plugin_host(plugin)
|
|
746
|
-
if host:
|
|
747
|
-
return host
|
|
748
|
-
return None
|
|
688
|
+
return resolve_host(ctx.integration_options, 'github-commit-sync')
|
|
749
689
|
|
|
750
690
|
|
|
751
691
|
async def _fetch_default_branch(
|
|
@@ -1278,70 +1218,35 @@ sync_tags_descriptor = ActionDescriptor(
|
|
|
1278
1218
|
)
|
|
1279
1219
|
|
|
1280
1220
|
|
|
1281
|
-
class
|
|
1282
|
-
"""
|
|
1221
|
+
class GitHubCommitSync(CommitSyncCapability):
|
|
1222
|
+
"""Commit / tag history sync capability handler.
|
|
1283
1223
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
short-lived installation token per call and caches it.
|
|
1224
|
+
Uses the Integration's shared credential blob (resolved by
|
|
1225
|
+
:func:`_resolve_bearer`): a static ``access_token`` PAT when present,
|
|
1226
|
+
otherwise a GitHub App (``app_id`` + ``private_key``, raw or base64
|
|
1227
|
+
PEM, with an optional ``installation_id``) whose short-lived
|
|
1228
|
+
installation token is minted per call and cached. The gateway-side
|
|
1229
|
+
incremental sync flows through the ``sync_commits`` / ``sync_tags``
|
|
1230
|
+
webhook actions catalogued by the plugin's ``webhook-actions``
|
|
1231
|
+
capability.
|
|
1293
1232
|
"""
|
|
1294
1233
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
required=False,
|
|
1312
|
-
),
|
|
1313
|
-
CredentialField(
|
|
1314
|
-
name='app_id',
|
|
1315
|
-
label='GitHub App ID',
|
|
1316
|
-
description=(
|
|
1317
|
-
'GitHub App identifier; with a private key the plugin '
|
|
1318
|
-
'mints short-lived installation tokens.'
|
|
1319
|
-
),
|
|
1320
|
-
required=False,
|
|
1321
|
-
),
|
|
1322
|
-
CredentialField(
|
|
1323
|
-
name='private_key',
|
|
1324
|
-
label='GitHub App Private Key',
|
|
1325
|
-
description=(
|
|
1326
|
-
'App private key, raw PEM or base64-encoded PEM.'
|
|
1327
|
-
),
|
|
1328
|
-
required=False,
|
|
1329
|
-
),
|
|
1330
|
-
CredentialField(
|
|
1331
|
-
name='installation_id',
|
|
1332
|
-
label='GitHub App Installation ID',
|
|
1333
|
-
description=(
|
|
1334
|
-
'Optional. When unset, the installation is discovered '
|
|
1335
|
-
'from the pushed repository.'
|
|
1336
|
-
),
|
|
1337
|
-
required=False,
|
|
1338
|
-
),
|
|
1339
|
-
],
|
|
1340
|
-
)
|
|
1341
|
-
|
|
1342
|
-
@classmethod
|
|
1343
|
-
def actions(cls) -> list[ActionDescriptor]:
|
|
1344
|
-
return [sync_commits_descriptor, sync_tags_descriptor]
|
|
1234
|
+
async def check_available(
|
|
1235
|
+
self,
|
|
1236
|
+
*,
|
|
1237
|
+
ctx: PluginContext,
|
|
1238
|
+
credentials: dict[str, str],
|
|
1239
|
+
) -> bool:
|
|
1240
|
+
"""Whether an on-demand sync can resolve a host + repo for ``ctx``."""
|
|
1241
|
+
del credentials
|
|
1242
|
+
host = _resolve_host_for_context(ctx)
|
|
1243
|
+
if host is None:
|
|
1244
|
+
return False
|
|
1245
|
+
try:
|
|
1246
|
+
resolve_owner_repo(ctx, host, 'github-commit-sync')
|
|
1247
|
+
except ValueError:
|
|
1248
|
+
return False
|
|
1249
|
+
return True
|
|
1345
1250
|
|
|
1346
1251
|
async def sync_all_history(
|
|
1347
1252
|
self,
|
|
@@ -1352,12 +1257,12 @@ class GitHubCommitSyncPlugin(WebhookActionPlugin):
|
|
|
1352
1257
|
"""Record the project's full default-branch history and all tags.
|
|
1353
1258
|
|
|
1354
1259
|
Host-invoked (no webhook payload): the host instantiates the
|
|
1355
|
-
|
|
1356
|
-
links and the
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1260
|
+
handler, builds a :class:`PluginContext` carrying the project's
|
|
1261
|
+
links and the Integration options, resolves the Integration's
|
|
1262
|
+
``credentials``, and awaits this method. The GitHub host/flavor
|
|
1263
|
+
is read from ``ctx.integration_options``, the ``(owner, repo)``
|
|
1264
|
+
from the project links, and the bearer token from the same
|
|
1265
|
+
PAT-or-App resolution the webhook actions use.
|
|
1361
1266
|
|
|
1362
1267
|
Walks every commit reachable from the default branch head plus the
|
|
1363
1268
|
repo's complete (lightweight) tag list, maps them onto
|
|
@@ -1377,7 +1282,7 @@ class GitHubCommitSyncPlugin(WebhookActionPlugin):
|
|
|
1377
1282
|
if host is None:
|
|
1378
1283
|
raise ValueError(
|
|
1379
1284
|
'github-commit-sync could not resolve a GitHub host for an '
|
|
1380
|
-
'on-demand sync:
|
|
1285
|
+
'on-demand sync: set the Integration flavor/host'
|
|
1381
1286
|
)
|
|
1382
1287
|
base = host_to_api_base(host)
|
|
1383
1288
|
owner, repo = resolve_owner_repo(ctx, host, 'github-commit-sync')
|