Skip to content

Commit 984dbfb

Browse files
committed
docs: add workflow to collect github stats
1 parent 68ad42e commit 984dbfb

8 files changed

Lines changed: 490 additions & 1 deletion

File tree

.github/workflows/daily-stats.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Daily Repo Stats to gh-pages
2+
3+
on:
4+
schedule:
5+
- cron: '0 2 * * *' # UTC 2:00 = Beijing 10:00
6+
workflow_dispatch:
7+
8+
jobs:
9+
update-stats:
10+
runs-on: ubuntu-latest
11+
defaults:
12+
run:
13+
working-directory: docs_roll
14+
permissions:
15+
contents: write
16+
17+
steps:
18+
- name: Checkout master branch
19+
uses: actions/checkout@v4
20+
with:
21+
ref: master
22+
23+
- name: Fetch current stats.json from gh-pages
24+
run: |
25+
mkdir -p ./public-data
26+
curl -s -o ./public-data/stats.json \
27+
https://raw.githubusercontent.com/${{ github.repository }}/gh-pages/stats.json || echo "{}" > ./public-data/stats.json
28+
29+
- name: Setup Node
30+
uses: actions/setup-node@v4
31+
with:
32+
node-version: '20'
33+
34+
- name: Cache dependencies
35+
uses: actions/cache@v4
36+
with:
37+
path: ~/.npm
38+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
39+
restore-keys: |
40+
${{ runner.os }}-node-
41+
42+
- name: Install dependencies
43+
run: npm install octokit
44+
45+
- name: Run stats script
46+
env:
47+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48+
REPO: ${{ github.repository }}
49+
STATS_FILE: ./public-data/stats.json
50+
run: node scripts/update-stats.js
51+
52+
- name: show stats.json content
53+
run: cat ./public-data/stats.json
54+
55+
- name: Deploy to gh-pages
56+
uses: peaceiris/actions-gh-pages@v4
57+
with:
58+
github_token: ${{ secrets.GITHUB_TOKEN }}
59+
publish_dir: ./docs_roll/public-data
60+
destination_dir: .
61+
keep_files: true

.github/workflows/deploy.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@ jobs:
4545
if: github.ref == 'refs/heads/main'
4646
with:
4747
github_token: ${{ secrets.GITHUB_TOKEN }}
48-
publish_dir: ./docs_roll/build
48+
publish_dir: ./docs_roll/build
49+
keep_files: true

docs_roll/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"antd": "^5.26.7",
2424
"axios": "^1.11.0",
2525
"clsx": "^1.2.1",
26+
"echarts": "^5.6.0",
27+
"octokit": "^5.0.5",
2628
"prism-react-renderer": "^2.1.0",
2729
"react": "^18.2.0",
2830
"react-dom": "^18.2.0"

docs_roll/scripts/update-stats.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env node
2+
// scripts/update-stats.js
3+
const fs = require('fs');
4+
5+
const OUTPUT_PATH = process.env.STATS_FILE || 'build/stats.json';
6+
7+
async function main() {
8+
const { Octokit } = await import('octokit');
9+
10+
const token = process.env.GITHUB_TOKEN;
11+
const repo = process.env.REPO; // format: owner/repo
12+
13+
if (!token || !repo) {
14+
throw new Error('Missing GITHUB_TOKEN or REPO environment variable');
15+
}
16+
17+
const [owner, repoName] = repo.split('/');
18+
const octokit = new Octokit({ auth: token });
19+
20+
console.log(`Fetching stats for ${owner}/${repoName}...`);
21+
22+
// 1. Repo basic info
23+
const repoData = await octokit.rest.repos.get({ owner, repo: repoName });
24+
const stars = repoData.data.stargazers_count;
25+
const forks = repoData.data.forks_count;
26+
27+
// 2. Contributors (all pages)
28+
const contributors = await octokit.paginate(octokit.rest.repos.listContributors, {
29+
owner,
30+
repo: repoName,
31+
per_page: 100,
32+
});
33+
const contributorCount = contributors.length;
34+
35+
// 3. Issues (all & open) — includes PRs
36+
const issuesAll = await octokit.paginate(octokit.rest.issues.listForRepo, {
37+
owner,
38+
repo: repoName,
39+
state: 'all',
40+
per_page: 100,
41+
});
42+
const issuesOpen = await octokit.paginate(octokit.rest.issues.listForRepo, {
43+
owner,
44+
repo: repoName,
45+
state: 'open',
46+
per_page: 100,
47+
});
48+
49+
const totalIssues = issuesAll.length;
50+
const openIssues = issuesOpen.length;
51+
52+
// 4. Pull Requests (all & open)
53+
const prsAll = await octokit.paginate(octokit.rest.pulls.list, {
54+
owner,
55+
repo: repoName,
56+
state: 'all',
57+
per_page: 100,
58+
});
59+
const prsOpen = await octokit.paginate(octokit.rest.pulls.list, {
60+
owner,
61+
repo: repoName,
62+
state: 'open',
63+
per_page: 100,
64+
});
65+
66+
const totalPRs = prsAll.length;
67+
const openPRs = prsOpen.length;
68+
69+
// Pure issues = total issues - PRs
70+
const pureTotalIssues = totalIssues - totalPRs;
71+
const pureOpenIssues = openIssues - openPRs;
72+
73+
const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
74+
75+
// Load existing data
76+
let stats = {};
77+
if (fs.existsSync(OUTPUT_PATH)) {
78+
try {
79+
stats = JSON.parse(fs.readFileSync(OUTPUT_PATH, 'utf8'));
80+
}
81+
catch (err) {
82+
console.log('fail to get stats file')
83+
}
84+
}
85+
86+
stats[date] = {
87+
stars,
88+
forks,
89+
contributors: contributorCount,
90+
issues: {
91+
total: pureTotalIssues,
92+
open: pureOpenIssues,
93+
fixRate: parseInt(100 - (pureOpenIssues / pureTotalIssues) * 100, 10),
94+
},
95+
prs: {
96+
total: totalPRs,
97+
open: openPRs,
98+
},
99+
};
100+
101+
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(stats, null, 2));
102+
console.log(`✅ Stats updated for ${date}`);
103+
}
104+
105+
main().catch(err => {
106+
console.error('❌ Error:', err.message);
107+
process.exit(1);
108+
});
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
2+
import * as echarts from 'echarts/core';
3+
// 引入柱状图图表,图表后缀都为 Chart
4+
import { LineChart } from 'echarts/charts';
5+
// 引入标题,提示框,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
6+
import {
7+
DatasetComponent,
8+
DataZoomComponent,
9+
GraphicComponent,
10+
GridComponent,
11+
LegendComponent,
12+
TitleComponent,
13+
TooltipComponent,
14+
TransformComponent,
15+
MarkLineComponent,
16+
MarkPointComponent,
17+
} from 'echarts/components';
18+
// 标签自动布局、全局过渡动画等特性
19+
20+
import { LabelLayout, UniversalTransition } from 'echarts/features';
21+
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
22+
import { CanvasRenderer } from 'echarts/renderers';
23+
import { useColorMode } from '@docusaurus/theme-common';
24+
25+
// 注册必须的组件
26+
echarts.use([
27+
LineChart,
28+
GraphicComponent,
29+
TitleComponent,
30+
TooltipComponent,
31+
LegendComponent,
32+
GridComponent,
33+
DatasetComponent,
34+
TransformComponent,
35+
LegendComponent,
36+
LabelLayout,
37+
UniversalTransition,
38+
CanvasRenderer,
39+
DataZoomComponent,
40+
MarkLineComponent,
41+
MarkPointComponent,
42+
]);
43+
44+
import { useEffect, useRef } from 'react';
45+
46+
const EchartsView = (props) => {
47+
const { option, className, style, onEvents = {}, onInit } = props;
48+
const echartsRef = useRef(null);
49+
const chartInstanceRef = useRef(null);
50+
const { colorMode } = useColorMode();
51+
52+
useEffect(() => {
53+
// 确保 DOM 元素存在且有尺寸
54+
if (!echartsRef.current) return;
55+
// 检查容器是否有尺寸
56+
const container = echartsRef.current;
57+
if (container.clientWidth === 0 || container.clientHeight === 0) {
58+
// 如果容器没有尺寸,使用 ResizeObserver 监听尺寸变化
59+
const resizeObserver = new ResizeObserver((entries) => {
60+
for (let entry of entries) {
61+
if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
62+
// 容器有尺寸了,初始化图表
63+
initializeChart();
64+
resizeObserver.disconnect();
65+
break;
66+
}
67+
}
68+
});
69+
70+
resizeObserver.observe(container);
71+
// 添加一个超时机制作为后备方案
72+
const timeoutId = setTimeout(() => {
73+
resizeObserver.disconnect();
74+
if (container.clientWidth > 0 || container.clientHeight > 0) {
75+
initializeChart();
76+
}
77+
}, 100);
78+
79+
return () => {
80+
resizeObserver.disconnect();
81+
clearTimeout(timeoutId);
82+
};
83+
} else {
84+
// 容器已经有尺寸,直接初始化
85+
initializeChart();
86+
}
87+
function initializeChart() {
88+
// 如果已经有图表实例,先销毁
89+
if (chartInstanceRef.current) {
90+
chartInstanceRef.current.dispose();
91+
}
92+
93+
// 初始化图表
94+
chartInstanceRef.current = echarts.init(echartsRef.current, colorMode);
95+
if (!chartInstanceRef.current) return;
96+
97+
chartInstanceRef.current.setOption(option);
98+
99+
if (onInit && typeof onInit === 'function') {
100+
onInit(chartInstanceRef.current);
101+
}
102+
// 绑定事件
103+
Object.keys(onEvents).forEach((eventName) => {
104+
chartInstanceRef.current.on(eventName, (params) => {
105+
onEvents[eventName](params, chartInstanceRef.current);
106+
});
107+
});
108+
}
109+
110+
// 添加窗口变化事件监听器
111+
const resizeHandler = () => {
112+
if (chartInstanceRef.current) {
113+
chartInstanceRef.current.resize();
114+
}
115+
};
116+
117+
window.addEventListener('resize', resizeHandler);
118+
119+
return () => {
120+
// 移除所有事件监听器
121+
Object.keys(onEvents).forEach((eventName) => {
122+
if (chartInstanceRef.current) {
123+
chartInstanceRef.current.off(eventName);
124+
}
125+
});
126+
// 移除窗口变化事件监听器
127+
window.removeEventListener('resize', resizeHandler);
128+
129+
// 销毁图表实例
130+
if (chartInstanceRef.current) {
131+
chartInstanceRef.current.dispose();
132+
chartInstanceRef.current = null;
133+
}
134+
};
135+
}, [option, colorMode]);
136+
137+
return <div ref={echartsRef} className={className} style={style}></div>;
138+
};
139+
140+
export default EchartsView;

0 commit comments

Comments
 (0)