diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml new file mode 100644 index 0000000..44c3546 --- /dev/null +++ b/.github/workflows/update-contributors.yml @@ -0,0 +1,43 @@ +name: Update contributors + +on: + push: + branches: + - main + - master + workflow_dispatch: + schedule: + # 每天北京时间 0:00(UTC 16:00)同步贡献者列表 + - cron: "0 16 * * *" + +permissions: + contents: write + +jobs: + update-contributors: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Generate contributors.json + run: node scripts/generate-contributors.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Commit and push if changed + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add assets/contributors.json + if git diff --staged --quiet; then + echo "contributors.json unchanged" + exit 0 + fi + git commit -m "chore: update contributors.json [skip ci]" + git push diff --git a/docs.js b/docs.js index b2ad016..1bdfcf2 100644 --- a/docs.js +++ b/docs.js @@ -900,40 +900,119 @@ }); } - var buddyContributorsState = { data: null, promise: null }; + var contributorsState = { data: null, promise: null }; - function fetchAllBuddyContributors(repo) { - if (buddyContributorsState.data) { - return Promise.resolve(buddyContributorsState.data); - } - if (buddyContributorsState.promise) { - return buddyContributorsState.promise; - } + var CONTRIBUTOR_REPOS = [ + "buddy-compiler/buddy-mlir", + "RuyiAI-Stack/ruyiai-stack.github.io", + "RuyiAI-Stack/triton-riscv", + "RuyiAI-Stack/tilelang-riscv", + "RuyiAI-Stack/pytorch", + "RuyiAI-Stack/llvm-project" + ]; + + var CONTRIBUTORS_JSON_URL = "./assets/contributors.json"; + + function fetchAllContributorsForRepo(repo) { var all = []; function fetchPage(page) { return fetch("https://api.github.com/repos/" + repo + "/contributors?per_page=100&anon=1&page=" + page) .then(function (r) { + if (r.status === 403) { + return Promise.reject(new Error("rate_limit")); + } if (!r.ok) throw new Error(String(r.status)); return r.json(); }) .then(function (list) { + if (!Array.isArray(list)) throw new Error("invalid_response"); if (!list.length) return all; all = all.concat(list); if (list.length < 100) return all; return fetchPage(page + 1); }); } - buddyContributorsState.promise = fetchPage(1) - .then(function (data) { - buddyContributorsState.data = data; - buddyContributorsState.promise = null; - return data; + return fetchPage(1); + } + + function contributorKey(c) { + if (c.login) return "login:" + String(c.login).toLowerCase(); + if (c.id != null) return "id:" + c.id; + return "anon:" + (c.avatar_url || ""); + } + + /** 按仓库顺序合并贡献者,login/id 去重,保留首次出现顺序 */ + function mergeContributorsDedup(repoLists) { + var seen = {}; + var merged = []; + repoLists.forEach(function (list) { + (list || []).forEach(function (c) { + var key = contributorKey(c); + if (seen[key]) return; + seen[key] = true; + merged.push(c); + }); + }); + return merged; + } + + function loadContributorsFromStaticJson() { + return fetch(resolveDocUrl(CONTRIBUTORS_JSON_URL), { cache: "no-store" }) + .then(function (r) { + if (!r.ok) throw new Error(String(r.status)); + return r.json(); + }) + .then(function (payload) { + if (payload && Array.isArray(payload.contributors)) return payload.contributors; + if (Array.isArray(payload)) return payload; + throw new Error("invalid_json"); + }); + } + + /** 逐仓库拉取,单个失败不影响其余仓库 */ + function fetchMergedContributorsFromApi(repos) { + function fetchRepoAt(idx, acc) { + if (idx >= repos.length) return Promise.resolve(acc); + return fetchAllContributorsForRepo(repos[idx]) + .then(function (list) { + acc.push(list); + return fetchRepoAt(idx + 1, acc); + }) + .catch(function () { + acc.push([]); + return fetchRepoAt(idx + 1, acc); + }); + } + return fetchRepoAt(0, []).then(function (repoLists) { + var merged = mergeContributorsDedup(repoLists); + if (!merged.length) { + return Promise.reject(new Error("all_failed")); + } + return merged; + }); + } + + function fetchMergedContributors(repos) { + if (contributorsState.data) { + return Promise.resolve(contributorsState.data); + } + if (contributorsState.promise) { + return contributorsState.promise; + } + contributorsState.promise = loadContributorsFromStaticJson() + .catch(function () { + return fetchMergedContributorsFromApi(repos); + }) + .then(function (merged) { + contributorsState.data = merged; + contributorsState.promise = null; + return merged; }) .catch(function (err) { - buddyContributorsState.promise = null; + contributorsState.promise = null; throw err; }); - return buddyContributorsState.promise; + return contributorsState.promise; } function contributorStatsText(count, lang) { @@ -977,10 +1056,9 @@ function loadBuddyContributors(bodyEl) { var container = bodyEl ? bodyEl.querySelector("#buddyContributors") : null; if (!container) return; - var repo = container.getAttribute("data-repo") || "buddy-compiler/buddy-mlir"; var lang = getCurrentLang(); container.innerHTML = '

' + (lang === "en" ? "Loading contributors…" : "加载贡献者中…") + "

"; - fetchAllBuddyContributors(repo) + fetchMergedContributors(CONTRIBUTOR_REPOS) .then(function (contributors) { if (!bodyEl.querySelector("#buddyContributors")) return; renderBuddyContributorsGrid(container, contributors); diff --git a/docs/about/about-en.md b/docs/about/about-en.md index 596f25e..30c2d61 100644 --- a/docs/about/about-en.md +++ b/docs/about/about-en.md @@ -32,4 +32,4 @@ Upholding the philosophy of open collaboration, we are committed to working with ## Contributors -
+
diff --git a/docs/about/about.md b/docs/about/about.md index 6159fd2..7319405 100644 --- a/docs/about/about.md +++ b/docs/about/about.md @@ -28,4 +28,4 @@ RuyiAI 由 **中国科学院软件研究所(ISCAS)智能软件研究中心** ## 贡献者 -
+
diff --git a/scripts/generate-contributors.js b/scripts/generate-contributors.js new file mode 100644 index 0000000..ac21fa9 --- /dev/null +++ b/scripts/generate-contributors.js @@ -0,0 +1,122 @@ +#!/usr/bin/env node +/** + * 拉取各仓库贡献者并生成 assets/contributors.json(去重、保留首次出现顺序)。 + * 用法:node scripts/generate-contributors.js + * 未认证 GitHub API 限流 60 次/小时;若失败请稍后重试或设置 GITHUB_TOKEN。 + */ +"use strict"; + +var fs = require("fs"); +var path = require("path"); +var https = require("https"); + +var REPOS = [ + "buddy-compiler/buddy-mlir", + "RuyiAI-Stack/ruyiai-stack.github.io", + "RuyiAI-Stack/triton-riscv", + "RuyiAI-Stack/tilelang-riscv", + "RuyiAI-Stack/pytorch", + "RuyiAI-Stack/llvm-project" +]; + +var OUT = path.join(__dirname, "..", "assets", "contributors.json"); +var TOKEN = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || ""; + +function ghGet(url) { + return new Promise(function (resolve, reject) { + var headers = { "User-Agent": "ruyiai-stack-contributors-script", Accept: "application/vnd.github+json" }; + if (TOKEN) headers.Authorization = "Bearer " + TOKEN; + https.get(url, { headers: headers }, function (res) { + var body = ""; + res.on("data", function (c) { body += c; }); + res.on("end", function () { + if (res.statusCode === 403 && body.indexOf("rate limit") !== -1) { + reject(new Error("GitHub API rate limit exceeded. Set GITHUB_TOKEN or retry later.")); + return; + } + if (res.statusCode < 200 || res.statusCode >= 300) { + reject(new Error("HTTP " + res.statusCode + " for " + url + ": " + body.slice(0, 200))); + return; + } + try { + resolve(JSON.parse(body)); + } catch (e) { + reject(e); + } + }); + }).on("error", reject); + }); +} + +function fetchRepoContributors(repo) { + var all = []; + function page(n) { + var url = "https://api.github.com/repos/" + repo + "/contributors?per_page=100&anon=1&page=" + n; + return ghGet(url).then(function (list) { + if (!list.length) return all; + all = all.concat(list); + if (list.length < 100) return all; + return page(n + 1); + }); + } + return page(1); +} + +function contributorKey(c) { + if (c.login) return "login:" + String(c.login).toLowerCase(); + if (c.id != null) return "id:" + c.id; + return "anon:" + (c.avatar_url || ""); +} + +function mergeDedup(repoLists) { + var seen = {}; + var merged = []; + repoLists.forEach(function (list) { + list.forEach(function (c) { + var key = contributorKey(c); + if (seen[key]) return; + seen[key] = true; + merged.push({ + login: c.login || null, + id: c.id, + avatar_url: c.avatar_url, + html_url: c.html_url || (c.login ? "https://github.com/" + c.login : null) + }); + }); + }); + return merged; +} + +function fetchAllSequential(repos) { + var results = []; + function next(i) { + if (i >= repos.length) return Promise.resolve(results); + var repo = repos[i]; + process.stderr.write("Fetching " + repo + "…\n"); + return fetchRepoContributors(repo) + .then(function (list) { + process.stderr.write(" " + list.length + " contributors\n"); + results.push(list); + return next(i + 1); + }); + } + return next(0); +} + +fetchAllSequential(REPOS) + .then(function (repoLists) { + var contributors = mergeDedup(repoLists); + var payload = { + generatedAt: new Date().toISOString(), + repos: REPOS, + count: contributors.length, + contributors: contributors + }; + fs.mkdirSync(path.dirname(OUT), { recursive: true }); + fs.writeFileSync(OUT, JSON.stringify(payload, null, 2) + "\n"); + process.stderr.write("Wrote " + contributors.length + " contributors to " + OUT + "\n"); + }) + .catch(function (err) { + process.stderr.write(String(err.message || err) + "\n"); + process.exit(1); + });