This package provides GitHub OAuth integration for Gadgets. It serves two purposes:
- Sign-in: when
githubis in the deployment'sAUTH_GATEKEEPERSallowlist, "Continue with GitHub" appears on the login page. Sign-in requests only minimal scopes (read:user user:email) to read the account's primary verified email, which becomes the user's identity. The sign-in grant is transient (discarded right after the email is read). - Connections: when a user connects GitHub (or signs in and later connects it), the full scopes
(
repo read:user user:email) are requested so gadgets can access repositories, issues, and pull requests on the user's behalf.
Use a GitHub OAuth App, not a GitHub App. Only OAuth Apps honor the OAuth
scopeparameter, which is what makes the minimal-on-login / full-on-connect behavior work, and theuser:emailscope is what grants access to the user's verified email. A GitHub App (client id starting withIv…) ignoresscopeentirely and will returnResource not accessible by integrationfor the email lookup unless you separately grant it the Email addresses account permission — and even then login won't be minimal-scope. See Using a GitHub App instead if you must.
Every queued action carries a stable kind (github.<action>, e.g. github.push), and the user may
opt in to approving a kind automatically on a connection. Every action is eligible, including
closing and reopening, requestChanges reviews and force pushes, except two that always wait for a
person: merging a pull request and an approving review. An approval shares the
github.postReview kind with comment and request-changes reviews, but is never auto-approvable,
so enabling that kind does not cover it. A force push has its own kind (github.forcePush), so
enabling ordinary pushes does not let history be rewritten. Excluded actions are still approved
by hand as before. Each binding advertises through getAutoApprovableActions() only the eligible
kinds its session can submit (an issue binding, for example, never lists pushes).
If you're running this project locally and want to use GitHub integrations, you'll need to create your own GitHub OAuth app. This guide walks you through the process.
- Go to GitHub Settings > Developer settings > OAuth Apps (OAuth Apps, not "GitHub Apps").
- Click New OAuth App
- Fill in the application details:
- Application name: Enter anything (e.g., "Gadgets Local Dev")
- Homepage URL:
http://localhost:3000 - Authorization callback URL:
http://localhost:8787/gatekeeper/github/oauth(replace the host with yourPUBLIC_BASE_URLwhen not running locally)
- Click Register application
GitHub enables expiring user tokens by default for OAuth apps registered since August 2026 (Optional features in the app's settings). The gatekeeper supports both kinds: it refreshes an expiring token shortly before its eight hours run out, and asks the user to reconnect only once GitHub rejects the refresh token itself (unused for six months, or revoked).
On the app's settings page after registration:
- Click Generate a new client secret
- Copy the Client ID and the generated Client secret — you'll need both in the next step.
Create a .env file in this package's directory (packages/gatekeeper-github/.env):
CLIENT_ID=your-client-id-here
CLIENT_SECRET=your-client-secret-hereNote: The
.envfile is gitignored and should never be committed.
To offer "Continue with GitHub" on the login page, add github to the deployment's
AUTH_GATEKEEPERS allowlist (e.g. in the root .dev.vars):
AUTH_GATEKEEPERS=cloudflare,google,github
Users are keyed by their GitHub primary verified email, so the OAuth App must be able to read it
(the user:email scope, requested automatically). No extra setup is needed for an OAuth App.
- Start the application in dev mode (see instructions in the root README.md).
- Create or open a gadget.
- Navigate to the Connections tab.
- Click + New Connection.
- Choose a GitHub resource type: repository, issue, or pull request.
- If prompted, connect a GitHub account.
- You should be redirected to GitHub's authorization page in a new tab.
- After granting access, the tab closes, and you're back to Gadgets.
- Use the picker to choose the repository, issue, or pull request to connect.
- Create the connection. The Gadget now has access only to the selected GitHub resource scope.
You can also see your connected accounts and add and remove them in the settings (accessed through the account menu in the upper-right).
Gadgets can subscribe to a repository's issue, pull request, comment, review, push and tag events,
or to one issue's or pull request's (subscribe() in src/types.d.ts). GitHub delivers them through
a repository webhook, so this needs a public URL:
- Set
WEBHOOK_ORIGINon this worker to the public origin GitHub should deliver to, e.g.https://gadgets.example.com. Without it, hooks are refused and everything else works as before. - If the deployment sits behind Cloudflare Access, add a bypass for
/gatekeeper/github/webhook/*; the worker accepts only deliveries signed with a webhook's secret instead.
Enabling a hook adds a webhook to the repository, delivering to
${WEBHOOK_ORIGIN}/gatekeeper/github/webhook/<id>, so GitHub allows it only if the connected
account is an admin of the repository. An account's hooks on one repository share its webhook,
which is deleted when the last of them is disabled, or when the account is disconnected. Should
GitHub fail to delete it, the hourly check tries again, and a disconnect that leaves it fails,
keeping the account connected, with the token that can delete it, until disconnecting again finds
it gone. Each webhook signs its deliveries with a secret only GitHub and this deployment hold; a
delivery of a payload already received, or of an event more than a day old, is ignored; and an
event is delivered only while the account can still read the repository, since a webhook outlives
its creator's access.
The repository's other admins can see the webhook, though not its secret. Every hour the worker
checks each webhook on GitHub, and restores one that someone has deleted, deactivated, pointed
elsewhere, switched to form-encoded payloads or unverified TLS, or given other events, another
secret or none: to stop the deliveries, disable the hooks. The same check moves them to a new
WEBHOOK_ORIGIN within the hour. GitHub never retries a failed delivery itself, so the same check
asks it to redeliver what it failed to deliver in the past two hours: up to 20 an hour for each
account, shared among its repositories so that a busy one can't crowd out the rest. That includes
what was refused while the webhook was misconfigured, by this worker or by another receiver, as
GitHub redelivers to the webhook as it is configured now. Failures beyond that, or older than two
hours, are lost, as is anything another receiver accepted while the webhook pointed there. A
delivery that reaches the worker is retried until the gadget's hook accepts it, eight attempts in
all. When every recent delivery failed, as when Cloudflare Access turns GitHub away, the check logs
hooks.webhook.deliveries.failing with the statuses GitHub got.
For local development, GitHub cannot reach localhost: run a tunnel to the dev server (e.g.
cloudflared tunnel --url http://localhost:8787) and set WEBHOOK_ORIGIN to its origin in the
root .dev.vars.
What this relies on GitHub for is checked against GitHub itself by pnpm test:contract
(__tests__/contract), which the github-contract workflow runs weekly and on demand once the
CONTRACT_GITHUB_TOKEN secret and CONTRACT_GITHUB_REPO variable name a disposable private
repository (the token fine-grained, with access to that repository alone; see the workflow):
- a reader without access gets a 404, whatever ETag it sends;
- a redelivery uses the webhook's current URL, encoding and secret;
- the delivery log is newest first;
- the account is the sender of what it does;
- each event's payload parses.
If you must use a GitHub App (client id Iv…) rather than an OAuth App, be aware:
- GitHub Apps ignore the OAuth
scopeparameter. Permissions are fixed in the App's configuration and apply to every user authorization, so sign-in cannot be limited to minimal scopes — it grants whatever the App is configured for. - To read the user's email for sign-in, you must grant the App the Email addresses account
permission: App settings → Permissions & events → Account permissions → Email
addresses → Read-only → save. Existing users must then re-authorize (re-run the sign-in flow) to
approve the added permission. Without it, the email lookup fails with
Resource not accessible by integrationand sign-in is rejected.
For these reasons an OAuth App is recommended for sign-in.
You're using a GitHub App that lacks the Email addresses account permission, so the email lookup required for sign-in is forbidden. Either switch to an OAuth App (recommended) or grant the App the Email-addresses permission and re-authorize — see Using a GitHub App instead.
The callback URL in your OAuth app settings doesn't match what the app is sending. Double-check that you set it to exactly http://localhost:8787/gatekeeper/github/oauth (no trailing slash, http not https).
The authorization code has expired or already been used. Return to Gadgets and try connecting again.
Your CLIENT_ID or CLIENT_SECRET is missing. Make sure the .env file exists at packages/gatekeeper-github/.env and contains both values, then restart the dev server.