imbi-plugin-github 2.11.6__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.
Files changed (34) hide show
  1. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/PKG-INFO +2 -2
  2. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/pyproject.toml +3 -12
  3. imbi_plugin_github-2.12.1/src/imbi_plugin_github/__init__.py +13 -0
  4. imbi_plugin_github-2.12.1/src/imbi_plugin_github/_hosts.py +113 -0
  5. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/_repos.py +2 -2
  6. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/commits.py +57 -127
  7. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/deployment.py +17 -97
  8. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/doctor.py +35 -46
  9. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/identity.py +17 -57
  10. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/lifecycle.py +21 -113
  11. imbi_plugin_github-2.12.1/src/imbi_plugin_github/plugin.py +335 -0
  12. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/pull_requests.py +32 -67
  13. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/tests/test_commits.py +61 -79
  14. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/tests/test_deployment.py +108 -130
  15. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/tests/test_doctor.py +61 -68
  16. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/tests/test_hosts.py +28 -0
  17. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/tests/test_identity.py +40 -38
  18. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/tests/test_lifecycle.py +83 -86
  19. imbi_plugin_github-2.12.1/tests/test_pull_requests.py +184 -0
  20. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/uv.lock +7 -7
  21. imbi_plugin_github-2.11.6/src/imbi_plugin_github/__init__.py +0 -21
  22. imbi_plugin_github-2.11.6/src/imbi_plugin_github/_hosts.py +0 -140
  23. imbi_plugin_github-2.11.6/src/imbi_plugin_github/connection.py +0 -79
  24. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/.github/workflows/publish.yml +0 -0
  25. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/.github/workflows/test.yml +0 -0
  26. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/.gitignore +0 -0
  27. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/.pre-commit-config.yaml +0 -0
  28. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/CLAUDE.md +0 -0
  29. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/LICENSE +0 -0
  30. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/README.md +0 -0
  31. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/justfile +0 -0
  32. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/src/imbi_plugin_github/_app_auth.py +0 -0
  33. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/tests/__init__.py +0 -0
  34. {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.1}/tests/test_repos.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imbi-plugin-github
3
- Version: 2.11.6
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.11.6
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.11.6"
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,25 +16,16 @@ classifiers = [
16
16
  ]
17
17
  dependencies = [
18
18
  "httpx>=0.27",
19
- "imbi-common[databases]==2.11.6",
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-connection = "imbi_plugin_github.connection:GitHubConnectionPlugin"
26
- github-identity = "imbi_plugin_github.identity:GitHubIdentityPlugin"
27
- github-deployment = "imbi_plugin_github.deployment:GitHubDeploymentPlugin"
28
- github-lifecycle = "imbi_plugin_github.lifecycle:GitHubLifecyclePlugin"
29
- github-commit-sync = "imbi_plugin_github.commits:GitHubCommitSyncPlugin"
30
- github-pr-sync = "imbi_plugin_github.pull_requests:GitHubPRSyncPlugin"
31
- github-doctor = "imbi_plugin_github.doctor:GitHubDoctorPlugin"
32
-
33
24
  [dependency-groups]
34
25
  dev = [
35
26
  "basedpyright",
36
27
  "coverage[toml]",
37
- "imbi-common[server,databases]==2.11.6",
28
+ "imbi-common[server,databases]==2.12.1",
38
29
  "pre-commit",
39
30
  "pytest",
40
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
- third-party-service slug) when present and pointing at ``host``,
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.third_party_service_slug,
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
- A single :class:`GitHubCommitSyncPlugin` exposes two webhook actions --
4
- ``sync_commits`` and ``sync_tags`` -- dispatched by ``imbi-gateway`` on
5
- ``push`` deliveries. Unlike the identity / deployment / lifecycle
6
- families, the host (github.com vs. GHEC tenant vs. GHES appliance) is
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 GitHub plugin connected to the same ``ThirdPartyService`` --
14
- surfaced on ``ctx.service_plugins`` (slug -> flavor,
15
- ``options['host']`` -> host), else
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 same plugin also exposes :meth:`GitHubCommitSyncPlugin.sync_all_history`
22
- for an on-demand, host-invoked backfill: there is no push payload, so the
23
- GitHub host is read from ``ctx.service_plugins`` and ``(owner, repo)`` from
24
- the project links; it walks the full default-branch history and the
25
- complete tag list rather than a single push delta.
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,19 +46,13 @@ from imbi_common.models import CommitRecord, TagRecord
51
46
  from imbi_common.plugins.base import (
52
47
  ActionDescriptor,
53
48
  CheckStatus,
54
- CredentialField,
49
+ CommitSyncCapability,
55
50
  PluginContext,
56
- PluginManifest,
57
- WebhookActionPlugin,
58
51
  )
59
52
  from imbi_common.plugins.errors import PluginRateLimited
60
53
 
61
54
  from imbi_plugin_github import _app_auth
62
- from imbi_plugin_github._hosts import (
63
- find_connection,
64
- flavor_host,
65
- host_to_api_base,
66
- )
55
+ from imbi_plugin_github._hosts import host_to_api_base, resolve_host
67
56
  from imbi_plugin_github._repos import resolve_owner_repo
68
57
  from imbi_plugin_github.deployment import (
69
58
  _auth_headers, # pyright: ignore[reportPrivateUsage]
@@ -304,28 +293,6 @@ def _branch_short_name(ref: str) -> str:
304
293
  return ref.removeprefix(prefix)
305
294
 
306
295
 
307
- def _connection_host(ctx: PluginContext, label: str) -> str | None:
308
- """Resolve the GitHub host from the ``github-connection`` sibling.
309
-
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.
314
- """
315
- plugin = find_connection(ctx.service_plugins)
316
- if plugin is None:
317
- return None
318
- try:
319
- return flavor_host(plugin.options, label)
320
- except ValueError as exc:
321
- LOGGER.warning(
322
- '%s: github-connection plugin has an unusable flavor/host: %s',
323
- label,
324
- exc,
325
- )
326
- return None
327
-
328
-
329
296
  def _api_base_from_repo_url(repo_url: object) -> str | None:
330
297
  """Derive the API base from the push payload's ``repository.url``.
331
298
 
@@ -352,18 +319,15 @@ def _resolve_api_base(
352
319
  """Pick the GitHub API base for this call (see module docstring)."""
353
320
  if explicit:
354
321
  return explicit.rstrip('/')
355
- host = _connection_host(ctx, 'github-commit-sync')
322
+ host = resolve_host(ctx.integration_options, 'github-commit-sync')
356
323
  if host:
357
324
  return host_to_api_base(host)
358
- endpoint = ctx.assignment_options.get('service_endpoint')
359
- if isinstance(endpoint, str) and endpoint:
360
- return endpoint.rstrip('/')
361
325
  base = _api_base_from_repo_url(_resolve(repo_url_pointer, event))
362
326
  if base:
363
327
  LOGGER.info(
364
328
  "github-commit-sync falling back to the event's repository.url "
365
- 'for the API base; no api_base_url, connected GitHub plugin, or '
366
- 'service_endpoint was available'
329
+ 'for the API base; no api_base_url or resolvable Integration '
330
+ 'flavor/host was available'
367
331
  )
368
332
  return base
369
333
  return None
@@ -715,12 +679,13 @@ async def _hydrate_ci(
715
679
  def _resolve_host_for_context(ctx: PluginContext) -> str | None:
716
680
  """Resolve the GitHub web host for an on-demand sync (no payload).
717
681
 
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``.
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``.
722
687
  """
723
- return _connection_host(ctx, 'github-commit-sync')
688
+ return resolve_host(ctx.integration_options, 'github-commit-sync')
724
689
 
725
690
 
726
691
  async def _fetch_default_branch(
@@ -1253,70 +1218,35 @@ sync_tags_descriptor = ActionDescriptor(
1253
1218
  )
1254
1219
 
1255
1220
 
1256
- class GitHubCommitSyncPlugin(WebhookActionPlugin):
1257
- """Webhook-action plugin syncing GitHub commit / tag history.
1258
-
1259
- Carries its own service credential -- it is *not* folded into the
1260
- identity / deployment / lifecycle plugins, which run as the acting
1261
- user. Two mutually exclusive auth modes are supported (resolved by
1262
- :func:`_resolve_bearer`):
1221
+ class GitHubCommitSync(CommitSyncCapability):
1222
+ """Commit / tag history sync capability handler.
1263
1223
 
1264
- * **PAT** -- a static ``access_token``.
1265
- * **GitHub App** -- ``app_id`` + ``private_key`` (raw or base64 PEM),
1266
- with an optional ``installation_id``; the plugin mints a
1267
- 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.
1268
1232
  """
1269
1233
 
1270
- manifest = PluginManifest(
1271
- slug='github-commit-sync',
1272
- name='GitHub Commit History Sync',
1273
- description=(
1274
- 'Syncs commit and tag history from GitHub push webhooks into '
1275
- 'ClickHouse for analytics.'
1276
- ),
1277
- plugin_type='webhook',
1278
- credentials=[
1279
- CredentialField(
1280
- name='access_token',
1281
- label='GitHub Token (PAT)',
1282
- description=(
1283
- 'Static personal/service access token. Use this *or* '
1284
- 'the GitHub App fields below.'
1285
- ),
1286
- required=False,
1287
- ),
1288
- CredentialField(
1289
- name='app_id',
1290
- label='GitHub App ID',
1291
- description=(
1292
- 'GitHub App identifier; with a private key the plugin '
1293
- 'mints short-lived installation tokens.'
1294
- ),
1295
- required=False,
1296
- ),
1297
- CredentialField(
1298
- name='private_key',
1299
- label='GitHub App Private Key',
1300
- description=(
1301
- 'App private key, raw PEM or base64-encoded PEM.'
1302
- ),
1303
- required=False,
1304
- ),
1305
- CredentialField(
1306
- name='installation_id',
1307
- label='GitHub App Installation ID',
1308
- description=(
1309
- 'Optional. When unset, the installation is discovered '
1310
- 'from the pushed repository.'
1311
- ),
1312
- required=False,
1313
- ),
1314
- ],
1315
- )
1316
-
1317
- @classmethod
1318
- def actions(cls) -> list[ActionDescriptor]:
1319
- 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
1320
1250
 
1321
1251
  async def sync_all_history(
1322
1252
  self,
@@ -1327,12 +1257,12 @@ class GitHubCommitSyncPlugin(WebhookActionPlugin):
1327
1257
  """Record the project's full default-branch history and all tags.
1328
1258
 
1329
1259
  Host-invoked (no webhook payload): the host instantiates the
1330
- plugin, builds a :class:`PluginContext` carrying the project's
1331
- links and the connected ``service_plugins``, resolves this
1332
- plugin's service ``credentials``, and awaits this method. The
1333
- GitHub host/flavor is read from ``service_plugins``, the
1334
- ``(owner, repo)`` from the project links, and the bearer token
1335
- from the same PAT-or-App resolution the webhook actions use.
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.
1336
1266
 
1337
1267
  Walks every commit reachable from the default branch head plus the
1338
1268
  repo's complete (lightweight) tag list, maps them onto
@@ -1352,7 +1282,7 @@ class GitHubCommitSyncPlugin(WebhookActionPlugin):
1352
1282
  if host is None:
1353
1283
  raise ValueError(
1354
1284
  'github-commit-sync could not resolve a GitHub host for an '
1355
- 'on-demand sync: connect a GitHub plugin to the service'
1285
+ 'on-demand sync: set the Integration flavor/host'
1356
1286
  )
1357
1287
  base = host_to_api_base(host)
1358
1288
  owner, repo = resolve_owner_repo(ctx, host, 'github-commit-sync')
@@ -1,9 +1,8 @@
1
- """GitHub deployment plugin.
1
+ """GitHub deployment capability handler.
2
2
 
3
- A single :class:`GitHubDeploymentPlugin` resolves its host from the
4
- ``github-connection`` plugin attached to the same ``ThirdPartyService``
5
- (github.com, a ``*.ghe.com`` GHEC tenant, or an operator-managed GHES
6
- appliance) instead of shipping a per-flavor subclass.
3
+ :class:`GitHubDeployment` resolves its host from the Integration's
4
+ ``flavor`` + ``host`` options on ``ctx.integration_options`` (github.com,
5
+ a ``*.ghe.com`` GHEC tenant, or an operator-managed GHES appliance).
7
6
 
8
7
  It drives the GitHub Deployments API
9
8
  (``POST /repos/{owner}/{repo}/deployments``) rather than
@@ -14,9 +13,9 @@ Tag/release creation is handled separately by ``create_tag`` and
14
13
  ``create_release`` and continues to feed projects whose deploys are
15
14
  triggered by ``on: release: [published]`` instead of ``on: deployment``.
16
15
 
17
- The plugin runs as the user via the paired ``IdentityPlugin``: callers
18
- materialize an :class:`~imbi_common.plugins.base.IdentityCredentials`
19
- and pass the access token through ``credentials['access_token']``.
16
+ The handler runs as the acting user: the host passes the materialized
17
+ access token through the Integration credential blob's
18
+ ``credentials['access_token']``.
20
19
  """
21
20
 
22
21
  from __future__ import annotations
@@ -36,16 +35,11 @@ from imbi_common.plugins.base import (
36
35
  CheckStatus,
37
36
  Commit,
38
37
  CompareResult,
39
- CredentialField,
38
+ DeploymentCapability,
40
39
  DeploymentEventStatus,
41
- DeploymentPlugin,
42
40
  DeploymentRun,
43
41
  LinkWriteback,
44
- OpsLogTemplate,
45
42
  PluginContext,
46
- PluginEdgeLabel,
47
- PluginManifest,
48
- PluginOption,
49
43
  Ref,
50
44
  RefInfo,
51
45
  ReleaseInfo,
@@ -54,10 +48,7 @@ from imbi_common.plugins.base import (
54
48
  )
55
49
  from imbi_common.plugins.errors import PluginAuthenticationFailed
56
50
 
57
- from imbi_plugin_github._hosts import (
58
- host_to_api_base,
59
- resolve_connection_host,
60
- )
51
+ from imbi_plugin_github._hosts import flavor_host, host_to_api_base
61
52
  from imbi_plugin_github._repos import (
62
53
  derive_owner_repo_from_links,
63
54
  parse_owner_repo,
@@ -287,19 +278,18 @@ def _check_runs_to_status(
287
278
  return 'unknown'
288
279
 
289
280
 
290
- class _DeploymentBase(DeploymentPlugin):
291
- """Shared base for the GitHub deployment plugin.
281
+ class GitHubDeployment(DeploymentCapability):
282
+ """GitHub deployment capability handler.
292
283
 
293
- The host is resolved per call from the ``github-connection`` sibling
294
- on the same service via :meth:`_resolve_host`. Each plugin instance
295
- is single-shot: callers pass ``credentials`` (from the paired
296
- :class:`IdentityPlugin`) and ``ctx`` per call.
284
+ The host is resolved per call from the Integration's ``flavor`` +
285
+ ``host`` options via :meth:`_resolve_host`. Each instance is
286
+ single-shot: callers pass ``credentials`` (the Integration's
287
+ decrypted blob, carrying the acting user's access token) and ``ctx``
288
+ per call.
297
289
  """
298
290
 
299
291
  def _resolve_host(self, ctx: PluginContext) -> str:
300
- return resolve_connection_host(
301
- ctx.service_plugins, 'github-deployment'
302
- )
292
+ return flavor_host(ctx.integration_options, 'github deployment')
303
293
 
304
294
  def _api_base(self, ctx: PluginContext) -> str:
305
295
  return host_to_api_base(self._resolve_host(ctx))
@@ -1113,73 +1103,3 @@ def _to_event_status(github_state: str) -> DeploymentEventStatus:
1113
1103
  if github_state == 'inactive':
1114
1104
  return 'rolled_back'
1115
1105
  return 'pending'
1116
-
1117
-
1118
- _COMMON_OPTIONS: list[PluginOption] = []
1119
-
1120
- # Promote behaviour is now inferred from the ``body.tag`` shape on the
1121
- # imbi-api side: semver tags trigger a Deployment, raw SHAs cut a tag
1122
- # + GitHub Release. Per-env workflow input overrides live on the
1123
- # ``USES_PLUGIN`` edge under ``env_payloads`` (keyed by env slug),
1124
- # resolved by the host and passed in via ``ctx.environment_config``.
1125
- _COMMON_EDGE_LABELS: list[PluginEdgeLabel] = []
1126
-
1127
- _COMMON_CREDENTIALS: list[CredentialField] = [
1128
- CredentialField(
1129
- name='access_token',
1130
- label='Service-account PAT (optional fallback)',
1131
- description=(
1132
- 'Personal access token used when no per-user identity is '
1133
- 'bound. Requires contents:write and actions:write scopes.'
1134
- ),
1135
- required=False,
1136
- ),
1137
- ]
1138
-
1139
-
1140
- # Templates for the operations-log JSON payload the API writes from
1141
- # ``_record_deployment_event`` in imbi-api: ``{action, plugin_slug,
1142
- # run_url, release_url, from_environment}``. Row-level fields
1143
- # ``version`` and ``environment`` (from the entry's
1144
- # ``environment_slug``/``environment.name``) are also in scope.
1145
- _COMMON_OPS_LOG_TEMPLATES: dict[str, OpsLogTemplate] = {
1146
- 'deploy': OpsLogTemplate(
1147
- label='Deployed {{version}} to {{environment}}',
1148
- summary='deployed',
1149
- ),
1150
- 'redeploy': OpsLogTemplate(
1151
- label='Re-deployed {{version}} to {{environment}}',
1152
- summary='re-deployed',
1153
- ),
1154
- 'promote': OpsLogTemplate(
1155
- label=(
1156
- 'Promoted {{from_environment}} to {{environment}} as {{version}}.'
1157
- ),
1158
- summary='promoted',
1159
- ),
1160
- 'resync': OpsLogTemplate(
1161
- label='Recorded {{version}} deploy in {{environment}}',
1162
- summary='recorded a deploy in',
1163
- ),
1164
- }
1165
-
1166
-
1167
- class GitHubDeploymentPlugin(_DeploymentBase):
1168
- manifest = PluginManifest(
1169
- slug='github-deployment',
1170
- name='GitHub Deployment',
1171
- description=(
1172
- 'Drive GitHub Deployments and record GitHub Releases '
1173
- 'on behalf of an Imbi project. Each promote creates a '
1174
- 'Deployment object so GitHub environment protection rules '
1175
- '(required reviewers, branch policies, wait timers) apply '
1176
- 'server-side. The GitHub host is resolved from the '
1177
- '``github-connection`` plugin on the same service.'
1178
- ),
1179
- plugin_type='deployment',
1180
- supports_deployment_sync=True,
1181
- options=_COMMON_OPTIONS,
1182
- credentials=_COMMON_CREDENTIALS,
1183
- edge_labels=_COMMON_EDGE_LABELS,
1184
- ops_log_templates=_COMMON_OPS_LOG_TEMPLATES,
1185
- )