13.1 GitHub
integrations:github
After this you can: connect a GitHub App, exchange an Actions OIDC token for an OpsChain identity, run actions from a pipeline with no repo-held secret, and take GitHub events as triggers.
Before you start — tick each one:
- Lesson 5.2 — Triggers (
opschain learn 5.2) - Lesson 7.3 — Tokens, automation principals and the review export (
opschain learn 7.3)
In the GUI: a GitHub-initiated change opens on its run page and in Chains like a native run, and every exchange, mint and attestation is a row on the Operations area's Activity panel — the connection and its policies are administered from the CLI.
GitHub's unit of management is the repository, so multi-repo estates
end up duplicating secrets, environment definitions and inventories per
repo — and they drift. With the GitHub integration the repository holds
no OpsChain configuration and no standing credential: each workflow
run authenticates with its own short-lived Actions OIDC token, exchanges
it for a short-lived OpsChain token under a trust policy you
declared, acts as a service account you control, and reports its
outcome back onto the change it ran. This lesson walks that loop once,
end to end, against an example my-org/payments repository. The guide
opschain docs getting-started/integrations-github is the day-to-day
reference; the full component reference is
opschain docs integrations-github.
Licence entitlement. The server side is gated on
integrations:github(available on the Basic tier, with limits: 3 trust policies, 1,000 exchanges and 1,000 just-in-time mints per UTC day). Registering a GitHub Enterprise Server host is Enterprise-gated. On lapse everything is retained; only new exchanges and registrations refuse, by name.
You need a GitHub App on your instance (github.com or your GHES host) with its PEM private key downloaded, admin rights on the OpsChain server, and a repository whose Actions workflows you can edit.
1. Register the App connection
opschain admin github-connections create corp-github \
--app-id 4242 \
--oidc-audience https://opschain.example.com \
--private-key-file ~/Downloads/opschain-app.pem
Created GitHub connection corp-github for github.com (private_key_ref secret-vault://github-connections/corp-github/credential)
The key is vaulted and the record stores only the reference — the same
custody story every provider family shares (lesson 3.5). Set
--oidc-audience to your server's URL so a token requested for this
instance can never exchange anywhere else. Validate the record without
calling GitHub:
opschain admin github-connections test corp-github
GitHub connection corp-github: record validation succeeded
2. The service identity and the trust policy
The exchanged token acts as a service account, never as whoever triggered the workflow — the same workflow always sees the same target set. Create one and grant it exactly what the pipeline needs (read on the paths it deploys from, create on the targets it runs; grants as in lesson 7.2), then declare the policy that maps the repository's claims onto it:
opschain admin service-accounts create gha-payments
Created service account gha-payments (id 01a2fa00-…)
Grant it authority via policy assignments or group membership, then point automations at it with run_as.
opschain admin github-trust-policies create payments-main \
--connection corp-github \
--repository 'my-org/payments' --ref 'refs/heads/main' \
--service-account gha-payments
Created trust policy payments-main (repository my-org/payments -> service account gha-payments)
opschain admin github-trust-policies list
NAME CONNECTION REPOSITORY REF ENABLED ID
payments-main corp-github my-org/payments refs/heads/main yes 01a2fa01-…
Patterns use * (any run of characters) and ? (one character);
policies match in name order, first match wins; --environment adds a
GitHub environment predicate. The minted token's TTL defaults to 900
seconds (--token-ttl tunes it, 60–3600).
3. Exchange from the workflow
Give the job id-token: write and exchange at the top. The starter
workflow and composite actions under examples/github-actions/ in the
distribution encode the whole shape, ::add-mask:: included:
permissions:
id-token: write
steps:
- name: Authenticate to OpsChain
run: |
OIDC_TOKEN=$(curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://opschain.example.com" | jq -r .value)
RESPONSE=$(curl -sS -X POST \
"$OPSCHAIN_URL/api/v2/opschain.api.v2.GitHubIntegrationService/ExchangeGitHubActionsToken" \
-H 'Content-Type: application/json' \
-d "{\"oidc_token\":\"$OIDC_TOKEN\",\"connection\":\"corp-github\"}")
TOKEN=$(echo "$RESPONSE" | jq -r .accessToken)
echo "::add-mask::$TOKEN"
echo "OPSCHAIN_TOKEN=$TOKEN" >> "$GITHUB_ENV"
Every later step calls the API (or the CLI) with that bearer. The token is short-lived and never renewable: a long job exchanges again. Push the workflow, let it run, and watch the exchange land on the server:
opschain events --type-search github --tail 3
WHEN ID TYPE NODE USER CORRELATION DETAIL
2026-10-27T12:01:14.102331Z 01a2fa10-… info:github:token_exchange - gha-payments 01a2fa10-… policy payments-main · my-org/payments refs/heads/main · run 18234567890
A workflow from another repository or branch is refused by name — a
warn:github:exchange_refused row carrying the reason — and the
pipeline's own step fails on the exchange, never mid-deploy.
4. Run an action from GitHub, and attest the outcome
The run-action seed action invokes an OpsChain action with the
exchanged token (the v2 CreateChange) and awaits its conclusion;
attest reports the workflow's outcome back onto the change:
- name: Run the OpsChain deploy action
id: opschain
uses: ./.github/actions/run-action
with:
node-path: /projects/payments/environments/prod/assets/app
action: deploy
- name: Attest the outcome to OpsChain
if: always()
uses: ./.github/actions/attest
with:
change-id: ${{ steps.opschain.outputs.change-id }}
On the server the change is an ordinary change — created by
gha-payments, with the GitHub actor in its audit metadata — and the
attestation is one more event on its trail:
opschain runs list --filter action=deploy
ID KIND ACTION TARGET STATUS CREATED DURATION
01a2fa20-5b6c-7d8e-9f0a-1b2c3d4e5f60 change deploy payments/environments/prod/assets/app success 2026-10-27T12:01:20Z 48.2s
opschain events --type-search github:attestation --tail 1
WHEN ID TYPE NODE USER CORRELATION DETAIL
2026-10-27T12:02:11.440812Z 01a2fa21-… info:github:attestation /projects/payments/environments/prod/assets/app gha-payments 01a2fa20-5b6c-7d8e-9f0a-1b2c3d4e5f60 success · my-org/payments run 18234567890
opschain chains show <correlation-id> renders the GitHub-initiated
work beside everything the change cascaded (lesson 5.1). A change the
funnel holds (lesson 10.1) holds for a GitHub-initiated run exactly as
for a person's.
5. Retire the stored PAT: just-in-time GH_TOKEN
Dispatching GitHub work from OpsChain (the packs:common
github:workflow_run / github:repo_dispatch actions) used a
hand-provisioned PAT in the opschain.env properties. Record the App's
installation id and point the property at the connection instead:
opschain admin github-connections update corp-github --installation-id 12345678
Updated GitHub connection corp-github
opschain properties edit payments # opschain.env.GH_TOKEN: github-app://corp-github
At step dispatch the server mints a short-lived App installation token
(about an hour, GitHub's cap), injects it as GH_TOKEN, masks it in
every log and never persists it — the pack actions are unchanged. Every
mint is an info:github:token_mint event; a mint that cannot happen
fails the step by name (warn:github:mint_refused carries the reason).
The secret-vault:// PAT remains the documented fallback for vaultless
or unconnected instances.
6. Webhooks: GitHub events as trigger sources
Provision ingestion once and point the GitHub App's webhook at the printed path:
opschain admin github-connections provision-webhook corp-github --service-account gha-payments
Provisioned GitHub webhook ingestion for corp-github as inbound connection github-corp-github
Point the GitHub App's webhook at <server URL>/api/hooks/github-corp-github
Signing secret (shown ONCE - paste it into the App's webhook configuration): 4f1e…
The signing secret is generated and shown exactly once (supply your own
with --secret-env/--secret-prompt). Verified deliveries land as
external:webhook:received events with the raw payload under
event.data.payload and, for push / pull_request / workflow_run /
release, clean typed fields under event.data.github — so a trigger
condition reads event.data.github.conclusion == "success" instead of
spelunking GitHub's document. Bind a trigger exactly as lesson 5.3
taught for the generic lane:
# promote_on_green.yaml
code: promote_on_green
subject: external
events: [external:webhook:received]
conditions:
- event.data.connection eq github-corp-github
- event.data.github.event eq workflow_run
- event.data.github.conclusion eq success
action_type: workflow_run
action_params:
workflow: promote
inputs:
git_ref: ${{ event.data.github.ref }}
opschain triggers create payments -f promote_on_green.yaml
Every firing executes as the connection's service account — never with
ambient authority. The connection lives on the generic inbound-webhook
lane (opschain admin inbound-webhooks list shows it beside the rest)
for secret rotation, rate and enablement.
7. Day to day
- Audit: every exchange is
info:github:token_exchange(policy, repository, ref, run id); every refusalwarn:github:exchange_refusedwith its reason; the Activity panel andopschain eventsshow both. - Rotation:
opschain admin github-connections update corp-github --private-key-file new.pemrotates the App key; revoking the service account's sessions kills live exchanged tokens. - Limits: over-cap calls refuse at their step with a named licence message, never a mid-pipeline mystery.
Check yourself
- What does the trust policy bind the exchanged token to — a person or a service account, and which one in this walk? Then: which event does a verified GitHub delivery raise, and under which key does a trigger read its typed fields?
What you learned
- A GitHub App connection vaults the key; a trust policy maps a repository and ref onto a service account; the workflow exchanges its own OIDC token for a short-lived OpsChain token and holds no secret.
- GitHub-initiated changes are ordinary changes, attested back with
info:github:attestation, visible in Chains like native runs. github-app://<connection>replaces the stored PAT with a just-in-time installation token.- GitHub deliveries land as
external:webhook:receivedwith typedevent.data.githubfields; firings run as the connection's service account.
Next: lesson 13.2 — Jira Service Management (opschain learn 13.2), or back to your track (opschain learn).