Star History 不可用之后,使用 Github Action 优雅替代的方案
如果你最近发现 README 里的 Star History 图片裂了,先别急着换 URL、加重试,也别先怀疑自己的 Markdown 写错了。
这次坏得比较彻底:GitHub 改了 Stargazer 接口的访问规则,Star History 赖以生成完整曲线的数据拿不到了。以前那种「输入任意公共仓库,马上吐出一张历史图」的玩法,前提已经没了。
我维护的 Knife4j Next 也中招了。后来我们没再找下一个在线图表服务,而是用 GitHub Actions 把数据和 SVG 生成本地化,放进自己仓库。
结论先行
整套做法分两阶段:
1 | 一次性初始化:有权限的 Token → 拉取现有 Stargazer 时间 → 聚合成 history.json |
核心思路是把需求收窄:
- 只为自己拥有或协作的仓库生成图表
- 有权限的 Token 只在初始化历史时用一次
- 日常任务只读公开的
stargazers_count - 只存日期和数量,不存 Stargazer 身份
- 数据与 SVG 放在独立分支,避免主分支每天多一堆提交
- 生成代码留在自己仓库里,不依赖第三方 Marketplace Action
2026 年 7 月 18 日这套已经在 Knife4j Next 上跑起来了,下面是实际生成的图:
Star History 为什么突然挂了
Star History 的历史曲线并不是 GitHub 直接提供的统计结果。它要调 Stargazer 列表接口,拿到每个 Stargazer 的 starred_at,再拼出累计曲线。
2026 年 6 月 30 日,GitHub 宣布收紧多个公共接口和页面,其中就包括:
1 | GET /repos/{owner}/{repo}/stargazers |
这个接口现在只允许仓库管理员和协作者访问。GitHub 的说法是:公开的 Stargazer / Watcher 列表被大量用来采集用户信息、发垃圾消息。
这下正好卡死了 Star History 的数据来源。官方随后在说明里也确认了几件事:
- README 里原来的实时嵌入图会受影响
- 不能再替任意公共仓库拉完整 Star 历史
- 仓库管理员或协作者仍可用自己的 Token 查看有权限的仓库
我们最早看到的错误是:
1 | HTTP 503 |
为了排除是 Knife4j Next 自己配置有问题,我又试了 openai/codex、torvalds/linux、vuejs/core 几个无关仓库,结果一样;与此同时 Star History 首页还能正常打开。所以坏的是图表生成链路,不是 README 写法,也不是我们仓库的个例。
为什么这么久都没有修好
加个重试、换个 endpoint,解决不了这件事——上游直接撤掉了产品依赖的能力。
Star History 维护者在 issue #542 里说得很直白:受 GitHub 新限制影响,完整恢复过去的体验不太可能;项目本身维护精力也有限。
社区试过 GraphQL。它一度还能返回 starredAt,但这并不是 GitHub 明确开的隐私豁免,更像 REST 和 GraphQL 限制不同步。更现实的问题是性能:维护者估算,精确扫一个 4 万 Star 的仓库大约 400 次请求,25 万 Star 大约 2500 次,撑不起公共网站的交互式查询。详细分析见这条维护者回复。
后来社区讨论下来,大致就三类路:
| 方案 | 能不能用 | 主要代价 |
|---|---|---|
| 给 Star History 一个加密后的细粒度 PAT | 能处理自己有权限的仓库 | 仍要信任第三方服务和 Token 处理方式 |
| 改用 GraphQL 扫完整历史 | 某些时段、某些账户能用 | 限制状态不稳定,大仓库请求量很高 |
| 仓库自己的 Action 定时生成静态图 | 能稳定处理自己的仓库 | 要维护一个小脚本,不再支持任意仓库在线对比 |
我们选了第三种。社区里也有人在 issue #542 提过类似的 nightly Action 思路。
目标没必要定成「再造一个 Star History」
如果你还想「匿名访问任意仓库,立刻出完整历史」,GitHub Action 也干不了——它同样没权限去读别人仓库的 Stargazer 列表。
但 README 里那张图,本来就是给自己仓库用的。需求收窄之后,其实不必每天反复扒用户列表:
- 过去的历史,初始化一次就够。
- 从今天起,每天记一次 Star 总数就行。
- 画图只要日期和累计数量,用户是谁无关紧要。
数据模型因此从「反复爬一份用户列表」,变成「维护一串按日聚合的快照」。
第一步:只初始化一次旧历史
初始化阶段用仓库管理员或协作者的 Token,分页读 Stargazer 和 starred_at,再按 UTC 日期聚合成类似下面的数据:
1 | { |
建议用只授权目标仓库、仅有 Metadata: Read-only 的 fine-grained PAT。Star History 的官方 Token 指南也是这个最小权限建议。
为了不把 Token 写进 shell history,可以这样跑:
1 | read -s STAR_HISTORY_TOKEN |
脚本最后只写出 history.json 和 star-history.svg。Token 不进文件、不进 Git,也不用长期存成 Actions Secret。
Knife4j Next 初始化时拿到 33 个有变化的日期点:第一个点是 2026 年 4 月 23 日,到 7 月 18 日是 43 Star。
第二步:以后每天只记一个公开数字
日常任务不再调受限的 Stargazer 列表,只读普通仓库信息:
1 | GET /repos/{owner}/{repo} |
响应里的 stargazers_count 就是仓库当前显示的 Star 总数。Action 把当天日期和这个数字合进 history.json,再重新生成 SVG。
同一天重复跑,脚本会覆盖当天记录,不会堆重复点;数据和图片没变化时,也不会空提交。合并方案后我们手动跑了一次,日志是:
1 | snapshot: wrote 33 points for songxychn/knife4j-next |
幂等很重要。手动补跑、定时任务误触发两次,都不会把数据分支搞脏。
为什么用独立的 orphan 分支
如果 Action 每天直接改 main,提交历史很快会被「更新 Star 图表」刷屏。我们把产物丢在只含两个文件的 star-history 分支:
1 | star-history |
工作流用 git worktree 检出这个分支;第一次不存在就建 orphan,之后在原分支上增量提交。主分支只留生成脚本和工作流定义。
README 最后只要引用自己的 Raw 文件:
1 | <a href="https://github.com/owner/repository/tree/star-history"> |
GitHub Raw 还能访问,README 图片就不绑死在另一个图表服务上。
工作流长什么样
完整实现可以直接看 Knife4j Next:
骨架大致如下:
1 | name: Update Star History |
分支准备和提交那段比较长,这里不整段贴;上面的完整工作流链接可以直接看。
我们没有发一个新的通用 Marketplace Action,也没有加 npm 包。生成器就是仓库里一个 .mjs 文件,只用 Node.js 标准库和内置 fetch。长期升级和供应链依赖都更少,仓库也可以按自己的口味改图表尺寸、颜色、标题和数据格式。
SVG 自带浅色 / 深色样式,靠 prefers-color-scheme 切换;同时带了 <title>、<desc> 和 role="img",README 里除了能显示,也留一点基本的可访问性信息。
Token 和权限边界
这里有两种 Token,别混:
| 阶段 | Token | 权限和生命周期 |
|---|---|---|
| 初始化旧历史 | 管理员/协作者的 fine-grained PAT | 仅目标仓库、Metadata 只读;本地用一次就删 |
| 每日快照 | GitHub 自动提供的 GITHUB_TOKEN |
contents: write,读当前总数并更新数据分支 |
日常 Action 不访问 Stargazer 用户列表,也就没必要长期保存个人 PAT。公开仓库里最后留下的只有日期和累计数量。
如果仓库或组织策略禁止 GITHUB_TOKEN 写内容,要在 GitHub Actions 设置里打开写权限;我们的工作流也显式写了 permissions: contents: write,不靠含糊的默认值。
还有哪些限制
第一,它只能服务自己拥有或协作的仓库,恢复不了 Star History 原来「任意对比公共仓库」的能力。
第二,一次性初始化也不是绝对完整的历史。GitHub 返回的是当前仍然 Star 该仓库的用户及其时间;曾经 Star 又取消的,通常还原不了。传统 Star History 同样受这个数据边界影响。
第三,初始化之后记的是「当天仓库显示的净 Star 总数」,不是逐个 Stargazer 事件。某天 Action 没跑,下次快照会直接接到新日期,不会补假数据。
第四,GitHub 明确说过定时工作流在 Actions 高负载时可能延迟,所以最好避开整点,并保留 workflow_dispatch 方便手动补跑;公共仓库连续 60 天没活动,定时工作流还会被自动禁用。可参考 GitHub 的 schedule 事件文档和工作流启停说明。
最后,大仓库做历史初始化仍要分页扫描。只做一次一般能接受,但别指望靠它再搭一个公共实时查询服务。
实际验收结果
改动通过 PR #547 合并。我们没停在「代码看起来能跑」,而是把整条链路过了一遍:
- 用真实权限初始化 33 个历史日期点
- 校验生成的 SVG 是合法 XML,并肉眼看了浅色图表
- 在临时 Git 远端分别验证数据分支首次创建和后续更新
- 发布真实的
star-historyorphan 分支 - 合并后手动跑了 Update Star History
- 确认工作流成功,并走通「已经是最新数据」的幂等路径
- 确认公开 SVG 返回
HTTP 200和image/svg+xml
比换一个新的在线图片 URL 多写了点代码,但出了问题边界清楚:图表坏了就查自己的 Action、自己的数据、自己的提交,不用再猜第三方服务什么时候活过来。
结语
这次 Star History 挂掉,提醒我一件事:依赖公开用户明细的产品,一旦平台收紧数据访问,很难靠换 API 或堆 Token 把原体验补回来。继续找「能替所有仓库爬 Stargazer」的下一个服务,性价比其实不高。
README 里那张增长图,真正要的东西很少:保住一次历史,从今天开始每天记一个数字,再生成一张静态图。
有权限时一次性取回历史数据,之后只维护匿名聚合快照,图表也由自己的仓库生成。
它补不全 Star History 的全部能力;对维护者自己的 README 来说,却更小、更稳,也更好长期养着。




