imbi-plugin-github 1.0.0__tar.gz → 1.3.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.3.0/PKG-INFO +82 -0
  2. imbi_plugin_github-1.3.0/README.md +63 -0
  3. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/pyproject.toml +11 -8
  4. imbi_plugin_github-1.3.0/src/imbi_plugin_github/_repos.py +113 -0
  5. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/src/imbi_plugin_github/deployment.py +366 -216
  6. imbi_plugin_github-1.3.0/src/imbi_plugin_github/lifecycle.py +375 -0
  7. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/tests/test_deployment.py +435 -196
  8. imbi_plugin_github-1.3.0/tests/test_lifecycle.py +437 -0
  9. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/uv.lock +36 -26
  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.3.0}/.github/workflows/publish.yml +0 -0
  13. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/.github/workflows/test.yml +0 -0
  14. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/.gitignore +0 -0
  15. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/.pre-commit-config.yaml +0 -0
  16. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/LICENSE +0 -0
  17. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/justfile +0 -0
  18. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/src/imbi_plugin_github/__init__.py +0 -0
  19. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/src/imbi_plugin_github/_hosts.py +0 -0
  20. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/src/imbi_plugin_github/identity.py +0 -0
  21. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/tests/__init__.py +0 -0
  22. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/tests/test_hosts.py +0 -0
  23. {imbi_plugin_github-1.0.0 → imbi_plugin_github-1.3.0}/tests/test_identity.py +0 -0
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: imbi-plugin-github
3
+ Version: 1.3.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>=2.5.5
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. Promote behaviour is inferred from the
44
+ ref shape by the host (semver → trigger Deployment, raw SHA → cut tag
45
+ + Release). Per-env workflow inputs ride on the `USES_PLUGIN` edge as
46
+ `env_payloads` and arrive on `PluginContext.environment_config`.
47
+
48
+ ### Lifecycle
49
+
50
+ Reacts to project archive / unarchive by archiving the matching repo via
51
+ `PATCH /repos/{owner}/{repo}` with `{"archived": true|false}`. When the
52
+ `archive_target_org` option is set, archive **also** transfers the repo to
53
+ that org first via `POST /repos/{owner}/{repo}/transfer` — useful for
54
+ moving sunset projects into a dedicated "archive" org so they no longer
55
+ crowd primary-org searches.
56
+
57
+ GitHub refuses to transfer archived repos, so an already-archived source
58
+ is briefly unarchived, transferred, and re-archived at the destination.
59
+ On unarchive the plugin only flips `archived` back to `false` at the
60
+ repo's current location — it does **not** transfer back to the original
61
+ org.
62
+
63
+ Archiving requires admin scope on the repo; transferring additionally
64
+ requires admin permission on the target organization.
65
+
66
+ ## Manifest options (identity)
67
+
68
+ | Option | Required | Description |
69
+ | ---------------- | --------- | -------------------------------------------------------------------------- |
70
+ | `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
71
+ | `default_scopes` | no | Space-separated default OAuth scopes (default: `read:user user:email`). |
72
+
73
+ ## Credentials (identity)
74
+
75
+ | Field | Required |
76
+ | ---------------- | -------- |
77
+ | `client_id` | yes |
78
+ | `client_secret` | yes |
79
+
80
+ ## License
81
+
82
+ BSD-3-Clause.
@@ -0,0 +1,63 @@
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. Promote behaviour is inferred from the
25
+ ref shape by the host (semver → trigger Deployment, raw SHA → cut tag
26
+ + Release). Per-env workflow inputs ride on the `USES_PLUGIN` edge as
27
+ `env_payloads` and arrive on `PluginContext.environment_config`.
28
+
29
+ ### Lifecycle
30
+
31
+ Reacts to project archive / unarchive by archiving the matching repo via
32
+ `PATCH /repos/{owner}/{repo}` with `{"archived": true|false}`. When the
33
+ `archive_target_org` option is set, archive **also** transfers the repo to
34
+ that org first via `POST /repos/{owner}/{repo}/transfer` — useful for
35
+ moving sunset projects into a dedicated "archive" org so they no longer
36
+ crowd primary-org searches.
37
+
38
+ GitHub refuses to transfer archived repos, so an already-archived source
39
+ is briefly unarchived, transferred, and re-archived at the destination.
40
+ On unarchive the plugin only flips `archived` back to `false` at the
41
+ repo's current location — it does **not** transfer back to the original
42
+ org.
43
+
44
+ Archiving requires admin scope on the repo; transferring additionally
45
+ requires admin permission on the target organization.
46
+
47
+ ## Manifest options (identity)
48
+
49
+ | Option | Required | Description |
50
+ | ---------------- | --------- | -------------------------------------------------------------------------- |
51
+ | `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
52
+ | `default_scopes` | no | Space-separated default OAuth scopes (default: `read:user user:email`). |
53
+
54
+ ## Credentials (identity)
55
+
56
+ | Field | Required |
57
+ | ---------------- | -------- |
58
+ | `client_id` | yes |
59
+ | `client_secret` | yes |
60
+
61
+ ## License
62
+
63
+ BSD-3-Clause.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "imbi-plugin-github"
3
- version = "1.0.0"
3
+ version = "1.3.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>=2.5.5",
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 = [
@@ -45,9 +48,6 @@ dist = ["build", "twine", "wheel"]
45
48
  requires = ["hatchling"]
46
49
  build-backend = "hatchling.build"
47
50
 
48
- [tool.hatch.build.targets.wheel]
49
- packages = ["src/imbi_plugin_github"]
50
-
51
51
  [tool.coverage.report]
52
52
  exclude_also = ["typing.TYPE_CHECKING"]
53
53
  fail_under = 85
@@ -57,9 +57,8 @@ show_missing = true
57
57
  branch = true
58
58
  source = ["src/imbi_plugin_github"]
59
59
 
60
- [tool.pytest.ini_options]
61
- asyncio_mode = "auto"
62
- asyncio_default_fixture_loop_scope = "function"
60
+ [tool.hatch.build.targets.wheel]
61
+ packages = ["src/imbi_plugin_github"]
63
62
 
64
63
  [tool.pyright]
65
64
  executionEnvironments = [
@@ -71,6 +70,10 @@ reportMissingSuperCall = "hint"
71
70
  reportMissingTypeStubs = false
72
71
  typeCheckingMode = "strict"
73
72
 
73
+ [tool.pytest.ini_options]
74
+ asyncio_mode = "auto"
75
+ asyncio_default_fixture_loop_scope = "function"
76
+
74
77
  [tool.ruff]
75
78
  line-length = 79
76
79
  target-version = "py314"
@@ -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
+ )