Repository navigation
feat(github): read GitHub as the App everywhere, tokens as fallback - #525
Merged
Merged
Conversation
GITHUB_TOKEN is a personal token, so the registry sync, page views and mention checks spent the same bucket as its owner's tools and CI. Every Worker GitHub read now uses the read App installation token, shared with builds through one isolate cache. GITHUB_TOKEN is read only for a read GitHub denied the App, and each use is reported. Without it, a denied read fails for that Repository only: the sync no longer counts a denial as a spent quota, and a build answers SOURCE_ACCESS_DENIED unless the signed-in requester's token can read it.
A 401 on the cached installation token sent every read to GITHUB_TOKEN until the token's stated expiry, and in the registry it paused the whole sync. The read now drops the token and mints a new one once; only a denial, or a new token GitHub rejects too, reaches the fallback. A read GITHUB_TOKEN repeats for a Repository that denies the App now answers as that Repository's denial when the token is spent or expired, and reports the App's quota. Its owner's bucket paused every Repository before. The REST, GraphQL and build paths share one rate limit message rule, so an abuse detection limit, or a FORBIDDEN error that names a limit, never reaches GITHUB_TOKEN. A summary batch never falls back as a whole.
The runbook and two comments still named GITHUB_TOKEN as the sync's bucket. The App's installation bucket paces the sync now, and a rejected token is renewed before the fallback.
1 of 6 tasks
This was referenced Oct 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces #505, rebuilt on main after #494, #495 and #498 merged.
🔗 Linked issue
Related to #491
❓ Type of change
📚 Description
GITHUB_TOKENis a personal token of theharlan-zwaccount. GitHub counts a personal token's quota per account, so the registry sync,/ghpage views and mention checks spent the same bucket as Harlan's own tools, harlan-github-agent and the CI runners. When it ran out, the site and CI stalled together. #491 moved builds to the read App. This moves every other Worker caller./api/repos,/api/orgs, featured and skill-related all go through it. The minting and the token cache move toshared/server/github-app-credential.ts, so the registry and builds share one token per isolate.GITHUB_TOKENbecomes a fallback. It repeats only a read GitHub denied the App: a 403 with quota left and no rate limit message, or aFORBIDDENGraphQL error. Each use emits agithub-credentialevent with outcomeapp-denied. A batch summary leaves a denied alias to the per-repository sync, and never repeats a whole batch with the fallback.GITHUB_TOKEN.app-token-rejectedevent. Otherwise a revoked token would send every build read to the fallback, and pause the whole registry sync, until the token's stated expiry.GITHUB_TOKENrepeats after a denial reports the App's quota, so one denied Repository never pauses the sync on its owner's bucket. IfGITHUB_TOKENis spent or expired, that read fails as the one Repository's denial.GITHUB_TOKEN. A denied registry read then answersdenied, which the sync records as a failure for that Repository instead of pausing every Repository for a spent quota. A denied build tries the signed-in requester's token from feat(artifact-delivery): fall back to the requester's GitHub token #495, then answersSOURCE_ACCESS_DENIEDwith "GitHub denies the skilld.dev read App access to this Repository, and no fallback token is set."GITHUB_TOKENstays the primary credential. It also serves every read while the App secrets are unusable or GitHub refuses to mint a token, and its quota then paces the sync.After #491 deployed, production built 150 runs in 41 minutes as the App, across 68 owners. 3 were denied, all
neondatabase/agent-skills, and none wasRATE_LIMITED. Theapp-deniedevents count denials per day once this deploys.Callers that still need a credential other than the App:
neondatabase. It needsGITHUB_TOKEN,ARTIFACT_GITHUB_TOKENfor builds, or a signed-in requester's own token.GITHUB_TOKENexpiry. Without the secret, it reports nothing.scripts/readGITHUB_TOKENfrom the local environment. They never run in the Worker.📝 Migration
skilld-dev: filteroutcome = app-deniedand group byoperationandgithub.credential.GITHUB_TOKEN, from~/sites/skilld.dev:CLOUDFLARE_ACCOUNT_ID=5904138d55ca25d5670dca6adf99894e pnpm exec wrangler secret delete GITHUB_TOKEN --config wrangler.jsoncCLOUDFLARE_ACCOUNT_ID=5904138d55ca25d5670dca6adf99894e pnpm exec wrangler secret put GITHUB_TOKEN --config wrangler.jsonc--config wrangler.jsoncmatters: a straywrangler.tomlin the checkout otherwise wins.