GitHub App
Forginate uses two distinct GitHub integrations. Don't conflate them:
- GitHub OAuth app (required) — user sign-in. Configured via the
forginate-github-oauthsecret (client-id,client-secret). Covered in Installation. - GitHub App (optional but recommended) — tenant repo access. Lets organizations install your App on their GitHub org and grant Forginate scoped, short-lived access to selected repositories for factory runs. This page covers the App.
How it works
Tenant UI ──► /orgs/:slug/github/install ──► GitHub App install flow
└─► /auth/github-app/callback
└─► persisted in github_installations
Run start ──► gateway mints an installation access token scoped to
{repository_ids: [repo], permissions: {contents:read, metadata:read}}
valid 1 hour ──► init container clones via GIT_ASKPASS ──► token gone
GitHub ──► POST /webhooks/github ──► HMAC-verified (webhook_secret)
├─ deduped on X-GitHub-Delivery
└─ persisted to github_events; installation events update stateThe clone token never touches the main runner container — the init container clones to /workspace/repo and exits.
Setup with the built-in wizard
The api-gateway ships a manifest-flow wizard at https://forginate.example.com/admin/github-app/setup (founder-only). It walks GitHub's App-manifest creation and hands you the resulting credentials. The wizard gates itself on whether GITHUB_APP_ID is already set — once configured, it's done.
The pod reads the App config from the forginate-github-app secret, which you create out of band after running the wizard. All six keys are required for the feature to light up; all are mounted optional: true so the gateway boots without them:
kubectl -n forginate create secret generic forginate-github-app \
--from-literal=app_id="1234567" \
--from-literal=slug="your-app-slug" \
--from-literal=client_id="Iv1.…" \
--from-literal=client_secret="…" \
--from-literal=webhook_secret="$(openssl rand -hex 32)" \
--from-file=private_key.pem=path/to/downloaded-key.pem
kubectl -n forginate rollout restart deploy/api-gatewayEnv mapping: GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_CLIENT_ID, GITHUB_APP_WEBHOOK_SECRET, GITHUB_APP_PRIVATE_KEY_PEM.
When creating the App manually instead of via the wizard, set:
- Webhook URL:
https://forginate.example.com/webhooks/github - Permissions: repository
contents: read,metadata: read - Events: installation lifecycle events at minimum
Webhook reachability
/webhooks/github must be reachable from GitHub's webhook IP ranges (published under hooks at https://api.github.com/meta). If you run an authenticating edge in front of the domain, add a bypass for this path — deliveries are HMAC-verified in-app, so an open path is safe. 401s on deliveries mean the webhook secret in the k8s secret has diverged from the one on GitHub.
Verifying
# All six keys present?
kubectl -n forginate get secret forginate-github-app -o jsonpath='{.data}' | jq 'keys'
# Live App JWT signing test — expect: 200 <your-app-slug>
POD=$(kubectl -n forginate get pods -l app.kubernetes.io/component=api-gateway -o name | head -1)
kubectl -n forginate exec "$POD" -- node --input-type=module -e "
import { GithubApp } from '/app/saas/api-gateway/dist/lib/github-app.js';
const app = new GithubApp({
appId: process.env.GITHUB_APP_ID,
privateKeyPem: process.env.GITHUB_APP_PRIVATE_KEY_PEM,
webhookSecret: process.env.GITHUB_APP_WEBHOOK_SECRET,
slug: process.env.GITHUB_APP_SLUG,
});
const r = await fetch('https://api.github.com/app', {
headers: { Authorization: 'Bearer ' + await app.appJwt() }
});
console.log(r.status, (await r.json()).slug);
"If gateway logs show github_app_not_configured: a key is missing from the secret, or the pod hasn't restarted since the secret was created.
Rotation
Webhook secret — update on GitHub first, then:
NEW=$(openssl rand -hex 32) # paste the same value into the App's webhook settings
kubectl -n forginate get secret forginate-github-app -o json \
| jq --arg s "$NEW" '.data.webhook_secret = ($s | @base64)' \
| kubectl apply -f -
kubectl -n forginate rollout restart deploy/api-gatewayDeliveries 401 between the GitHub-side change and rollout completion; GitHub retries with exponential backoff, so a prompt rollout loses nothing. Replaying deliveries is safe — the handler dedups on the X-GitHub-Delivery UUID.
Private key — generate a new key on GitHub (do not delete the old one yet; multiple active keys are allowed), update private_key.pem in the secret, restart, re-run the JWT signing test, and only then delete the old key on GitHub.
Operational notes
- Unlinking an installation in Forginate does not uninstall the App on GitHub; users remove it via GitHub's UI.
installation.deletedwebhooks mark the row suspended (kept for audit).- Failed clone in the init container usually means the repo was removed from the installation's repository selection, or deleted — the init container logs show the git error with the token redacted.
Skipping the GitHub App
Without it, sign-in and greenfield runs still work; tenants just can't attach existing GitHub repositories to runs. You can add it any time later.