imbi-plugin-github 2.17.0__tar.gz → 2.19.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-2.19.0/PKG-INFO +154 -0
- imbi_plugin_github-2.19.0/README.md +135 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/pyproject.toml +2 -2
- imbi_plugin_github-2.19.0/src/imbi/plugins/github/README.md +135 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/deployment.py +102 -2
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/tests/test_deployment.py +226 -0
- imbi_plugin_github-2.17.0/PKG-INFO +0 -126
- imbi_plugin_github-2.17.0/README.md +0 -107
- imbi_plugin_github-2.17.0/src/imbi/plugins/github/README.md +0 -107
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/.gitignore +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/moon.yml +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/__init__.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/_app_auth.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/_hosts.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/_repos.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/commits.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/doctor.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/identity.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/lifecycle.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/plugin.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/pull_requests.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/py.typed +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/tests/__init__.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/tests/test_commits.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/tests/test_doctor.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/tests/test_hosts.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/tests/test_identity.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/tests/test_lifecycle.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/tests/test_pull_requests.py +0 -0
- {imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/tests/test_repos.py +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: imbi-plugin-github
|
|
3
|
+
Version: 2.19.0
|
|
4
|
+
Summary: GitHub identity plugin for Imbi (github.com / GHEC / GHES)
|
|
5
|
+
Author-email: "Gavin M. Roy" <gavinr@aweber.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Natural Language :: English
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Python: >=3.14
|
|
14
|
+
Requires-Dist: httpx>=0.27
|
|
15
|
+
Requires-Dist: imbi-common[databases]==2.19.0
|
|
16
|
+
Requires-Dist: pydantic>=2
|
|
17
|
+
Requires-Dist: pyjwt[crypto]>=2.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# imbi-plugin-github
|
|
21
|
+
|
|
22
|
+
GitHub plugin for Imbi. A single plugin — slug **`github`** — backs every
|
|
23
|
+
GitHub Integration; the integration-level `flavor` option selects
|
|
24
|
+
github.com (`github`), GitHub Enterprise Cloud (`ghec`), or GitHub
|
|
25
|
+
Enterprise Server (`ghes`), and `host` names the tenant or appliance.
|
|
26
|
+
|
|
27
|
+
## Capabilities
|
|
28
|
+
|
|
29
|
+
| Kind | Handler |
|
|
30
|
+
| ----------------- | ------------------------- |
|
|
31
|
+
| `identity` | `GitHubIdentity` |
|
|
32
|
+
| `deployment` | `GitHubDeployment` |
|
|
33
|
+
| `lifecycle` | `GitHubLifecycle` |
|
|
34
|
+
| `webhook-actions` | `GitHubWebhookActions` |
|
|
35
|
+
| `commit-sync` | `GitHubCommitSync` |
|
|
36
|
+
| `pr-sync` | `GitHubPullRequestSync` |
|
|
37
|
+
| `analysis` | `GitHubDoctor` |
|
|
38
|
+
|
|
39
|
+
### Identity
|
|
40
|
+
|
|
41
|
+
Implements the OAuth App flow. The access token returned by the OAuth
|
|
42
|
+
grant is passed straight to GitHub APIs as a `Bearer` token, so
|
|
43
|
+
`materialize()` is a no-op.
|
|
44
|
+
|
|
45
|
+
### Deployment
|
|
46
|
+
|
|
47
|
+
Drives the GitHub Deployments API (`POST /repos/{owner}/{repo}/deployments`)
|
|
48
|
+
plus tag and release creation. Promote behaviour is inferred from the
|
|
49
|
+
ref shape by the host (semver → trigger Deployment, raw SHA → cut tag
|
|
50
|
+
+ Release). Per-env workflow inputs ride on the `USES_PLUGIN` edge as
|
|
51
|
+
`env_payloads` and arrive on `PluginContext.environment_config`.
|
|
52
|
+
|
|
53
|
+
### Lifecycle
|
|
54
|
+
|
|
55
|
+
Reacts to project archive / unarchive by archiving the matching repo via
|
|
56
|
+
`PATCH /repos/{owner}/{repo}` with `{"archived": true|false}`. When the
|
|
57
|
+
`archive_target_org` option is set, archive **also** transfers the repo to
|
|
58
|
+
that org first via `POST /repos/{owner}/{repo}/transfer` — useful for
|
|
59
|
+
moving sunset projects into a dedicated "archive" org so they no longer
|
|
60
|
+
crowd primary-org searches.
|
|
61
|
+
|
|
62
|
+
GitHub refuses to transfer archived repos, so an already-archived source
|
|
63
|
+
is briefly unarchived, transferred, and re-archived at the destination.
|
|
64
|
+
On unarchive the plugin only flips `archived` back to `false` at the
|
|
65
|
+
repo's current location — it does **not** transfer back to the original
|
|
66
|
+
org.
|
|
67
|
+
|
|
68
|
+
Archiving requires admin scope on the repo; transferring additionally
|
|
69
|
+
requires admin permission on the target organization.
|
|
70
|
+
|
|
71
|
+
### Webhook actions (commit / tag / PR sync)
|
|
72
|
+
|
|
73
|
+
The `webhook-actions` capability exposes the actions the gateway
|
|
74
|
+
dispatches from webhook deliveries. A `WebhookRule.handler` is
|
|
75
|
+
`"<plugin_slug>#<action_name>"`, and the slug is the **plugin** slug —
|
|
76
|
+
`github` — not the capability kind:
|
|
77
|
+
|
|
78
|
+
| Action | Handler | Records into ClickHouse |
|
|
79
|
+
| --------------------- | ---------------------------- | ----------------------- |
|
|
80
|
+
| `sync_commits` | `github#sync_commits` | `commits` |
|
|
81
|
+
| `sync_tags` | `github#sync_tags` | `tags` |
|
|
82
|
+
| `sync_pull_requests` | `github#sync_pull_requests` | `pull_requests` |
|
|
83
|
+
|
|
84
|
+
`sync_commits` and `sync_tags` are dispatched from `push` deliveries;
|
|
85
|
+
`sync_pull_requests` from `pull_request` deliveries.
|
|
86
|
+
|
|
87
|
+
`sync_commits` fetches the full set of commits in a push via the compare
|
|
88
|
+
API (paginated, so it isn't capped by the 20-commit inline payload limit);
|
|
89
|
+
`sync_tags` records the pushed tag and, with `reconcile_all`, the repo's
|
|
90
|
+
full tag list. Branch/tag gating is the rule's CEL `filter_expression`,
|
|
91
|
+
which evaluates against the recorded event — the webhook body is under
|
|
92
|
+
`payload` (e.g. `payload.ref == "refs/heads/main"`,
|
|
93
|
+
`payload.ref.startsWith("refs/tags/")`). The API
|
|
94
|
+
flavor (github.com / GHEC / GHES) is resolved at runtime — explicit
|
|
95
|
+
`api_base_url`, else a connected GitHub plugin on the same service, else
|
|
96
|
+
the service endpoint, else the payload's `repository.url`.
|
|
97
|
+
|
|
98
|
+
Unlike identity/deployment/lifecycle (which act as the OAuth user),
|
|
99
|
+
commit-sync runs without an actor and authenticates with a **service**
|
|
100
|
+
credential in one of two modes, resolved per call:
|
|
101
|
+
|
|
102
|
+
- **PAT** — a static `access_token`.
|
|
103
|
+
- **GitHub App** — `app_id` + `private_key`; the plugin signs an App JWT
|
|
104
|
+
and mints a short-lived **installation token** (cached process-wide
|
|
105
|
+
until shortly before it expires), so no static, expiring token is
|
|
106
|
+
stored. `installation_id` is optional — when unset it is discovered
|
|
107
|
+
from the pushed repository (`GET /repos/{owner}/{repo}/installation`).
|
|
108
|
+
The App needs **Contents: Read-only**.
|
|
109
|
+
|
|
110
|
+
## Integration options
|
|
111
|
+
|
|
112
|
+
Asked once per Integration and delivered to every capability on
|
|
113
|
+
`PluginContext.integration_options`:
|
|
114
|
+
|
|
115
|
+
| Option | Required | Description |
|
|
116
|
+
| -------- | --------- | ----------------------------------------------------------------------- |
|
|
117
|
+
| `flavor` | yes | `github`, `ghec`, or `ghes`. |
|
|
118
|
+
| `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
|
|
119
|
+
|
|
120
|
+
## Capability options
|
|
121
|
+
|
|
122
|
+
Scoped to one capability and delivered on
|
|
123
|
+
`PluginContext.capability_options`:
|
|
124
|
+
|
|
125
|
+
| Capability | Option | Description |
|
|
126
|
+
| ----------- | -------------------- | --------------------------------------------------------------------------------------- |
|
|
127
|
+
| `identity` | `default_scopes` | Space-separated OAuth scopes (default: `read:user user:email repo workflow`). |
|
|
128
|
+
| `lifecycle` | `archive_target_org` | Org to transfer repos to before archiving; blank archives in place. |
|
|
129
|
+
| `lifecycle` | `create_org` | Default org for repo creation when no `org_mapping` entry matches. |
|
|
130
|
+
| `lifecycle` | `org_mapping` | Per-project-type-slug org overrides; the first match wins over `create_org`. |
|
|
131
|
+
|
|
132
|
+
## Credentials
|
|
133
|
+
|
|
134
|
+
One credential store per Integration — every capability receives the
|
|
135
|
+
same decrypted blob. All fields are individually optional and validated
|
|
136
|
+
per call, so an identity-only or App-only Integration is valid:
|
|
137
|
+
|
|
138
|
+
| Field | Used by | Description |
|
|
139
|
+
| ----------------- | ------------------ | ------------------------------------------------- |
|
|
140
|
+
| `access_token` | service (PAT mode) | Static personal/service token. |
|
|
141
|
+
| `app_id` | service (App mode) | GitHub App identifier. |
|
|
142
|
+
| `private_key` | service (App mode) | App private key — raw PEM or base64-encoded PEM. |
|
|
143
|
+
| `installation_id` | service (App mode) | Optional; discovered from the repo when unset. |
|
|
144
|
+
| `client_id` | `identity` | OAuth App client id. |
|
|
145
|
+
| `client_secret` | `identity` | OAuth App client secret. |
|
|
146
|
+
|
|
147
|
+
For `commit-sync`, `pr-sync`, and `deployment` provide **either**
|
|
148
|
+
`access_token` **or** `app_id` + `private_key`. `lifecycle` acts as the
|
|
149
|
+
user and takes only `access_token` (or `token`) — it does not fall back
|
|
150
|
+
to GitHub App credentials.
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
BSD-3-Clause.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# imbi-plugin-github
|
|
2
|
+
|
|
3
|
+
GitHub plugin for Imbi. A single plugin — slug **`github`** — backs every
|
|
4
|
+
GitHub Integration; the integration-level `flavor` option selects
|
|
5
|
+
github.com (`github`), GitHub Enterprise Cloud (`ghec`), or GitHub
|
|
6
|
+
Enterprise Server (`ghes`), and `host` names the tenant or appliance.
|
|
7
|
+
|
|
8
|
+
## Capabilities
|
|
9
|
+
|
|
10
|
+
| Kind | Handler |
|
|
11
|
+
| ----------------- | ------------------------- |
|
|
12
|
+
| `identity` | `GitHubIdentity` |
|
|
13
|
+
| `deployment` | `GitHubDeployment` |
|
|
14
|
+
| `lifecycle` | `GitHubLifecycle` |
|
|
15
|
+
| `webhook-actions` | `GitHubWebhookActions` |
|
|
16
|
+
| `commit-sync` | `GitHubCommitSync` |
|
|
17
|
+
| `pr-sync` | `GitHubPullRequestSync` |
|
|
18
|
+
| `analysis` | `GitHubDoctor` |
|
|
19
|
+
|
|
20
|
+
### Identity
|
|
21
|
+
|
|
22
|
+
Implements the OAuth App flow. The access token returned by the OAuth
|
|
23
|
+
grant is passed straight to GitHub APIs as a `Bearer` token, so
|
|
24
|
+
`materialize()` is a no-op.
|
|
25
|
+
|
|
26
|
+
### Deployment
|
|
27
|
+
|
|
28
|
+
Drives the GitHub Deployments API (`POST /repos/{owner}/{repo}/deployments`)
|
|
29
|
+
plus tag and release creation. Promote behaviour is inferred from the
|
|
30
|
+
ref shape by the host (semver → trigger Deployment, raw SHA → cut tag
|
|
31
|
+
+ Release). Per-env workflow inputs ride on the `USES_PLUGIN` edge as
|
|
32
|
+
`env_payloads` and arrive on `PluginContext.environment_config`.
|
|
33
|
+
|
|
34
|
+
### Lifecycle
|
|
35
|
+
|
|
36
|
+
Reacts to project archive / unarchive by archiving the matching repo via
|
|
37
|
+
`PATCH /repos/{owner}/{repo}` with `{"archived": true|false}`. When the
|
|
38
|
+
`archive_target_org` option is set, archive **also** transfers the repo to
|
|
39
|
+
that org first via `POST /repos/{owner}/{repo}/transfer` — useful for
|
|
40
|
+
moving sunset projects into a dedicated "archive" org so they no longer
|
|
41
|
+
crowd primary-org searches.
|
|
42
|
+
|
|
43
|
+
GitHub refuses to transfer archived repos, so an already-archived source
|
|
44
|
+
is briefly unarchived, transferred, and re-archived at the destination.
|
|
45
|
+
On unarchive the plugin only flips `archived` back to `false` at the
|
|
46
|
+
repo's current location — it does **not** transfer back to the original
|
|
47
|
+
org.
|
|
48
|
+
|
|
49
|
+
Archiving requires admin scope on the repo; transferring additionally
|
|
50
|
+
requires admin permission on the target organization.
|
|
51
|
+
|
|
52
|
+
### Webhook actions (commit / tag / PR sync)
|
|
53
|
+
|
|
54
|
+
The `webhook-actions` capability exposes the actions the gateway
|
|
55
|
+
dispatches from webhook deliveries. A `WebhookRule.handler` is
|
|
56
|
+
`"<plugin_slug>#<action_name>"`, and the slug is the **plugin** slug —
|
|
57
|
+
`github` — not the capability kind:
|
|
58
|
+
|
|
59
|
+
| Action | Handler | Records into ClickHouse |
|
|
60
|
+
| --------------------- | ---------------------------- | ----------------------- |
|
|
61
|
+
| `sync_commits` | `github#sync_commits` | `commits` |
|
|
62
|
+
| `sync_tags` | `github#sync_tags` | `tags` |
|
|
63
|
+
| `sync_pull_requests` | `github#sync_pull_requests` | `pull_requests` |
|
|
64
|
+
|
|
65
|
+
`sync_commits` and `sync_tags` are dispatched from `push` deliveries;
|
|
66
|
+
`sync_pull_requests` from `pull_request` deliveries.
|
|
67
|
+
|
|
68
|
+
`sync_commits` fetches the full set of commits in a push via the compare
|
|
69
|
+
API (paginated, so it isn't capped by the 20-commit inline payload limit);
|
|
70
|
+
`sync_tags` records the pushed tag and, with `reconcile_all`, the repo's
|
|
71
|
+
full tag list. Branch/tag gating is the rule's CEL `filter_expression`,
|
|
72
|
+
which evaluates against the recorded event — the webhook body is under
|
|
73
|
+
`payload` (e.g. `payload.ref == "refs/heads/main"`,
|
|
74
|
+
`payload.ref.startsWith("refs/tags/")`). The API
|
|
75
|
+
flavor (github.com / GHEC / GHES) is resolved at runtime — explicit
|
|
76
|
+
`api_base_url`, else a connected GitHub plugin on the same service, else
|
|
77
|
+
the service endpoint, else the payload's `repository.url`.
|
|
78
|
+
|
|
79
|
+
Unlike identity/deployment/lifecycle (which act as the OAuth user),
|
|
80
|
+
commit-sync runs without an actor and authenticates with a **service**
|
|
81
|
+
credential in one of two modes, resolved per call:
|
|
82
|
+
|
|
83
|
+
- **PAT** — a static `access_token`.
|
|
84
|
+
- **GitHub App** — `app_id` + `private_key`; the plugin signs an App JWT
|
|
85
|
+
and mints a short-lived **installation token** (cached process-wide
|
|
86
|
+
until shortly before it expires), so no static, expiring token is
|
|
87
|
+
stored. `installation_id` is optional — when unset it is discovered
|
|
88
|
+
from the pushed repository (`GET /repos/{owner}/{repo}/installation`).
|
|
89
|
+
The App needs **Contents: Read-only**.
|
|
90
|
+
|
|
91
|
+
## Integration options
|
|
92
|
+
|
|
93
|
+
Asked once per Integration and delivered to every capability on
|
|
94
|
+
`PluginContext.integration_options`:
|
|
95
|
+
|
|
96
|
+
| Option | Required | Description |
|
|
97
|
+
| -------- | --------- | ----------------------------------------------------------------------- |
|
|
98
|
+
| `flavor` | yes | `github`, `ghec`, or `ghes`. |
|
|
99
|
+
| `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
|
|
100
|
+
|
|
101
|
+
## Capability options
|
|
102
|
+
|
|
103
|
+
Scoped to one capability and delivered on
|
|
104
|
+
`PluginContext.capability_options`:
|
|
105
|
+
|
|
106
|
+
| Capability | Option | Description |
|
|
107
|
+
| ----------- | -------------------- | --------------------------------------------------------------------------------------- |
|
|
108
|
+
| `identity` | `default_scopes` | Space-separated OAuth scopes (default: `read:user user:email repo workflow`). |
|
|
109
|
+
| `lifecycle` | `archive_target_org` | Org to transfer repos to before archiving; blank archives in place. |
|
|
110
|
+
| `lifecycle` | `create_org` | Default org for repo creation when no `org_mapping` entry matches. |
|
|
111
|
+
| `lifecycle` | `org_mapping` | Per-project-type-slug org overrides; the first match wins over `create_org`. |
|
|
112
|
+
|
|
113
|
+
## Credentials
|
|
114
|
+
|
|
115
|
+
One credential store per Integration — every capability receives the
|
|
116
|
+
same decrypted blob. All fields are individually optional and validated
|
|
117
|
+
per call, so an identity-only or App-only Integration is valid:
|
|
118
|
+
|
|
119
|
+
| Field | Used by | Description |
|
|
120
|
+
| ----------------- | ------------------ | ------------------------------------------------- |
|
|
121
|
+
| `access_token` | service (PAT mode) | Static personal/service token. |
|
|
122
|
+
| `app_id` | service (App mode) | GitHub App identifier. |
|
|
123
|
+
| `private_key` | service (App mode) | App private key — raw PEM or base64-encoded PEM. |
|
|
124
|
+
| `installation_id` | service (App mode) | Optional; discovered from the repo when unset. |
|
|
125
|
+
| `client_id` | `identity` | OAuth App client id. |
|
|
126
|
+
| `client_secret` | `identity` | OAuth App client secret. |
|
|
127
|
+
|
|
128
|
+
For `commit-sync`, `pr-sync`, and `deployment` provide **either**
|
|
129
|
+
`access_token` **or** `app_id` + `private_key`. `lifecycle` acts as the
|
|
130
|
+
user and takes only `access_token` (or `token`) — it does not fall back
|
|
131
|
+
to GitHub App credentials.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
BSD-3-Clause.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "imbi-plugin-github"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.19.0"
|
|
4
4
|
description = "GitHub identity plugin for Imbi (github.com / GHEC / GHES)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
|
18
18
|
]
|
|
19
19
|
dependencies = [
|
|
20
20
|
"httpx>=0.27",
|
|
21
|
-
"imbi-common[databases]==2.
|
|
21
|
+
"imbi-common[databases]==2.19.0",
|
|
22
22
|
"pydantic>=2",
|
|
23
23
|
"pyjwt[crypto]>=2.8",
|
|
24
24
|
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# imbi-plugin-github
|
|
2
|
+
|
|
3
|
+
GitHub plugin for Imbi. A single plugin — slug **`github`** — backs every
|
|
4
|
+
GitHub Integration; the integration-level `flavor` option selects
|
|
5
|
+
github.com (`github`), GitHub Enterprise Cloud (`ghec`), or GitHub
|
|
6
|
+
Enterprise Server (`ghes`), and `host` names the tenant or appliance.
|
|
7
|
+
|
|
8
|
+
## Capabilities
|
|
9
|
+
|
|
10
|
+
| Kind | Handler |
|
|
11
|
+
| ----------------- | ------------------------- |
|
|
12
|
+
| `identity` | `GitHubIdentity` |
|
|
13
|
+
| `deployment` | `GitHubDeployment` |
|
|
14
|
+
| `lifecycle` | `GitHubLifecycle` |
|
|
15
|
+
| `webhook-actions` | `GitHubWebhookActions` |
|
|
16
|
+
| `commit-sync` | `GitHubCommitSync` |
|
|
17
|
+
| `pr-sync` | `GitHubPullRequestSync` |
|
|
18
|
+
| `analysis` | `GitHubDoctor` |
|
|
19
|
+
|
|
20
|
+
### Identity
|
|
21
|
+
|
|
22
|
+
Implements the OAuth App flow. The access token returned by the OAuth
|
|
23
|
+
grant is passed straight to GitHub APIs as a `Bearer` token, so
|
|
24
|
+
`materialize()` is a no-op.
|
|
25
|
+
|
|
26
|
+
### Deployment
|
|
27
|
+
|
|
28
|
+
Drives the GitHub Deployments API (`POST /repos/{owner}/{repo}/deployments`)
|
|
29
|
+
plus tag and release creation. Promote behaviour is inferred from the
|
|
30
|
+
ref shape by the host (semver → trigger Deployment, raw SHA → cut tag
|
|
31
|
+
+ Release). Per-env workflow inputs ride on the `USES_PLUGIN` edge as
|
|
32
|
+
`env_payloads` and arrive on `PluginContext.environment_config`.
|
|
33
|
+
|
|
34
|
+
### Lifecycle
|
|
35
|
+
|
|
36
|
+
Reacts to project archive / unarchive by archiving the matching repo via
|
|
37
|
+
`PATCH /repos/{owner}/{repo}` with `{"archived": true|false}`. When the
|
|
38
|
+
`archive_target_org` option is set, archive **also** transfers the repo to
|
|
39
|
+
that org first via `POST /repos/{owner}/{repo}/transfer` — useful for
|
|
40
|
+
moving sunset projects into a dedicated "archive" org so they no longer
|
|
41
|
+
crowd primary-org searches.
|
|
42
|
+
|
|
43
|
+
GitHub refuses to transfer archived repos, so an already-archived source
|
|
44
|
+
is briefly unarchived, transferred, and re-archived at the destination.
|
|
45
|
+
On unarchive the plugin only flips `archived` back to `false` at the
|
|
46
|
+
repo's current location — it does **not** transfer back to the original
|
|
47
|
+
org.
|
|
48
|
+
|
|
49
|
+
Archiving requires admin scope on the repo; transferring additionally
|
|
50
|
+
requires admin permission on the target organization.
|
|
51
|
+
|
|
52
|
+
### Webhook actions (commit / tag / PR sync)
|
|
53
|
+
|
|
54
|
+
The `webhook-actions` capability exposes the actions the gateway
|
|
55
|
+
dispatches from webhook deliveries. A `WebhookRule.handler` is
|
|
56
|
+
`"<plugin_slug>#<action_name>"`, and the slug is the **plugin** slug —
|
|
57
|
+
`github` — not the capability kind:
|
|
58
|
+
|
|
59
|
+
| Action | Handler | Records into ClickHouse |
|
|
60
|
+
| --------------------- | ---------------------------- | ----------------------- |
|
|
61
|
+
| `sync_commits` | `github#sync_commits` | `commits` |
|
|
62
|
+
| `sync_tags` | `github#sync_tags` | `tags` |
|
|
63
|
+
| `sync_pull_requests` | `github#sync_pull_requests` | `pull_requests` |
|
|
64
|
+
|
|
65
|
+
`sync_commits` and `sync_tags` are dispatched from `push` deliveries;
|
|
66
|
+
`sync_pull_requests` from `pull_request` deliveries.
|
|
67
|
+
|
|
68
|
+
`sync_commits` fetches the full set of commits in a push via the compare
|
|
69
|
+
API (paginated, so it isn't capped by the 20-commit inline payload limit);
|
|
70
|
+
`sync_tags` records the pushed tag and, with `reconcile_all`, the repo's
|
|
71
|
+
full tag list. Branch/tag gating is the rule's CEL `filter_expression`,
|
|
72
|
+
which evaluates against the recorded event — the webhook body is under
|
|
73
|
+
`payload` (e.g. `payload.ref == "refs/heads/main"`,
|
|
74
|
+
`payload.ref.startsWith("refs/tags/")`). The API
|
|
75
|
+
flavor (github.com / GHEC / GHES) is resolved at runtime — explicit
|
|
76
|
+
`api_base_url`, else a connected GitHub plugin on the same service, else
|
|
77
|
+
the service endpoint, else the payload's `repository.url`.
|
|
78
|
+
|
|
79
|
+
Unlike identity/deployment/lifecycle (which act as the OAuth user),
|
|
80
|
+
commit-sync runs without an actor and authenticates with a **service**
|
|
81
|
+
credential in one of two modes, resolved per call:
|
|
82
|
+
|
|
83
|
+
- **PAT** — a static `access_token`.
|
|
84
|
+
- **GitHub App** — `app_id` + `private_key`; the plugin signs an App JWT
|
|
85
|
+
and mints a short-lived **installation token** (cached process-wide
|
|
86
|
+
until shortly before it expires), so no static, expiring token is
|
|
87
|
+
stored. `installation_id` is optional — when unset it is discovered
|
|
88
|
+
from the pushed repository (`GET /repos/{owner}/{repo}/installation`).
|
|
89
|
+
The App needs **Contents: Read-only**.
|
|
90
|
+
|
|
91
|
+
## Integration options
|
|
92
|
+
|
|
93
|
+
Asked once per Integration and delivered to every capability on
|
|
94
|
+
`PluginContext.integration_options`:
|
|
95
|
+
|
|
96
|
+
| Option | Required | Description |
|
|
97
|
+
| -------- | --------- | ----------------------------------------------------------------------- |
|
|
98
|
+
| `flavor` | yes | `github`, `ghec`, or `ghes`. |
|
|
99
|
+
| `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
|
|
100
|
+
|
|
101
|
+
## Capability options
|
|
102
|
+
|
|
103
|
+
Scoped to one capability and delivered on
|
|
104
|
+
`PluginContext.capability_options`:
|
|
105
|
+
|
|
106
|
+
| Capability | Option | Description |
|
|
107
|
+
| ----------- | -------------------- | --------------------------------------------------------------------------------------- |
|
|
108
|
+
| `identity` | `default_scopes` | Space-separated OAuth scopes (default: `read:user user:email repo workflow`). |
|
|
109
|
+
| `lifecycle` | `archive_target_org` | Org to transfer repos to before archiving; blank archives in place. |
|
|
110
|
+
| `lifecycle` | `create_org` | Default org for repo creation when no `org_mapping` entry matches. |
|
|
111
|
+
| `lifecycle` | `org_mapping` | Per-project-type-slug org overrides; the first match wins over `create_org`. |
|
|
112
|
+
|
|
113
|
+
## Credentials
|
|
114
|
+
|
|
115
|
+
One credential store per Integration — every capability receives the
|
|
116
|
+
same decrypted blob. All fields are individually optional and validated
|
|
117
|
+
per call, so an identity-only or App-only Integration is valid:
|
|
118
|
+
|
|
119
|
+
| Field | Used by | Description |
|
|
120
|
+
| ----------------- | ------------------ | ------------------------------------------------- |
|
|
121
|
+
| `access_token` | service (PAT mode) | Static personal/service token. |
|
|
122
|
+
| `app_id` | service (App mode) | GitHub App identifier. |
|
|
123
|
+
| `private_key` | service (App mode) | App private key — raw PEM or base64-encoded PEM. |
|
|
124
|
+
| `installation_id` | service (App mode) | Optional; discovered from the repo when unset. |
|
|
125
|
+
| `client_id` | `identity` | OAuth App client id. |
|
|
126
|
+
| `client_secret` | `identity` | OAuth App client secret. |
|
|
127
|
+
|
|
128
|
+
For `commit-sync`, `pr-sync`, and `deployment` provide **either**
|
|
129
|
+
`access_token` **or** `app_id` + `private_key`. `lifecycle` acts as the
|
|
130
|
+
user and takes only `access_token` (or `token`) — it does not fall back
|
|
131
|
+
to GitHub App credentials.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
BSD-3-Clause.
|
{imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/deployment.py
RENAMED
|
@@ -26,6 +26,7 @@ import contextlib
|
|
|
26
26
|
import datetime
|
|
27
27
|
import hashlib
|
|
28
28
|
import logging
|
|
29
|
+
import re
|
|
29
30
|
import time
|
|
30
31
|
import typing
|
|
31
32
|
import urllib.parse
|
|
@@ -1002,10 +1003,16 @@ class GitHubDeployment(DeploymentCapability):
|
|
|
1002
1003
|
with explicit pagination if it ever needs to.
|
|
1003
1004
|
"""
|
|
1004
1005
|
page_size = max(1, min(limit, 100))
|
|
1006
|
+
# One run commonly backs several deployments in a sweep; cache
|
|
1007
|
+
# the triggering-actor lookup by run id so we resolve each run
|
|
1008
|
+
# at most once. Shared across the parallel per-env fan-out.
|
|
1009
|
+
run_cache: dict[str, tuple[str, str] | None] = {}
|
|
1005
1010
|
async with self._client(ctx, credentials) as client:
|
|
1006
1011
|
per_env = await asyncio.gather(
|
|
1007
1012
|
*(
|
|
1008
|
-
self._list_deployments_for_env(
|
|
1013
|
+
self._list_deployments_for_env(
|
|
1014
|
+
client, env, page_size, run_cache
|
|
1015
|
+
)
|
|
1009
1016
|
for env in environments
|
|
1010
1017
|
)
|
|
1011
1018
|
)
|
|
@@ -1035,6 +1042,7 @@ class GitHubDeployment(DeploymentCapability):
|
|
|
1035
1042
|
client: httpx.AsyncClient,
|
|
1036
1043
|
environment: str,
|
|
1037
1044
|
page_size: int,
|
|
1045
|
+
run_cache: dict[str, tuple[str, str] | None],
|
|
1038
1046
|
) -> list[RemoteDeployment]:
|
|
1039
1047
|
try:
|
|
1040
1048
|
resp = await client.get(
|
|
@@ -1067,7 +1075,7 @@ class GitHubDeployment(DeploymentCapability):
|
|
|
1067
1075
|
observed: list[RemoteDeployment] = []
|
|
1068
1076
|
for deployment in deployments:
|
|
1069
1077
|
run = await self._observe_deployment(
|
|
1070
|
-
client, environment, deployment
|
|
1078
|
+
client, environment, deployment, run_cache
|
|
1071
1079
|
)
|
|
1072
1080
|
if run is not None:
|
|
1073
1081
|
observed.append(run)
|
|
@@ -1078,6 +1086,7 @@ class GitHubDeployment(DeploymentCapability):
|
|
|
1078
1086
|
client: httpx.AsyncClient,
|
|
1079
1087
|
environment: str,
|
|
1080
1088
|
deployment: dict[str, typing.Any],
|
|
1089
|
+
run_cache: dict[str, tuple[str, str] | None],
|
|
1081
1090
|
) -> RemoteDeployment | None:
|
|
1082
1091
|
deployment_id = deployment.get('id')
|
|
1083
1092
|
sha = deployment.get('sha')
|
|
@@ -1113,6 +1122,18 @@ class GitHubDeployment(DeploymentCapability):
|
|
|
1113
1122
|
creator_id = creator_dict.get('id')
|
|
1114
1123
|
if isinstance(creator_id, int):
|
|
1115
1124
|
creator_subject = str(creator_id)
|
|
1125
|
+
# Deployments made by an Actions workflow carry the app bot
|
|
1126
|
+
# as creator (``deployer[bot]``), never the human who
|
|
1127
|
+
# triggered the run. Re-attribute to the run's triggering
|
|
1128
|
+
# actor so the host can resolve the deploy to a real user.
|
|
1129
|
+
if _is_bot(creator_dict):
|
|
1130
|
+
run_id = _run_id_from_status_url(status_url)
|
|
1131
|
+
if run_id is not None:
|
|
1132
|
+
actor = await self._resolve_triggering_actor(
|
|
1133
|
+
client, run_id, run_cache
|
|
1134
|
+
)
|
|
1135
|
+
if actor is not None:
|
|
1136
|
+
creator_login, creator_subject = actor
|
|
1116
1137
|
return RemoteDeployment(
|
|
1117
1138
|
environment=environment,
|
|
1118
1139
|
sha=str(sha),
|
|
@@ -1128,6 +1149,53 @@ class GitHubDeployment(DeploymentCapability):
|
|
|
1128
1149
|
creator_subject=creator_subject,
|
|
1129
1150
|
)
|
|
1130
1151
|
|
|
1152
|
+
async def _resolve_triggering_actor(
|
|
1153
|
+
self,
|
|
1154
|
+
client: httpx.AsyncClient,
|
|
1155
|
+
run_id: str,
|
|
1156
|
+
run_cache: dict[str, tuple[str, str] | None],
|
|
1157
|
+
) -> tuple[str, str] | None:
|
|
1158
|
+
"""Resolve a workflow run's human trigger to ``(login, subject)``.
|
|
1159
|
+
|
|
1160
|
+
Attributes a bot-created deployment to the person who started
|
|
1161
|
+
the Actions run via ``GET /actions/runs/{run_id}``, preferring
|
|
1162
|
+
``triggering_actor`` (the account that re-ran or dispatched the
|
|
1163
|
+
run) and falling back to ``actor``. Best-effort: a fetch/parse
|
|
1164
|
+
error, a missing actor, or an actor that is itself a bot yields
|
|
1165
|
+
``None`` so the caller keeps the bot creator -- attribution must
|
|
1166
|
+
never fail the resync. Results (including ``None``) are cached
|
|
1167
|
+
per sweep because one run backs several deployments.
|
|
1168
|
+
"""
|
|
1169
|
+
if run_id in run_cache:
|
|
1170
|
+
return run_cache[run_id]
|
|
1171
|
+
result: tuple[str, str] | None = None
|
|
1172
|
+
try:
|
|
1173
|
+
resp = await client.get(f'/actions/runs/{run_id}')
|
|
1174
|
+
resp.raise_for_status()
|
|
1175
|
+
run = typing.cast(dict[str, typing.Any], resp.json())
|
|
1176
|
+
except (httpx.HTTPError, PluginAuthenticationFailed, ValueError):
|
|
1177
|
+
LOGGER.warning(
|
|
1178
|
+
'Failed to fetch workflow run %s for deploy attribution',
|
|
1179
|
+
run_id,
|
|
1180
|
+
exc_info=True,
|
|
1181
|
+
)
|
|
1182
|
+
run_cache[run_id] = None
|
|
1183
|
+
return None
|
|
1184
|
+
actor_raw = run.get('triggering_actor') or run.get('actor')
|
|
1185
|
+
if isinstance(actor_raw, dict):
|
|
1186
|
+
actor = typing.cast(dict[str, typing.Any], actor_raw)
|
|
1187
|
+
login = actor.get('login')
|
|
1188
|
+
actor_id = actor.get('id')
|
|
1189
|
+
if (
|
|
1190
|
+
isinstance(login, str)
|
|
1191
|
+
and login
|
|
1192
|
+
and isinstance(actor_id, int)
|
|
1193
|
+
and not _is_bot(actor)
|
|
1194
|
+
):
|
|
1195
|
+
result = (login, str(actor_id))
|
|
1196
|
+
run_cache[run_id] = result
|
|
1197
|
+
return result
|
|
1198
|
+
|
|
1131
1199
|
async def _release_notes_for_ref(
|
|
1132
1200
|
self, client: httpx.AsyncClient, ref: str
|
|
1133
1201
|
) -> str | None:
|
|
@@ -1195,6 +1263,38 @@ class GitHubDeployment(DeploymentCapability):
|
|
|
1195
1263
|
return _to_event_status(state), str(log_url) if log_url else None
|
|
1196
1264
|
|
|
1197
1265
|
|
|
1266
|
+
_RUN_ID_RE = re.compile(r'/actions/runs/(\d+)')
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def _is_bot(user: dict[str, typing.Any]) -> bool:
|
|
1270
|
+
"""Return ``True`` when a GitHub user object denotes an app/bot.
|
|
1271
|
+
|
|
1272
|
+
GitHub marks app identities with ``type == 'Bot'`` on the user
|
|
1273
|
+
object and gives them a ``login`` suffixed ``[bot]``
|
|
1274
|
+
(``github-actions[bot]``, ``deployer[bot]``). Either signal alone is
|
|
1275
|
+
sufficient; both comparisons are case-insensitive.
|
|
1276
|
+
"""
|
|
1277
|
+
if str(user.get('type') or '').lower() == 'bot':
|
|
1278
|
+
return True
|
|
1279
|
+
login = user.get('login')
|
|
1280
|
+
return isinstance(login, str) and login.lower().endswith('[bot]')
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def _run_id_from_status_url(url: str | None) -> str | None:
|
|
1284
|
+
"""Extract the Actions run id from a deployment status URL.
|
|
1285
|
+
|
|
1286
|
+
A GitHub Actions deploy posts its status ``log_url``/``target_url``
|
|
1287
|
+
pointing at the workflow run
|
|
1288
|
+
(``.../actions/runs/{run_id}`` optionally followed by
|
|
1289
|
+
``/job/{job_id}``). Any URL that is not an Actions run link yields
|
|
1290
|
+
``None``.
|
|
1291
|
+
"""
|
|
1292
|
+
if not url:
|
|
1293
|
+
return None
|
|
1294
|
+
match = _RUN_ID_RE.search(url)
|
|
1295
|
+
return match.group(1) if match else None
|
|
1296
|
+
|
|
1297
|
+
|
|
1198
1298
|
def _to_event_status(github_state: str) -> DeploymentEventStatus:
|
|
1199
1299
|
"""Map a GitHub deployment-status ``state`` to the host vocabulary.
|
|
1200
1300
|
|
|
@@ -1297,6 +1297,232 @@ class ListRecentDeploymentsTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
1297
1297
|
self.assertTrue(all(e.release_notes is None for e in events))
|
|
1298
1298
|
self.assertEqual(first.call_count, 1)
|
|
1299
1299
|
|
|
1300
|
+
@respx.mock
|
|
1301
|
+
async def test_bot_creator_attributed_to_triggering_actor(self) -> None:
|
|
1302
|
+
# A workflow-created deployment lists the app bot as creator; the
|
|
1303
|
+
# status URL points at the Actions run, so attribution follows to
|
|
1304
|
+
# the run's triggering actor (the human who started it).
|
|
1305
|
+
respx.get('https://api.github.com/repos/octo/demo/deployments').mock(
|
|
1306
|
+
return_value=httpx.Response(
|
|
1307
|
+
200,
|
|
1308
|
+
json=[
|
|
1309
|
+
{
|
|
1310
|
+
'id': 123,
|
|
1311
|
+
'sha': 'botsha',
|
|
1312
|
+
'ref': 'main',
|
|
1313
|
+
'created_at': '2026-05-13T14:00:00Z',
|
|
1314
|
+
'creator': {
|
|
1315
|
+
'login': 'deployer[bot]',
|
|
1316
|
+
'id': 111,
|
|
1317
|
+
'type': 'Bot',
|
|
1318
|
+
},
|
|
1319
|
+
}
|
|
1320
|
+
],
|
|
1321
|
+
)
|
|
1322
|
+
)
|
|
1323
|
+
respx.get(
|
|
1324
|
+
'https://api.github.com/repos/octo/demo/deployments/123/statuses'
|
|
1325
|
+
).mock(
|
|
1326
|
+
return_value=httpx.Response(
|
|
1327
|
+
200,
|
|
1328
|
+
json=[
|
|
1329
|
+
{
|
|
1330
|
+
'state': 'success',
|
|
1331
|
+
'log_url': (
|
|
1332
|
+
'https://github.com/octo/demo'
|
|
1333
|
+
'/actions/runs/9001/job/55'
|
|
1334
|
+
),
|
|
1335
|
+
'created_at': '2026-05-13T14:01:00Z',
|
|
1336
|
+
}
|
|
1337
|
+
],
|
|
1338
|
+
)
|
|
1339
|
+
)
|
|
1340
|
+
run = respx.get(
|
|
1341
|
+
'https://api.github.com/repos/octo/demo/actions/runs/9001'
|
|
1342
|
+
).mock(
|
|
1343
|
+
return_value=httpx.Response(
|
|
1344
|
+
200,
|
|
1345
|
+
json={
|
|
1346
|
+
'triggering_actor': {'login': 'octocat', 'id': 583231},
|
|
1347
|
+
'actor': {'login': 'someone-else', 'id': 42},
|
|
1348
|
+
},
|
|
1349
|
+
)
|
|
1350
|
+
)
|
|
1351
|
+
respx.get(
|
|
1352
|
+
'https://api.github.com/repos/octo/demo/releases/tags/main'
|
|
1353
|
+
).mock(return_value=httpx.Response(404, json={'message': 'Not Found'}))
|
|
1354
|
+
plugin = GitHubDeployment()
|
|
1355
|
+
events = await plugin.list_recent_deployments(
|
|
1356
|
+
_ctx(), _CREDS, ['production']
|
|
1357
|
+
)
|
|
1358
|
+
self.assertEqual(len(events), 1)
|
|
1359
|
+
self.assertTrue(run.called)
|
|
1360
|
+
self.assertEqual(events[0].creator, 'octocat')
|
|
1361
|
+
self.assertEqual(events[0].creator_subject, '583231')
|
|
1362
|
+
|
|
1363
|
+
@respx.mock
|
|
1364
|
+
async def test_bot_detection_is_case_insensitive(self) -> None:
|
|
1365
|
+
# Bot detection must not depend on GitHub's casing: a creator
|
|
1366
|
+
# with type 'bot' and a mixed-case '[Bot]' login suffix is still
|
|
1367
|
+
# re-attributed to the run's triggering actor.
|
|
1368
|
+
respx.get('https://api.github.com/repos/octo/demo/deployments').mock(
|
|
1369
|
+
return_value=httpx.Response(
|
|
1370
|
+
200,
|
|
1371
|
+
json=[
|
|
1372
|
+
{
|
|
1373
|
+
'id': 123,
|
|
1374
|
+
'sha': 'botsha',
|
|
1375
|
+
'ref': 'main',
|
|
1376
|
+
'created_at': '2026-05-13T14:00:00Z',
|
|
1377
|
+
'creator': {
|
|
1378
|
+
'login': 'Deployer[Bot]',
|
|
1379
|
+
'id': 111,
|
|
1380
|
+
'type': 'bot',
|
|
1381
|
+
},
|
|
1382
|
+
}
|
|
1383
|
+
],
|
|
1384
|
+
)
|
|
1385
|
+
)
|
|
1386
|
+
respx.get(
|
|
1387
|
+
'https://api.github.com/repos/octo/demo/deployments/123/statuses'
|
|
1388
|
+
).mock(
|
|
1389
|
+
return_value=httpx.Response(
|
|
1390
|
+
200,
|
|
1391
|
+
json=[
|
|
1392
|
+
{
|
|
1393
|
+
'state': 'success',
|
|
1394
|
+
'log_url': (
|
|
1395
|
+
'https://github.com/octo/demo'
|
|
1396
|
+
'/actions/runs/9001/job/55'
|
|
1397
|
+
),
|
|
1398
|
+
'created_at': '2026-05-13T14:01:00Z',
|
|
1399
|
+
}
|
|
1400
|
+
],
|
|
1401
|
+
)
|
|
1402
|
+
)
|
|
1403
|
+
run = respx.get(
|
|
1404
|
+
'https://api.github.com/repos/octo/demo/actions/runs/9001'
|
|
1405
|
+
).mock(
|
|
1406
|
+
return_value=httpx.Response(
|
|
1407
|
+
200,
|
|
1408
|
+
json={
|
|
1409
|
+
'triggering_actor': {'login': 'octocat', 'id': 583231},
|
|
1410
|
+
},
|
|
1411
|
+
)
|
|
1412
|
+
)
|
|
1413
|
+
respx.get(
|
|
1414
|
+
'https://api.github.com/repos/octo/demo/releases/tags/main'
|
|
1415
|
+
).mock(return_value=httpx.Response(404, json={'message': 'Not Found'}))
|
|
1416
|
+
plugin = GitHubDeployment()
|
|
1417
|
+
events = await plugin.list_recent_deployments(
|
|
1418
|
+
_ctx(), _CREDS, ['production']
|
|
1419
|
+
)
|
|
1420
|
+
self.assertEqual(len(events), 1)
|
|
1421
|
+
self.assertTrue(run.called)
|
|
1422
|
+
self.assertEqual(events[0].creator, 'octocat')
|
|
1423
|
+
self.assertEqual(events[0].creator_subject, '583231')
|
|
1424
|
+
|
|
1425
|
+
@respx.mock
|
|
1426
|
+
async def test_bot_creator_kept_when_run_fetch_fails(self) -> None:
|
|
1427
|
+
# The run lookup 500s; attribution degrades gracefully to the bot
|
|
1428
|
+
# creator rather than raising and failing the resync.
|
|
1429
|
+
respx.get('https://api.github.com/repos/octo/demo/deployments').mock(
|
|
1430
|
+
return_value=httpx.Response(
|
|
1431
|
+
200,
|
|
1432
|
+
json=[
|
|
1433
|
+
{
|
|
1434
|
+
'id': 123,
|
|
1435
|
+
'sha': 'botsha',
|
|
1436
|
+
'ref': 'main',
|
|
1437
|
+
'created_at': '2026-05-13T14:00:00Z',
|
|
1438
|
+
'creator': {
|
|
1439
|
+
'login': 'github-actions[bot]',
|
|
1440
|
+
'id': 111,
|
|
1441
|
+
},
|
|
1442
|
+
}
|
|
1443
|
+
],
|
|
1444
|
+
)
|
|
1445
|
+
)
|
|
1446
|
+
respx.get(
|
|
1447
|
+
'https://api.github.com/repos/octo/demo/deployments/123/statuses'
|
|
1448
|
+
).mock(
|
|
1449
|
+
return_value=httpx.Response(
|
|
1450
|
+
200,
|
|
1451
|
+
json=[
|
|
1452
|
+
{
|
|
1453
|
+
'state': 'success',
|
|
1454
|
+
'target_url': (
|
|
1455
|
+
'https://github.com/octo/demo/actions/runs/9001'
|
|
1456
|
+
),
|
|
1457
|
+
'created_at': '2026-05-13T14:01:00Z',
|
|
1458
|
+
}
|
|
1459
|
+
],
|
|
1460
|
+
)
|
|
1461
|
+
)
|
|
1462
|
+
run = respx.get(
|
|
1463
|
+
'https://api.github.com/repos/octo/demo/actions/runs/9001'
|
|
1464
|
+
).mock(return_value=httpx.Response(500, json={'message': 'oops'}))
|
|
1465
|
+
respx.get(
|
|
1466
|
+
'https://api.github.com/repos/octo/demo/releases/tags/main'
|
|
1467
|
+
).mock(return_value=httpx.Response(404, json={'message': 'Not Found'}))
|
|
1468
|
+
plugin = GitHubDeployment()
|
|
1469
|
+
events = await plugin.list_recent_deployments(
|
|
1470
|
+
_ctx(), _CREDS, ['production']
|
|
1471
|
+
)
|
|
1472
|
+
self.assertEqual(len(events), 1)
|
|
1473
|
+
self.assertTrue(run.called)
|
|
1474
|
+
self.assertEqual(events[0].creator, 'github-actions[bot]')
|
|
1475
|
+
self.assertEqual(events[0].creator_subject, '111')
|
|
1476
|
+
|
|
1477
|
+
@respx.mock
|
|
1478
|
+
async def test_human_creator_skips_run_lookup(self) -> None:
|
|
1479
|
+
# A human-created deployment must never issue the extra run
|
|
1480
|
+
# fetch; the creator is used as-is.
|
|
1481
|
+
respx.get('https://api.github.com/repos/octo/demo/deployments').mock(
|
|
1482
|
+
return_value=httpx.Response(
|
|
1483
|
+
200,
|
|
1484
|
+
json=[
|
|
1485
|
+
{
|
|
1486
|
+
'id': 123,
|
|
1487
|
+
'sha': 'humansha',
|
|
1488
|
+
'ref': 'main',
|
|
1489
|
+
'created_at': '2026-05-13T14:00:00Z',
|
|
1490
|
+
'creator': {'login': 'octocat', 'id': 583231},
|
|
1491
|
+
}
|
|
1492
|
+
],
|
|
1493
|
+
)
|
|
1494
|
+
)
|
|
1495
|
+
respx.get(
|
|
1496
|
+
'https://api.github.com/repos/octo/demo/deployments/123/statuses'
|
|
1497
|
+
).mock(
|
|
1498
|
+
return_value=httpx.Response(
|
|
1499
|
+
200,
|
|
1500
|
+
json=[
|
|
1501
|
+
{
|
|
1502
|
+
'state': 'success',
|
|
1503
|
+
'log_url': (
|
|
1504
|
+
'https://github.com/octo/demo/actions/runs/9001'
|
|
1505
|
+
),
|
|
1506
|
+
'created_at': '2026-05-13T14:01:00Z',
|
|
1507
|
+
}
|
|
1508
|
+
],
|
|
1509
|
+
)
|
|
1510
|
+
)
|
|
1511
|
+
run = respx.get(
|
|
1512
|
+
'https://api.github.com/repos/octo/demo/actions/runs/9001'
|
|
1513
|
+
).mock(return_value=httpx.Response(200, json={}))
|
|
1514
|
+
respx.get(
|
|
1515
|
+
'https://api.github.com/repos/octo/demo/releases/tags/main'
|
|
1516
|
+
).mock(return_value=httpx.Response(404, json={'message': 'Not Found'}))
|
|
1517
|
+
plugin = GitHubDeployment()
|
|
1518
|
+
events = await plugin.list_recent_deployments(
|
|
1519
|
+
_ctx(), _CREDS, ['production']
|
|
1520
|
+
)
|
|
1521
|
+
self.assertEqual(len(events), 1)
|
|
1522
|
+
self.assertFalse(run.called)
|
|
1523
|
+
self.assertEqual(events[0].creator, 'octocat')
|
|
1524
|
+
self.assertEqual(events[0].creator_subject, '583231')
|
|
1525
|
+
|
|
1300
1526
|
|
|
1301
1527
|
class GetReleaseNotesTestCase(unittest.IsolatedAsyncioTestCase):
|
|
1302
1528
|
@respx.mock
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: imbi-plugin-github
|
|
3
|
-
Version: 2.17.0
|
|
4
|
-
Summary: GitHub identity plugin for Imbi (github.com / GHEC / GHES)
|
|
5
|
-
Author-email: "Gavin M. Roy" <gavinr@aweber.com>
|
|
6
|
-
License-Expression: BSD-3-Clause
|
|
7
|
-
Classifier: Development Status :: 3 - Alpha
|
|
8
|
-
Classifier: Intended Audience :: Developers
|
|
9
|
-
Classifier: Natural Language :: English
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
-
Requires-Python: >=3.14
|
|
14
|
-
Requires-Dist: httpx>=0.27
|
|
15
|
-
Requires-Dist: imbi-common[databases]==2.17.0
|
|
16
|
-
Requires-Dist: pydantic>=2
|
|
17
|
-
Requires-Dist: pyjwt[crypto]>=2.8
|
|
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
|
-
| Webhook | `github-commit-sync` |
|
|
34
|
-
|
|
35
|
-
### Identity
|
|
36
|
-
|
|
37
|
-
Implements the OAuth App flow. The access token returned by the OAuth
|
|
38
|
-
grant is passed straight to GitHub APIs as a `Bearer` token, so
|
|
39
|
-
`materialize()` is a no-op.
|
|
40
|
-
|
|
41
|
-
### Deployment
|
|
42
|
-
|
|
43
|
-
Drives the GitHub Deployments API (`POST /repos/{owner}/{repo}/deployments`)
|
|
44
|
-
plus tag and release creation. Promote behaviour is inferred from the
|
|
45
|
-
ref shape by the host (semver → trigger Deployment, raw SHA → cut tag
|
|
46
|
-
+ Release). Per-env workflow inputs ride on the `USES_PLUGIN` edge as
|
|
47
|
-
`env_payloads` and arrive on `PluginContext.environment_config`.
|
|
48
|
-
|
|
49
|
-
### Lifecycle
|
|
50
|
-
|
|
51
|
-
Reacts to project archive / unarchive by archiving the matching repo via
|
|
52
|
-
`PATCH /repos/{owner}/{repo}` with `{"archived": true|false}`. When the
|
|
53
|
-
`archive_target_org` option is set, archive **also** transfers the repo to
|
|
54
|
-
that org first via `POST /repos/{owner}/{repo}/transfer` — useful for
|
|
55
|
-
moving sunset projects into a dedicated "archive" org so they no longer
|
|
56
|
-
crowd primary-org searches.
|
|
57
|
-
|
|
58
|
-
GitHub refuses to transfer archived repos, so an already-archived source
|
|
59
|
-
is briefly unarchived, transferred, and re-archived at the destination.
|
|
60
|
-
On unarchive the plugin only flips `archived` back to `false` at the
|
|
61
|
-
repo's current location — it does **not** transfer back to the original
|
|
62
|
-
org.
|
|
63
|
-
|
|
64
|
-
Archiving requires admin scope on the repo; transferring additionally
|
|
65
|
-
requires admin permission on the target organization.
|
|
66
|
-
|
|
67
|
-
### Webhook (commit / tag sync)
|
|
68
|
-
|
|
69
|
-
A single `github-commit-sync` webhook-action plugin exposes two actions
|
|
70
|
-
the gateway dispatches on `push` deliveries:
|
|
71
|
-
|
|
72
|
-
| Action | Handler | Records into ClickHouse |
|
|
73
|
-
| -------------- | -------------------------------- | ----------------------- |
|
|
74
|
-
| `sync_commits` | `github-commit-sync#sync_commits`| `commits` |
|
|
75
|
-
| `sync_tags` | `github-commit-sync#sync_tags` | `tags` |
|
|
76
|
-
|
|
77
|
-
`sync_commits` fetches the full set of commits in a push via the compare
|
|
78
|
-
API (paginated, so it isn't capped by the 20-commit inline payload limit);
|
|
79
|
-
`sync_tags` records the pushed tag and, with `reconcile_all`, the repo's
|
|
80
|
-
full tag list. Branch/tag gating is the rule's CEL `filter_expression`
|
|
81
|
-
(e.g. `ref == "refs/heads/main"`, `ref.startsWith("refs/tags/")`). The API
|
|
82
|
-
flavor (github.com / GHEC / GHES) is resolved at runtime — explicit
|
|
83
|
-
`api_base_url`, else a connected GitHub plugin on the same service, else
|
|
84
|
-
the service endpoint, else the payload's `repository.url`.
|
|
85
|
-
|
|
86
|
-
Unlike identity/deployment/lifecycle (which act as the OAuth user),
|
|
87
|
-
commit-sync runs without an actor and authenticates with a **service**
|
|
88
|
-
credential in one of two modes, resolved per call:
|
|
89
|
-
|
|
90
|
-
- **PAT** — a static `access_token`.
|
|
91
|
-
- **GitHub App** — `app_id` + `private_key`; the plugin signs an App JWT
|
|
92
|
-
and mints a short-lived **installation token** (cached process-wide
|
|
93
|
-
until shortly before it expires), so no static, expiring token is
|
|
94
|
-
stored. `installation_id` is optional — when unset it is discovered
|
|
95
|
-
from the pushed repository (`GET /repos/{owner}/{repo}/installation`).
|
|
96
|
-
The App needs **Contents: Read-only**.
|
|
97
|
-
|
|
98
|
-
## Manifest options (identity)
|
|
99
|
-
|
|
100
|
-
| Option | Required | Description |
|
|
101
|
-
| ---------------- | --------- | -------------------------------------------------------------------------- |
|
|
102
|
-
| `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
|
|
103
|
-
| `default_scopes` | no | Space-separated default OAuth scopes (default: `read:user user:email repo workflow`). |
|
|
104
|
-
|
|
105
|
-
## Credentials (identity)
|
|
106
|
-
|
|
107
|
-
| Field | Required |
|
|
108
|
-
| ---------------- | -------- |
|
|
109
|
-
| `client_id` | yes |
|
|
110
|
-
| `client_secret` | yes |
|
|
111
|
-
|
|
112
|
-
## Credentials (commit-sync)
|
|
113
|
-
|
|
114
|
-
Provide **either** the PAT field **or** the GitHub App fields (all
|
|
115
|
-
individually optional; validated per call):
|
|
116
|
-
|
|
117
|
-
| Field | Mode | Description |
|
|
118
|
-
| ----------------- | ---- | ------------------------------------------------------ |
|
|
119
|
-
| `access_token` | PAT | Static personal/service token. |
|
|
120
|
-
| `app_id` | App | GitHub App identifier. |
|
|
121
|
-
| `private_key` | App | App private key — raw PEM or base64-encoded PEM. |
|
|
122
|
-
| `installation_id` | App | Optional; discovered from the repo when unset. |
|
|
123
|
-
|
|
124
|
-
## License
|
|
125
|
-
|
|
126
|
-
BSD-3-Clause.
|
|
@@ -1,107 +0,0 @@
|
|
|
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
|
-
| Webhook | `github-commit-sync` |
|
|
15
|
-
|
|
16
|
-
### Identity
|
|
17
|
-
|
|
18
|
-
Implements the OAuth App flow. The access token returned by the OAuth
|
|
19
|
-
grant is passed straight to GitHub APIs as a `Bearer` token, so
|
|
20
|
-
`materialize()` is a no-op.
|
|
21
|
-
|
|
22
|
-
### Deployment
|
|
23
|
-
|
|
24
|
-
Drives the GitHub Deployments API (`POST /repos/{owner}/{repo}/deployments`)
|
|
25
|
-
plus tag and release creation. Promote behaviour is inferred from the
|
|
26
|
-
ref shape by the host (semver → trigger Deployment, raw SHA → cut tag
|
|
27
|
-
+ Release). Per-env workflow inputs ride on the `USES_PLUGIN` edge as
|
|
28
|
-
`env_payloads` and arrive on `PluginContext.environment_config`.
|
|
29
|
-
|
|
30
|
-
### Lifecycle
|
|
31
|
-
|
|
32
|
-
Reacts to project archive / unarchive by archiving the matching repo via
|
|
33
|
-
`PATCH /repos/{owner}/{repo}` with `{"archived": true|false}`. When the
|
|
34
|
-
`archive_target_org` option is set, archive **also** transfers the repo to
|
|
35
|
-
that org first via `POST /repos/{owner}/{repo}/transfer` — useful for
|
|
36
|
-
moving sunset projects into a dedicated "archive" org so they no longer
|
|
37
|
-
crowd primary-org searches.
|
|
38
|
-
|
|
39
|
-
GitHub refuses to transfer archived repos, so an already-archived source
|
|
40
|
-
is briefly unarchived, transferred, and re-archived at the destination.
|
|
41
|
-
On unarchive the plugin only flips `archived` back to `false` at the
|
|
42
|
-
repo's current location — it does **not** transfer back to the original
|
|
43
|
-
org.
|
|
44
|
-
|
|
45
|
-
Archiving requires admin scope on the repo; transferring additionally
|
|
46
|
-
requires admin permission on the target organization.
|
|
47
|
-
|
|
48
|
-
### Webhook (commit / tag sync)
|
|
49
|
-
|
|
50
|
-
A single `github-commit-sync` webhook-action plugin exposes two actions
|
|
51
|
-
the gateway dispatches on `push` deliveries:
|
|
52
|
-
|
|
53
|
-
| Action | Handler | Records into ClickHouse |
|
|
54
|
-
| -------------- | -------------------------------- | ----------------------- |
|
|
55
|
-
| `sync_commits` | `github-commit-sync#sync_commits`| `commits` |
|
|
56
|
-
| `sync_tags` | `github-commit-sync#sync_tags` | `tags` |
|
|
57
|
-
|
|
58
|
-
`sync_commits` fetches the full set of commits in a push via the compare
|
|
59
|
-
API (paginated, so it isn't capped by the 20-commit inline payload limit);
|
|
60
|
-
`sync_tags` records the pushed tag and, with `reconcile_all`, the repo's
|
|
61
|
-
full tag list. Branch/tag gating is the rule's CEL `filter_expression`
|
|
62
|
-
(e.g. `ref == "refs/heads/main"`, `ref.startsWith("refs/tags/")`). The API
|
|
63
|
-
flavor (github.com / GHEC / GHES) is resolved at runtime — explicit
|
|
64
|
-
`api_base_url`, else a connected GitHub plugin on the same service, else
|
|
65
|
-
the service endpoint, else the payload's `repository.url`.
|
|
66
|
-
|
|
67
|
-
Unlike identity/deployment/lifecycle (which act as the OAuth user),
|
|
68
|
-
commit-sync runs without an actor and authenticates with a **service**
|
|
69
|
-
credential in one of two modes, resolved per call:
|
|
70
|
-
|
|
71
|
-
- **PAT** — a static `access_token`.
|
|
72
|
-
- **GitHub App** — `app_id` + `private_key`; the plugin signs an App JWT
|
|
73
|
-
and mints a short-lived **installation token** (cached process-wide
|
|
74
|
-
until shortly before it expires), so no static, expiring token is
|
|
75
|
-
stored. `installation_id` is optional — when unset it is discovered
|
|
76
|
-
from the pushed repository (`GET /repos/{owner}/{repo}/installation`).
|
|
77
|
-
The App needs **Contents: Read-only**.
|
|
78
|
-
|
|
79
|
-
## Manifest options (identity)
|
|
80
|
-
|
|
81
|
-
| Option | Required | Description |
|
|
82
|
-
| ---------------- | --------- | -------------------------------------------------------------------------- |
|
|
83
|
-
| `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
|
|
84
|
-
| `default_scopes` | no | Space-separated default OAuth scopes (default: `read:user user:email repo workflow`). |
|
|
85
|
-
|
|
86
|
-
## Credentials (identity)
|
|
87
|
-
|
|
88
|
-
| Field | Required |
|
|
89
|
-
| ---------------- | -------- |
|
|
90
|
-
| `client_id` | yes |
|
|
91
|
-
| `client_secret` | yes |
|
|
92
|
-
|
|
93
|
-
## Credentials (commit-sync)
|
|
94
|
-
|
|
95
|
-
Provide **either** the PAT field **or** the GitHub App fields (all
|
|
96
|
-
individually optional; validated per call):
|
|
97
|
-
|
|
98
|
-
| Field | Mode | Description |
|
|
99
|
-
| ----------------- | ---- | ------------------------------------------------------ |
|
|
100
|
-
| `access_token` | PAT | Static personal/service token. |
|
|
101
|
-
| `app_id` | App | GitHub App identifier. |
|
|
102
|
-
| `private_key` | App | App private key — raw PEM or base64-encoded PEM. |
|
|
103
|
-
| `installation_id` | App | Optional; discovered from the repo when unset. |
|
|
104
|
-
|
|
105
|
-
## License
|
|
106
|
-
|
|
107
|
-
BSD-3-Clause.
|
|
@@ -1,107 +0,0 @@
|
|
|
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
|
-
| Webhook | `github-commit-sync` |
|
|
15
|
-
|
|
16
|
-
### Identity
|
|
17
|
-
|
|
18
|
-
Implements the OAuth App flow. The access token returned by the OAuth
|
|
19
|
-
grant is passed straight to GitHub APIs as a `Bearer` token, so
|
|
20
|
-
`materialize()` is a no-op.
|
|
21
|
-
|
|
22
|
-
### Deployment
|
|
23
|
-
|
|
24
|
-
Drives the GitHub Deployments API (`POST /repos/{owner}/{repo}/deployments`)
|
|
25
|
-
plus tag and release creation. Promote behaviour is inferred from the
|
|
26
|
-
ref shape by the host (semver → trigger Deployment, raw SHA → cut tag
|
|
27
|
-
+ Release). Per-env workflow inputs ride on the `USES_PLUGIN` edge as
|
|
28
|
-
`env_payloads` and arrive on `PluginContext.environment_config`.
|
|
29
|
-
|
|
30
|
-
### Lifecycle
|
|
31
|
-
|
|
32
|
-
Reacts to project archive / unarchive by archiving the matching repo via
|
|
33
|
-
`PATCH /repos/{owner}/{repo}` with `{"archived": true|false}`. When the
|
|
34
|
-
`archive_target_org` option is set, archive **also** transfers the repo to
|
|
35
|
-
that org first via `POST /repos/{owner}/{repo}/transfer` — useful for
|
|
36
|
-
moving sunset projects into a dedicated "archive" org so they no longer
|
|
37
|
-
crowd primary-org searches.
|
|
38
|
-
|
|
39
|
-
GitHub refuses to transfer archived repos, so an already-archived source
|
|
40
|
-
is briefly unarchived, transferred, and re-archived at the destination.
|
|
41
|
-
On unarchive the plugin only flips `archived` back to `false` at the
|
|
42
|
-
repo's current location — it does **not** transfer back to the original
|
|
43
|
-
org.
|
|
44
|
-
|
|
45
|
-
Archiving requires admin scope on the repo; transferring additionally
|
|
46
|
-
requires admin permission on the target organization.
|
|
47
|
-
|
|
48
|
-
### Webhook (commit / tag sync)
|
|
49
|
-
|
|
50
|
-
A single `github-commit-sync` webhook-action plugin exposes two actions
|
|
51
|
-
the gateway dispatches on `push` deliveries:
|
|
52
|
-
|
|
53
|
-
| Action | Handler | Records into ClickHouse |
|
|
54
|
-
| -------------- | -------------------------------- | ----------------------- |
|
|
55
|
-
| `sync_commits` | `github-commit-sync#sync_commits`| `commits` |
|
|
56
|
-
| `sync_tags` | `github-commit-sync#sync_tags` | `tags` |
|
|
57
|
-
|
|
58
|
-
`sync_commits` fetches the full set of commits in a push via the compare
|
|
59
|
-
API (paginated, so it isn't capped by the 20-commit inline payload limit);
|
|
60
|
-
`sync_tags` records the pushed tag and, with `reconcile_all`, the repo's
|
|
61
|
-
full tag list. Branch/tag gating is the rule's CEL `filter_expression`
|
|
62
|
-
(e.g. `ref == "refs/heads/main"`, `ref.startsWith("refs/tags/")`). The API
|
|
63
|
-
flavor (github.com / GHEC / GHES) is resolved at runtime — explicit
|
|
64
|
-
`api_base_url`, else a connected GitHub plugin on the same service, else
|
|
65
|
-
the service endpoint, else the payload's `repository.url`.
|
|
66
|
-
|
|
67
|
-
Unlike identity/deployment/lifecycle (which act as the OAuth user),
|
|
68
|
-
commit-sync runs without an actor and authenticates with a **service**
|
|
69
|
-
credential in one of two modes, resolved per call:
|
|
70
|
-
|
|
71
|
-
- **PAT** — a static `access_token`.
|
|
72
|
-
- **GitHub App** — `app_id` + `private_key`; the plugin signs an App JWT
|
|
73
|
-
and mints a short-lived **installation token** (cached process-wide
|
|
74
|
-
until shortly before it expires), so no static, expiring token is
|
|
75
|
-
stored. `installation_id` is optional — when unset it is discovered
|
|
76
|
-
from the pushed repository (`GET /repos/{owner}/{repo}/installation`).
|
|
77
|
-
The App needs **Contents: Read-only**.
|
|
78
|
-
|
|
79
|
-
## Manifest options (identity)
|
|
80
|
-
|
|
81
|
-
| Option | Required | Description |
|
|
82
|
-
| ---------------- | --------- | -------------------------------------------------------------------------- |
|
|
83
|
-
| `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
|
|
84
|
-
| `default_scopes` | no | Space-separated default OAuth scopes (default: `read:user user:email repo workflow`). |
|
|
85
|
-
|
|
86
|
-
## Credentials (identity)
|
|
87
|
-
|
|
88
|
-
| Field | Required |
|
|
89
|
-
| ---------------- | -------- |
|
|
90
|
-
| `client_id` | yes |
|
|
91
|
-
| `client_secret` | yes |
|
|
92
|
-
|
|
93
|
-
## Credentials (commit-sync)
|
|
94
|
-
|
|
95
|
-
Provide **either** the PAT field **or** the GitHub App fields (all
|
|
96
|
-
individually optional; validated per call):
|
|
97
|
-
|
|
98
|
-
| Field | Mode | Description |
|
|
99
|
-
| ----------------- | ---- | ------------------------------------------------------ |
|
|
100
|
-
| `access_token` | PAT | Static personal/service token. |
|
|
101
|
-
| `app_id` | App | GitHub App identifier. |
|
|
102
|
-
| `private_key` | App | App private key — raw PEM or base64-encoded PEM. |
|
|
103
|
-
| `installation_id` | App | Optional; discovered from the repo when unset. |
|
|
104
|
-
|
|
105
|
-
## License
|
|
106
|
-
|
|
107
|
-
BSD-3-Clause.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/_app_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/lifecycle.py
RENAMED
|
File without changes
|
|
File without changes
|
{imbi_plugin_github-2.17.0 → imbi_plugin_github-2.19.0}/src/imbi/plugins/github/pull_requests.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|