miniex/who-gets-the-starstruck

GitHub: miniex/who-gets-the-starstruck

通过逆向分析 GitHub 公开事件日志,揭示 Starstruck 成就徽章实际上归属仓库创建者而非组织所有者的未记录规则。

Stars: 2 | Forks: 0

# 谁获得了 Starstruck?从公开事件日志中逆向推导一条未记录的 GitHub 规则

🇺🇸 English (US) / 🇰🇷 한국어

## 问题 GitHub 的 Starstruck 成就颁发给由你“创建的”且突破 16、128、512 或 4096 颗星 (默认、青铜、白银、黄金)的“repository”。但是,当 repository 位于**组织**下而不是你的 个人账户下时,谁会获得徽章,它到底算不算数?GitHub 从未记录过这一点,而且大多数 指南都在重复同样的假设:只有你的个人 repository 才算数。 这个假设是错误的。徽章跟随**创建**该 repository 的人,即使它位于创建者并不个人拥有的组织中。 以下是我如何遇到这个问题的,以及我如何从公开记录中证明它。 ## 我是如何注意到的 我一直认为 Starstruck 只能来自个人 repository。然后我对 gorillamoe 的一个项目提交了 pull request,当我在查看他的个人资料时,我看到他的 Starstruck 徽章正指向那个 repository,该 repository 位于 `mistweaverco` 组织下,而不是他的个人账户下。这与 我对该徽章的所有认知相矛盾,因此我去寻找实际的规则。这就是为什么 gorillamoe 是 案例 01 和 02 的原因:他的个人资料正是这个问题开始的地方。 ## 答案 触发条件是作为 repository 的创建者,而不是作为组织所有者或成员。 - 创建一个 repository,甚至直接在你并不个人拥有的组织内创建,当它在该组织拥有时突破阈值, 徽章就会落在你的个人资料上。 - 作为所有者或成员加入组织但没有创建受欢迎的 repository,你什么也得不到。这一缺失是 长期存在且至今未被解答的 GitHub 功能请求的主题。 - 徽章与创建者绑定。一旦获得,即使 repository 后来被转移,它也会留在该人的个人资料上。 ## 我是如何证明的 困难的部分是将“在组织中创建”与“个人创建然后转入”区分开来,并展示 repository 在突破阈值的确切时刻是由组织拥有的。结合使用的三个来源可以解决这个问题。这也就是 ["Six Million (Suspected) Fake Stars on GitHub"](https://arxiv.org/abs/2412.13459) 研究人员 (ICSE 2026) 和 [Dagster 的假星检测器](https://github.com/dagster-io/fake-star-detector) 所使用的相同的 GH Archive 取证方法,只不过这里针对的是另一个 问题。 1. 实时 HTTP 重定向检查。GitHub 在 repository 转移后会在其旧路径保留永久的 301 重定向,因此 如果 `user/repo` 返回没有重定向的 404,则该 repository 几乎肯定从未在那里存在过。作为对照, 像 `GoogleCloudPlatform/kubernetes` 和 `joyent/node` 这样十年前的转移至今仍然会重定向,这 表明重定向是持久有效的。 2. Wayback Machine。快照证明页面曾在特定的 URL 和时间存在过,因此它是强有力的正面 证据。由于覆盖范围不完整,它作为负面证据的效力较弱。用于在 组织 URL 读取历史星数。 3. GH Archive 事件日志,这是决定性的。每个公开事件(`WatchEvent` 代表加星,`CreateEvent` 代表 创建 repository 等等)都只以追加方式记录,并附带当时的 repository 名称,而且转移 永远不会重写过去的事件。`CreateEvent (ref_type=repository)` 显示了 repository 最初是在哪里以及由谁 创建的,而每颗星都带有给予该星时 repository 的名称。 ### 堵住漏洞的证明 一个明显的反驳是:也许创建者短暂地将 repository 转移到了个人账户,让它 在那里突破了阈值,然后再转移回来,所以徽章实际上是个人获得的。事件日志排除了 这一点。以突破 4096 颗星(黄金)的 `mistweaverco/bananas` 为例。在整个突破窗口期内,每一天的 星都记录在 `mistweaverco/bananas` 下: ``` 02-24: 104 02-25: 93 02-26: 163 02-27: 111 02-28: 45 03-01: 21 03-02: 28 03-03: 76 ``` `gorillamoe/bananas` 在整个存档中没有任何事件,并且在该窗口期内唯一收到任何星的 `*/bananas` repository 就是组织下的那个。要使转移的说法成立,每天大约 100 颗导致突破的星 就必须出现在个人名下,但一颗也没有。 第二个例子更加清晰。Chakra UI 曾同时存在两个同名的 repository:个人的 `segunadebayo/chakra-ui`(首先创建)和组织下的 `chakra-ui/chakra-ui`。该组织 repo 于 2019-09-04 14:33 突破 16 颗星;个人的那个于 2019-09-05 09:52 突破 16 颗星,晚了大约 19 小时。成就 会颁发给最先达到里程碑的 repository,因此组织 repo 以 19 小时的优势赢得了徽章,并且 GitHub 的成就详情页面只将该组织 repo 记入功劳。 ## 各个案例 十三个人创建的十四个 repository,每个都直接在组织内创建,每个都在组织拥有时为其 创建者赢得了 Starstruck 等级。包含确切等级突破时间戳和 各个 repo 检查的完整档案位于 [`cases/`](cases/README.md)。每一个都通过了相同的五项测试:一个组织诞生的 `CreateEvent`,一个不存在或太小而无法支撑一个等级的同名个人 repository,记录在 组织名下的每一次等级突破,针对每个基准名称的全量捕获,以及一个将该组织 repo 记入功劳的 GitHub 成就详情页面。 | # | 创建者 | 组织 repo | 星数 | 最高等级 | | --- | --------------- | ------------------------------- | ----- | -------- | | 01 | gorillamoe | mistweaverco/kulala.nvim | 2.1k | silver | | 02 | gorillamoe | mistweaverco/bananas | 6.2k | gold | | 03 | jmikrut | payloadcms/payload | 43k | gold | | 04 | timothycarambat | Mintplex-Labs/anything-llm | 62k | gold | | 05 | rubenfiszel | windmill-labs/windmill | 17k | gold | | 06 | Pocco81 | catppuccin/catppuccin | 19k | gold | | 07 | cart | bevyengine/bevy | 47k | gold | | 08 | Fenny | gofiber/fiber | 40k | gold | | 09 | ishaan-jaff | BerriAI/litellm | 51k | gold | | 10 | vigoux | nvim-treesitter/nvim-treesitter | 14k | gold | | 11 | danielhanchen | unslothai/unsloth | 67k | gold | | 12 | eldadfux | appwrite/appwrite | 56k | gold | | 13 | segunadebayo | chakra-ui/chakra-ui | 40k | gold | | 14 | Rich-Harris | gobblejs/gobble | 330 | bronze | 这些案例跨越了 2014 年到 2024 年,涵盖 Rust、JavaScript/TypeScript、Go、Python 和 Neovim 插件,因此该模式 是独立重现的,而不是依赖于单个例子。 ### 被排除的候选者和反例 有两个候选者没有通过,并写在 [`cases/00-excluded.md`](cases/00-excluded.md) 中:`carllerche`, 他的徽章显示的是 `rust-lang/cargo`(一个他并没有创建的 repo),以及 `juliusmarminge`,他的 `t3-oss/create-t3-turbo` 没有组织诞生事件,因此是转移进来的。 许多知名的组织 repo 完全没有返回组织 `CreateEvent`,这意味着它们是在个人账户上创建 并被转移到组织中的,这是一条不同的路径:`oven-sh/bun`、`ollama/ollama`、`withastro/astro`、 `biomejs/biome`、`zed-industries/zed`、`vitejs/vite` 和 `langchain-ai/langchain`(以前是 `hwchase17/langchain`)等等。 ## 自己动手 相同的配方适用于任何用户或 repo。将第一个命令指向某个人,看看他们的徽章 将哪些 repo 记入功劳,然后将剩下的命令指向一个 repo,看看它是在哪里诞生的,以及它在每颗星时的名称。 ``` # 用户的 Starstruck 积分包含哪些 repo?输出中的 org owner 表示徽章归属 org。 curl -s -H "Turbo-Frame: achievement-detail" -H "X-Requested-With: XMLHttpRequest" \ "https://github.com/users//achievements/starstruck/detail?hovercard=1" \ | grep -oE '[A-Za-z0-9._-]+/[A-Za-z0-9._-]+' ``` ``` ch() { curl -s 'https://play.clickhouse.com/?user=play&default_format=TabSeparatedWithNames' --data-binary "$1"; } # repo 最初是在哪里以及由谁创建的?repo_name 中的 owner 代表位置;actor 是创建者。 ch "SELECT created_at, repo_name, actor_login FROM github_events WHERE event_type='CreateEvent' AND ref_type='repository' AND repo_name='/'" # 是否曾经存在过个人副本或获得过 star? ch "SELECT count() FROM github_events WHERE repo_name='/'" # 每日 star 及其记录时所用的名称,跨越一个时间窗口。 ch "SELECT toDate(created_at) AS d, countIf(event_type='WatchEvent') AS stars FROM github_events WHERE repo_name='/' AND created_at BETWEEN '' AND '' GROUP BY d ORDER BY d" ``` ``` # Transfer trace:返回 404 且有 0 个 redirect 表示该 repo 从未存在于该路径。 curl -s -L -o /dev/null -w "code=%{http_code} redirects=%{num_redirects}\n" "https://github.com//" ``` ## 限制 - GitHub 没有将这条规则写入文档。“创建者获得它”是从行为中重构出来的,而不是从规范中得出的,因此 GitHub 可能会改变它。 - 成就详情页面只显示第一个达到每个等级的 repo,因此如果较旧的 repo 已经填满了这些位置,那么创建者的旗舰组织 repo 可能会缺失。Rich Harris 是最明显的例子:他在 组织中创建了 `sveltejs/svelte`,但他的徽章显示的是较旧的 repo。 - 缺失的 Wayback 快照什么也证明不了。这里的每一个决定性主张都依赖于 GH Archive 事件记录和 GitHub 自己的创建和徽章数据。 ## 许可证 这项工作采用 [CC BY 4.0](LICENSE) 许可。 © 2026 Han Damin ([@miniex](https://github.com/miniex))
标签:BSD, 云资产清单, 代码示例, 多线程, 应用安全, 数据分析, 数据挖掘, 逆向工程