GitHub integration — the bidirectional loop
integrations:github
GitHub's unit of management is the repository, so multi-repo estates duplicate secrets, environment definitions and inventories per repo — and they drift. The GitHub integration makes the OpsChain model natively consumable from GitHub Actions instead: the repository holds no OpsChain configuration and no standing credentials. Each workflow run authenticates itself with its own short-lived GitHub Actions OIDC token, exchanges it for a short-lived OpsChain token under an admin-declared trust policy, and pulls what it needs — estate reads, resolved properties, secrets — at run time, scoped to a service identity you control.
The server-side capability is gated on the integrations:github
entitlement (Basic-available; GitHub Enterprise Server registration is
Enterprise-gated). The full reference is opschain docs integrations-github.
1. Register the GitHub App connection
Create (or reuse) a GitHub App on your instance — github.com or your GHES host — and download its PEM private key. Then register it:
opschain admin github-connections create corp-github \
--app-id 4242 \
--oidc-audience https://opschain.example.com \
--private-key-file ~/Downloads/opschain-app.pem
The key is vaulted and the record stores only the reference — the same
custody story every provider family shares. Set --oidc-audience to
your server's URL so a token requested for this instance can never
exchange anywhere else. opschain admin github-connections test corp-github validates the record without calling GitHub.
2. Create the service identity and declare a 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, nothing else), then declare the policy:
opschain admin service-accounts create gha-payments
opschain admin github-trust-policies create payments-main \
--connection corp-github \
--repository 'my-org/payments' --ref 'refs/heads/main' \
--service-account gha-payments
Patterns use * (any run of characters) and ? (one character);
policies match in name order, first match wins. 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:
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
starter workflow and composite actions under examples/github-actions/
in the distribution encode the whole shape, ::add-mask:: included.
The token is short-lived and never renewable: a long job exchanges
again.
4. Run OpsChain actions from GitHub — and attest the outcome
The shortest path is the published reusable workflow: one uses: line,
the job's OIDC token, no credential and no binary —
jobs:
deploy:
permissions:
id-token: write
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
The job exchanges, raises the change, waits for it and ends with the
change's result (opschain docs integrations-github, "The published
workflows"). Composed by hand, the run-action seed action invokes an
OpsChain action with the exchanged token (v2 CreateChange) and awaits
its conclusion; attest reports the workflow's outcome back onto the
change, so GitHub-initiated work lands in Chains and the audit trail
like native runs:
- 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 }}
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. With a connection
registered, record the App's installation id and point the property
at the connection instead:
opschain admin github-connections update corp-github --installation-id 12345678
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. The
secret-vault:// PAT remains the documented fallback for vaultless or
unconnected instances. 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).
A reference can narrow the token it mints —
github-app://corp-github?repos=payments&permissions=contents:write —
and the connection's --allowed-repositories / --allowed-permissions
bound what any reference may ask; a reference beyond the bound fails
its step by name (warn:github:token_scope_refused), and the scope
every token was minted under is on the step's setup lines.
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
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. The connection lives on the generic
inbound-webhook lane (opschain admin inbound-webhooks) for secret
rotation, rate and enablement.
Day-to-day
- Audit: every exchange is an
info:github:token_exchangeevent (policy, repository, ref, run id); every refusal awarn:github:exchange_refusedwith its reason. The Events screen andopschain events listshow both. - Limits: Basic allows 3 trust policies, 1,000 exchanges and 1,000 JIT mints per UTC day; over-cap calls refuse at their step with a named licence message, never a mid-pipeline mystery.
- Rotation: rotate the App key with
opschain admin github-connections update corp-github --private-key-file new.pem; kill live exchanged tokens by revoking the service account's sessions. - Lapse: on licence lapse everything is retained and only new exchanges and registrations refuse — the pipeline's refusal names the remedy.