Skip to content

Commit def1375

Browse files
committed
wip
1 parent b04b4c9 commit def1375

File tree

6 files changed

+150
-148
lines changed

6 files changed

+150
-148
lines changed

scripts/sync-landing-schema/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
"name": "@graphql-website/sync-landing-schema",
33
"private": true,
44
"type": "module",
5-
"main": "./get-contributors.ts",
5+
"main": "./src/index.ts",
66
"scripts": {
77
"codegen": "gql-gen --config graphql-codegen.ts",
8-
"start": "node get-contributors.ts"
8+
"start": "node ./src/index.ts"
99
},
1010
"dependencies": {
1111
"@graphql-codegen/cli": "^6.0.0",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { ExecutionResult } from "graphql"
2+
3+
import type { TypedDocumentString } from "../generated/graphql.ts"
4+
5+
export async function execute<TResult, TVariables>(
6+
query: TypedDocumentString<TResult, TVariables>,
7+
variables?: TVariables,
8+
headers?: Record<string, string>,
9+
): Promise<ExecutionResult<TResult>> {
10+
const response = await fetch("https://api.github.com/graphql", {
11+
method: "POST",
12+
headers: {
13+
"Content-Type": "application/json",
14+
...headers,
15+
},
16+
body: JSON.stringify({ query, variables }),
17+
})
18+
19+
if (!response.ok) {
20+
console.error("Network response was not ok:", response)
21+
throw new Error("Network response was not ok")
22+
}
23+
24+
return (await response.json()) as ExecutionResult<TResult>
25+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { setTimeout as sleep } from "node:timers/promises"
2+
import { execute } from "./execute.ts"
3+
import { QUERY } from "./query.ts"
4+
5+
/**
6+
* Fetch contributors (by commit authors tied to GitHub users) for a single repo.
7+
* Traverses the full commit history of the default branch using pagination.
8+
*/
9+
export async function fetchRepoContributors(
10+
owner: string,
11+
repo: string,
12+
accessToken: string,
13+
) {
14+
const contributors = new Map<
15+
string /* handle */,
16+
{ contributions: number; website?: string }
17+
>()
18+
let after: string | null = null
19+
let page = 0
20+
let hasMore = true
21+
22+
const fetchMore = () =>
23+
execute(
24+
QUERY,
25+
{
26+
owner,
27+
name: repo,
28+
after,
29+
},
30+
{
31+
Authorization: `Bearer ${accessToken}`,
32+
"User-Agent": "graphql.org contributors sync client",
33+
},
34+
)
35+
36+
while (hasMore) {
37+
const response = await fetchMore()
38+
39+
if (response.errors?.length) {
40+
throw new Error(
41+
`GitHub GraphQL errors for ${owner}/${repo}: ${response.errors
42+
.map((e: { message: string }) => e.message)
43+
.join("; ")}`,
44+
)
45+
}
46+
47+
const repoData = response.data?.repository
48+
if (!repoData) {
49+
console.warn(`Repository not found: ${owner}/${repo}`)
50+
break
51+
}
52+
53+
const defaultBranchRef = repoData.defaultBranchRef
54+
if (!defaultBranchRef?.target) {
55+
console.warn(`Default branch not found for ${owner}/${repo}`)
56+
break
57+
}
58+
59+
if (!("history" in defaultBranchRef.target)) {
60+
console.warn(`History not found for ${owner}/${repo}`)
61+
break
62+
}
63+
64+
const history = defaultBranchRef.target.history
65+
66+
for (const node of history.nodes || []) {
67+
const user = node?.author?.user
68+
if (!user?.login) continue
69+
const prev = contributors.get(user.login)
70+
if (prev) {
71+
prev.contributions += 1
72+
// keep existing website unless we don't have one and GitHub provides it
73+
prev.website ||= user.websiteUrl
74+
} else {
75+
contributors.set(user.login, {
76+
contributions: 1,
77+
website: user.websiteUrl ?? undefined,
78+
})
79+
}
80+
}
81+
82+
const hasNext = history.pageInfo?.hasNextPage
83+
after = history.pageInfo?.endCursor || null
84+
hasMore = !!hasNext
85+
page += 1
86+
87+
if (page % 5 === 0) await sleep(200)
88+
}
89+
90+
return contributors
91+
}

scripts/sync-landing-schema/get-contributors.ts renamed to scripts/sync-landing-schema/src/index.ts

Lines changed: 3 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,12 @@ import { setTimeout as sleep } from "node:timers/promises"
22
import { readFile, writeFile } from "node:fs/promises"
33
import { fileURLToPath, pathToFileURL } from "node:url"
44
import { dirname, resolve } from "node:path"
5-
6-
import type { ExecutionResult } from "graphql"
7-
8-
import { graphql } from "./generated/index.ts"
9-
import type { TypedDocumentString } from "./generated/graphql.ts"
5+
import { fetchRepoContributors } from "./fetch-repo-contributors.ts"
106

117
type RepoRef = `${string}/${string}`
128

13-
const QUERY = graphql(`
14-
query RepoContributors($owner: String!, $name: String!, $after: String) {
15-
repository(owner: $owner, name: $name) {
16-
defaultBranchRef {
17-
target {
18-
... on Commit {
19-
history(first: 100, after: $after) {
20-
pageInfo {
21-
hasNextPage
22-
endCursor
23-
}
24-
nodes {
25-
author {
26-
user {
27-
login
28-
websiteUrl
29-
}
30-
}
31-
}
32-
}
33-
}
34-
}
35-
}
36-
}
37-
}
38-
`)
9+
const __dirname = dirname(fileURLToPath(import.meta.url))
10+
const outPath = resolve(__dirname, "data.json")
3911

4012
export const REPO_TO_PROJECT: Record<RepoRef, string> = {
4113
"graphql/graphql-spec": "GraphQL",
@@ -134,100 +106,8 @@ export async function getContributors(
134106
return result
135107
}
136108

137-
/**
138-
* Fetch contributors (by commit authors tied to GitHub users) for a single repo.
139-
* Traverses the full commit history of the default branch using pagination.
140-
*
141-
* Returns a Map: login -> { contributions, website }
142-
*/
143-
async function fetchRepoContributors(
144-
owner: string,
145-
repo: string,
146-
accessToken: string,
147-
) {
148-
const contributors = new Map<
149-
string /* handle */,
150-
{ contributions: number; website?: string }
151-
>()
152-
let after: string | null = null
153-
let page = 0
154-
let hasMore = true
155-
156-
const fetchMore = () =>
157-
execute(
158-
QUERY,
159-
{
160-
owner,
161-
name: repo,
162-
after,
163-
},
164-
{
165-
Authorization: `Bearer ${accessToken}`,
166-
"User-Agent": "graphql.org contributors sync client",
167-
},
168-
)
169-
170-
while (hasMore) {
171-
const response = await fetchMore()
172-
173-
if (response.errors?.length) {
174-
throw new Error(
175-
`GitHub GraphQL errors for ${owner}/${repo}: ${response.errors
176-
.map((e: { message: string }) => e.message)
177-
.join("; ")}`,
178-
)
179-
}
180-
181-
const repoData = response.data?.repository
182-
if (!repoData) {
183-
console.warn(`Repository not found: ${owner}/${repo}`)
184-
break
185-
}
186-
187-
const defaultBranchRef = repoData.defaultBranchRef
188-
if (!defaultBranchRef?.target) {
189-
console.warn(`Default branch not found for ${owner}/${repo}`)
190-
break
191-
}
192-
193-
if (!("history" in defaultBranchRef.target)) {
194-
console.warn(`History not found for ${owner}/${repo}`)
195-
break
196-
}
197-
198-
const history = defaultBranchRef.target.history
199-
200-
for (const node of history.nodes || []) {
201-
const user = node?.author?.user
202-
if (!user?.login) continue
203-
const prev = contributors.get(user.login)
204-
if (prev) {
205-
prev.contributions += 1
206-
// keep existing website unless we don't have one and GitHub provides it
207-
prev.website ||= user.websiteUrl
208-
} else {
209-
contributors.set(user.login, {
210-
contributions: 1,
211-
website: user.websiteUrl ?? undefined,
212-
})
213-
}
214-
}
215-
216-
const hasNext = history.pageInfo?.hasNextPage
217-
after = history.pageInfo?.endCursor || null
218-
hasMore = !!hasNext
219-
page += 1
220-
221-
if (page % 5 === 0) await sleep(200)
222-
}
223-
224-
return contributors
225-
}
226-
227109
// CLI entrypoint: when executed directly, write contributors to data.json next to this file
228110
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
229-
const __dirname = dirname(fileURLToPath(import.meta.url))
230-
const outPath = resolve(__dirname, "data.json")
231111
getContributors()
232112
.then(async data => {
233113
await writeFile(outPath, JSON.stringify(data, null, 2) + "\n", "utf8")
@@ -240,25 +120,3 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
240120
process.exitCode = 1
241121
})
242122
}
243-
244-
async function execute<TResult, TVariables>(
245-
query: TypedDocumentString<TResult, TVariables>,
246-
variables?: TVariables,
247-
headers?: Record<string, string>,
248-
): Promise<ExecutionResult<TResult>> {
249-
const response = await fetch("https://api.github.com/graphql", {
250-
method: "POST",
251-
headers: {
252-
"Content-Type": "application/json",
253-
...headers,
254-
},
255-
body: JSON.stringify({ query, variables }),
256-
})
257-
258-
if (!response.ok) {
259-
console.error("Network response was not ok:", response)
260-
throw new Error("Network response was not ok")
261-
}
262-
263-
return (await response.json()) as ExecutionResult<TResult>
264-
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { graphql } from "../generated/index.ts"
2+
3+
export const QUERY = graphql(`
4+
query RepoContributors($owner: String!, $name: String!, $after: String) {
5+
repository(owner: $owner, name: $name) {
6+
defaultBranchRef {
7+
target {
8+
... on Commit {
9+
history(first: 100, after: $after) {
10+
pageInfo {
11+
hasNextPage
12+
endCursor
13+
}
14+
nodes {
15+
author {
16+
user {
17+
login
18+
websiteUrl
19+
}
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}
27+
}
28+
`)

scripts/sync-landing-schema/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
"noEmit": true,
55
"allowImportingTsExtensions": true
66
},
7-
"include": ["*.ts"]
7+
"include": ["*.ts", "src/**/*.ts"]
88
}

0 commit comments

Comments
 (0)