GitHub App
At a glance — GantryCD connects to GitHub through a GitHub App. The App’s installation token mints clone credentials and reads pull requests; its webhook secret verifies deliveries; and, if you grant the extra permissions, it publishes deployment status back to commits and PRs.
Configure
- Create a GitHub App (under your org or personal account) and install it
on the repositories — or the whole org — you want GantryCD to track. Note the
App ID and generate a private key (
.pem); you’ll register both below. - Grant the permissions GantryCD needs and subscribe to the webhook events it acts on.
- Register the integration in GantryCD under Org settings → SCM integrations. It stores three secrets — the App ID, the private key, and a webhook secret you choose — plus the repository-URL pattern that decides which repos this App serves. The private key and webhook secret are write-only: they never leave the backend and are never returned by the API.
- Wire up the webhook — copy the integration’s webhook URL and secret
into the GitHub App’s webhook settings. GantryCD verifies every delivery’s
signature and rejects a mismatch with HTTP
401. - Attach repositories to stacks — see Connecting repositories.
You can run one App per org or route different repositories through different Apps (matched by URL pattern and priority) — see Connecting repositories.
Permissions
GantryCD uses fine-grained repository permissions. Grant the always-required set for any integration; add the rest only for the feature that needs them.
Always required
The baseline that lets GantryCD clone repos and react to pushes and pull requests.
| Permission | Access | Used for |
|---|---|---|
| Metadata | Read | Mandatory baseline (GitHub auto-selects it). |
| Contents | Read | Per-run clone credentials, private module fetches, commit lookups, and compare diffs for path-scoped sync. Required to receive push. |
| Pull requests | Read | PR projection, changed-file lists, and review/approval counts. Required to receive pull_request and pull_request_review. |
Add for promotion gates
Only if a stack uses promotion requirements (required approvals / CI checks).
| Permission | Access | Used for |
|---|---|---|
| Checks | Read | Read CI check-run state when evaluating a gate. Required to receive check_suite, which re-evaluates a held plan when CI finishes. |
| Commit statuses | Read | Read legacy commit-status contexts when evaluating a gate. |
Add for deployment status
Only if you want GantryCD to publish each deployment’s state back to GitHub — see Deployment status.
| Permission | Access | Used for |
|---|---|---|
| Commit statuses | Read and write | Publish lane (gantrycd/<stack>) and PR-preview (gantrycd/<stack>/preview) check lines. |
| Deployments | Read and write | Create GitHub Deployment objects and their statuses. |
Commit statuses appears twice on purpose: promotion gates need only Read, while deployment-status publishing needs write. GitHub’s fine-grained permission has no write-only level, so selecting Read and write covers both — pick it if you use either feature that touches commit statuses.
If the same App also drives GitHub Actions runner groups, add Actions: Read and write — see GitHub Actions. If it also drives SSO, add Organization → Members: Read so org/team entitlements resolve.
After changing permissions on an installed App, accept the updated permission request on the installation — GitHub does not apply new scopes until you do.
Private modules
A stack whose OpenTofu pulls a module from a private GitHub repository works with
no extra configuration. The installation token that clones the stack is also
handed to tofu init, written into a git config file the run owns that attaches
the token to github.com requests as an Authorization header:
[http "https://github.com/"]
extraHeader = Authorization: Basic <base64 of x-access-token:the-token>
So an https://github.com/... module source — including the github.com/acme/modules//vpc
shorthand and any git::https://github.com/... source — is fetched with the
token. Nothing to add to the stack, and no PAT to store: the token is minted per
run and dies with it. Contents: Read is the permission it needs, which the
baseline already grants. (The token is carried in a header rather than in the
module URL specifically so that turning on GIT_TRACE=1 to debug a fetch cannot
print it into the run log — git redacts the header from its traces.)
Three limits are worth knowing:
- Same account only. An installation token reaches the repositories of one account — the one that owns the stack’s repository. A module in a different GitHub organization is not covered, even by the same App: install the App on that account as well and it still will not help, because a run carries one token. Host such modules in the stack’s own account, or vendor them.
- git over HTTPS only. An SSH module source (
git::ssh://git@github.com/...) is left alone and keeps resolving through whatever key the runner has, because redirecting it at the token would break the repositories the App is not installed on. Ahttps://.../archive.ziparchive source (fetched over plain HTTP, not git) and a private OpenTofu registry module (authenticated withTF_TOKEN_<host>, not git config) are likewise not covered — use agit::https://github.com/...source for a private module.
A run’s git configuration is exactly this file and nothing else — the runner
pins GIT_CONFIG_GLOBAL at it, so a ~/.gitconfig on the runner host is not
read. A stack that needs more git configuration than the rewrite (a second host,
a corporate mirror) can append to $GIT_CONFIG_GLOBAL from a pre_init
lifecycle hook; the variable is in the hook’s
environment and the file is writable. See
Runner runtime credentials.
Webhook
Each SCM integration has its own webhook endpoint, scoped to the org:
POST {GANTRYCD_BACKEND_PUBLIC_BASE_URL}/api/v1/orgs/{org_id}/scm-integrations/{integration_id}/webhook
You don’t build this URL by hand. GantryCD surfaces it (as webhook_url) on the
integration’s page and API response — but only when the backend knows its own
public address, i.e. when GANTRYCD_BACKEND_PUBLIC_BASE_URL
is set. If the field is blank, set that variable and reload.
To turn deliveries on:
- Copy the integration’s webhook URL into the GitHub App’s Webhook → Payload URL.
- Set Content type to
application/json. - Paste the integration’s webhook secret into Secret — the same value you
registered in GantryCD. Every delivery is authenticated by an HMAC-SHA256
signature over the raw body; a mismatch is rejected with
401and a missing or wrong secret means no events arrive. - Subscribe to the events below.
The endpoint is intentionally unauthenticated apart from the signature — a provider delivery has no user session behind it — and org-scoped, so an event can only ever fan out to that org’s stacks.
Webhook events
Subscribe to these — every other delivery is verified and ignored:
| Event | When you need it |
|---|---|
push | Always — syncs a stack and creates a plan on a push to its tracked branch. |
pull_request | Always — drives PR preview plans (opened / reopened / synchronized / edited / closed). |
pull_request_review | Only for promotion gates — re-evaluates a held plan when an approval is submitted or dismissed. |
check_suite | Only for promotion gates — retracts/re-evaluates a held plan when CI starts, reruns, or finishes (needs the Checks: Read permission). |
The last two events are re-evaluation triggers, not data: when a gate runs, GantryCD reads the PR’s live approval and check state from the API. Subscribing just lets a now-passing PR un-hold its preview without waiting for the next push. Skip them if no stack uses promotion gates.
How it behaves
- Deliveries are deduped. Each normalized event carries a distinct external id, so a redelivered webhook doesn’t trigger a duplicate deployment.
- Clone credentials are short-lived. They’re minted per run from the App installation, never stored on the stack.
- Pull requests are projected into GantryCD so preview plans attach to the right PR and follow its state.
Internals
The webhook signature contract, event normalization, the durable inbox, and PR refresh are in SCM And Pull Requests. The promotion-gate model is in Promotion Requirements.