fix(github): key the response cache by installation identity, not the raw token - #2584
Conversation
… raw token Closes #2538. The self-host GitHub response cache derived its Redis key from a hash of the literal Authorization header, so every installation-token rotation (roughly hourly by design, plus on auth failure, plus on redeploy if the token isn't persisted) invalidated the entire cached-response namespace for that installation across every cache class at once, even though the underlying resources hadn't changed and every entry was still within its own TTL. Key by the same stable per-installation/public-token identity already used for rate-limit admission scoping instead, via the existing githubRateLimitAdmissionKey plumbing. A caller that doesn't thread an admission key falls back to the previous token-hash behavior -- still correctly isolated, just without the cross-rotation benefit. Also fixes the one caller authenticating with the shared public token instead of an installation token (resolveUpstreamCommitSha) to pass its own distinctly- scoped key, so it gets the same benefit without colliding with any installation-scoped entry.
|
Tip 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Gittensory review result - approve/merge recommendedReview updated: 2026-07-02 10:44:51 UTC
✅ Suggested Action - Approve/Merge
Review summary Nits — 5 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2584 +/- ##
==========================================
+ Coverage 95.94% 95.98% +0.04%
==========================================
Files 228 229 +1
Lines 25559 25812 +253
Branches 9303 9390 +87
==========================================
+ Hits 24522 24776 +254
+ Misses 426 425 -1
Partials 611 611
🚀 New features to boost your workflow:
|
Summary
Closes #2538.
The self-host GitHub response cache (used for branch-protection, bare-commit, and repo/user/installation-metadata reads) derives its Redis cache key from a hash of the literal
Authorizationheader value. Installation tokens rotate roughly hourly by design, plus on any auth failure, plus (if the token isn't preserved across restarts) on every redeploy. Because the cache key hashes the token bytes rather than a stable identity, every rotation invalidated the ENTIRE cached-response namespace for that installation across all cache classes simultaneously — not just the entries that were actually stale — even though every entry was still within its own TTL.An equivalent bug for a sibling credential (the GitHub App's own signing JWT) was already identified and fixed by reusing a minted JWT for part of its validity window specifically to keep its cache key stable (#1940). The same fix pattern had never been applied to the longer-lived installation token.
What changed
src/github/client.ts:responseCacheKeynow accepts the same stable per-installation/public-token admission-key identity already threaded through the codebase for rate-limit admission scoping (githubRateLimitAdmissionKey), and uses it in place of the token hash when a caller provides one. A caller that doesn't yet thread an admission key falls back to the previous token-hash behavior — still correctly isolated, just without the cross-rotation benefit. The cache-key version prefix moved fromv2tov3so old and new key shapes never ambiguously interact during a rolling deploy (old entries simply lapse via their own TTL).cacheKeyAdmissionIdentity, deliberately NOT gated on the existinggithubRateLimitAdmissionboolean (unlike the siblingrateLimitAdmissionKeyhelper used for admission-control observation) — a caller can supply a stable identity for cache keying without opting into local rate-limit observation for that specific call.src/upstream/commit.ts:resolveUpstreamCommitSha(the one caller reading a cacheable endpoint — bare/commits/{ref}— with the shared public token rather than an installation token) now passesgithubRateLimitAdmissionKeyForPublicToken(), giving it the same stable-key benefit with a scope that's distinct from any installation-scoped key by construction.No change to what gets cached, cache TTLs, or cache-class eligibility — this is purely a key-derivation fix.
Tests
test/unit/github-client.test.ts: a cacheable GET keyed by a stable admission identity survives a simulated token rotation (single fetch across both calls); two installations' admission-keyed entries stay isolated even when they momentarily share raw token bytes; the public-token admission key is scoped distinctly from an installation-scoped key for the same URL; a blank/whitespace admission key is treated as absent and falls back to the token-hash keying (proving no accidental cache leak from a malformed key).test/unit/upstream-commit.test.ts:resolveUpstreamCommitSha's cache entry survives aGITHUB_PUBLIC_TOKENvalue change (a single network fetch across both calls).Validation
npm run typechecknpx vitest run test/unit/github-client.test.ts test/unit/upstream-commit.test.tsnpm run test:changednpm run test:coverage(unsharded, full suite — 100% line and branch coverage on both changed files percoverage/lcov.info)npm run test:workersnpm run db:migrations:check/npm run ui:openapi:check/npm run ui:version-audit(no-op — no schema/API/binding changes)npm run ui:lint/npm run ui:typecheck(no UI files touched)npm run test:ci(the full local gate, exit 0)npm audit --audit-level=moderategit diff --checkScope
site/,CNAME,**/lovable/**, orCHANGELOG.mdSafety
public-tokenvsinstallation:{id})