GitHub integration (integrations:github)
integrations:github
The GitHub-native integration makes the OpsChain model — the estate, resolved properties, vault-backed secrets, path-based authorisation — natively consumable from GitHub Actions, so multi-repo estates stop duplicating configuration and secrets per repository. The repository holds no OpsChain configuration and no standing credentials: each workflow run authenticates itself with its own short-lived GitHub Actions OIDC token and pulls what it needs at run time.
The pieces
- GitHub App connection (
opschain admin github-connections): the per-instance registration — one GitHub App ongithub.comor on a GitHub Enterprise Server host. The App's PEM private key is held reference-first (private_key_ref, asecret-vault://address; the record never stores the key), and the connection carries the OIDC audience the token exchange requires on presented workflow tokens. Sign-in with GitHub is a separate, identity-side capability — this feature begins at the App registration. - Trust policies (
opschain admin github-trust-policies): admin-defined claim predicates (repository, ref, GitHub environment —*/?patterns) mapped onto a service identity and a token TTL. Read-only by default in the sense that the exchanged token holds exactly the service account's own authorisation grants — grant that account read on the paths the pipeline needs and nothing else. - The token exchange (
ExchangeGitHubActionsTokenon the v2GitHubIntegrationService, served open): a workflow presents its Actions OIDC token (requested with the connection's audience); the server verifies it against the instance's OIDC issuer (derived from the connection host — GitHub Enterprise Server instances use their own issuer), matches an enabled trust policy (name order, first match wins), and answers a short-lived OpsChain access token (github-actionsaudience) acting as the policy's service account. The minted token authenticates on the API exactly like a session token, is never renewable (the pipeline exchanges again), and dies with session revocation. - Just-in-time
GH_TOKEN(outbound): a property value that is, in its entirety,github-app://<connection code>— typicallyopschain.env.GH_TOKEN— is swapped at step dispatch for a freshly minted GitHub App installation token (about an hour's lifetime, GitHub's own cap), minted server-side from the connection's vaulted App key against the instance's REST API. Thepacks:commonGitHub actions are unchanged — only the token's provenance changes — and the hand-provisionedsecret-vault://PAT remains the documented fallback for vaultless or unconnected instances. The connection must record the App installation id (--installation-id); minted values join the run's mask set and are never persisted, and the OpsChain actor rides the step environment asOPSCHAIN_INITIATED_BY— dispatch metadata, never authority. - Run attestation (
AttestGitHubRun): a GitHub workflow reports its outcome back against an OpsChain change — oneinfo:github:attestationevent (status, summary, repository, Actions run id, evidence links) on the change's audit trail, so GitHub-initiated work lands in Chains like native runs. The caller must be able to read the change; theopschain/attestseed action wraps the call. - Webhook ingestion (
opschain admin github-connections provision-webhook): provisions one signed inbound-webhook connection on the generic inbound lane — GitHub deliveries POST to/api/hooks/github-<code>, authenticated byX-Hub-Signature-256, deduplicated on the delivery id, and executed (when a trigger matches) as the named service account. Common events (push,pull_request,workflow_run,release) additionally expose a normalised typed field set underevent.data.github(repository, ref, sha, action, conclusion…) for trigger conditions; the raw payload stays underevent.data.payload. An omitted signing secret is generated and shown exactly once.
Who the pipeline is
The exchanged token acts as the trust policy's service account, never as the human who triggered the run — if visibility followed whoever clicked, the same workflow would deploy to different target sets depending on who ran it. The GitHub actor rides audit metadata only. Estate visibility, properties and secret reads all follow the service identity's authorisation overlay.
Audit trail
Every exchange lands an info:github:token_exchange event (connection,
policy, repository, ref, environment, workflow run id, session id) and
one exchange-register row; every refusal lands
warn:github:exchange_refused with its reason. Every JIT mint lands
info:github:token_mint plus one mint-register row, and every refused
mint is a named step failure with warn:github:mint_refused beside it.
Every attestation lands info:github:attestation on the change, and
webhook provisioning lands api:github_connections:provision_webhook.
Connection and policy writes are audited as api:github_connections:*
and api:github_trust_policies:*. No event, response or error ever
carries token, key or signing-secret material.
Entitlement and limits
The server-side capability is gated on integrations:github,
available on the Basic tier with limits as the lever:
| Limit key | Basic | Meaning |
|---|---|---|
github_trust_policies | 3 | Trust policies that may exist. |
github_token_exchanges_per_day | 1,000 | Exchanges per UTC day; over-cap exchanges refuse at setup with a clear licence message, never a mid-pipeline mystery. |
github_token_mints_per_day | 1,000 | Just-in-time GH_TOKEN installation-token mints per UTC day; an over-cap mint fails the step by name and lands warn:github:mint_refused. |
github_self_hosted_instances | 0 | GitHub Enterprise Server registrations — the tier boundary itself. github.com (GitHub Enterprise Cloud and EMU orgs included) never counts. |
Expiry degrades politely, never destructively: new exchanges and registrations refuse with a named licence message (each an audit event), stored configuration is retained untouched, and everything lights back up on upgrade.
Wiring a workflow
- Register the App connection and declare a trust policy pointing at a service account with the grants the pipeline needs.
- Give the job
permissions: id-token: writeand request the OIDC token with the connection's audience. - Exchange it:
POST /api/v2/opschain.api.v2.GitHubIntegrationService/ExchangeGitHubActionsTokenwith{"oidc_token": "...", "connection": "<code>"}— the answer carriesaccess_token(a Bearer token) and its expiry. - Call the API with that bearer: estate reads, resolved properties, secret resolution — whatever the service identity may see.
The starter workflow under examples/github-actions/ in the
distribution encodes this shape end to end, and the run-action /
attest seed actions close the loop the other way: a workflow invokes
an OpsChain action (v2 CreateChange), awaits its conclusion, and
attests the outcome back onto the change.
The loop the other way: OpsChain → GitHub
A step whose scope declares opschain.env.GH_TOKEN: github-app://<connection code> dispatches GitHub work (the
packs:common github:workflow_run / github:repo_dispatch actions)
under a just-in-time installation token: no stored PAT, nothing to
rotate, HA-safe (any cluster member mints). github:repo_dispatch
forwards the OpsChain actor automatically in client_payload .initiated_by; github:workflow_run forwards it only when the call
site sets forward_initiated_by: "true" (a workflow_dispatch target
refuses undeclared inputs, so the target must declare initiated_by).
Conclusions flow back on the webhook lane: provision ingestion once and
point a trigger at external:webhook:received with
event.data.github.* conditions.
See also
opschain docs cli/admin-github-connections— the connection verbs.opschain docs cli/admin-github-trust-policies— the policy verbs.opschain docs entitlements— the feature and limit vocabulary.opschain docs identity— service accounts and the session model.