Skip to main content
Version: v5.7.0

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 on github.com or on a GitHub Enterprise Server host. The App's PEM private key is held reference-first (private_key_ref, a secret-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_prefix on 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 lands warn:authz:denied with reason: scope_clamp.
  • The token exchange (ExchangeGitHubActionsToken on the v2 GitHubIntegrationService, 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-actions audience) 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> — typically opschain.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. The packs:common GitHub actions are unchanged — only the token's provenance changes — and the hand-provisioned secret-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 as OPSCHAIN_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 — repos names repositories within the installation's account (never owner/name), permissions names name:level pairs (read, write or admin), 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_scope on the record) bounds what any reference may ask: a reference beyond the bound fails its step by name with warn:github:token_scope_refused beside 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 …, or the installation's full grant) and on the info:github:token_mint event.
  • Run attestation (AttestGitHubRun): a GitHub workflow reports its outcome back against an OpsChain change — one info:github:attestation event (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; the opschain/attest seed 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 by X-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 under event.data.github (repository, ref, sha, action, conclusion…) for trigger conditions; the raw payload stays under event.data.payload. An omitted signing secret is generated and shown exactly once. A push delivery is also a fetch hint: every active Git remote on the pushed repository is fetched now and the info:git_remote:ref_moved events 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 keyBasicMeaning
github_trust_policies3Trust policies that may exist.
github_token_exchanges_per_day1,000Exchanges per UTC day; over-cap exchanges refuse at setup with a clear licence message, never a mid-pipeline mystery.
github_token_mints_per_day1,000Just-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_instances0GitHub 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​

  1. Register the App connection and declare a trust policy pointing at a service account with the grants the pipeline needs.
  2. Give the job permissions: id-token: write and request the OIDC token with the connection's audience.
  3. Exchange it: POST /api/v2/opschain.api.v2.GitHubIntegrationService/ExchangeGitHubActionsToken with {"oidc_token": "...", "connection": "<code>"} — the answer carries access_token (a Bearer token) and its expiry.
  4. 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::

WorkflowWhat it does
LimePoint/opschain-action/.github/workflows/opschain-change.yml@v1Exchanges 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@v1The 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.