Skip to content

chore: add github sponsors on supporters#8531

Open
bjohansebas wants to merge 18 commits intomainfrom
github_sponsors
Open

chore: add github sponsors on supporters#8531
bjohansebas wants to merge 18 commits intomainfrom
github_sponsors

Conversation

@bjohansebas
Copy link
Copy Markdown
Member

@bjohansebas bjohansebas commented Jan 9, 2026

Description

Note that there’s no REST API for GitHub Sponsors, only a GraphQL API. I’m still working on this.

Preview:
imagen

Validation

Related Issues

closes #8199

Check List

  • I have read the Contributing Guidelines and made commit messages that follow the guideline.
  • I have run pnpm format to ensure the code follows the style guide.
  • I have run pnpm test to check if all tests are passing.
  • I have run pnpm build to check if the website builds without errors.
  • I've covered new added functionality with unit tests if necessary.

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nodejs-org Ready Ready Preview Apr 12, 2026 10:32pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 9, 2026

👋 Codeowner Review Request

The following codeowners have been identified for the changed files:

Team reviewers: @nodejs/nodejs-website

Please review the changes when you have a chance. Thank you! 🙏

@codecov

This comment was marked as off-topic.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 9, 2026

📦 Build Size Comparison

Summary

Metric Value
Old Total Size 3.51 MB
New Total Size 3.51 MB
Delta 5.00 B (0.00%)

Changes

➕ Added Assets (2)
Name Size
.next/static/chunks/4ab402c4ccfd82ec.js 41.55 KB
.next/static/chunks/9683dd9e3611b206.js 197.63 KB
➖ Removed Assets (2)
Name Size
.next/static/chunks/46deeb46308b8a8b.js 41.54 KB
.next/static/chunks/3a4ee3cc36de6c5f.js 197.63 KB

Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com>
Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com>
Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com>
@bjohansebas
Copy link
Copy Markdown
Member Author

@nodejs/web-infra I think the new environment variable should also be set in Vercel, since that’s where the website is built, both for production and for previews

imagen

@bjohansebas bjohansebas marked this pull request as ready for review February 8, 2026 04:22
@bjohansebas bjohansebas requested a review from a team as a code owner February 8, 2026 04:22
Copilot AI review requested due to automatic review settings February 8, 2026 04:22

This comment was marked as resolved.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com>
Comment on lines +113 to +114
organization(login: "nodejs") {
sponsorshipsAsMaintainer (first: 100, includePrivate: false, after: "${cursor}", activeOnly: false) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitely, I’d prefer that we only show sponsors that are active, but why do we want to show everyone? see #8268.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt @nodejs/nodejs-website @nodejs/marketing ?

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com>
@MattIPv4
Copy link
Copy Markdown
Member

MattIPv4 commented Apr 12, 2026

I have saved a token for NEXT_GITHUB_READ_API_KEY in Vercel, for production + preview. It is a fine-grained access token for the @openjs-vercel account with read-only repo access and no additional permissions.

@ovflowd
Copy link
Copy Markdown
Member

ovflowd commented Apr 12, 2026

If that was done already, then @bjohansebas please proceed.

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 12, 2026

PR Summary

Medium Risk
Adds a new GitHub GraphQL data source (token-authenticated) and changes how supporter URLs/keys are derived, which may impact build-time data generation and rate/permission failures.

Overview
Supporters data generation now merges OpenCollective backers + GitHub Sponsors (via GitHub GraphQL) and shuffles the combined list periodically, tolerating individual source failures via Promise.allSettled.

This updates the Supporter typing/UI to accept github sources, uses url consistently for avatar links (and a source:name key), and renames/configures the GitHub read token/env (GITHUB_READ_API_KEY) plus adds GITHUB_GRAPHQL_URL. The partners page copy is updated to mention both funding platforms.

Reviewed by Cursor Bugbot for commit dfdf2ab. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: GraphQL cursor interpolated as string "null" on first request
    • The sponsorship query now serializes cursor with JSON.stringify(cursor), so the first request sends GraphQL null instead of the string "null".
  • ✅ Fixed: Duplicate n.sponsorEntity fallback provides no additional coverage
    • Sponsor extraction was consolidated to node.sponsor || node.sponsorEntity, removing the duplicated fallback in both mapping paths.
  • ✅ Fixed: Component loses OpenCollective profile links by switching to url
    • The supporters component now passes profile || url to Avatar, restoring OpenCollective profile links while keeping website URLs as fallback.
  • ✅ Fixed: Missing deduplication between sponsorship and activity queries
    • GitHub sponsors are now added through a deduplicating helper keyed by sponsor identity so overlaps between sponsorships and activity events no longer duplicate entries.

Create PR

Or push these changes by commenting:

@cursor push 77dce1aa43
Preview (77dce1aa43)
diff --git a/apps/site/components/Common/Supporters/index.tsx b/apps/site/components/Common/Supporters/index.tsx
--- a/apps/site/components/Common/Supporters/index.tsx
+++ b/apps/site/components/Common/Supporters/index.tsx
@@ -11,13 +11,13 @@
 
 const SupportersList: FC<SupportersListProps> = ({ supporters }) => (
   <div className="flex max-w-full flex-wrap items-center justify-center gap-1">
-    {supporters.map(({ name, image, url }) => (
+    {supporters.map(({ name, image, profile, url }) => (
       <Avatar
         nickname={name}
         fallback={getAcronymFromString(name)}
         image={image}
         key={name}
-        url={url}
+        url={profile || url}
       />
     ))}
   </div>

diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs
--- a/apps/site/next-data/generators/supportersData.mjs
+++ b/apps/site/next-data/generators/supportersData.mjs
@@ -46,7 +46,26 @@
   }
 
   const sponsors = [];
+  const seenSponsorKeys = new Set();
 
+  const addSponsor = node => {
+    const s = node.sponsor || node.sponsorEntity; // support different field names
+    const key = s?.id ?? s?.login ?? s?.url ?? s?.name ?? s?.avatarUrl;
+    if (key && seenSponsorKeys.has(key)) {
+      return;
+    }
+    if (key) {
+      seenSponsorKeys.add(key);
+    }
+
+    sponsors.push({
+      name: s?.name || s?.login || null,
+      image: s?.avatarUrl || null,
+      url: s?.url || null,
+      source: 'github',
+    });
+  };
+
   // Fetch sponsorship pages
   let cursor = null;
 
@@ -64,18 +83,8 @@
     }
 
     const { nodes, pageInfo } = nodeRes;
-    const mapped = nodes.map(n => {
-      const s = n.sponsor || n.sponsorEntity || n.sponsorEntity; // support different field names
-      return {
-        name: s?.name || s?.login || null,
-        image: s?.avatarUrl || null,
-        url: s?.url || null,
-        source: 'github',
-      };
-    });
+    nodes.forEach(addSponsor);
 
-    sponsors.push(...mapped);
-
     if (!pageInfo.hasNextPage) {
       break;
     }
@@ -96,18 +105,8 @@
   }
 
   const { nodes } = nodeRes;
-  const mapped = nodes.map(n => {
-    const s = n.sponsor || n.sponsorEntity || n.sponsorEntity; // support different field names
-    return {
-      name: s?.name || s?.login || null,
-      image: s?.avatarUrl || null,
-      url: s?.url || null,
-      source: 'github',
-    };
-  });
+  nodes.forEach(addSponsor);
 
-  sponsors.push(...mapped);
-
   return sponsors;
 }
 
@@ -115,7 +114,7 @@
   return `
     query {
         organization(login: "nodejs") {
-            sponsorshipsAsMaintainer (first: 100, includePrivate: false, after: "${cursor}", activeOnly: false) {
+            sponsorshipsAsMaintainer (first: 100, includePrivate: false, after: ${JSON.stringify(cursor)}, activeOnly: false) {
                 nodes {
                     sponsor: sponsorEntity {
                         ...on User {

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 492c9ed. Configure here.


const { nodes, pageInfo } = nodeRes;
const mapped = nodes.map(n => {
const s = n.sponsor || n.sponsorEntity; // support different field names
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GraphQL alias makes n.sponsorEntity fallback unreachable dead code

Low Severity

The || n.sponsorEntity fallback in both fetchSponsorshipsQuery and fetchDonationsQuery is unreachable. In SPONSORSHIPS_QUERY, the GraphQL alias sponsor: sponsorEntity ensures the response field is always sponsorsponsorEntity never appears in the JSON response. In DONATIONS_QUERY, the field is natively named sponsor. So n.sponsorEntity is always undefined, making the fallback dead code with a misleading comment.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 492c9ed. Configure here.

@bjohansebas bjohansebas requested a review from a team as a code owner April 12, 2026 19:42
@MattIPv4
Copy link
Copy Markdown
Member

MattIPv4 commented Apr 12, 2026

X-posting from Slack... the fine-grained token I generated did not work:

[{"type":"FORBIDDEN","path":["organization"],"extensions":{"saml_failure":false},"locations":[{"line":3,"column":5}],"message":"The 'Node.js' enterprise forbids access via a fine-grained personal access tokens if the token's lifetime is greater than 366 days. Please adjust your token's lifetime at the following URL: https://github.com/settings/personal-access-tokens/13409736"}]

I attempted switching the preview to a classic token with no permissions set, which also did not work:

[{"type":"INSUFFICIENT_SCOPES","locations":[{"line":6,"column":11}],"message":"Your token has not been granted the required scopes to execute this query. The 'id' field requires one of the following scopes: ['read:user', 'read:org'], but your token has only been granted the: [''] scopes. Please modify your token's scopes at: https://github.com/settings/tokens."},{"type":"INSUFFICIENT_SCOPES","locations":[{"line":17,"column":15}],"message":"Your token has not been granted the required scopes to execute this query. The 'databaseId' field requires one of the following scopes: ['read:org'], but your token has only been granted the: [''] scopes. Please modify your token's scopes at: https://github.com/settings/tokens."},{"type":"INSUFFICIENT_SCOPES","locations":[{"line":18,"column":15}],"message":"Your token has not been granted the required scopes to execute this query. The 'name' field requires one of the following scopes: ['read:org'], but your token has only been granted the: [''] scopes. Please modify your token's scopes at: https://github.com/settings/tokens."},{"type":"INSUFFICIENT_SCOPES","locations":[{"line":19,"column":15}],"message":"Your token has not been granted the required scopes to execute this query. The 'login' field requires one of the following scopes: ['read:org'], but your token has only been granted the: [''] scopes. Please modify your token's scopes at: https://github.com/settings/tokens."},{"type":"INSUFFICIENT_SCOPES","locations":[{"line":20,"column":15}],"message":"Your token has not been granted the required scopes to execute this query. The 'avatarUrl' field requires one of the following scopes: ['read:org'], but your token has only been granted the: [''] scopes. Please modify your token's scopes at: https://github.com/settings/tokens."},{"type":"INSUFFICIENT_SCOPES","locations":[{"line":21,"column":15}],"message":"Your token has not been granted the required scopes to execute this query. The 'url' field requires one of the following scopes: ['read:org'], but your token has only been granted the: [''] scopes. Please modify your token's scopes at: https://github.com/settings/tokens."},{"type":"INSUFFICIENT_SCOPES","locations":[{"line":22,"column":15}],"message":"Your token has not been granted the required scopes to execute this query. The 'websiteUrl' field requires one of the following scopes: ['read:org'], but your token has only been granted the: [''] scopes. Please modify your token's scopes at: https://github.com/settings/tokens."},{"type":"INSUFFICIENT_SCOPES","locations":[{"line":25,"column":11}],"message":"Your token has not been granted the required scopes to execute this query. The 'timestamp' field requires one of the following scopes: ['read:user', 'read:org'], but your token has only been granted the: [''] scopes. Please modify your token's scopes at: https://github.com/settings/tokens."},{"type":"INSUFFICIENT_SCOPES","locations":[{"line":27,"column":13}],"message":"Your token has not been granted the required scopes to execute this query. The 'monthlyPriceInDollars' field requires one of the following scopes: ['read:user', 'read:org'], but your token has only been granted the: [''] scopes. Please modify your token's scopes at: https://github.com/settings/tokens."},{"type":"INSUFFICIENT_SCOPES","locations":[{"line":28,"column":13}],"message":"Your token has not been granted the required scopes to execute this query. The 'isOneTime' field requires one of the following scopes: ['read:user', 'read:org'], but your token has only been granted the: [''] scopes. Please modify your token's scopes at: https://github.com/settings/tokens."}]

Given our security posture for this project is that anyone can submit a PR that will immediately be built on Vercel, anyone could extract this token, so I do not believe it is safe to grant read:user or read:org to this token, as this can access non-public information.

We could attempt using a fine-grained token that has an expiration date, to comply with the org requirement (even though the fine-grained token resource owner is not the org), but that then becomes a yearly maintenance burden for us to ensure it is regenerated before it expires and breaks this feature.

I feel like we may be at an impasse here, unless we want to try a classic token with those permissions, but production only, so that this feature will not work in preview deployments?

@MattIPv4
Copy link
Copy Markdown
Member

I have generated a new classic token for @openjs-vercel with just the read:org permission (this does include potential access to private information). As this contains more than just public access, this token is now only set in the Vercel production environment, and specifically for preview deployments of this branch only -- however, importantly, other preview deployments will not have access to the token and this feature will not work for them.

With that scope applied to the token, https://nodejs-org-git-githubsponsors-openjs.vercel.app/en/about/partners is showing sponsors 🎉

@bjohansebas
Copy link
Copy Markdown
Member Author

Cool, so @nodejs/nodejs-website and @nodejs/web-infra, could you please review it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement GitHub Sponsors data fetching

8 participants