Skip to content

Commit 59c5dda

Browse files
committed
feat: add secure repository-owned star history
1 parent 40327d7 commit 59c5dda

12 files changed

Lines changed: 4918 additions & 9 deletions

.github/star-history/history.json

Lines changed: 1714 additions & 0 deletions
Large diffs are not rendered by default.

.github/workflows/update-star-history.yml

Lines changed: 416 additions & 0 deletions
Large diffs are not rendered by default.

.gitignore

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,13 @@ test_results/
324324
# Office 文件临时
325325
~$*
326326

327+
# 本地 AI 规划产物(不要提交到公开仓库)
328+
/docs/superpowers/
329+
/.superpowers/
330+
331+
# 私有 Word 文档
332+
*.[dD][oO][cC][xX]
333+
327334
# ==== 版本控制 ====
328335
# Git
329336
.git/
@@ -353,4 +360,4 @@ forward_prs.sh
353360
CLAUDE.md
354361
findings.md
355362
progress.md
356-
task_plan.md
363+
task_plan.md

README-EN.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -780,11 +780,11 @@ Thanks to these excellent contributors:
780780

781781
## 📈 Project Statistics
782782

783-
<a href="https://www.star-history.com/#666ghj/BettaFish&type=date&legend=top-left">
783+
<a href="https://github.com/666ghj/BettaFish">
784784
<picture>
785-
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=666ghj/BettaFish&type=date&theme=dark&legend=top-left" />
786-
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=666ghj/BettaFish&type=date&legend=top-left" />
787-
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=666ghj/BettaFish&type=date&legend=top-left" />
785+
<source media="(prefers-color-scheme: dark)" srcset="static/image/star-history-dark.svg" />
786+
<source media="(prefers-color-scheme: light)" srcset="static/image/star-history-light.svg" />
787+
<img alt="BettaFish Star History Chart" src="static/image/star-history-light.svg" />
788788
</picture>
789789
</a>
790790

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -785,11 +785,11 @@ class DeepSearchAgent:
785785

786786
## 📈 项目统计
787787

788-
<a href="https://www.star-history.com/#666ghj/BettaFish&type=date&legend=top-left">
788+
<a href="https://github.com/666ghj/BettaFish">
789789
<picture>
790-
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=666ghj/BettaFish&type=date&theme=dark&legend=top-left" />
791-
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=666ghj/BettaFish&type=date&legend=top-left" />
792-
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=666ghj/BettaFish&type=date&legend=top-left" />
790+
<source media="(prefers-color-scheme: dark)" srcset="static/image/star-history-dark.svg" />
791+
<source media="(prefers-color-scheme: light)" srcset="static/image/star-history-light.svg" />
792+
<img alt="BettaFish Star History Chart" src="static/image/star-history-light.svg" />
793793
</picture>
794794
</a>
795795

THIRD_PARTY_NOTICES.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Third-party notices
2+
3+
## Star History logo icon
4+
5+
The self-contained Star History SVG renderer includes `logo-icon.png` from
6+
[star-history/star-history](https://github.com/star-history/star-history), used
7+
as the `star-history.com` watermark.
8+
9+
MIT License
10+
11+
Copyright (c) 2025 Star History
12+
13+
Permission is hereby granted, free of charge, to any person obtaining a copy
14+
of this software and associated documentation files (the "Software"), to deal
15+
in the Software without restriction, including without limitation the rights
16+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
copies of the Software, and to permit persons to whom the Software is
18+
furnished to do so, subject to the following conditions:
19+
20+
The above copyright notice and this permission notice shall be included in all
21+
copies or substantial portions of the Software.
22+
23+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
SOFTWARE.

scripts/fetch_star_count.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python3
2+
"""Fetch BettaFish's aggregate GitHub Star count without loading the renderer.
3+
4+
The successful stdout contract is deliberately tiny: one non-negative decimal
5+
integer followed by a newline. Errors are fixed, sanitized messages on stderr.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import json
11+
import os
12+
import sys
13+
import urllib.error
14+
import urllib.request
15+
from typing import Any
16+
17+
18+
API_URL = "https://api.github.com/repos/666ghj/BettaFish"
19+
API_VERSION = "2026-03-10"
20+
MAX_HTTP_BYTES = 1_000_000
21+
TIMEOUT_SECONDS = 20
22+
23+
24+
class FetchError(RuntimeError):
25+
"""A safe error whose message never includes response or secret data."""
26+
27+
28+
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
29+
"""Refuse every redirect so credentials cannot be forwarded elsewhere."""
30+
31+
def redirect_request(self, *_args: Any, **_kwargs: Any) -> None:
32+
return None
33+
34+
35+
def _build_opener() -> urllib.request.OpenerDirector:
36+
return urllib.request.build_opener(NoRedirectHandler())
37+
38+
39+
def _status_error(status: int) -> FetchError:
40+
if status in {301, 302, 303, 307, 308}:
41+
return FetchError("GitHub API redirect was refused")
42+
if status == 401:
43+
return FetchError("GitHub API authentication failed")
44+
if status == 403:
45+
return FetchError("GitHub API request was denied")
46+
if status == 404:
47+
return FetchError("BettaFish repository metadata was not found")
48+
if status == 429:
49+
return FetchError("GitHub API rate limit was exhausted")
50+
if 500 <= status <= 599:
51+
return FetchError("GitHub API is unavailable")
52+
return FetchError("GitHub API request failed")
53+
54+
55+
def _read_response(response: Any) -> bytes:
56+
raw_length = response.headers.get("Content-Length")
57+
if raw_length is not None:
58+
try:
59+
content_length = int(raw_length, 10)
60+
except (TypeError, ValueError) as exc:
61+
raise FetchError("GitHub API returned invalid response metadata") from exc
62+
if content_length < 0 or content_length > MAX_HTTP_BYTES:
63+
raise FetchError("GitHub API response exceeded the size limit")
64+
65+
payload = response.read(MAX_HTTP_BYTES + 1)
66+
if len(payload) > MAX_HTTP_BYTES:
67+
raise FetchError("GitHub API response exceeded the size limit")
68+
return payload
69+
70+
71+
def fetch_star_count(token: str, opener: Any | None = None) -> int:
72+
if not token or len(token) > 4_096 or "\r" in token or "\n" in token:
73+
raise FetchError("GITHUB_TOKEN is missing or invalid")
74+
75+
request = urllib.request.Request(
76+
API_URL,
77+
headers={
78+
"Accept": "application/vnd.github+json",
79+
"Authorization": f"Bearer {token}",
80+
"User-Agent": "BettaFish-Star-History-Fetcher",
81+
"X-GitHub-Api-Version": API_VERSION,
82+
},
83+
method="GET",
84+
)
85+
client = opener or _build_opener()
86+
try:
87+
response = client.open(request, timeout=TIMEOUT_SECONDS)
88+
except urllib.error.HTTPError as exc:
89+
status = exc.code
90+
exc.close()
91+
raise _status_error(status) from None
92+
except (urllib.error.URLError, TimeoutError, OSError):
93+
raise FetchError("GitHub API network request failed") from None
94+
except Exception:
95+
raise FetchError("GitHub API request could not be started") from None
96+
97+
try:
98+
with response:
99+
if response.geturl() != API_URL:
100+
raise FetchError("GitHub API redirect was refused")
101+
status = response.getcode()
102+
if status != 200:
103+
raise _status_error(status)
104+
payload = _read_response(response)
105+
except FetchError:
106+
raise
107+
except (TimeoutError, OSError):
108+
raise FetchError("GitHub API response could not be read") from None
109+
except Exception:
110+
raise FetchError("GitHub API response could not be processed") from None
111+
112+
try:
113+
document = json.loads(payload)
114+
except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
115+
raise FetchError("GitHub API returned malformed JSON") from None
116+
if not isinstance(document, dict):
117+
raise FetchError("GitHub API response had an unexpected shape")
118+
119+
count = document.get("stargazers_count")
120+
if type(count) is not int or count < 0:
121+
raise FetchError("GitHub API returned an invalid stargazers_count")
122+
return count
123+
124+
125+
def main(argv: list[str] | None = None) -> int:
126+
arguments = sys.argv[1:] if argv is None else argv
127+
if arguments:
128+
print("error: this command accepts no arguments", file=sys.stderr)
129+
return 2
130+
131+
try:
132+
count = fetch_star_count(os.environ.get("GITHUB_TOKEN", ""))
133+
except FetchError as exc:
134+
print(f"error: {exc}", file=sys.stderr)
135+
return 1
136+
except Exception:
137+
print("error: unexpected internal failure", file=sys.stderr)
138+
return 1
139+
140+
print(count)
141+
return 0
142+
143+
144+
if __name__ == "__main__":
145+
raise SystemExit(main())

0 commit comments

Comments
 (0)