fix: match sponsors on immutable github_id instead of mutable login#1764
Merged
Priyanshu-byte-coder merged 1 commit intoMay 31, 2026
Conversation
The sponsor sync job previously identified sponsors by github_login, a
mutable string. GitHub allows account owners to rename themselves at any
time, and recycles usernames after a grace period. This meant:
1. A sponsor renames their account — subsequent syncs see the new login
and no longer find a matching row, so their is_sponsor flag gets
cleared even though they are still an active sponsor.
2. A different GitHub user claims the freed username and signs into
DevTrack — the next sync sees that login in the sponsor list and
grants is_sponsor = true to the wrong account.
The users table already stores github_id (the immutable numeric GitHub
account ID) with a UNIQUE NOT NULL constraint, populated from the OAuth
profile at sign-in. GitHub's GraphQL Sponsors API exposes the same value
as databaseId on both User and Organization nodes.
Changes to src/app/api/sponsors/sync/route.ts:
- Add databaseId to both the User and Organization fragments in the
GraphQL query.
- Collect SponsorIdentity objects containing githubId (String(databaseId))
and login (kept for logging/response only).
- Skip nodes that have no databaseId (e.g. deleted/ghost accounts).
- Query existing sponsors with .select("github_id") and build the diff
set from github_id values instead of github_login values.
- Apply both grant and revoke operations with .in("github_id", ids),
not .in("github_login", logins).
- Extend the response to include granted and revoked counts alongside the
existing sponsorCount and sponsors fields.
No schema changes are required; users.github_id already exists and is
indexed as the primary immutable identity column.
test/sponsors-sync.test.ts — 16 new tests covering:
- Auth and config guards (missing CRON_SECRET, wrong header, missing token)
- GitHub API error paths (non-ok response, GraphQL errors, null user)
- Core fix: grant and revoke are issued against github_id not github_login
- Regression: recycled username does not transfer sponsor privileges
- Regression: renamed sponsor retains sponsor status (no spurious revoke)
- No-op when sponsor list is already in sync
- Grant + revoke in the same run
- Nodes without databaseId are silently skipped
- Response shape includes sponsorCount, granted, revoked, sponsors
Closes Priyanshu-byte-coder#1751
|
@Ridanshi is attempting to deploy a commit to the PRIYANSHU DOSHI's projects Team on Vercel. A member of the Team first needs to authorize it. |
GSSoC Label Checklist 🏷️@Priyanshu-byte-coder — please apply the appropriate labels before merging: Difficulty (pick one):
Quality (optional):
Validation (required to score):
|
a13c637
into
Priyanshu-byte-coder:main
4 of 5 checks passed
|
🎉 Merged! Thanks for contributing to DevTrack. If the project has been useful to you, a ⭐ star on the repo is the easiest way to support it — it helps DevTrack get discovered by more developers. Keep an eye on open issues for your next contribution! |
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.
Closes #1751
Problem
The sponsor sync job (
GET /api/sponsors/sync) previously identified active sponsors by matchinggithub_loginstrings returned by the GitHub GraphQL API against theusers.github_logincolumn in the database.github_loginis mutable — GitHub account owners can rename themselves at any time. GitHub also recycles usernames after a grace period. This created two distinct failure modes:Scenario A — sponsor loses status after renaming their account
login: "alice",github_id: 100) sponsors the project.alicefound in sponsor list →is_sponsor = trueset on the row wheregithub_login = 'alice'.alice2.aliceis no longer in the sponsor list →is_sponsor = falseincorrectly set, even though User A is still an active sponsor (they now appear asalice2).Scenario B — recycled username grants privileges to a different account
login: "alice",github_id: 100) is a sponsor;is_sponsor = true.aliceis eventually freed.github_id: 999) creates a new GitHub account withlogin: "alice"and signs into DevTrack.aliceis still in GitHub's sponsor list (associated with the originaldatabaseId: 100) →.update({ is_sponsor: true }).in("github_login", ["alice"])setsis_sponsor = trueon User B's row (the one currently holdinggithub_login = 'alice').Root cause
The GraphQL query only requested
login:All DB operations used
github_loginas the matching key. Theuserstable already hasgithub_id text unique not nullstoring GitHub's immutable numeric account ID, but the sync job never used it.Fix
src/app/api/sponsors/sync/route.tsdatabaseIdto bothUserandOrganizationfragments in the GraphQL query. GitHub'sdatabaseIdis the same immutable numeric ID asusers.github_id.{ githubId: String(databaseId), login }pairs.loginis kept only for the response payload and logging — it plays no role in DB matching.databaseId(e.g. deleted/ghost accounts) rather than falling back to login matching..select("github_id")and build the diff ongithub_idvalues..update({ is_sponsor: true/false }).in("github_id", ids)so privilege assignment follows the account, not the name.grantedandrevokedcounts.No schema migration is needed.
users.github_idalready exists, isNOT NULL, and has aUNIQUEindex.Identity chain
With this fix:
alice→alice2) continues to holdis_sponsor = truebecause the match is ongithub_id = 100, which doesn't change.alice(github_id = 999) will not receive sponsor privileges because999is not in the sponsor ID set.Tests
test/sponsors-sync.test.ts— 16 new tests:data.usergithub_idnotgithub_login; revoke usesgithub_idnotgithub_loginsuccess,sponsorCount,granted,revoked,sponsorsAll 16 tests pass. The single pre-existing failure in
test/dateUtils.test.ts(timezone boundary) is unrelated to this change and was already failing onmain.