diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index c443602..6c0a8d3 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -23,10 +23,8 @@ jobs: with: node-version: '20' - - run: npm install - - name: Generate leaderboard - run: node scripts/updateReadme.js + run: node profile/scripts/updateReadme.js env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -34,6 +32,6 @@ jobs: run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" - git add README.md + git add profile/README.md git commit -m "Update leaderboard" || echo "No changes" git push diff --git a/profile/README.md b/profile/README.md index a83fd9d..8234ab7 100644 --- a/profile/README.md +++ b/profile/README.md @@ -51,6 +51,7 @@ Browse issues labeled **`good first issue`** across our repos to get started. # 🏆 ReactSphere Top Contributors + > Last updated: 2026-03-09 > Showing **Top 10 Contributors** @@ -66,6 +67,7 @@ Browse issues labeled **`good first issue`** across our repos to get started. | 8 | - | - | - | - | - | - | - | - | | 9 | - | - | - | - | - | - | - | - | | 10 | - | - | - | - | - | - | - | - | + _Generated automatically by the leaderboard workflow._ diff --git a/profile/scripts/updateReadme.js b/profile/scripts/updateReadme.js index b753a52..34aa111 100644 --- a/profile/scripts/updateReadme.js +++ b/profile/scripts/updateReadme.js @@ -1,17 +1,170 @@ -import fs from 'fs'; +'use strict'; -// Load the generated leaderboard table -const leaderboardTable = fs.readFileSync('./leaderboard.md', 'utf8'); +const fs = require('fs'); +const https = require('https'); +const path = require('path'); -// Load your main README -let readme = fs.readFileSync('README.md', 'utf8'); +const ORG = 'ReactSphere'; +const TOKEN = process.env.GITHUB_TOKEN; +const README_PATH = path.join(__dirname, '..', 'README.md'); +const TOP_N = 10; -// Replace the section between the markers -readme = readme.replace( - /[\s\S]*/, - `\n${leaderboardTable}\n` -); +function apiGet(apiPath) { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: apiPath, + headers: { + 'User-Agent': 'leaderboard-action', + 'Authorization': `token ${TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + }, + }; + https.get(options, (res) => { + let body = ''; + res.on('data', (chunk) => { body += chunk; }); + res.on('end', () => { + try { + resolve(JSON.parse(body)); + } catch (e) { + reject(new Error(`Failed to parse response (HTTP ${res.statusCode}) for ${apiPath}: ${body}`)); + } + }); + }).on('error', reject); + }); +} -// Write back to README -fs.writeFileSync('README.md', readme); -console.log('README leaderboard updated!'); +async function getAllPages(apiPath) { + const results = []; + for (let page = 1; ; page++) { + const sep = apiPath.includes('?') ? '&' : '?'; + const data = await apiGet(`${apiPath}${sep}per_page=100&page=${page}`); + if (!Array.isArray(data) || data.length === 0) break; + results.push(...data); + if (data.length < 100) break; + } + return results; +} + +async function main() { + if (!TOKEN) { + console.error('GITHUB_TOKEN is not set'); + process.exit(1); + } + + const repos = await getAllPages(`/orgs/${ORG}/repos?type=public`); + console.log(`Found ${repos.length} repos`); + + const contributors = {}; + + for (const repo of repos) { + // Commits + try { + const contribs = await getAllPages(`/repos/${ORG}/${repo.name}/contributors`); + for (const c of contribs) { + if (!contributors[c.login]) { + contributors[c.login] = { + login: c.login, + avatar_url: c.avatar_url, + commits: 0, + prs: 0, + issues: 0, + reviews: 0, + }; + } + contributors[c.login].commits += c.contributions; + } + } catch (e) { + console.warn(`Skipping contributors for ${repo.name}: ${e.message}`); + } + + // Merged PRs + try { + const prs = await getAllPages(`/repos/${ORG}/${repo.name}/pulls?state=closed`); + for (const pr of prs) { + if (!pr.merged_at) continue; + const login = pr.user && pr.user.login; + if (!login) continue; + if (!contributors[login]) { + contributors[login] = { + login, + avatar_url: pr.user.avatar_url, + commits: 0, + prs: 0, + issues: 0, + reviews: 0, + }; + } + contributors[login].prs += 1; + } + } catch (e) { + console.warn(`Skipping PRs for ${repo.name}: ${e.message}`); + } + + // Issues (not PRs) + try { + const issues = await getAllPages(`/repos/${ORG}/${repo.name}/issues?state=all`); + for (const issue of issues) { + if (issue.pull_request) continue; + const login = issue.user && issue.user.login; + if (!login) continue; + if (!contributors[login]) { + contributors[login] = { + login, + avatar_url: issue.user.avatar_url, + commits: 0, + prs: 0, + issues: 0, + reviews: 0, + }; + } + contributors[login].issues += 1; + } + } catch (e) { + console.warn(`Skipping issues for ${repo.name}: ${e.message}`); + } + } + + const sorted = Object.values(contributors) + .map((c) => ({ + ...c, + total: c.commits + c.prs * 3 + c.issues + c.reviews * 2, + })) + .sort((a, b) => b.total - a.total) + .slice(0, TOP_N); + + const medals = ['🥇', '🥈', '🥉']; + const rows = sorted.map((c, i) => { + const rank = i < medals.length ? medals[i] : String(i + 1); + const avatar = ``; + const username = `[@${c.login}](https://github.com/${c.login})`; + return `| ${rank} | ${avatar} | ${username} | **${c.total}** | ${c.commits} | ${c.prs} | ${c.issues} | ${c.reviews} | 0 |`; // Docs column: not tracked via API + }); + + while (rows.length < TOP_N) { + rows.push(`| ${rows.length + 1} | - | - | - | - | - | - | - | - |`); + } + + const today = new Date().toISOString().split('T')[0]; + const table = [ + `> Last updated: ${today} `, + `> Showing **Top ${TOP_N} Contributors**`, + '', + '| Rank | Avatar | Username | Total | Commits | PRs | Issues | Reviews | Docs |', + '|------|--------|----------|------:|--------:|----:|------:|--------:|----:|', + ...rows, + ].join('\n'); + + let readme = fs.readFileSync(README_PATH, 'utf8'); + readme = readme.replace( + /[\s\S]*?/, + `\n${table}\n` + ); + fs.writeFileSync(README_PATH, readme); + console.log('README leaderboard updated!'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});