Skip to main content
Version: v5.7.0

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_exchange event (policy, repository, ref, run id); every refusal a warn:github:exchange_refused with its reason. The Events screen and opschain events list show 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.