Metadata-Version: 2.5
Name: imbi-plugin-github
Version: 2.44.0
Summary: GitHub identity plugin for Imbi (github.com / GHEC / GHES)
Author-email: "Gavin M. Roy" <gavinr@aweber.com>
License-Expression: BSD-3-Clause
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.14
Requires-Dist: httpx>=0.27
Requires-Dist: imbi-common[databases]==2.44.0
Requires-Dist: pydantic>=2
Requires-Dist: pyjwt[crypto]>=2.8
Description-Content-Type: text/markdown

# imbi-plugin-github

GitHub plugin for Imbi. A single plugin — slug **`github`** — backs every
GitHub Integration; the integration-level `flavor` option selects
github.com (`github`), GitHub Enterprise Cloud (`ghec`), or GitHub
Enterprise Server (`ghes`), and `host` names the tenant or appliance.

## Capabilities

| Kind              | Handler                   |
| ----------------- | ------------------------- |
| `identity`        | `GitHubIdentity`          |
| `deployment`      | `GitHubDeployment`        |
| `lifecycle`       | `GitHubLifecycle`         |
| `webhook-actions` | `GitHubWebhookActions`    |
| `commit-sync`     | `GitHubCommitSync`        |
| `pr-sync`         | `GitHubPullRequestSync`   |
| `analysis`        | `GitHubDoctor`            |

### Identity

Implements the OAuth App flow. The access token returned by the OAuth
grant is passed straight to GitHub APIs as a `Bearer` token, so
`materialize()` is a no-op.

### Deployment

Drives the GitHub Deployments API (`POST /repos/{owner}/{repo}/deployments`)
plus tag and release creation. Promote behaviour is inferred from the
ref shape by the host (semver → trigger Deployment, raw SHA → cut tag
+ Release).  Per-env workflow inputs ride on the `USES_PLUGIN` edge as
`env_payloads` and arrive on `PluginContext.environment_config`.

### Lifecycle

Reacts to project archive / unarchive by archiving the matching repo via
`PATCH /repos/{owner}/{repo}` with `{"archived": true|false}`. When the
`archive_target_org` option is set, archive **also** transfers the repo to
that org first via `POST /repos/{owner}/{repo}/transfer` — useful for
moving sunset projects into a dedicated "archive" org so they no longer
crowd primary-org searches.

GitHub refuses to transfer archived repos, so an already-archived source
is briefly unarchived, transferred, and re-archived at the destination.
On unarchive the plugin only flips `archived` back to `false` at the
repo's current location — it does **not** transfer back to the original
org.

Archiving requires admin scope on the repo; transferring additionally
requires admin permission on the target organization.

### Webhook actions (commit / tag / PR sync)

The `webhook-actions` capability exposes the actions the gateway
dispatches from webhook deliveries. A `WebhookRule.handler` is
`"<plugin_slug>#<action_name>"`, and the slug is the **plugin** slug —
`github` — not the capability kind:

| Action                | Handler                      | Records into ClickHouse |
| --------------------- | ---------------------------- | ----------------------- |
| `sync_commits`        | `github#sync_commits`        | `commits`               |
| `sync_tags`           | `github#sync_tags`           | `tags`                  |
| `sync_pull_requests`  | `github#sync_pull_requests`  | `pull_requests`         |

`sync_commits` and `sync_tags` are dispatched from `push` deliveries;
`sync_pull_requests` from `pull_request` deliveries.

`sync_commits` fetches the full set of commits in a push via the compare
API (paginated, so it isn't capped by the 20-commit inline payload limit);
`sync_tags` records the pushed tag and, with `reconcile_all`, the repo's
full tag list. Branch/tag gating is the rule's CEL `filter_expression`,
which evaluates against the recorded event — the webhook body is under
`payload` (e.g. `payload.ref == "refs/heads/main"`,
`payload.ref.startsWith("refs/tags/")`). The API
flavor (github.com / GHEC / GHES) is resolved at runtime — explicit
`api_base_url`, else a connected GitHub plugin on the same service, else
the service endpoint, else the payload's `repository.url`.

Unlike identity/deployment/lifecycle (which act as the OAuth user),
commit-sync runs without an actor and authenticates with a **service**
credential in one of two modes, resolved per call:

- **PAT** — a static `access_token`.
- **GitHub App** — `app_id` + `private_key`; the plugin signs an App JWT
  and mints a short-lived **installation token** (cached process-wide
  until shortly before it expires), so no static, expiring token is
  stored. `installation_id` is optional — when unset it is discovered
  from the pushed repository (`GET /repos/{owner}/{repo}/installation`).
  The App needs **Contents: Read-only**.

### Pull request history sync

`GitHubPullRequestSync.sync_all_history` is the on-demand `pr-sync`
backfill. It walks `GET /repos/{owner}/{repo}/pulls?state=all` (up to 100
pages of 100) and publishes a `pull_requests` row for every PR.

After a complete walk it reconciles: it deletes the project's `open` rows
on the synced host whose `pr_id` GitHub did not list, so a PR filed under
the project from another repository does not stay `open` forever. It
selects those rows first, logs their URLs, and deletes exactly those
`pr_id`s; a sync with nothing stale issues no delete.

It never deletes:

- closed or merged rows, so PR history survives a move to a new
  repository;
- rows on another host, from a second `pr-sync` Integration;
- rows recorded less than five minutes before the sync started, which
  covers webhook rows written during the sync and clock skew between
  hosts;
- the row of a PR GitHub lists but is too malformed to record.

The reconcile is skipped when the listing may be incomplete (a 404 on any
page, a next link it cannot follow, or the page cap), when the GitHub App
is not installed for the repository, and when the repository did not come
from the project's repository link (the Integration's link key or
`github-repository`). It runs even if the publish failed, and a
ClickHouse failure is logged and does not fail the sync.

## Integration options

Asked once per Integration and delivered to every capability on
`PluginContext.integration_options`:

| Option   | Required  | Description                                                             |
| -------- | --------- | ----------------------------------------------------------------------- |
| `flavor` | yes       | `github`, `ghec`, or `ghes`.                                            |
| `host`   | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |

## Capability options

Scoped to one capability and delivered on
`PluginContext.capability_options`:

| Capability  | Option               | Description                                                                             |
| ----------- | -------------------- | --------------------------------------------------------------------------------------- |
| `identity`  | `default_scopes`     | Space-separated OAuth scopes (default: `read:user user:email repo workflow`).            |
| `lifecycle` | `archive_target_org` | Org to transfer repos to before archiving; blank archives in place.                     |
| `lifecycle` | `create_org`         | Default org for repo creation when no `org_mapping` entry matches.                       |
| `lifecycle` | `org_mapping`        | Per-project-type-slug org overrides; the first match wins over `create_org`.             |

## Credentials

One credential store per Integration — every capability receives the
same decrypted blob. All fields are individually optional and validated
per call, so an identity-only or App-only Integration is valid:

| Field             | Used by            | Description                                       |
| ----------------- | ------------------ | ------------------------------------------------- |
| `access_token`    | service (PAT mode) | Static personal/service token.                    |
| `app_id`          | service (App mode) | GitHub App identifier.                            |
| `private_key`     | service (App mode) | App private key — raw PEM or base64-encoded PEM.  |
| `installation_id` | service (App mode) | Optional; discovered from the repo when unset.    |
| `client_id`       | `identity`         | OAuth App client id.                              |
| `client_secret`   | `identity`         | OAuth App client secret.                          |

For `commit-sync`, `pr-sync`, and `deployment` provide **either**
`access_token` **or** `app_id` + `private_key`. `lifecycle` acts as the
user and takes only `access_token` (or `token`) — it does not fall back
to GitHub App credentials.

## License

BSD-3-Clause.
