Skip to content

GitHub App

Forginate uses two distinct GitHub integrations. Don't conflate them:

  1. GitHub OAuth app (required) — user sign-in. Configured via the forginate-github-oauth secret (client-id, client-secret). Covered in Installation.
  2. 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 state

The 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:

bash
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-gateway

Env 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

bash
# 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:

bash
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-gateway

Deliveries 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.deleted webhooks 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.

Forginate — build software with an AI factory.