Skip to content

Commit b5b8eff

Browse files
bjohansebasCopilotbmuenzenmeyer
authored
chore: add github sponsors on supporters (#8531)
* feat: implement fetch for github sponsors sponsorhip * fixup * feat: implement donations queary and fix links Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com> * feat: update text of supporters * feat: include past sponsors Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com> * feat: sort supporters Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com> * Update apps/site/next-data/generators/supportersData.mjs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com> * fixup! * fix: update supporters data mapping to include source and adjust URL handling * refactor: consolidate GraphQL queries for sponsorships and donations in supportersData.mjs * refactor: streamline fetching of GitHub sponsors by combining sponsorships and donations queries * test: add end-to-end test for partners page to verify no 500 error * refactor: simplify cursor handling and clean up sponsor field mapping in GraphQL queries * fix: correct wording in supporters section for clarity * chore: standardize naming for GitHub sponsor supporter and update API key handling * remove test * capture at least the name or image, but omit if both are missing --------- Signed-off-by: Sebastian Beltran <bjohansebas@gmail.com> Signed-off-by: Brian Muenzenmeyer <brian.muenzenmeyer@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Brian Muenzenmeyer <brian.muenzenmeyer@gmail.com>
1 parent 739bbfa commit b5b8eff

5 files changed

Lines changed: 230 additions & 12 deletions

File tree

apps/site/components/Common/Supporters/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@ import type { Supporter } from '#site/types';
66
import type { FC } from 'react';
77

88
type SupportersListProps = {
9-
supporters: Array<Supporter<'opencollective'>>;
9+
supporters: Array<Supporter<'opencollective' | 'github'>>;
1010
};
1111

1212
const SupportersList: FC<SupportersListProps> = ({ supporters }) => (
1313
<div className="flex max-w-full flex-wrap items-center justify-center gap-1">
14-
{supporters.map(({ name, image, profile }) => (
14+
{supporters.map(({ name, image, source, url }) => (
1515
<Avatar
1616
nickname={name}
1717
fallback={getAcronymFromString(name)}
1818
image={image}
19-
key={name}
20-
url={profile}
19+
key={`${source}:${name}`}
20+
url={url}
2121
/>
2222
))}
2323
</div>

apps/site/next-data/generators/supportersData.mjs

Lines changed: 217 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,85 @@
1-
import { OPENCOLLECTIVE_MEMBERS_URL } from '#site/next.constants.mjs';
1+
import {
2+
OPENCOLLECTIVE_MEMBERS_URL,
3+
GITHUB_GRAPHQL_URL,
4+
GITHUB_READ_API_KEY,
5+
} from '#site/next.constants.mjs';
26
import { fetchWithRetry } from '#site/next.fetch.mjs';
7+
import { shuffle } from '#site/util/array';
8+
9+
const SPONSORSHIPS_QUERY = `
10+
query ($cursor: String) {
11+
organization(login: "nodejs") {
12+
sponsorshipsAsMaintainer(
13+
first: 100
14+
includePrivate: false
15+
after: $cursor
16+
activeOnly: false
17+
) {
18+
nodes {
19+
sponsor: sponsorEntity {
20+
...on User {
21+
id: databaseId
22+
name
23+
login
24+
avatarUrl
25+
url
26+
websiteUrl
27+
}
28+
...on Organization {
29+
id: databaseId
30+
name
31+
login
32+
avatarUrl
33+
url
34+
websiteUrl
35+
}
36+
}
37+
}
38+
pageInfo {
39+
endCursor
40+
startCursor
41+
hasNextPage
42+
hasPreviousPage
43+
}
44+
}
45+
}
46+
}
47+
`;
48+
49+
const DONATIONS_QUERY = `
50+
query {
51+
organization(login: "nodejs") {
52+
sponsorsActivities(first: 100, includePrivate: false) {
53+
nodes {
54+
id
55+
sponsor {
56+
...on User {
57+
id: databaseId
58+
name
59+
login
60+
avatarUrl
61+
url
62+
websiteUrl
63+
}
64+
...on Organization {
65+
id: databaseId
66+
name
67+
login
68+
avatarUrl
69+
url
70+
websiteUrl
71+
}
72+
}
73+
timestamp
74+
tier: sponsorsTier {
75+
monthlyPriceInDollars
76+
isOneTime
77+
}
78+
}
79+
}
80+
}
81+
}
82+
`;
383

484
/**
585
* Fetches supporters data from Open Collective API, filters active backers,
@@ -14,13 +94,13 @@ async function fetchOpenCollectiveData() {
1494

1595
const members = payload
1696
.filter(({ role, isActive }) => role === 'BACKER' && isActive)
97+
.filter(({ name, image }) => name || image) // Ensure we have a name or image for the supporter
1798
.sort((a, b) => b.totalAmountDonated - a.totalAmountDonated)
18-
.map(({ name, website, image, profile }) => ({
99+
.map(({ name, image, profile }) => ({
19100
name,
20101
image,
21-
url: website,
22102
// If profile starts with the guest- prefix, it's a non-existing account
23-
profile: profile.startsWith('https://opencollective.com/guest-')
103+
url: profile.startsWith('https://opencollective.com/guest-')
24104
? undefined
25105
: profile,
26106
source: 'opencollective',
@@ -29,4 +109,136 @@ async function fetchOpenCollectiveData() {
29109
return members;
30110
}
31111

32-
export default fetchOpenCollectiveData;
112+
/**
113+
* Fetches supporters data from Github API, filters active backers,
114+
* and maps it to the Supporters type.
115+
*
116+
* @returns {Promise<Array<import('#site/types/supporters').GitHubSponsorSupporter>>} Array of supporters
117+
*/
118+
async function fetchGithubSponsorsData() {
119+
if (!GITHUB_READ_API_KEY) {
120+
return [];
121+
}
122+
123+
const [sponsorships, donations] = await Promise.all([
124+
fetchSponsorshipsQuery(),
125+
fetchDonationsQuery(),
126+
]);
127+
128+
return [...sponsorships, ...donations];
129+
}
130+
131+
async function fetchSponsorshipsQuery() {
132+
const sponsors = [];
133+
let cursor = null;
134+
135+
while (true) {
136+
const data = await graphql(
137+
SPONSORSHIPS_QUERY,
138+
cursor ? { cursor } : undefined
139+
);
140+
141+
if (data.errors) {
142+
throw new Error(JSON.stringify(data.errors));
143+
}
144+
145+
const nodeRes = data.data.organization?.sponsorshipsAsMaintainer;
146+
if (!nodeRes) {
147+
break;
148+
}
149+
150+
const { nodes, pageInfo } = nodeRes;
151+
const mapped = nodes.map(n => {
152+
const s = n.sponsor || n.sponsorEntity; // support different field names
153+
return {
154+
name: s?.name || s?.login || null,
155+
image: s?.avatarUrl || null,
156+
url: s?.url || null,
157+
source: 'github',
158+
};
159+
});
160+
161+
sponsors.push(...mapped);
162+
163+
if (!pageInfo.hasNextPage) {
164+
break;
165+
}
166+
167+
cursor = pageInfo.endCursor;
168+
}
169+
170+
return sponsors;
171+
}
172+
173+
async function fetchDonationsQuery() {
174+
const data = await graphql(DONATIONS_QUERY);
175+
176+
if (data.errors) {
177+
throw new Error(JSON.stringify(data.errors));
178+
}
179+
180+
const nodeRes = data.data.organization?.sponsorsActivities;
181+
if (!nodeRes) {
182+
return [];
183+
}
184+
185+
const { nodes } = nodeRes;
186+
return nodes.map(n => {
187+
const s = n.sponsor || n.sponsorEntity; // support different field names
188+
return {
189+
name: s?.name || s?.login || null,
190+
image: s?.avatarUrl || null,
191+
url: s?.url || null,
192+
source: 'github',
193+
};
194+
});
195+
}
196+
197+
const graphql = async (query, variables = {}) => {
198+
const res = await fetchWithRetry(GITHUB_GRAPHQL_URL, {
199+
method: 'POST',
200+
headers: {
201+
'Content-Type': 'application/json',
202+
Authorization: `Bearer ${GITHUB_READ_API_KEY}`,
203+
},
204+
body: JSON.stringify({ query, variables }),
205+
});
206+
207+
if (!res.ok) {
208+
const text = await res.text();
209+
throw new Error(`GitHub API error: ${res.status} ${text}`);
210+
}
211+
212+
return res.json();
213+
};
214+
215+
/**
216+
* Fetches supporters data from Open Collective API and GitHub Sponsors, filters active backers,
217+
* and maps it to the Supporters type.
218+
*
219+
* @returns {Promise<Array<import('#site/types/supporters').OpenCollectiveSupporter | import('#site/types/supporters').GitHubSponsorSupporter>>} Array of supporters
220+
*/
221+
async function sponsorsData() {
222+
const seconds = 300; // Change every 5 minutes
223+
const seed = Math.floor(Date.now() / (seconds * 1000));
224+
225+
const sponsorsResults = await Promise.allSettled([
226+
fetchGithubSponsorsData(),
227+
fetchOpenCollectiveData(),
228+
]);
229+
230+
const sponsors = sponsorsResults.flatMap(result => {
231+
if (result.status === 'fulfilled') {
232+
return result.value;
233+
}
234+
235+
console.error('Supporters data source failed:', result.reason);
236+
return [];
237+
});
238+
239+
const shuffled = await shuffle(sponsors, seed);
240+
241+
return shuffled;
242+
}
243+
244+
export default sponsorsData;

apps/site/next.constants.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export const EXTERNAL_LINKS_SITEMAP = [
131131
*
132132
* Note: This has no NEXT_PUBLIC prefix as it should not be exposed to the Browser.
133133
*/
134-
export const GITHUB_API_KEY = process.env.NEXT_GITHUB_API_KEY || '';
134+
export const GITHUB_READ_API_KEY = process.env.NEXT_GITHUB_READ_API_KEY || '';
135135

136136
/**
137137
* The resource we point people to when discussing internationalization efforts.
@@ -178,6 +178,11 @@ export const VULNERABILITIES_URL =
178178
export const OPENCOLLECTIVE_MEMBERS_URL =
179179
'https://opencollective.com/nodejs/members/all.json';
180180

181+
/**
182+
* The location of the GitHub GraphQL API
183+
*/
184+
export const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql';
185+
181186
/**
182187
* Orama DB URLs for the Learn and API sections of the website
183188
*/

apps/site/pages/en/about/partners.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ without we can't test and release new versions of Node.js.
2222

2323
## Supporters
2424

25-
Supporters are individuals and organizations that provide financial support through
26-
[OpenCollective](https://opencollective.com/nodejs) of the Node.js project.
25+
Supporters are individuals and organizations who financially support the Node.js project
26+
through [OpenCollective](https://opencollective.com/nodejs) and [GitHub Sponsors](https://github.com/sponsors/nodejs).
2727

2828
<WithSupporters />
2929

apps/site/types/supporters.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export type Supporter<T extends string> = {
77
};
88

99
export type OpenCollectiveSupporter = Supporter<'opencollective'>;
10+
export type GitHubSponsorSupporter = Supporter<'github'>;

0 commit comments

Comments
 (0)