imbi-plugin-github 2.11.6__tar.gz → 2.12.2__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.6 → imbi_plugin_github-2.12.2}/PKG-INFO +2 -2
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/pyproject.toml +3 -12
- imbi_plugin_github-2.12.2/src/imbi_plugin_github/__init__.py +13 -0
- imbi_plugin_github-2.12.2/src/imbi_plugin_github/_hosts.py +113 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/src/imbi_plugin_github/_repos.py +2 -2
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/src/imbi_plugin_github/commits.py +57 -127
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/src/imbi_plugin_github/deployment.py +17 -97
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/src/imbi_plugin_github/doctor.py +35 -46
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/src/imbi_plugin_github/identity.py +17 -57
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/src/imbi_plugin_github/lifecycle.py +21 -113
- imbi_plugin_github-2.12.2/src/imbi_plugin_github/plugin.py +335 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/src/imbi_plugin_github/pull_requests.py +32 -67
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/tests/test_commits.py +61 -79
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/tests/test_deployment.py +108 -130
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/tests/test_doctor.py +61 -68
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/tests/test_hosts.py +28 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/tests/test_identity.py +40 -38
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/tests/test_lifecycle.py +83 -86
- imbi_plugin_github-2.12.2/tests/test_pull_requests.py +184 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/uv.lock +7 -7
- imbi_plugin_github-2.11.6/src/imbi_plugin_github/__init__.py +0 -21
- imbi_plugin_github-2.11.6/src/imbi_plugin_github/_hosts.py +0 -140
- imbi_plugin_github-2.11.6/src/imbi_plugin_github/connection.py +0 -79
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/.github/workflows/publish.yml +0 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/.github/workflows/test.yml +0 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/.gitignore +0 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/.pre-commit-config.yaml +0 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/CLAUDE.md +0 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/LICENSE +0 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/README.md +0 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/justfile +0 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/src/imbi_plugin_github/_app_auth.py +0 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/tests/__init__.py +0 -0
- {imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/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.
|
|
3
|
+
Version: 2.12.2
|
|
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.2
|
|
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.2"
|
|
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.
|
|
19
|
+
"imbi-common[databases]==2.12.2",
|
|
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.
|
|
28
|
+
"imbi-common[server,databases]==2.12.2",
|
|
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
|
-
|
|
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,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
|
-
|
|
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 =
|
|
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
|
|
366
|
-
'
|
|
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 ``
|
|
719
|
-
and returns
|
|
720
|
-
appliance). Unlike the webhook path there is
|
|
721
|
-
back to, so
|
|
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
|
|
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
|
|
1257
|
-
"""
|
|
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
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
-
|
|
1331
|
-
links and the
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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:
|
|
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')
|
{imbi_plugin_github-2.11.6 → imbi_plugin_github-2.12.2}/src/imbi_plugin_github/deployment.py
RENAMED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
"""GitHub deployment
|
|
1
|
+
"""GitHub deployment capability handler.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
``
|
|
5
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
291
|
-
"""
|
|
281
|
+
class GitHubDeployment(DeploymentCapability):
|
|
282
|
+
"""GitHub deployment capability handler.
|
|
292
283
|
|
|
293
|
-
The host is resolved per call from the ``
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
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
|
-
)
|