如果你最近发现 README 里的 Star History 图片裂了,先别急着换 URL、加重试,也别先怀疑自己的 Markdown 写错了。

这次坏得比较彻底:GitHub 改了 Stargazer 接口的访问规则,Star History 赖以生成完整曲线的数据拿不到了。以前那种「输入任意公共仓库,马上吐出一张历史图」的玩法,前提已经没了。

我维护的 Knife4j Next 也中招了。后来我们没再找下一个在线图表服务,而是用 GitHub Actions 把数据和 SVG 生成本地化,放进自己仓库。

结论先行

整套做法分两阶段:

1
2
3
一次性初始化:有权限的 Token → 拉取现有 Stargazer 时间 → 聚合成 history.json

长期每日运行:公开 Star 总数 → 追加当天快照 → 生成 SVG → 推送到独立分支

核心思路是把需求收窄:

  • 只为自己拥有或协作的仓库生成图表
  • 有权限的 Token 只在初始化历史时用一次
  • 日常任务只读公开的 stargazers_count
  • 只存日期和数量,不存 Stargazer 身份
  • 数据与 SVG 放在独立分支,避免主分支每天多一堆提交
  • 生成代码留在自己仓库里,不依赖第三方 Marketplace Action

2026 年 7 月 18 日这套已经在 Knife4j Next 上跑起来了,下面是实际生成的图:

Knife4j Next Star History

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
2
HTTP 503
All GitHub API tokens are rate-limited, try again later

为了排除是 Knife4j Next 自己配置有问题,我又试了 openai/codextorvalds/linuxvuejs/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 里那张图,本来就是给自己仓库用的。需求收窄之后,其实不必每天反复扒用户列表:

  1. 过去的历史,初始化一次就够。
  2. 从今天起,每天记一次 Star 总数就行。
  3. 画图只要日期和累计数量,用户是谁无关紧要。

数据模型因此从「反复爬一份用户列表」,变成「维护一串按日聚合的快照」。

第一步:只初始化一次旧历史

初始化阶段用仓库管理员或协作者的 Token,分页读 Stargazer 和 starred_at,再按 UTC 日期聚合成类似下面的数据:

1
2
3
4
5
6
7
8
9
{
"repository": "songxychn/knife4j-next",
"updated": "2026-07-18",
"points": [
{ "date": "2026-04-23", "stars": 1 },
{ "date": "2026-05-08", "stars": 2 },
{ "date": "2026-07-18", "stars": 43 }
]
}

建议用只授权目标仓库、仅有 Metadata: Read-only 的 fine-grained PAT。Star History 的官方 Token 指南也是这个最小权限建议。

为了不把 Token 写进 shell history,可以这样跑:

1
2
3
4
5
6
7
read -s STAR_HISTORY_TOKEN
export STAR_HISTORY_TOKEN
export GITHUB_REPOSITORY=owner/repository

node .github/scripts/star-history.mjs bootstrap /tmp/star-history

unset STAR_HISTORY_TOKEN

脚本最后只写出 history.jsonstar-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
2
snapshot: wrote 33 points for songxychn/knife4j-next
Star history is already up to date

幂等很重要。手动补跑、定时任务误触发两次,都不会把数据分支搞脏。

为什么用独立的 orphan 分支

如果 Action 每天直接改 main,提交历史很快会被「更新 Star 图表」刷屏。我们把产物丢在只含两个文件的 star-history 分支:

1
2
3
star-history
├── history.json
└── star-history.svg

工作流用 git worktree 检出这个分支;第一次不存在就建 orphan,之后在原分支上增量提交。主分支只留生成脚本和工作流定义。

README 最后只要引用自己的 Raw 文件:

1
2
3
4
<a href="https://github.com/owner/repository/tree/star-history">
<img alt="Star History Chart"
src="https://raw.githubusercontent.com/owner/repository/star-history/star-history.svg" />
</a>

GitHub Raw 还能访问,README 图片就不绑死在另一个图表服务上。

工作流长什么样

完整实现可以直接看 Knife4j Next:

骨架大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
name: Update Star History

on:
schedule:
- cron: "17 1 * * *"
workflow_dispatch:

permissions:
contents: write

concurrency:
group: star-history
cancel-in-progress: false

jobs:
update:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Generate snapshot
env:
GITHUB_TOKEN: ${{ github.token }}
run: node .github/scripts/star-history.mjs snapshot "$STAR_HISTORY_DIR"

分支准备和提交那段比较长,这里不整段贴;上面的完整工作流链接可以直接看。

我们没有发一个新的通用 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-history orphan 分支
  • 合并后手动跑了 Update Star History
  • 确认工作流成功,并走通「已经是最新数据」的幂等路径
  • 确认公开 SVG 返回 HTTP 200image/svg+xml

比换一个新的在线图片 URL 多写了点代码,但出了问题边界清楚:图表坏了就查自己的 Action、自己的数据、自己的提交,不用再猜第三方服务什么时候活过来。

结语

这次 Star History 挂掉,提醒我一件事:依赖公开用户明细的产品,一旦平台收紧数据访问,很难靠换 API 或堆 Token 把原体验补回来。继续找「能替所有仓库爬 Stargazer」的下一个服务,性价比其实不高。

README 里那张增长图,真正要的东西很少:保住一次历史,从今天开始每天记一个数字,再生成一张静态图。

有权限时一次性取回历史数据,之后只维护匿名聚合快照,图表也由自己的仓库生成。

它补不全 Star History 的全部能力;对维护者自己的 README 来说,却更小、更稳,也更好长期养着。