imbi-plugin-github 1.0.0__tar.gz → 1.1.0__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-1.1.0/PKG-INFO +80 -0
- imbi_plugin_github-1.1.0/README.md +61 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/pyproject.toml +5 -2
- imbi_plugin_github-1.1.0/src/imbi_plugin_github/_repos.py +113 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/src/imbi_plugin_github/deployment.py +181 -214
- imbi_plugin_github-1.1.0/src/imbi_plugin_github/lifecycle.py +375 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/tests/test_deployment.py +203 -191
- imbi_plugin_github-1.1.0/tests/test_lifecycle.py +437 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/uv.lock +5 -5
- imbi_plugin_github-1.0.0/PKG-INFO +0 -52
- imbi_plugin_github-1.0.0/README.md +0 -33
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/.github/workflows/publish.yml +0 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/.github/workflows/test.yml +0 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/.gitignore +0 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/.pre-commit-config.yaml +0 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/LICENSE +0 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/justfile +0 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/src/imbi_plugin_github/__init__.py +0 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/src/imbi_plugin_github/_hosts.py +0 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/src/imbi_plugin_github/identity.py +0 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/tests/__init__.py +0 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/tests/test_hosts.py +0 -0
- {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/tests/test_identity.py +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: imbi-plugin-github
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: GitHub identity plugin for Imbi (github.com / GHEC / GHES)
|
|
5
|
+
Author-email: "Gavin M. Roy" <gavinr@aweber.com>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Requires-Dist: httpx>=0.27
|
|
16
|
+
Requires-Dist: imbi-common>=0.11.0
|
|
17
|
+
Requires-Dist: pydantic>=2
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# imbi-plugin-github
|
|
21
|
+
|
|
22
|
+
GitHub plugins for Imbi. Three flavors (github.com, GitHub Enterprise Cloud,
|
|
23
|
+
GitHub Enterprise Server) of each plugin type so the admin UI can wire
|
|
24
|
+
projects to the right backend.
|
|
25
|
+
|
|
26
|
+
## Plugin types
|
|
27
|
+
|
|
28
|
+
| Type | Slugs |
|
|
29
|
+
| ---------- | -------------------------------------------------------------------- |
|
|
30
|
+
| Identity | `github`, `github-enterprise-cloud`, `github-enterprise-server` |
|
|
31
|
+
| Deployment | `github-deployment`, `github-deployment-ec`, `github-deployment-es` |
|
|
32
|
+
| Lifecycle | `github-lifecycle`, `github-lifecycle-ec`, `github-lifecycle-es` |
|
|
33
|
+
|
|
34
|
+
### Identity
|
|
35
|
+
|
|
36
|
+
Phase 1 ships the OAuth App flow only; GitHub App installation tokens are
|
|
37
|
+
deferred. The access token returned by the OAuth grant is passed straight
|
|
38
|
+
to GitHub APIs as a `Bearer` token, so `materialize()` is a no-op.
|
|
39
|
+
|
|
40
|
+
### Deployment
|
|
41
|
+
|
|
42
|
+
Drives the GitHub Deployments API (`POST /repos/{owner}/{repo}/deployments`)
|
|
43
|
+
plus tag and release creation. See `deployment.py` for the per-environment
|
|
44
|
+
`DEPLOYS_VIA` edge contract.
|
|
45
|
+
|
|
46
|
+
### Lifecycle
|
|
47
|
+
|
|
48
|
+
Reacts to project archive / unarchive by archiving the matching repo via
|
|
49
|
+
`PATCH /repos/{owner}/{repo}` with `{"archived": true|false}`. When the
|
|
50
|
+
`archive_target_org` option is set, archive **also** transfers the repo to
|
|
51
|
+
that org first via `POST /repos/{owner}/{repo}/transfer` — useful for
|
|
52
|
+
moving sunset projects into a dedicated "archive" org so they no longer
|
|
53
|
+
crowd primary-org searches.
|
|
54
|
+
|
|
55
|
+
GitHub refuses to transfer archived repos, so an already-archived source
|
|
56
|
+
is briefly unarchived, transferred, and re-archived at the destination.
|
|
57
|
+
On unarchive the plugin only flips `archived` back to `false` at the
|
|
58
|
+
repo's current location — it does **not** transfer back to the original
|
|
59
|
+
org.
|
|
60
|
+
|
|
61
|
+
Archiving requires admin scope on the repo; transferring additionally
|
|
62
|
+
requires admin permission on the target organization.
|
|
63
|
+
|
|
64
|
+
## Manifest options (identity)
|
|
65
|
+
|
|
66
|
+
| Option | Required | Description |
|
|
67
|
+
| ---------------- | --------- | -------------------------------------------------------------------------- |
|
|
68
|
+
| `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
|
|
69
|
+
| `default_scopes` | no | Space-separated default OAuth scopes (default: `read:user user:email`). |
|
|
70
|
+
|
|
71
|
+
## Credentials (identity)
|
|
72
|
+
|
|
73
|
+
| Field | Required |
|
|
74
|
+
| ---------------- | -------- |
|
|
75
|
+
| `client_id` | yes |
|
|
76
|
+
| `client_secret` | yes |
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
BSD-3-Clause.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# imbi-plugin-github
|
|
2
|
+
|
|
3
|
+
GitHub plugins for Imbi. Three flavors (github.com, GitHub Enterprise Cloud,
|
|
4
|
+
GitHub Enterprise Server) of each plugin type so the admin UI can wire
|
|
5
|
+
projects to the right backend.
|
|
6
|
+
|
|
7
|
+
## Plugin types
|
|
8
|
+
|
|
9
|
+
| Type | Slugs |
|
|
10
|
+
| ---------- | -------------------------------------------------------------------- |
|
|
11
|
+
| Identity | `github`, `github-enterprise-cloud`, `github-enterprise-server` |
|
|
12
|
+
| Deployment | `github-deployment`, `github-deployment-ec`, `github-deployment-es` |
|
|
13
|
+
| Lifecycle | `github-lifecycle`, `github-lifecycle-ec`, `github-lifecycle-es` |
|
|
14
|
+
|
|
15
|
+
### Identity
|
|
16
|
+
|
|
17
|
+
Phase 1 ships the OAuth App flow only; GitHub App installation tokens are
|
|
18
|
+
deferred. The access token returned by the OAuth grant is passed straight
|
|
19
|
+
to GitHub APIs as a `Bearer` token, so `materialize()` is a no-op.
|
|
20
|
+
|
|
21
|
+
### Deployment
|
|
22
|
+
|
|
23
|
+
Drives the GitHub Deployments API (`POST /repos/{owner}/{repo}/deployments`)
|
|
24
|
+
plus tag and release creation. See `deployment.py` for the per-environment
|
|
25
|
+
`DEPLOYS_VIA` edge contract.
|
|
26
|
+
|
|
27
|
+
### Lifecycle
|
|
28
|
+
|
|
29
|
+
Reacts to project archive / unarchive by archiving the matching repo via
|
|
30
|
+
`PATCH /repos/{owner}/{repo}` with `{"archived": true|false}`. When the
|
|
31
|
+
`archive_target_org` option is set, archive **also** transfers the repo to
|
|
32
|
+
that org first via `POST /repos/{owner}/{repo}/transfer` — useful for
|
|
33
|
+
moving sunset projects into a dedicated "archive" org so they no longer
|
|
34
|
+
crowd primary-org searches.
|
|
35
|
+
|
|
36
|
+
GitHub refuses to transfer archived repos, so an already-archived source
|
|
37
|
+
is briefly unarchived, transferred, and re-archived at the destination.
|
|
38
|
+
On unarchive the plugin only flips `archived` back to `false` at the
|
|
39
|
+
repo's current location — it does **not** transfer back to the original
|
|
40
|
+
org.
|
|
41
|
+
|
|
42
|
+
Archiving requires admin scope on the repo; transferring additionally
|
|
43
|
+
requires admin permission on the target organization.
|
|
44
|
+
|
|
45
|
+
## Manifest options (identity)
|
|
46
|
+
|
|
47
|
+
| Option | Required | Description |
|
|
48
|
+
| ---------------- | --------- | -------------------------------------------------------------------------- |
|
|
49
|
+
| `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
|
|
50
|
+
| `default_scopes` | no | Space-separated default OAuth scopes (default: `read:user user:email`). |
|
|
51
|
+
|
|
52
|
+
## Credentials (identity)
|
|
53
|
+
|
|
54
|
+
| Field | Required |
|
|
55
|
+
| ---------------- | -------- |
|
|
56
|
+
| `client_id` | yes |
|
|
57
|
+
| `client_secret` | yes |
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
BSD-3-Clause.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "imbi-plugin-github"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.1.0"
|
|
4
4
|
description = "GitHub identity plugin for Imbi (github.com / GHEC / GHES)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -16,7 +16,7 @@ classifiers = [
|
|
|
16
16
|
]
|
|
17
17
|
dependencies = [
|
|
18
18
|
"httpx>=0.27",
|
|
19
|
-
"imbi-common>=0.
|
|
19
|
+
"imbi-common>=0.11.0",
|
|
20
20
|
"pydantic>=2",
|
|
21
21
|
]
|
|
22
22
|
|
|
@@ -27,6 +27,9 @@ github-enterprise-server = "imbi_plugin_github.identity:GitHubEnterpriseServerPl
|
|
|
27
27
|
github-deployment = "imbi_plugin_github.deployment:GitHubDeploymentPlugin"
|
|
28
28
|
github-deployment-ec = "imbi_plugin_github.deployment:GitHubEnterpriseCloudDeploymentPlugin"
|
|
29
29
|
github-deployment-es = "imbi_plugin_github.deployment:GitHubEnterpriseServerDeploymentPlugin"
|
|
30
|
+
github-lifecycle = "imbi_plugin_github.lifecycle:GitHubLifecyclePlugin"
|
|
31
|
+
github-lifecycle-ec = "imbi_plugin_github.lifecycle:GitHubEnterpriseCloudLifecyclePlugin"
|
|
32
|
+
github-lifecycle-es = "imbi_plugin_github.lifecycle:GitHubEnterpriseServerLifecyclePlugin"
|
|
30
33
|
|
|
31
34
|
[dependency-groups]
|
|
32
35
|
dev = [
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Shared owner/repo resolution for the GitHub plugin family.
|
|
2
|
+
|
|
3
|
+
Identity, deployment, and lifecycle plugins all derive ``(owner, repo)``
|
|
4
|
+
from a project's external links (preferring the ``github-repository``
|
|
5
|
+
link key) plus, as a final fallback, the project's slug and type slug.
|
|
6
|
+
Centralising the logic here keeps the rules — reserved GitHub URL
|
|
7
|
+
prefixes, ``.git`` stripping, host case-folding — in one place so every
|
|
8
|
+
plugin agrees on what counts as a "real" repo URL.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import urllib.parse
|
|
14
|
+
|
|
15
|
+
from imbi_common.plugins.base import PluginContext
|
|
16
|
+
|
|
17
|
+
# Reserved GitHub URL prefixes that share the host with repository URLs
|
|
18
|
+
# but never point at a real ``<owner>/<repo>`` pair. Any link whose
|
|
19
|
+
# first path segment matches one of these is skipped during owner/repo
|
|
20
|
+
# derivation so e.g. ``github.com/orgs/<org>`` or a marketplace link
|
|
21
|
+
# can't silently bind a plugin to the wrong target.
|
|
22
|
+
_RESERVED_LINK_PREFIXES = frozenset(
|
|
23
|
+
{
|
|
24
|
+
'orgs',
|
|
25
|
+
'marketplace',
|
|
26
|
+
'settings',
|
|
27
|
+
'enterprises',
|
|
28
|
+
'features',
|
|
29
|
+
'pricing',
|
|
30
|
+
'about',
|
|
31
|
+
'login',
|
|
32
|
+
'logout',
|
|
33
|
+
'signup',
|
|
34
|
+
'sponsors',
|
|
35
|
+
'topics',
|
|
36
|
+
'collections',
|
|
37
|
+
'trending',
|
|
38
|
+
'codespaces',
|
|
39
|
+
'notifications',
|
|
40
|
+
'issues',
|
|
41
|
+
'pulls',
|
|
42
|
+
'search',
|
|
43
|
+
'stars',
|
|
44
|
+
'explore',
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_owner_repo(url: str, target_host: str) -> tuple[str, str] | None:
|
|
50
|
+
"""Extract ``(owner, repo)`` from ``url`` when it is a repo URL on
|
|
51
|
+
``target_host``. Returns ``None`` for other hosts, short paths,
|
|
52
|
+
or reserved-prefix paths like ``/orgs/<org>``.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
parsed = urllib.parse.urlparse(url)
|
|
56
|
+
except ValueError:
|
|
57
|
+
return None
|
|
58
|
+
if (parsed.hostname or '').lower() != target_host:
|
|
59
|
+
return None
|
|
60
|
+
parts = [p for p in parsed.path.split('/') if p]
|
|
61
|
+
if len(parts) < 2:
|
|
62
|
+
return None
|
|
63
|
+
if parts[0].lower() in _RESERVED_LINK_PREFIXES:
|
|
64
|
+
return None
|
|
65
|
+
return parts[0], parts[1].removesuffix('.git')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def derive_owner_repo_from_links(
|
|
69
|
+
links: dict[str, str], host: str
|
|
70
|
+
) -> tuple[str, str] | None:
|
|
71
|
+
"""Find a project link pointing at ``host`` and parse owner/repo.
|
|
72
|
+
|
|
73
|
+
Prefers an explicit ``github-repository`` link key when one is
|
|
74
|
+
present and points at ``host``; otherwise scans the remaining
|
|
75
|
+
same-host links and returns the first usable one. Returns
|
|
76
|
+
``None`` when nothing matches.
|
|
77
|
+
"""
|
|
78
|
+
target = host.lower()
|
|
79
|
+
preferred = links.get('github-repository')
|
|
80
|
+
if preferred is not None:
|
|
81
|
+
owner_repo = parse_owner_repo(preferred, target)
|
|
82
|
+
if owner_repo is not None:
|
|
83
|
+
return owner_repo
|
|
84
|
+
for key, url in links.items():
|
|
85
|
+
if key == 'github-repository':
|
|
86
|
+
continue
|
|
87
|
+
owner_repo = parse_owner_repo(url, target)
|
|
88
|
+
if owner_repo is not None:
|
|
89
|
+
return owner_repo
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def resolve_owner_repo(
|
|
94
|
+
ctx: PluginContext, host: str, plugin_label: str
|
|
95
|
+
) -> tuple[str, str]:
|
|
96
|
+
"""Resolve the repo for a plugin call from ``ctx``.
|
|
97
|
+
|
|
98
|
+
First tries the project links; falls back to
|
|
99
|
+
``<project_type_slug>/<project_slug>`` as a convention. Raises
|
|
100
|
+
:class:`ValueError` when neither path produces a candidate so the
|
|
101
|
+
caller can surface a clear "set the GitHub Repository link"
|
|
102
|
+
message to the operator.
|
|
103
|
+
"""
|
|
104
|
+
derived = derive_owner_repo_from_links(ctx.project_links, host)
|
|
105
|
+
if derived is not None:
|
|
106
|
+
return derived
|
|
107
|
+
if ctx.project_type_slugs and ctx.project_slug:
|
|
108
|
+
return ctx.project_type_slugs[0], ctx.project_slug
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f'{plugin_label} could not determine the target repository: '
|
|
111
|
+
"set the project's GitHub Repository link or tag the project "
|
|
112
|
+
'with a ProjectType'
|
|
113
|
+
)
|