update-contributors.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. #!/usr/bin/env node
  2. /**
  3. * This script fetches contributor data from GitHub and updates the README.md file
  4. * with a contributors section showing avatars and usernames.
  5. * It also updates all localized README files in the locales directory.
  6. */
  7. const https = require("https")
  8. const fs = require("fs")
  9. const { promisify } = require("util")
  10. const path = require("path")
  11. // Promisify filesystem operations
  12. const readFileAsync = promisify(fs.readFile)
  13. const writeFileAsync = promisify(fs.writeFile)
  14. // GitHub API URL for fetching contributors
  15. const GITHUB_API_URL = "https://api.github.com/repos/RooCodeInc/Roo-Code/contributors?per_page=100"
  16. const README_PATH = path.join(__dirname, "..", "README.md")
  17. const LOCALES_DIR = path.join(__dirname, "..", "locales")
  18. // Sentinel markers for contributors section
  19. const START_MARKER = "<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->"
  20. const END_MARKER = "<!-- END CONTRIBUTORS SECTION -->"
  21. // HTTP options for GitHub API request
  22. const options = {
  23. headers: {
  24. "User-Agent": "Roo-Code-Contributors-Script",
  25. },
  26. }
  27. // Add GitHub token for authentication if available
  28. if (process.env.GITHUB_TOKEN) {
  29. options.headers.Authorization = `token ${process.env.GITHUB_TOKEN}`
  30. console.log("Using GitHub token from environment variable")
  31. }
  32. /**
  33. * Parses the GitHub API Link header to extract pagination URLs
  34. * Based on RFC 5988 format for the Link header
  35. * @param {string} header The Link header from GitHub API response
  36. * @returns {Object} Object containing URLs for next, prev, first, last pages (if available)
  37. */
  38. function parseLinkHeader(header) {
  39. // Return empty object if no header is provided
  40. if (!header || header.trim() === "") return {}
  41. // Initialize links object
  42. const links = {}
  43. // Split the header into individual link entries
  44. // Example: <https://api.github.com/...?page=2>; rel="next", <https://api.github.com/...?page=5>; rel="last"
  45. const entries = header.split(/,\s*/)
  46. // Process each link entry
  47. for (const entry of entries) {
  48. // Extract the URL (between < and >) and the parameters (after >)
  49. const segments = entry.split(";")
  50. if (segments.length < 2) continue
  51. // Extract URL from the first segment, removing < and >
  52. const urlMatch = segments[0].match(/<(.+)>/)
  53. if (!urlMatch) continue
  54. const url = urlMatch[1]
  55. // Find the rel="value" parameter
  56. let rel = null
  57. for (let i = 1; i < segments.length; i++) {
  58. const relMatch = segments[i].match(/\s*rel\s*=\s*"?([^"]+)"?/)
  59. if (relMatch) {
  60. rel = relMatch[1]
  61. break
  62. }
  63. }
  64. // Only add to links if both URL and rel were found
  65. if (rel) {
  66. links[rel] = url
  67. }
  68. }
  69. return links
  70. }
  71. /**
  72. * Performs an HTTP GET request and returns the response
  73. * @param {string} url The URL to fetch
  74. * @param {Object} options Request options
  75. * @returns {Promise<Object>} Response object with status, headers and body
  76. */
  77. function httpGet(url, options) {
  78. return new Promise((resolve, reject) => {
  79. https
  80. .get(url, options, (res) => {
  81. let data = ""
  82. res.on("data", (chunk) => {
  83. data += chunk
  84. })
  85. res.on("end", () => {
  86. resolve({
  87. statusCode: res.statusCode,
  88. headers: res.headers,
  89. body: data,
  90. })
  91. })
  92. })
  93. .on("error", (error) => {
  94. reject(error)
  95. })
  96. })
  97. }
  98. /**
  99. * Fetches a single page of contributors from GitHub API
  100. * @param {string} url The API URL to fetch
  101. * @returns {Promise<Object>} Object containing contributors and pagination links
  102. */
  103. async function fetchContributorsPage(url) {
  104. try {
  105. // Make the HTTP request
  106. const response = await httpGet(url, options)
  107. // Check for successful response
  108. if (response.statusCode !== 200) {
  109. throw new Error(`GitHub API request failed with status code: ${response.statusCode}`)
  110. }
  111. // Parse the Link header for pagination
  112. const linkHeader = response.headers.link
  113. const links = parseLinkHeader(linkHeader)
  114. // Parse the JSON response
  115. const contributors = JSON.parse(response.body)
  116. return { contributors, links }
  117. } catch (error) {
  118. throw new Error(`Failed to fetch contributors page: ${error.message}`)
  119. }
  120. }
  121. /**
  122. * Fetches all contributors data from GitHub API (handling pagination)
  123. * @returns {Promise<Array>} Array of all contributor objects
  124. */
  125. async function fetchContributors() {
  126. let allContributors = []
  127. let currentUrl = GITHUB_API_URL
  128. let pageCount = 1
  129. // Loop through all pages of contributors
  130. while (currentUrl) {
  131. console.log(`Fetching contributors page ${pageCount}...`)
  132. const { contributors, links } = await fetchContributorsPage(currentUrl)
  133. allContributors = allContributors.concat(contributors)
  134. // Move to the next page if it exists
  135. currentUrl = links.next
  136. pageCount++
  137. }
  138. console.log(`Fetched ${allContributors.length} contributors from ${pageCount - 1} pages`)
  139. return allContributors
  140. }
  141. /**
  142. * Reads the README.md file
  143. * @returns {Promise<string>} README content
  144. */
  145. async function readReadme() {
  146. try {
  147. return await readFileAsync(README_PATH, "utf8")
  148. } catch (err) {
  149. throw new Error(`Failed to read README.md: ${err.message}`)
  150. }
  151. }
  152. /**
  153. * Creates HTML for the contributors section
  154. * @param {Array} contributors Array of contributor objects from GitHub API
  155. * @returns {string} HTML for contributors section
  156. */
  157. function formatContributorsSection(contributors) {
  158. // Filter out GitHub Actions bot
  159. const filteredContributors = contributors.filter((c) => !c.login.includes("[bot]") && !c.login.includes("R00-B0T"))
  160. // Start building with Markdown table format
  161. let markdown = `${START_MARKER}
  162. `
  163. // Number of columns in the table
  164. const COLUMNS = 6
  165. // Create contributor cell HTML
  166. const createCell = (contributor) => {
  167. return `<a href="${contributor.html_url}"><img src="${contributor.avatar_url}" width="100" height="100" alt="${contributor.login}"/><br /><sub><b>${contributor.login}</b></sub></a>`
  168. }
  169. if (filteredContributors.length > 0) {
  170. // Table header is the first row of contributors
  171. const headerCells = filteredContributors.slice(0, COLUMNS).map(createCell)
  172. // Fill any empty cells in header row
  173. while (headerCells.length < COLUMNS) {
  174. headerCells.push(" ")
  175. }
  176. // Add header row
  177. markdown += `|${headerCells.join("|")}|\n`
  178. // Add alignment row
  179. markdown += "|"
  180. for (let i = 0; i < COLUMNS; i++) {
  181. markdown += ":---:|"
  182. }
  183. markdown += "\n"
  184. // Add remaining contributor rows starting with the second batch
  185. for (let i = COLUMNS; i < filteredContributors.length; i += COLUMNS) {
  186. const rowContributors = filteredContributors.slice(i, i + COLUMNS)
  187. // Create cells for each contributor in this row
  188. const cells = rowContributors.map(createCell)
  189. // Fill any empty cells to maintain table structure
  190. while (cells.length < COLUMNS) {
  191. cells.push(" ")
  192. }
  193. // Add row to the table
  194. markdown += `|${cells.join("|")}|\n`
  195. }
  196. }
  197. markdown += `${END_MARKER}`
  198. return markdown
  199. }
  200. /**
  201. * Updates the README.md file with contributors section
  202. * @param {string} readmeContent Original README content
  203. * @param {string} contributorsSection HTML for contributors section
  204. * @returns {Promise<void>}
  205. */
  206. async function updateReadme(readmeContent, contributorsSection) {
  207. // Find existing contributors section markers
  208. const startPos = readmeContent.indexOf(START_MARKER)
  209. const endPos = readmeContent.indexOf(END_MARKER)
  210. if (startPos === -1 || endPos === -1) {
  211. console.warn("Warning: Could not find contributors section markers in README.md")
  212. console.warn("Skipping update - please add markers to enable automatic updates.")
  213. return
  214. }
  215. // Replace existing section, trimming whitespace at section boundaries
  216. const beforeSection = readmeContent.substring(0, startPos).trimEnd()
  217. const afterSection = readmeContent.substring(endPos + END_MARKER.length).trimStart()
  218. // Ensure single newline separators between sections
  219. const updatedContent = beforeSection + "\n\n" + contributorsSection.trim() + "\n\n" + afterSection
  220. await writeReadme(updatedContent)
  221. }
  222. /**
  223. * Writes updated content to README.md
  224. * @param {string} content Updated README content
  225. * @returns {Promise<void>}
  226. */
  227. async function writeReadme(content) {
  228. try {
  229. await writeFileAsync(README_PATH, content, "utf8")
  230. } catch (err) {
  231. throw new Error(`Failed to write updated README.md: ${err.message}`)
  232. }
  233. }
  234. /**
  235. * Finds all localized README files in the locales directory
  236. * @returns {Promise<string[]>} Array of README file paths
  237. */
  238. async function findLocalizedReadmes() {
  239. const readmeFiles = []
  240. // Check if locales directory exists
  241. if (!fs.existsSync(LOCALES_DIR)) {
  242. // No localized READMEs found
  243. return readmeFiles
  244. }
  245. // Get all language subdirectories
  246. const languageDirs = fs
  247. .readdirSync(LOCALES_DIR, { withFileTypes: true })
  248. .filter((dirent) => dirent.isDirectory())
  249. .map((dirent) => dirent.name)
  250. // Add all localized READMEs to the list
  251. for (const langDir of languageDirs) {
  252. const readmePath = path.join(LOCALES_DIR, langDir, "README.md")
  253. if (fs.existsSync(readmePath)) {
  254. readmeFiles.push(readmePath)
  255. }
  256. }
  257. return readmeFiles
  258. }
  259. /**
  260. * Updates a localized README file with contributors section
  261. * @param {string} filePath Path to the README file
  262. * @param {string} contributorsSection HTML for contributors section
  263. * @returns {Promise<void>}
  264. */
  265. async function updateLocalizedReadme(filePath, contributorsSection) {
  266. try {
  267. // Read the file content
  268. const readmeContent = await readFileAsync(filePath, "utf8")
  269. // Find existing contributors section markers
  270. const startPos = readmeContent.indexOf(START_MARKER)
  271. const endPos = readmeContent.indexOf(END_MARKER)
  272. if (startPos === -1 || endPos === -1) {
  273. console.warn(`Warning: Could not find contributors section markers in ${filePath}`)
  274. console.warn(`Skipping update for ${filePath}`)
  275. return
  276. }
  277. // Replace existing section, trimming whitespace at section boundaries
  278. const beforeSection = readmeContent.substring(0, startPos).trimEnd()
  279. const afterSection = readmeContent.substring(endPos + END_MARKER.length).trimStart()
  280. // Ensure single newline separators between sections
  281. const updatedContent = beforeSection + "\n\n" + contributorsSection.trim() + "\n\n" + afterSection
  282. // Write the updated content
  283. await writeFileAsync(filePath, updatedContent, "utf8")
  284. console.log(`Updated ${filePath}`)
  285. } catch (err) {
  286. console.warn(`Warning: Could not update ${filePath}: ${err.message}`)
  287. }
  288. }
  289. /**
  290. * Main function that orchestrates the update process
  291. */
  292. async function main() {
  293. try {
  294. // Fetch contributors from GitHub (now handles pagination)
  295. const contributors = await fetchContributors()
  296. console.log(`Total contributors: ${contributors.length}`)
  297. // Generate contributors section
  298. const contributorsSection = formatContributorsSection(contributors)
  299. // Update main README
  300. const readmeContent = await readReadme()
  301. await updateReadme(readmeContent, contributorsSection)
  302. console.log(`Updated ${README_PATH}`)
  303. // Find and update all localized README files
  304. const localizedReadmes = await findLocalizedReadmes()
  305. console.log(`Found ${localizedReadmes.length} localized README files`)
  306. // Update each localized README
  307. for (const readmePath of localizedReadmes) {
  308. await updateLocalizedReadme(readmePath, contributorsSection)
  309. }
  310. console.log("Contributors section update complete")
  311. } catch (error) {
  312. console.error(`Error: ${error.message}`)
  313. process.exit(1)
  314. }
  315. }
  316. // Run the script
  317. main()