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.
Files changed (23) hide show
  1. imbi_plugin_github-1.1.0/PKG-INFO +80 -0
  2. imbi_plugin_github-1.1.0/README.md +61 -0
  3. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/pyproject.toml +5 -2
  4. imbi_plugin_github-1.1.0/src/imbi_plugin_github/_repos.py +113 -0
  5. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/src/imbi_plugin_github/deployment.py +181 -214
  6. imbi_plugin_github-1.1.0/src/imbi_plugin_github/lifecycle.py +375 -0
  7. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/tests/test_deployment.py +203 -191
  8. imbi_plugin_github-1.1.0/tests/test_lifecycle.py +437 -0
  9. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/uv.lock +5 -5
  10. imbi_plugin_github-1.0.0/PKG-INFO +0 -52
  11. imbi_plugin_github-1.0.0/README.md +0 -33
  12. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/.github/workflows/publish.yml +0 -0
  13. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/.github/workflows/test.yml +0 -0
  14. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/.gitignore +0 -0
  15. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/.pre-commit-config.yaml +0 -0
  16. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/LICENSE +0 -0
  17. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/justfile +0 -0
  18. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/src/imbi_plugin_github/__init__.py +0 -0
  19. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/src/imbi_plugin_github/_hosts.py +0 -0
  20. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/src/imbi_plugin_github/identity.py +0 -0
  21. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/tests/__init__.py +0 -0
  22. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.1.0}/tests/test_hosts.py +0 -0
  23. {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.0.0"
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.8.4",
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
+ )