imbi-plugin-github 2.11.4__tar.gz → 2.11.6__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.4 → imbi_plugin_github-2.11.6}/CLAUDE.md +31 -21
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/PKG-INFO +2 -2
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/pyproject.toml +6 -10
- imbi_plugin_github-2.11.6/src/imbi_plugin_github/__init__.py +21 -0
- imbi_plugin_github-2.11.6/src/imbi_plugin_github/_hosts.py +140 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/commits.py +21 -46
- imbi_plugin_github-2.11.6/src/imbi_plugin_github/connection.py +79 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/deployment.py +25 -93
- imbi_plugin_github-2.11.6/src/imbi_plugin_github/doctor.py +687 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/identity.py +63 -184
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/lifecycle.py +111 -185
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/tests/test_commits.py +31 -51
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/tests/test_deployment.py +83 -48
- imbi_plugin_github-2.11.6/tests/test_doctor.py +538 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/tests/test_identity.py +94 -74
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/tests/test_lifecycle.py +67 -58
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/uv.lock +7 -7
- imbi_plugin_github-2.11.4/src/imbi_plugin_github/__init__.py +0 -13
- imbi_plugin_github-2.11.4/src/imbi_plugin_github/_hosts.py +0 -67
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/.github/workflows/publish.yml +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/.github/workflows/test.yml +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/.gitignore +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/.pre-commit-config.yaml +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/LICENSE +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/README.md +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/justfile +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/_app_auth.py +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/_repos.py +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/pull_requests.py +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/tests/__init__.py +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/tests/test_hosts.py +0 -0
- {imbi_plugin_github-2.11.4 → imbi_plugin_github-2.11.6}/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.11.
|
|
3
|
+
Version: 2.11.6
|
|
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.6
|
|
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.6"
|
|
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,25 @@ classifiers = [
|
|
|
16
16
|
]
|
|
17
17
|
dependencies = [
|
|
18
18
|
"httpx>=0.27",
|
|
19
|
-
"imbi-common[databases]==2.11.
|
|
19
|
+
"imbi-common[databases]==2.11.6",
|
|
20
20
|
"pydantic>=2",
|
|
21
21
|
"pyjwt[crypto]>=2.8",
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
[project.entry-points."imbi.plugins"]
|
|
25
|
-
github = "imbi_plugin_github.
|
|
26
|
-
github-
|
|
27
|
-
github-enterprise-server = "imbi_plugin_github.identity:GitHubEnterpriseServerPlugin"
|
|
25
|
+
github-connection = "imbi_plugin_github.connection:GitHubConnectionPlugin"
|
|
26
|
+
github-identity = "imbi_plugin_github.identity:GitHubIdentityPlugin"
|
|
28
27
|
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
28
|
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
29
|
github-commit-sync = "imbi_plugin_github.commits:GitHubCommitSyncPlugin"
|
|
35
30
|
github-pr-sync = "imbi_plugin_github.pull_requests:GitHubPRSyncPlugin"
|
|
31
|
+
github-doctor = "imbi_plugin_github.doctor:GitHubDoctorPlugin"
|
|
36
32
|
|
|
37
33
|
[dependency-groups]
|
|
38
34
|
dev = [
|
|
39
35
|
"basedpyright",
|
|
40
36
|
"coverage[toml]",
|
|
41
|
-
"imbi-common[server,databases]==2.11.
|
|
37
|
+
"imbi-common[server,databases]==2.11.6",
|
|
42
38
|
"pre-commit",
|
|
43
39
|
"pytest",
|
|
44
40
|
"pytest-asyncio",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Imbi GitHub plugins.
|
|
2
|
+
|
|
3
|
+
A single connection plugin holds the flavor/host and shared App/PAT
|
|
4
|
+
credentials for the service; one host-agnostic plugin per behavior
|
|
5
|
+
(identity, deployment, lifecycle, commit-sync, pr-sync, doctor) reads the
|
|
6
|
+
host from that connection plugin.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from imbi_plugin_github.connection import GitHubConnectionPlugin
|
|
10
|
+
from imbi_plugin_github.deployment import GitHubDeploymentPlugin
|
|
11
|
+
from imbi_plugin_github.doctor import GitHubDoctorPlugin
|
|
12
|
+
from imbi_plugin_github.identity import GitHubIdentityPlugin
|
|
13
|
+
from imbi_plugin_github.lifecycle import GitHubLifecyclePlugin
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
'GitHubConnectionPlugin',
|
|
17
|
+
'GitHubDeploymentPlugin',
|
|
18
|
+
'GitHubDoctorPlugin',
|
|
19
|
+
'GitHubIdentityPlugin',
|
|
20
|
+
'GitHubLifecyclePlugin',
|
|
21
|
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Shared host resolution utilities for the GitHub plugins.
|
|
2
|
+
|
|
3
|
+
Identity (``plugin.py``) and deployment (``deployment.py``) plugins both
|
|
4
|
+
accept a ``host`` option and need to normalise it to a bare hostname
|
|
5
|
+
that URLs can be safely composed against. This module is the single
|
|
6
|
+
source of truth for that validation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import collections.abc
|
|
12
|
+
import typing
|
|
13
|
+
import urllib.parse
|
|
14
|
+
|
|
15
|
+
#: Slug of the GitHub connection plugin. A single connection plugin is
|
|
16
|
+
#: attached to each GitHub ``ThirdPartyService`` and carries the
|
|
17
|
+
#: ``flavor`` + ``host`` options (and the shared App/PAT credentials)
|
|
18
|
+
#: that every sibling GitHub plugin reads. Behavioral plugins resolve
|
|
19
|
+
#: their host from this sibling rather than from a per-flavor variant of
|
|
20
|
+
#: their own class.
|
|
21
|
+
GITHUB_CONNECTION_SLUG = 'github-connection'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def normalize_host(raw: typing.Any, label: str) -> str:
|
|
25
|
+
"""Validate and normalize a manifest ``host`` value.
|
|
26
|
+
|
|
27
|
+
Strips whitespace, accepts an optional scheme, and rejects values
|
|
28
|
+
with paths / queries / fragments so callers can compose URLs from
|
|
29
|
+
the result without producing malformed endpoints.
|
|
30
|
+
"""
|
|
31
|
+
host = str(raw or '').strip()
|
|
32
|
+
if not host:
|
|
33
|
+
raise ValueError(f'{label} requires the "host" option')
|
|
34
|
+
parsed = urllib.parse.urlsplit(
|
|
35
|
+
host if '://' in host else f'https://{host}'
|
|
36
|
+
)
|
|
37
|
+
if (
|
|
38
|
+
not parsed.hostname
|
|
39
|
+
or parsed.port is not None
|
|
40
|
+
or parsed.path not in ('', '/')
|
|
41
|
+
or parsed.query
|
|
42
|
+
or parsed.fragment
|
|
43
|
+
):
|
|
44
|
+
raise ValueError(f'{label} got invalid host value: {host!r}')
|
|
45
|
+
return parsed.hostname
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def require_ghec_tenant_host(host: str, label: str) -> str:
|
|
49
|
+
"""Refuse anything that isn't a ``*.ghe.com`` tenant host."""
|
|
50
|
+
if (
|
|
51
|
+
not host.endswith('.ghe.com')
|
|
52
|
+
or host == '.ghe.com'
|
|
53
|
+
or host.startswith('api.')
|
|
54
|
+
):
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f'{label} requires a tenant host like "tenant.ghe.com"; '
|
|
57
|
+
f'got {host!r}'
|
|
58
|
+
)
|
|
59
|
+
return host
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def host_to_api_base(host: str) -> str:
|
|
63
|
+
"""Map a resolved GitHub host to its REST API base.
|
|
64
|
+
|
|
65
|
+
The single source of truth for GitHub's flavor routing:
|
|
66
|
+
``github.com`` -> ``api.github.com``, a ``*.ghe.com`` tenant ->
|
|
67
|
+
``api.<tenant>.ghe.com``, and a GHES appliance -> ``<host>/api/v3``.
|
|
68
|
+
Shared by every behavioral plugin once it has resolved its host from
|
|
69
|
+
the connection plugin via :func:`resolve_connection_host`.
|
|
70
|
+
"""
|
|
71
|
+
if host == 'github.com':
|
|
72
|
+
return 'https://api.github.com'
|
|
73
|
+
if host.endswith('.ghe.com'):
|
|
74
|
+
return f'https://api.{host}'
|
|
75
|
+
return f'https://{host}/api/v3'
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _ConnectionLike(typing.Protocol):
|
|
79
|
+
"""Structural view of an :class:`imbi_common.plugins.ServicePlugin`."""
|
|
80
|
+
|
|
81
|
+
slug: str
|
|
82
|
+
options: dict[str, typing.Any]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def flavor_host(options: dict[str, typing.Any], label: str) -> str:
|
|
86
|
+
"""Validate the connection plugin's ``flavor`` + ``host`` to a host.
|
|
87
|
+
|
|
88
|
+
The operator picks an explicit ``flavor`` (``github.com`` / ``ghec``
|
|
89
|
+
/ ``ghes``); the ``host`` is required for the two enterprise flavors
|
|
90
|
+
and ignored for ``github.com``. Returns the bare hostname the rest of
|
|
91
|
+
the plugin composes URLs against (``github.com``, the validated
|
|
92
|
+
``*.ghe.com`` tenant, or the normalized GHES appliance host).
|
|
93
|
+
"""
|
|
94
|
+
flavor = str(options.get('flavor') or '').strip()
|
|
95
|
+
if flavor == 'github.com':
|
|
96
|
+
return 'github.com'
|
|
97
|
+
if flavor == 'ghec':
|
|
98
|
+
return require_ghec_tenant_host(
|
|
99
|
+
normalize_host(options.get('host'), label), label
|
|
100
|
+
)
|
|
101
|
+
if flavor == 'ghes':
|
|
102
|
+
return normalize_host(options.get('host'), label)
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f'{label} got invalid connection flavor {flavor!r}; expected one '
|
|
105
|
+
f'of "github.com", "ghec", or "ghes"'
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def find_connection(
|
|
110
|
+
service_plugins: collections.abc.Iterable[_ConnectionLike],
|
|
111
|
+
) -> _ConnectionLike | None:
|
|
112
|
+
"""Return the ``github-connection`` sibling, or ``None`` if absent.
|
|
113
|
+
|
|
114
|
+
Locates the connection plugin without validating its options, so
|
|
115
|
+
callers that want to fall back when no connection plugin is attached
|
|
116
|
+
(the webhook path) can distinguish "absent" from "misconfigured".
|
|
117
|
+
"""
|
|
118
|
+
for plugin in service_plugins:
|
|
119
|
+
if plugin.slug == GITHUB_CONNECTION_SLUG:
|
|
120
|
+
return plugin
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def resolve_connection_host(
|
|
125
|
+
service_plugins: collections.abc.Iterable[_ConnectionLike], label: str
|
|
126
|
+
) -> str:
|
|
127
|
+
"""Return the GitHub host from the connection plugin sibling.
|
|
128
|
+
|
|
129
|
+
Scans ``service_plugins`` for the single ``github-connection`` entry
|
|
130
|
+
and resolves its ``flavor`` + ``host`` to a bare hostname. Raises
|
|
131
|
+
``ValueError`` (operator-facing) when no connection plugin is
|
|
132
|
+
attached to the service or its flavor/host is unusable.
|
|
133
|
+
"""
|
|
134
|
+
plugin = find_connection(service_plugins)
|
|
135
|
+
if plugin is None:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f'{label}: no {GITHUB_CONNECTION_SLUG} plugin is attached to '
|
|
138
|
+
f'the service; cannot resolve the GitHub host'
|
|
139
|
+
)
|
|
140
|
+
return flavor_host(plugin.options, label)
|
|
@@ -54,16 +54,15 @@ from imbi_common.plugins.base import (
|
|
|
54
54
|
CredentialField,
|
|
55
55
|
PluginContext,
|
|
56
56
|
PluginManifest,
|
|
57
|
-
ServicePlugin,
|
|
58
57
|
WebhookActionPlugin,
|
|
59
58
|
)
|
|
60
59
|
from imbi_common.plugins.errors import PluginRateLimited
|
|
61
60
|
|
|
62
61
|
from imbi_plugin_github import _app_auth
|
|
63
62
|
from imbi_plugin_github._hosts import (
|
|
63
|
+
find_connection,
|
|
64
|
+
flavor_host,
|
|
64
65
|
host_to_api_base,
|
|
65
|
-
normalize_host,
|
|
66
|
-
require_ghec_tenant_host,
|
|
67
66
|
)
|
|
68
67
|
from imbi_plugin_github._repos import resolve_owner_repo
|
|
69
68
|
from imbi_plugin_github.deployment import (
|
|
@@ -82,10 +81,6 @@ LOGGER = logging.getLogger(__name__)
|
|
|
82
81
|
|
|
83
82
|
_HTTP_TIMEOUT_SECONDS = 10.0
|
|
84
83
|
_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
84
|
# GitHub's compare endpoint caps ``commits[]`` at 250 per page and
|
|
90
85
|
# paginates the rest; bound the walk so a pathological single push
|
|
91
86
|
# (force-push of thousands of commits) can't pin us on one endpoint.
|
|
@@ -309,32 +304,26 @@ def _branch_short_name(ref: str) -> str:
|
|
|
309
304
|
return ref.removeprefix(prefix)
|
|
310
305
|
|
|
311
306
|
|
|
312
|
-
def
|
|
313
|
-
"""Resolve the GitHub host from
|
|
307
|
+
def _connection_host(ctx: PluginContext, label: str) -> str | None:
|
|
308
|
+
"""Resolve the GitHub host from the ``github-connection`` sibling.
|
|
314
309
|
|
|
315
|
-
Returns ``None``
|
|
316
|
-
|
|
317
|
-
|
|
310
|
+
Returns ``None`` when no connection plugin is attached so callers can
|
|
311
|
+
fall through to the next resolution source. A connection plugin that
|
|
312
|
+
*is* present but carries an unusable flavor/host is logged and also
|
|
313
|
+
treated as a fall-through rather than failing the delivery.
|
|
318
314
|
"""
|
|
319
|
-
|
|
320
|
-
if
|
|
315
|
+
plugin = find_connection(ctx.service_plugins)
|
|
316
|
+
if plugin is None:
|
|
321
317
|
return None
|
|
322
|
-
label = f'github-commit-sync (via {slug})'
|
|
323
318
|
try:
|
|
324
|
-
|
|
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)
|
|
319
|
+
return flavor_host(plugin.options, label)
|
|
330
320
|
except ValueError as exc:
|
|
331
321
|
LOGGER.warning(
|
|
332
|
-
'
|
|
333
|
-
|
|
322
|
+
'%s: github-connection plugin has an unusable flavor/host: %s',
|
|
323
|
+
label,
|
|
334
324
|
exc,
|
|
335
325
|
)
|
|
336
326
|
return None
|
|
337
|
-
return 'github.com'
|
|
338
327
|
|
|
339
328
|
|
|
340
329
|
def _api_base_from_repo_url(repo_url: object) -> str | None:
|
|
@@ -363,15 +352,9 @@ def _resolve_api_base(
|
|
|
363
352
|
"""Pick the GitHub API base for this call (see module docstring)."""
|
|
364
353
|
if explicit:
|
|
365
354
|
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)
|
|
355
|
+
host = _connection_host(ctx, 'github-commit-sync')
|
|
356
|
+
if host:
|
|
357
|
+
return host_to_api_base(host)
|
|
375
358
|
endpoint = ctx.assignment_options.get('service_endpoint')
|
|
376
359
|
if isinstance(endpoint, str) and endpoint:
|
|
377
360
|
return endpoint.rstrip('/')
|
|
@@ -732,20 +715,12 @@ async def _hydrate_ci(
|
|
|
732
715
|
def _resolve_host_for_context(ctx: PluginContext) -> str | None:
|
|
733
716
|
"""Resolve the GitHub web host for an on-demand sync (no payload).
|
|
734
717
|
|
|
735
|
-
|
|
736
|
-
returns
|
|
737
|
-
appliance)
|
|
738
|
-
|
|
739
|
-
path there is no push payload to fall back to, so the absence of a
|
|
740
|
-
connected GitHub plugin yields ``None``.
|
|
718
|
+
Reads the ``github-connection`` sibling on ``ctx.service_plugins``
|
|
719
|
+
and returns its resolved host (github.com, a GHEC tenant, or a GHES
|
|
720
|
+
appliance). Unlike the webhook path there is no push payload to fall
|
|
721
|
+
back to, so the absence of a connection plugin yields ``None``.
|
|
741
722
|
"""
|
|
742
|
-
|
|
743
|
-
if plugin.slug == _SELF_SLUG:
|
|
744
|
-
continue
|
|
745
|
-
host = _github_plugin_host(plugin)
|
|
746
|
-
if host:
|
|
747
|
-
return host
|
|
748
|
-
return None
|
|
723
|
+
return _connection_host(ctx, 'github-commit-sync')
|
|
749
724
|
|
|
750
725
|
|
|
751
726
|
async def _fetch_default_branch(
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""GitHub connection plugin.
|
|
2
|
+
|
|
3
|
+
A single connection plugin is attached to each GitHub
|
|
4
|
+
``ThirdPartyService`` and is the one place an operator configures the
|
|
5
|
+
GitHub flavor / host and the shared server-to-server credentials. It
|
|
6
|
+
carries no behavior of its own -- the identity, deployment, lifecycle,
|
|
7
|
+
and commit-/pr-sync plugins read its ``flavor`` + ``host`` options off
|
|
8
|
+
``ctx.service_plugins`` to resolve their host, and the Imbi host
|
|
9
|
+
resolves their App/PAT credentials from this plugin's
|
|
10
|
+
``plugin_configuration`` when they carry none themselves.
|
|
11
|
+
|
|
12
|
+
The identity plugin keeps its own OAuth App ``client_id`` /
|
|
13
|
+
``client_secret`` (a GitHub OAuth App is a separate registration from a
|
|
14
|
+
GitHub App), so those credentials are deliberately *not* declared here.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from imbi_common.plugins.base import (
|
|
20
|
+
ConnectionPlugin,
|
|
21
|
+
CredentialField,
|
|
22
|
+
PluginManifest,
|
|
23
|
+
PluginOption,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GitHubConnectionPlugin(ConnectionPlugin):
|
|
28
|
+
manifest = PluginManifest(
|
|
29
|
+
slug='github-connection',
|
|
30
|
+
name='GitHub Connection',
|
|
31
|
+
description=(
|
|
32
|
+
'Holds the GitHub flavor/host and shared App/PAT credentials '
|
|
33
|
+
'for every GitHub plugin on the service.'
|
|
34
|
+
),
|
|
35
|
+
plugin_type='connection',
|
|
36
|
+
auth_type='api_token',
|
|
37
|
+
options=[
|
|
38
|
+
PluginOption(
|
|
39
|
+
name='flavor',
|
|
40
|
+
label='Flavor',
|
|
41
|
+
description=(
|
|
42
|
+
'github.com, GitHub Enterprise Cloud (ghec), or '
|
|
43
|
+
'GitHub Enterprise Server (ghes).'
|
|
44
|
+
),
|
|
45
|
+
type='string',
|
|
46
|
+
choices=['github.com', 'ghec', 'ghes'],
|
|
47
|
+
required=True,
|
|
48
|
+
),
|
|
49
|
+
PluginOption(
|
|
50
|
+
name='host',
|
|
51
|
+
label='Host',
|
|
52
|
+
description=(
|
|
53
|
+
'Tenant host (tenant.ghe.com) for ghec or the '
|
|
54
|
+
'appliance hostname for ghes; ignored for github.com.'
|
|
55
|
+
),
|
|
56
|
+
type='string',
|
|
57
|
+
),
|
|
58
|
+
],
|
|
59
|
+
credentials=[
|
|
60
|
+
CredentialField(
|
|
61
|
+
name='app_id',
|
|
62
|
+
label='GitHub App ID',
|
|
63
|
+
description='GitHub App ID (App auth).',
|
|
64
|
+
required=False,
|
|
65
|
+
),
|
|
66
|
+
CredentialField(
|
|
67
|
+
name='private_key',
|
|
68
|
+
label='GitHub App private key',
|
|
69
|
+
description='PEM or base64-encoded PEM (App auth).',
|
|
70
|
+
required=False,
|
|
71
|
+
),
|
|
72
|
+
CredentialField(
|
|
73
|
+
name='access_token',
|
|
74
|
+
label='Personal access token',
|
|
75
|
+
description='PAT used when not using App auth.',
|
|
76
|
+
required=False,
|
|
77
|
+
),
|
|
78
|
+
],
|
|
79
|
+
)
|