Skip to content

Commit 7ff9c64

Browse files
committed
Handle multiple API pages worth of contributors
1 parent ec01dd2 commit 7ff9c64

File tree

1 file changed

+178
-94
lines changed

1 file changed

+178
-94
lines changed

scripts/update-contributors.js

Lines changed: 178 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@
88

99
const https = require("https")
1010
const fs = require("fs")
11+
const { promisify } = require("util")
1112
const path = require("path")
1213

14+
// Promisify filesystem operations
15+
const readFileAsync = promisify(fs.readFile)
16+
const writeFileAsync = promisify(fs.writeFile)
17+
1318
// GitHub API URL for fetching contributors
1419
const GITHUB_API_URL = "https://api.github.com/repos/RooVetGit/Roo-Code/contributors?per_page=100"
1520
const README_PATH = path.join(__dirname, "..", "README.md")
@@ -33,52 +38,144 @@ if (process.env.GITHUB_TOKEN) {
3338
}
3439

3540
/**
36-
* Fetches contributors data from GitHub API
37-
* @returns {Promise<Array>} Array of contributor objects
41+
* Parses the GitHub API Link header to extract pagination URLs
42+
* Based on RFC 5988 format for the Link header
43+
* @param {string} header The Link header from GitHub API response
44+
* @returns {Object} Object containing URLs for next, prev, first, last pages (if available)
3845
*/
39-
function fetchContributors() {
46+
function parseLinkHeader(header) {
47+
// Return empty object if no header is provided
48+
if (!header || header.trim() === "") return {}
49+
50+
// Initialize links object
51+
const links = {}
52+
53+
// Split the header into individual link entries
54+
// Example: <https://api.github.com/...?page=2>; rel="next", <https://api.github.com/...?page=5>; rel="last"
55+
const entries = header.split(/,\s*/)
56+
57+
// Process each link entry
58+
for (const entry of entries) {
59+
// Extract the URL (between < and >) and the parameters (after >)
60+
const segments = entry.split(";")
61+
if (segments.length < 2) continue
62+
63+
// Extract URL from the first segment, removing < and >
64+
const urlMatch = segments[0].match(/<(.+)>/)
65+
if (!urlMatch) continue
66+
const url = urlMatch[1]
67+
68+
// Find the rel="value" parameter
69+
let rel = null
70+
for (let i = 1; i < segments.length; i++) {
71+
const relMatch = segments[i].match(/\s*rel\s*=\s*"?([^"]+)"?/)
72+
if (relMatch) {
73+
rel = relMatch[1]
74+
break
75+
}
76+
}
77+
78+
// Only add to links if both URL and rel were found
79+
if (rel) {
80+
links[rel] = url
81+
}
82+
}
83+
84+
return links
85+
}
86+
87+
/**
88+
* Performs an HTTP GET request and returns the response
89+
* @param {string} url The URL to fetch
90+
* @param {Object} options Request options
91+
* @returns {Promise<Object>} Response object with status, headers and body
92+
*/
93+
function httpGet(url, options) {
4094
return new Promise((resolve, reject) => {
4195
https
42-
.get(GITHUB_API_URL, options, (res) => {
43-
if (res.statusCode !== 200) {
44-
reject(new Error(`GitHub API request failed with status code: ${res.statusCode}`))
45-
return
46-
}
47-
96+
.get(url, options, (res) => {
4897
let data = ""
4998
res.on("data", (chunk) => {
5099
data += chunk
51100
})
52101

53102
res.on("end", () => {
54-
try {
55-
const contributors = JSON.parse(data)
56-
resolve(contributors)
57-
} catch (error) {
58-
reject(new Error(`Failed to parse GitHub API response: ${error.message}`))
59-
}
103+
resolve({
104+
statusCode: res.statusCode,
105+
headers: res.headers,
106+
body: data,
107+
})
60108
})
61109
})
62110
.on("error", (error) => {
63-
reject(new Error(`GitHub API request failed: ${error.message}`))
111+
reject(error)
64112
})
65113
})
66114
}
67115

116+
/**
117+
* Fetches a single page of contributors from GitHub API
118+
* @param {string} url The API URL to fetch
119+
* @returns {Promise<Object>} Object containing contributors and pagination links
120+
*/
121+
async function fetchContributorsPage(url) {
122+
try {
123+
// Make the HTTP request
124+
const response = await httpGet(url, options)
125+
126+
// Check for successful response
127+
if (response.statusCode !== 200) {
128+
throw new Error(`GitHub API request failed with status code: ${response.statusCode}`)
129+
}
130+
131+
// Parse the Link header for pagination
132+
const linkHeader = response.headers.link
133+
const links = parseLinkHeader(linkHeader)
134+
135+
// Parse the JSON response
136+
const contributors = JSON.parse(response.body)
137+
138+
return { contributors, links }
139+
} catch (error) {
140+
throw new Error(`Failed to fetch contributors page: ${error.message}`)
141+
}
142+
}
143+
144+
/**
145+
* Fetches all contributors data from GitHub API (handling pagination)
146+
* @returns {Promise<Array>} Array of all contributor objects
147+
*/
148+
async function fetchContributors() {
149+
let allContributors = []
150+
let currentUrl = GITHUB_API_URL
151+
let pageCount = 1
152+
153+
// Loop through all pages of contributors
154+
while (currentUrl) {
155+
console.log(`Fetching contributors page ${pageCount}...`)
156+
const { contributors, links } = await fetchContributorsPage(currentUrl)
157+
158+
allContributors = allContributors.concat(contributors)
159+
160+
// Move to the next page if it exists
161+
currentUrl = links.next
162+
pageCount++
163+
}
164+
165+
console.log(`Fetched ${allContributors.length} contributors from ${pageCount - 1} pages`)
166+
return allContributors
167+
}
168+
68169
/**
69170
* Reads the README.md file
70171
* @returns {Promise<string>} README content
71172
*/
72-
function readReadme() {
73-
return new Promise((resolve, reject) => {
74-
fs.readFile(README_PATH, "utf8", (err, data) => {
75-
if (err) {
76-
reject(new Error(`Failed to read README.md: ${err.message}`))
77-
return
78-
}
79-
resolve(data)
80-
})
81-
})
173+
async function readReadme() {
174+
try {
175+
return await readFileAsync(README_PATH, "utf8")
176+
} catch (err) {
177+
throw new Error(`Failed to read README.md: ${err.message}`)
178+
}
82179
}
83180

84181
/**
@@ -147,7 +244,7 @@ function formatContributorsSection(contributors) {
147244
* @param {string} contributorsSection HTML for contributors section
148245
* @returns {Promise<void>}
149246
*/
150-
function updateReadme(readmeContent, contributorsSection) {
247+
async function updateReadme(readmeContent, contributorsSection) {
151248
// Find existing contributors section markers
152249
const startPos = readmeContent.indexOf(START_MARKER)
153250
const endPos = readmeContent.indexOf(END_MARKER)
@@ -164,55 +261,49 @@ function updateReadme(readmeContent, contributorsSection) {
164261
// Ensure single newline separators between sections
165262
const updatedContent = beforeSection + "\n\n" + contributorsSection.trim() + "\n\n" + afterSection
166263

167-
return writeReadme(updatedContent)
264+
await writeReadme(updatedContent)
168265
}
169266

170267
/**
171268
* Writes updated content to README.md
172269
* @param {string} content Updated README content
173270
* @returns {Promise<void>}
174271
*/
175-
function writeReadme(content) {
176-
return new Promise((resolve, reject) => {
177-
fs.writeFile(README_PATH, content, "utf8", (err) => {
178-
if (err) {
179-
reject(new Error(`Failed to write updated README.md: ${err.message}`))
180-
return
181-
}
182-
resolve()
183-
})
184-
})
272+
async function writeReadme(content) {
273+
try {
274+
await writeFileAsync(README_PATH, content, "utf8")
275+
} catch (err) {
276+
throw new Error(`Failed to write updated README.md: ${err.message}`)
277+
}
185278
}
186279
/**
187280
* Finds all localized README files in the locales directory
188281
* @returns {Promise<string[]>} Array of README file paths
189282
*/
190-
function findLocalizedReadmes() {
191-
return new Promise((resolve) => {
192-
const readmeFiles = []
193-
194-
// Check if locales directory exists
195-
if (!fs.existsSync(LOCALES_DIR)) {
196-
// No localized READMEs found
197-
return resolve(readmeFiles)
198-
}
283+
async function findLocalizedReadmes() {
284+
const readmeFiles = []
199285

200-
// Get all language subdirectories
201-
const languageDirs = fs
202-
.readdirSync(LOCALES_DIR, { withFileTypes: true })
203-
.filter((dirent) => dirent.isDirectory())
204-
.map((dirent) => dirent.name)
205-
206-
// Add all localized READMEs to the list
207-
for (const langDir of languageDirs) {
208-
const readmePath = path.join(LOCALES_DIR, langDir, "README.md")
209-
if (fs.existsSync(readmePath)) {
210-
readmeFiles.push(readmePath)
211-
}
286+
// Check if locales directory exists
287+
if (!fs.existsSync(LOCALES_DIR)) {
288+
// No localized READMEs found
289+
return readmeFiles
290+
}
291+
292+
// Get all language subdirectories
293+
const languageDirs = fs
294+
.readdirSync(LOCALES_DIR, { withFileTypes: true })
295+
.filter((dirent) => dirent.isDirectory())
296+
.map((dirent) => dirent.name)
297+
298+
// Add all localized READMEs to the list
299+
for (const langDir of languageDirs) {
300+
const readmePath = path.join(LOCALES_DIR, langDir, "README.md")
301+
if (fs.existsSync(readmePath)) {
302+
readmeFiles.push(readmePath)
212303
}
304+
}
213305

214-
resolve(readmeFiles)
215-
})
306+
return readmeFiles
216307
}
217308

218309
/**
@@ -221,50 +312,43 @@ function findLocalizedReadmes() {
221312
* @param {string} contributorsSection HTML for contributors section
222313
* @returns {Promise<void>}
223314
*/
224-
function updateLocalizedReadme(filePath, contributorsSection) {
225-
return new Promise((resolve, reject) => {
226-
fs.readFile(filePath, "utf8", (err, readmeContent) => {
227-
if (err) {
228-
console.warn(`Warning: Could not read ${filePath}: ${err.message}`)
229-
return resolve()
230-
}
315+
async function updateLocalizedReadme(filePath, contributorsSection) {
316+
try {
317+
// Read the file content
318+
const readmeContent = await readFileAsync(filePath, "utf8")
231319

232-
// Find existing contributors section markers
233-
const startPos = readmeContent.indexOf(START_MARKER)
234-
const endPos = readmeContent.indexOf(END_MARKER)
320+
// Find existing contributors section markers
321+
const startPos = readmeContent.indexOf(START_MARKER)
322+
const endPos = readmeContent.indexOf(END_MARKER)
235323

236-
if (startPos === -1 || endPos === -1) {
237-
console.warn(`Warning: Could not find contributors section markers in ${filePath}`)
238-
console.warn(`Skipping update for ${filePath}`)
239-
return resolve()
240-
}
324+
if (startPos === -1 || endPos === -1) {
325+
console.warn(`Warning: Could not find contributors section markers in ${filePath}`)
326+
console.warn(`Skipping update for ${filePath}`)
327+
return
328+
}
241329

242-
// Replace existing section, trimming whitespace at section boundaries
243-
const beforeSection = readmeContent.substring(0, startPos).trimEnd()
244-
const afterSection = readmeContent.substring(endPos + END_MARKER.length).trimStart()
245-
// Ensure single newline separators between sections
246-
const updatedContent = beforeSection + "\n\n" + contributorsSection.trim() + "\n\n" + afterSection
247-
248-
fs.writeFile(filePath, updatedContent, "utf8", (writeErr) => {
249-
if (writeErr) {
250-
console.warn(`Warning: Failed to update ${filePath}: ${writeErr.message}`)
251-
return resolve()
252-
}
253-
console.log(`Updated ${filePath}`)
254-
resolve()
255-
})
256-
})
257-
})
330+
// Replace existing section, trimming whitespace at section boundaries
331+
const beforeSection = readmeContent.substring(0, startPos).trimEnd()
332+
const afterSection = readmeContent.substring(endPos + END_MARKER.length).trimStart()
333+
// Ensure single newline separators between sections
334+
const updatedContent = beforeSection + "\n\n" + contributorsSection.trim() + "\n\n" + afterSection
335+
336+
// Write the updated content
337+
await writeFileAsync(filePath, updatedContent, "utf8")
338+
console.log(`Updated ${filePath}`)
339+
} catch (err) {
340+
console.warn(`Warning: Could not update ${filePath}: ${err.message}`)
341+
}
258342
}
259343

260344
/**
261345
* Main function that orchestrates the update process
262346
*/
263347
async function main() {
264348
try {
265-
// Fetch contributors from GitHub
349+
// Fetch contributors from GitHub (now handles pagination)
266350
const contributors = await fetchContributors()
267-
console.log(`Fetched ${contributors.length} contributors from GitHub`)
351+
console.log(`Total contributors: ${contributors.length}`)
268352

269353
// Generate contributors section
270354
const contributorsSection = formatContributorsSection(contributors)

0 commit comments

Comments
 (0)