imbi-plugin-github 2.11.5__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.
Files changed (32) hide show
  1. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/CLAUDE.md +31 -21
  2. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/PKG-INFO +2 -2
  3. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/pyproject.toml +6 -10
  4. imbi_plugin_github-2.11.6/src/imbi_plugin_github/__init__.py +21 -0
  5. imbi_plugin_github-2.11.6/src/imbi_plugin_github/_hosts.py +140 -0
  6. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/commits.py +21 -46
  7. imbi_plugin_github-2.11.6/src/imbi_plugin_github/connection.py +79 -0
  8. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/deployment.py +25 -93
  9. imbi_plugin_github-2.11.6/src/imbi_plugin_github/doctor.py +687 -0
  10. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/identity.py +63 -184
  11. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/lifecycle.py +111 -185
  12. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/tests/test_commits.py +31 -51
  13. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/tests/test_deployment.py +83 -48
  14. imbi_plugin_github-2.11.6/tests/test_doctor.py +538 -0
  15. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/tests/test_identity.py +94 -74
  16. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/tests/test_lifecycle.py +67 -58
  17. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/uv.lock +7 -7
  18. imbi_plugin_github-2.11.5/src/imbi_plugin_github/__init__.py +0 -13
  19. imbi_plugin_github-2.11.5/src/imbi_plugin_github/_hosts.py +0 -67
  20. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/.github/workflows/publish.yml +0 -0
  21. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/.github/workflows/test.yml +0 -0
  22. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/.gitignore +0 -0
  23. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/.pre-commit-config.yaml +0 -0
  24. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/LICENSE +0 -0
  25. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/README.md +0 -0
  26. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/justfile +0 -0
  27. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/_app_auth.py +0 -0
  28. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/_repos.py +0 -0
  29. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/src/imbi_plugin_github/pull_requests.py +0 -0
  30. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/tests/__init__.py +0 -0
  31. {imbi_plugin_github-2.11.5 → imbi_plugin_github-2.11.6}/tests/test_hosts.py +0 -0
  32. {imbi_plugin_github-2.11.5 → 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 **three plugin types**
9
- (identity, deployment, lifecycle), each in **three host flavors**
10
- (github.com, GitHub Enterprise Cloud, GitHub Enterprise Server) — nine
11
- plugins total. The Imbi host discovers them through the
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 base/subclass/host-flavor pattern
44
+ ### The connection-plugin / host-agnostic pattern
42
45
 
43
- Every plugin type follows the same shape: one `_*Base` class holds all
44
- behavior, and three thin concrete subclasses differ *only* in their
45
- `_resolve_host(options)` classmethod:
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
- - github.com flavor returns `'github.com'`.
48
- - GHEC flavor → reads the required `host` option, validates it's a
49
- `*.ghe.com` tenant via `require_ghec_tenant_host`.
50
- - GHES flavor reads the required `host` option, normalized only.
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
- `_resolve_host` feeds URL construction, and the three backends route
53
- differently this mapping is the single most important thing to keep
54
- consistent and is **duplicated** across the three modules
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
- When you change routing for one plugin type, check whether the other two
64
- need the same change.
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` (strip/validate a bare hostname, reject
69
- paths/ports/queries) and `require_ghec_tenant_host`. All host-option
70
- validation lives here.
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.5
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.5
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.5"
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.5",
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.identity:GitHubPlugin"
26
- github-enterprise-cloud = "imbi_plugin_github.identity:GitHubEnterpriseCloudPlugin"
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.5",
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 _github_plugin_host(plugin: ServicePlugin) -> str | None:
313
- """Resolve the GitHub host from a connected plugin's slug + options.
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`` 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.
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
- slug = plugin.slug
320
- if not slug.startswith('github'):
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
- 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)
319
+ return flavor_host(plugin.options, label)
330
320
  except ValueError as exc:
331
321
  LOGGER.warning(
332
- 'Connected GitHub plugin %r has an unusable host option: %s',
333
- slug,
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
- for plugin in ctx.service_plugins:
367
- # Skip our own entry: its slug starts with "github" but carries
368
- # no host option, so _github_plugin_host would mis-resolve it to
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
- Walks the connected GitHub plugins on ``ctx.service_plugins`` and
736
- returns the first usable host (github.com, a GHEC tenant, or a GHES
737
- appliance), skipping this plugin's own entry so a commit-sync row on a
738
- GHEC/GHES service can't be read as github.com. Unlike the webhook
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
- for plugin in ctx.service_plugins:
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
+ )