update-contributors.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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. const EXCLUDED_LOGIN_SUBSTRINGS = ['[bot]', 'R00-B0T'];
  158. const EXCLUDED_LOGIN_EXACTS = ['cursor', 'roomote'];
  159. function formatContributorsSection(contributors) {
  160. // Filter out GitHub Actions bot, cursor, and roomote
  161. const filteredContributors = contributors.filter((c) =>
  162. !EXCLUDED_LOGIN_SUBSTRINGS.some(sub => c.login.includes(sub)) &&
  163. !EXCLUDED_LOGIN_EXACTS.includes(c.login)
  164. )
  165. // Start building with Markdown table format
  166. let markdown = `${START_MARKER}
  167. `
  168. // Number of columns in the table
  169. const COLUMNS = 6
  170. // Create contributor cell HTML
  171. const createCell = (contributor) => {
  172. 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>`
  173. }
  174. if (filteredContributors.length > 0) {
  175. // Table header is the first row of contributors
  176. const headerCells = filteredContributors.slice(0, COLUMNS).map(createCell)
  177. // Fill any empty cells in header row
  178. while (headerCells.length < COLUMNS) {
  179. headerCells.push(" ")
  180. }
  181. // Add header row
  182. markdown += `|${headerCells.join("|")}|\n`
  183. // Add alignment row
  184. markdown += "|"
  185. for (let i = 0; i < COLUMNS; i++) {
  186. markdown += ":---:|"
  187. }
  188. markdown += "\n"
  189. // Add remaining contributor rows starting with the second batch
  190. for (let i = COLUMNS; i < filteredContributors.length; i += COLUMNS) {
  191. const rowContributors = filteredContributors.slice(i, i + COLUMNS)
  192. // Create cells for each contributor in this row
  193. const cells = rowContributors.map(createCell)
  194. // Fill any empty cells to maintain table structure
  195. while (cells.length < COLUMNS) {
  196. cells.push(" ")
  197. }
  198. // Add row to the table
  199. markdown += `|${cells.join("|")}|\n`
  200. }
  201. }
  202. markdown += `${END_MARKER}`
  203. return markdown
  204. }
  205. /**
  206. * Updates the README.md file with contributors section
  207. * @param {string} readmeContent Original README content
  208. * @param {string} contributorsSection HTML for contributors section
  209. * @returns {Promise<void>}
  210. */
  211. async function updateReadme(readmeContent, contributorsSection) {
  212. // Find existing contributors section markers
  213. const startPos = readmeContent.indexOf(START_MARKER)
  214. const endPos = readmeContent.indexOf(END_MARKER)
  215. if (startPos === -1 || endPos === -1) {
  216. console.warn("Warning: Could not find contributors section markers in README.md")
  217. console.warn("Skipping update - please add markers to enable automatic updates.")
  218. return
  219. }
  220. // Replace existing section, trimming whitespace at section boundaries
  221. const beforeSection = readmeContent.substring(0, startPos).trimEnd()
  222. const afterSection = readmeContent.substring(endPos + END_MARKER.length).trimStart()
  223. // Ensure single newline separators between sections
  224. const updatedContent = beforeSection + "\n\n" + contributorsSection.trim() + "\n\n" + afterSection
  225. await writeReadme(updatedContent)
  226. }
  227. /**
  228. * Writes updated content to README.md
  229. * @param {string} content Updated README content
  230. * @returns {Promise<void>}
  231. */
  232. async function writeReadme(content) {
  233. try {
  234. await writeFileAsync(README_PATH, content, "utf8")
  235. } catch (err) {
  236. throw new Error(`Failed to write updated README.md: ${err.message}`)
  237. }
  238. }
  239. /**
  240. * Finds all localized README files in the locales directory
  241. * @returns {Promise<string[]>} Array of README file paths
  242. */
  243. async function findLocalizedReadmes() {
  244. const readmeFiles = []
  245. // Check if locales directory exists
  246. if (!fs.existsSync(LOCALES_DIR)) {
  247. // No localized READMEs found
  248. return readmeFiles
  249. }
  250. // Get all language subdirectories
  251. const languageDirs = fs
  252. .readdirSync(LOCALES_DIR, { withFileTypes: true })
  253. .filter((dirent) => dirent.isDirectory())
  254. .map((dirent) => dirent.name)
  255. // Add all localized READMEs to the list
  256. for (const langDir of languageDirs) {
  257. const readmePath = path.join(LOCALES_DIR, langDir, "README.md")
  258. if (fs.existsSync(readmePath)) {
  259. readmeFiles.push(readmePath)
  260. }
  261. }
  262. return readmeFiles
  263. }
  264. /**
  265. * Updates a localized README file with contributors section
  266. * @param {string} filePath Path to the README file
  267. * @param {string} contributorsSection HTML for contributors section
  268. * @returns {Promise<void>}
  269. */
  270. async function updateLocalizedReadme(filePath, contributorsSection) {
  271. try {
  272. // Read the file content
  273. const readmeContent = await readFileAsync(filePath, "utf8")
  274. // Find existing contributors section markers
  275. const startPos = readmeContent.indexOf(START_MARKER)
  276. const endPos = readmeContent.indexOf(END_MARKER)
  277. if (startPos === -1 || endPos === -1) {
  278. console.warn(`Warning: Could not find contributors section markers in ${filePath}`)
  279. console.warn(`Skipping update for ${filePath}`)
  280. return
  281. }
  282. // Replace existing section, trimming whitespace at section boundaries
  283. const beforeSection = readmeContent.substring(0, startPos).trimEnd()
  284. const afterSection = readmeContent.substring(endPos + END_MARKER.length).trimStart()
  285. // Ensure single newline separators between sections
  286. const updatedContent = beforeSection + "\n\n" + contributorsSection.trim() + "\n\n" + afterSection
  287. // Write the updated content
  288. await writeFileAsync(filePath, updatedContent, "utf8")
  289. console.log(`Updated ${filePath}`)
  290. } catch (err) {
  291. console.warn(`Warning: Could not update ${filePath}: ${err.message}`)
  292. }
  293. }
  294. /**
  295. * Main function that orchestrates the update process
  296. */
  297. async function main() {
  298. try {
  299. // Fetch contributors from GitHub (now handles pagination)
  300. const contributors = await fetchContributors()
  301. console.log(`Total contributors: ${contributors.length}`)
  302. // Generate contributors section
  303. const contributorsSection = formatContributorsSection(contributors)
  304. // Update main README
  305. const readmeContent = await readReadme()
  306. await updateReadme(readmeContent, contributorsSection)
  307. console.log(`Updated ${README_PATH}`)
  308. // Find and update all localized README files
  309. const localizedReadmes = await findLocalizedReadmes()
  310. console.log(`Found ${localizedReadmes.length} localized README files`)
  311. // Update each localized README
  312. for (const readmePath of localizedReadmes) {
  313. await updateLocalizedReadme(readmePath, contributorsSection)
  314. }
  315. console.log("Contributors section update complete")
  316. } catch (error) {
  317. console.error(`Error: ${error.message}`)
  318. process.exit(1)
  319. }
  320. }
  321. // Run the script
  322. main()