Repository navigation
fix(api): stop serving public titles once a repo is made private - #1067
Conversation
|
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (2)
🚫 Excluded labels (none allowed) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
uploads-api | 8c02e01 | Commit Preview URL Branch Preview URL |
Oct 04 2026, 07:29 PM |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
uploads-web | 8c02e01 | Commit Preview URL Branch Preview URL |
Oct 04 2026, 07:29 PM |
|
Operator step done: the GitHub App is now subscribed to the |
Handle the repository webhook's privatized and publicized actions. Both write the new visibility to the ghpriv: cache. privatized also bumps a per-repo public-title epoch (ghpubgen:<repo>); public cache entries carry the epoch they were written under and only match the current one, so the repo's ghref:pub: titles stop being served at once. Closes #1066
90d73e3 to
8c02e01
Compare
In plain terms
Public pages read PR and issue titles from a cache that #1065 adds. If a repo was made private after its titles were cached, those titles kept showing on public pages until they expired: up to 1 hour for open items and 24 hours for closed or merged ones. With this change, when GitHub reports that a repo was made private, its cached public titles stop being served right away.
What it does / what it is not
repositorywebhook:privatized: writes "private" to the repo-visibility cache (ghpriv:) and bumps a per-repo public-title epoch (ghpubgen:<repo>). Every existingghref:pub:entry for that repo stops matching at once.publicized: only writes "public" to the visibility cache. Titles return when the public ladder next resolves them.repositoryactions are ignored.titleCacheKeysstill returns bothghref:andghref:pub:, andissues/pull_requestdeliveries still delete both.ghref:) is not affected. Making a repo private does not change what the linked workspace's members can see.repositorytoday. It is not inREQUIRED_WEBHOOK_EVENTSorRECOMMENDED_WEBHOOK_EVENTS, and the docs did not list it. Until someone ticks Repository under the App's Settings → Permissions & events → Subscribe to events, this code receives nothing and titles expire on their TTL as before. The event needs only the Metadata (read) permission, which every App already has, so no installation has to re-approve anything.repositoryto thegithub health/uploads github doctorcheck. The CLI's "recommended" note is written forissue_comment("enables bot-comment self-healing…"), so listingrepositorythere would print a misleading reason. Fixing that wording is a CLI change and needs a changeset, so it is a separate follow-up.Technical notes
Why an epoch instead of
list({ prefix }). KV listing is paginated and eventually consistent. A delete sweep over it could miss keys or need many round trips inside one webhook. The epoch is a single write, and the result does not depend on how many refs the repo has.Why the epoch is stamped on the value, not folded into the key. Each public entry stores the epoch it was written under (
{ v, e }). A read serves it only wheneequals the currentghpubgen:<repo>. Putting the epoch in the key would give the same unreachability, buttitleCacheKeys(ref)would then need the current epoch, so it would become async and add a KV read to everyissues/pull_requestdelivery. With the epoch on the value, the key staysghref:pub:<ref>, so per-ref deletes stay exact and synchronous.Read cost. The public ladder reads the entry and the epoch in parallel (
Promise.all), so there is no extra serial round trip within the ~1.4swithPublicTitleBudget. The epoch read is shared per repo per batch, in the same way as the visibility probe. If the epoch read fails, that ref resolves tonull(fail closed).Details:
Date.now(), so concurrent bumps need no read-modify-write.SETTLED_TTL + NEGATIVE_TTL(25h), which outlives every entry written before the bump.{ v }, noe). Existing entries stay valid through deploy.Why the public ladder does not re-check
ghpriv:on a cache hit.ghpriv:expiry) would put GitHub latency and failures on the all-hit path every 10 minutes per repo, and titles would vanish during GitHub blips.ghpriv:lives. It also needs the sameprivatizeddelivery the epoch already handles.Casing.
repositoryevents key bothghpriv:andghpubgen:by the lowercased name, because that is what the title ladder reads (refs arrive lowercased). The existingpull_request/issue_commentwrite-through still usesfull_nameas sent. I left that alone because it is outside this change.Retries. The epoch bump is not wrapped in the best-effort try/catch that the privacy priming uses. If it fails, the queue consumer retries the event. Every step of the event is idempotent.
Remaining gap. KV is eventually consistent, so another location can keep serving the old epoch for up to about 60 seconds after the bump.
Test plan
CI Test/Lint is skipped on stacked PRs, so these are local runs on this branch:
ghref:pub:title was still served after aprivatizeddelivery (3 of the new webhook tests failed before the fix).pnpm test(root, full suite): 410 files passed, 1 skipped; 6427 tests passed, 2 skipped; no unhandled errors.pnpm --filter @uploads/api typechecknpx oxlinton the four changed.tsfiles: cleanpnpm format:check: cleanprivatizeddelivery hides the repo's cached public titles and leaves other repos' titles aloneghpriv:reads private afterprivatizedand public afterpublicizednullrepositoryevent (Settings → Permissions & events → Subscribe to events → Repository)./f/page.Closes #1066