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; in the GUI, Administration → Operate → GitHub, Connections tab — list, create, edit, delete, test, the allowed mint scope and webhook provisioning): 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; in the GUI, the Trust policies tab beside the connections, scope prefix included): 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. A policy may additionally carry a scope prefix (--scope-prefix /projects/payments;scope_prefixon the record): every session it mints is clamped to that node-path subtree — inside it the service account's grants decide exactly as before; outside it every authorisation decision denies, the superuser short-circuit, the auditor and security-administrator floors and list ranges included, so one service account can serve several pipelines without any of them reaching another project. The clamp rides the minted token itself (no policy read on any later request), is named on the exchange answer (scope_prefix), and a denial it causes landswarn:authz:deniedwithreason: scope_clamp. - 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. Per-mint scoping. The reference may narrow the token it mints:github-app://<code>?repos=payments,ledger&permissions=contents:write,pull_requests:read—reposnames repositories within the installation's account (neverowner/name),permissionsnamesname:levelpairs (read,writeoradmin), either may be omitted, and the server passes exactly that set to GitHub's installation access-token mint. The connection's allowed scope (--allowed-repositories,--allowed-permissions;allowed_scopeon the record) bounds what any reference may ask: a reference beyond the bound fails its step by name withwarn:github:token_scope_refusedbeside it (the requested and the allowed sets, never a token) and nothing reaches GitHub; a reference asking for nothing on a bounded connection mints the allowed set; a connection with no allowed scope is unrestricted — the installation's own grant is the bound, as before. A malformed scope is not a reference at all: the value is left in place verbatim and nothing mints. The scope every token was minted under is recorded on the step's setup evidence (scope: repositories …; permissions …, orthe installation's full grant) and on theinfo:github:token_mintevent. - 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. Apushdelivery is also a fetch hint: every active Git remote on the pushed repository is fetched now and theinfo:git_remote:ref_movedevents that fetch records — the observed SHAs, not the payload's — are the recommended trigger source for git-driven workflows (opschain docs triggers, "The git ref-move source"); the delivery event stays as the record.
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.
Usage telemetry
Every connection's traffic is counted on the provider usage substrate
(opschain docs provider-usage): api_calls / api_errors (the
installation-token mint against the GitHub REST API, on the project of
the run), tokens_minted, rate_limit_remaining (a gauge — GitHub's
X-RateLimit-Remaining as it last answered the mint), and on the
connection's provisioned inbound webhook webhooks_received and
signature_failures. opschain admin providers usage --kind github --code <code> reads them; the connection's record opens its operational
dashboard.
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 published workflows (LimePoint/opschain-action)
The public LimePoint/opschain-action repository carries the whole
GitHub packaging: the composite action for the local lane (a job
runs the checked-out repository's action on the binary with no server —
opschain docs ci) and, for this server-backed lane, two reusable
workflows a caller invokes with uses: and with::
| Workflow | What it does |
|---|---|
LimePoint/opschain-action/.github/workflows/opschain-change.yml@v1 | Exchanges the job's Actions OIDC token under a trust policy, raises a change (action) on the server against project / environment / optional asset, waits for the terminal state and sets run-id, status and url as outputs; the job's result follows the change's. |
LimePoint/opschain-action/.github/workflows/opschain-workflow.yml@v1 | The same for a workflow run (workflow). |
Inputs on both: server-url, connection, audience (the connection's
oidc_audience), project, environment, asset, action or
workflow, inputs (one key=value per line), ref (the git revision
to pin), timeout-minutes. The caller's job needs permissions: id-token: write; nothing else — no stored credential, no binary.
jobs:
deploy:
permissions:
id-token: write
contents: read
uses: LimePoint/opschain-action/.github/workflows/opschain-change.yml@v1
with:
server-url: https://opschain.example.com
connection: corp-github
audience: https://opschain.example.com
project: payments
environment: prod
asset: app
action: deploy
ref: ${{ github.sha }}
The repository's seed/ folder holds the starter caller workflows and
the composite seed actions (setup-opschain, estate, properties,
secrets, run-action, attest) that examples/github-actions/
mirrors. v1 moves with the release train: each release re-points
the v1 tag at the workflows that match it, and a release-pinned tag
(v5.7.0) is cut beside it. GitHub Enterprise Server estates mirror
the repository into the instance (a fork or an import of
LimePoint/opschain-action, re-synced on each release and pinned by tag
or commit) and point uses: at the mirror — GitHub Actions resolves
uses: on the instance, not on github.com. The workflows reach only
the OpsChain server the caller names; a GHES-hosted mirror needs
nothing else.
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.
2026-11-11 — a complete two-direction demonstration with a presenter runbook: github.com/LimePoint/opschain-github-demo (the asset template, the caller workflows, the server seed and the governance wrap).
See also
opschain docs cli/admin-github-connections— the connection verbs.opschain docs cli/admin-github-trust-policies— the policy verbs.opschain docs ci— the local lane the same repository packages.opschain docs entitlements— the feature and limit vocabulary.opschain docs identity— service accounts and the session model.