GitHub App: creating it from the manifest, and storing its credentials
On this page
The manifest is in docs/platform/github-app/manifest.json. /workspace/out/github-app-create.html posts it to https://github.com/organizations/agents-sdk-space/settings/apps/new?state=<random>. The state value is kept next to the HTML file in github-app-state.txt. The app has not been created. An org owner has to open the HTML page and confirm on GitHub.
Permissions (least privilege)#
| Permission | Level | Why |
|---|---|---|
| contents | read | read the file tree and blobs for builds (no write access, unlike today's OAuth repo scope) |
| metadata | read | required by GitHub; repo id, name and branches |
| pull_requests | read | PR preview builds |
| deployments | write | report preview/production deployment status on the repo |
| checks | write | build check runs with a link to the DevStack logs |
Events: push, pull_request, repository. installation and installation_repositories are always sent to GitHub Apps, so they can't be listed in default_events and don't need to be.
Redirect code exchange (to build next; these routes don't exist yet)#
- GitHub redirects to
https://devstack.bid/api/github/app/manifest-callback?code=…&state=…. - The handler checks
stateagainst a one-time value stored server-side (D1 row or signed cookie, valid for 10 minutes) and only accepts the request when the signed-in user is the DevStack owner (admin allowlist). Otherwise it returns 403. - It calls
POST https://api.github.com/app-manifests/{code}/conversionswithin 1 hour; the code can be used only once. The response holdsid,slug,client_id,client_secret,webhook_secretandpem. - None of these values are logged, returned to the browser or written to D1 in plaintext. A Worker can't safely set its own secrets at runtime, so the handler does one of the following:
- Preferred: stores the bundle once, sealed with
CREDENTIALS_KEY(AES-GCM v2, bindinggithub-app:owner), in a single-row table. It then shows the owner only the app id and slug, plus the exact commands for the owner to run:wrangler secret put GITHUB_APP_ID,GITHUB_APP_PRIVATE_KEY,GITHUB_APP_WEBHOOK_SECRET,GITHUB_APP_CLIENT_ID,GITHUB_APP_CLIENT_SECRET. The values are pasted at the prompt, never on the command line. Alternatively they go into the Secrets Store (wrangler secrets-store secret create …) withsecrets_store_secretsbindings in wrangler.jsonc. - After the owner confirms the secrets are set, the sealed row is deleted.
- Preferred: stores the bundle once, sealed with
- Webhooks (
POST /api/github/webhook): verifyX-Hub-Signature-256(HMAC-SHA256 over the raw body, constant-time compare) before parsing the JSON. Deduplicate onX-GitHub-Deliverywithwebhook_deliveries(migration 0014). Return 202 quickly and enqueue the build. Invalid signatures get 401 and are recorded withsignature_ok=0. - Installation tokens: sign a JWT with RS256 using
GITHUB_APP_PRIVATE_KEY(≤10-minute expiry), then callPOST /app/installations/{id}/access_tokensscoped to the single repository. These tokens live 1 hour, stay in memory only and are added to the log-redaction secrets list.
Rotation#
Generate a new private key in the GitHub App settings, run wrangler secret put GITHUB_APP_PRIVATE_KEY, then delete the old key on GitHub. The webhook secret is rotated the same way; accept both secrets for 10 minutes during the switch.
Source: docs/platform/GITHUB-APP.md · /docs/platform/github-app.md