Skip to content

Commit 4b3978d

Browse files
committed
chore(website): derive the counters from data, refresh them weekly
The star, fork, contributor and release counts were typed into the templates by hand, so they only moved when somebody remembered. They had already drifted: stars read 48,042 against 48,067, forks 4,314 against 4,320, and contributors 224 against 225. They move to website/data/stats.yaml, which .github/ci/refresh-site-counters.sh rewrites from the GitHub API, run weekly by a new workflow. The contributors and releases endpoints never report a total, so the script asks for one item per page and reads the count out of the Link header. It refuses to write a zero or a non-number, which is what a rate-limited or failed call looks like, and the workflow commits only when a number actually moved. The Discord count has no API behind it, so the script reads the existing value back and carries it through. The engine count was wrong in a second way. The hero said 18, the section heading said "Eighteen engines", the timeline said "Nineteen engines of our own", and the /engines/ page derived 19 from the data file. All of them now derive from that same file, so they cannot disagree again. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5[1m]
1 parent 3f4e446 commit 4b3978d

6 files changed

Lines changed: 135 additions & 15 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
# Refreshes the counters shown on the landing page from the GitHub API.
3+
#
4+
# The numbers used to be typed into the templates by hand, which meant they
5+
# only moved when somebody remembered, and a stale star count on the front
6+
# page is worse than no star count. Everything the API can answer for lives
7+
# in website/data/stats.yaml and is rewritten wholesale by this script.
8+
#
9+
# Anything the API cannot answer for (the Discord member count) is read back
10+
# out of the existing file and carried through untouched.
11+
set -euo pipefail
12+
13+
REPO="${REPO:-mudler/LocalAI}"
14+
OUT="${OUT:-website/data/stats.yaml}"
15+
16+
# The contributors and releases endpoints are paginated and never report a
17+
# total. Asking for one item per page makes the last page number equal to the
18+
# item count, which the Link header hands over.
19+
count_via_link_header() {
20+
local path="$1" link last
21+
link=$(gh api -i "${path}?per_page=1" 2>/dev/null | tr -d '\r' | grep -i '^link:' || true)
22+
if [ -z "$link" ]; then
23+
# No Link header means a single page, so count that page directly.
24+
gh api "${path}?per_page=100" --jq 'length'
25+
return
26+
fi
27+
last=$(sed -n 's/.*[?&]page=\([0-9]*\)>; rel="last".*/\1/p' <<<"$link")
28+
[ -n "$last" ] || { gh api "${path}?per_page=100" --jq 'length'; return; }
29+
printf '%s\n' "$last"
30+
}
31+
32+
read -r stars forks < <(gh api "repos/${REPO}" --jq '"\(.stargazers_count) \(.forks_count)"')
33+
contributors=$(count_via_link_header "repos/${REPO}/contributors")
34+
releases=$(count_via_link_header "repos/${REPO}/releases")
35+
36+
# Not derivable from the GitHub API, so keep whatever is already on disk.
37+
discord=$(sed -n 's/^discord: *\([0-9]*\).*/\1/p' "$OUT" 2>/dev/null | head -1)
38+
discord="${discord:-0}"
39+
40+
for n in stars forks contributors releases; do
41+
v="${!n}"
42+
[[ "$v" =~ ^[0-9]+$ ]] && [ "$v" -gt 0 ] || {
43+
echo "refusing to write: ${n} came back as '${v}'" >&2
44+
exit 1
45+
}
46+
done
47+
48+
cat > "$OUT" <<YAML
49+
# Counters shown on the landing page.
50+
#
51+
# The four GitHub fields are rewritten by .github/ci/refresh-site-counters.sh,
52+
# which runs weekly from .github/workflows/refresh-site-counters.yml. Editing
53+
# them by hand works but will be overwritten on the next run.
54+
stars: ${stars}
55+
forks: ${forks}
56+
contributors: ${contributors}
57+
releases: ${releases}
58+
59+
# The GitHub API cannot answer for this one, so it is maintained by hand and
60+
# the refresh script carries it through untouched.
61+
discord: ${discord}
62+
YAML
63+
64+
echo "stars=${stars} forks=${forks} contributors=${contributors} releases=${releases} discord=${discord}"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Refresh site counters
2+
3+
# The landing page shows a star count, a contributor count and a release
4+
# count. They were typed in by hand, so they drifted the moment somebody
5+
# forgot. This pulls the real numbers once a week and commits them only when
6+
# they have actually moved, which in turn triggers the usual Pages deploy.
7+
8+
on:
9+
schedule:
10+
# Mondays, 06:17 UTC. Off the hour on purpose, since the scheduler queues
11+
# everything that asks for :00 and drops what it cannot run.
12+
- cron: '17 6 * * 1'
13+
workflow_dispatch:
14+
15+
permissions:
16+
contents: write
17+
18+
concurrency:
19+
group: refresh-site-counters
20+
cancel-in-progress: false
21+
22+
jobs:
23+
refresh:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- name: Read the counts off the GitHub API
29+
env:
30+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31+
run: ./.github/ci/refresh-site-counters.sh
32+
33+
- name: Commit only if something moved
34+
run: |
35+
if git diff --quiet -- website/data/stats.yaml; then
36+
echo "counters unchanged, nothing to commit"
37+
exit 0
38+
fi
39+
git diff --unified=0 -- website/data/stats.yaml
40+
git config user.name "github-actions[bot]"
41+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
42+
git add website/data/stats.yaml
43+
git commit -m "chore(website): refresh the counters"
44+
git push

website/data/stats.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Counters shown on the landing page.
2+
#
3+
# The four GitHub fields are rewritten by .github/ci/refresh-site-counters.sh,
4+
# which runs weekly from .github/workflows/refresh-site-counters.yml. Editing
5+
# them by hand works but will be overwritten on the next run.
6+
stars: 48067
7+
forks: 4320
8+
contributors: 225
9+
releases: 133
10+
11+
# The GitHub API cannot answer for this one, so it is maintained by hand and
12+
# the refresh script carries it through untouched.
13+
discord: 3187

website/hugo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ enableEmoji = true
1616
discord = 'https://discord.gg/uJAeKSAGDy'
1717
x = 'https://twitter.com/LocalAI_API'
1818
huggingface = 'https://huggingface.co/mudler'
19-
# Refreshed by hand or by CI; shown in the nav and the traction band.
20-
stars = '48,042'
21-
contributors = '224'
19+
# Counters live in data/stats.yaml, which .github/ci/refresh-site-counters.sh
20+
# rewrites weekly. They used to sit here as hand-typed strings and drifted.
2221

2322
[markup.goldmark.renderer]
2423
unsafe = true

website/layouts/index.html

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ <h1><u><b>Make AI run on</b></u><u><b><s>every machine.</s></b></u></h1>
1616
<a class="btn btn--o" href="{{ .Site.Params.github }}">&#9733; Star on GitHub</a>
1717
</div>
1818
<div class="figures fd">
19-
<div><b class="tnum" data-count="48042">0</b><span>GitHub stars</span></div>
19+
<div><b class="tnum" data-count="{{ .Site.Data.stats.stars }}">0</b><span>GitHub stars</span></div>
2020
<div><b class="tnum" data-count="73">0</b><span>Backends</span></div>
21-
<div><b class="tnum" data-count="18">0</b><span>Engines we wrote</span></div>
21+
<div><b class="tnum" data-count="{{ len .Site.Data.engines.engines }}">0</b><span>Engines we wrote</span></div>
2222
<div><b class="tnum" data-count="1585">0</b><span>Models, one click</span></div>
2323
</div>
2424
</div>
@@ -164,7 +164,7 @@ <h2 class="rv mt1" style="max-width:23ch">Nobody was at the keyboard.</h2>
164164
<div class="shell">
165165
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
166166
<p class="kicker rv">Engines we build</p>
167-
<h2 class="rv mt1" style="max-width:22ch">Eighteen engines, written from scratch.</h2>
167+
<h2 class="rv mt1" style="max-width:22ch">{{ len .Site.Data.engines.engines }} engines, written from scratch.</h2>
168168
<p class="lede rv mt2">Most backends wrap somebody else's engine. These do not. They exist because the thing we needed was a 9 GB Python install, or closed, or nobody had built it yet. Each one is a binary and a GGUF file, checked against the reference implementation in CI.</p>
169169

170170
<div class="spot rv">
@@ -247,7 +247,7 @@ <h4>vllm.cpp</h4>
247247
<video src="/media/vllm-race.mp4" muted loop playsinline preload="none" data-lazy aria-label="vllm.cpp ahead of vLLM at every concurrency level"></video>
248248
</div>
249249

250-
<p class="rv mt2"><a class="btn btn--o" href="/engines/">All eighteen engines →</a></p>
250+
<p class="rv mt2"><a class="btn btn--o" href="/engines/">All {{ len .Site.Data.engines.engines }} engines →</a></p>
251251
</div>
252252
</section>
253253

@@ -363,14 +363,14 @@ <h2 class="rv mt1" style="max-width:16ch">An agent you can drop on any box you S
363363
<h2 class="rv mt1" style="max-width:22ch">Forty-eight thousand stars, and still shipping every week.</h2>
364364
<div class="trend rv">
365365
<img src="/img/trendshift.svg" alt="LocalAI on Trendshift">
366-
<p>LocalAI has been <b>trending on GitHub</b> repeatedly since it launched, and it is one of the most starred self-hosted AI projects there is. <b>224 people</b> have contributed code, <b>3,187</b> are in the Discord, and the README is kept translated into <b>eight languages</b> because the users are everywhere.</p>
366+
<p>LocalAI has been <b>trending on GitHub</b> repeatedly since it launched, and it is one of the most starred self-hosted AI projects there is. <b>{{ lang.FormatNumberCustom 0 .Site.Data.stats.contributors }} people</b> have contributed code, <b>{{ lang.FormatNumberCustom 0 .Site.Data.stats.discord }}</b> are in the Discord, and the README is kept translated into <b>eight languages</b> because the users are everywhere.</p>
367367
</div>
368368
<div class="big rv">
369-
<div><b class="tnum" data-count="48042">0</b><span>Stars</span></div>
370-
<div><b class="tnum" data-count="4314">0</b><span>Forks</span></div>
371-
<div><b class="tnum" data-count="224">0</b><span>Contributors</span></div>
372-
<div><b class="tnum" data-count="133">0</b><span>Releases</span></div>
373-
<div><b class="tnum" data-count="3187">0</b><span>In Discord</span></div>
369+
<div><b class="tnum" data-count="{{ .Site.Data.stats.stars }}">0</b><span>Stars</span></div>
370+
<div><b class="tnum" data-count="{{ .Site.Data.stats.forks }}">0</b><span>Forks</span></div>
371+
<div><b class="tnum" data-count="{{ .Site.Data.stats.contributors }}">0</b><span>Contributors</span></div>
372+
<div><b class="tnum" data-count="{{ .Site.Data.stats.releases }}">0</b><span>Releases</span></div>
373+
<div><b class="tnum" data-count="{{ .Site.Data.stats.discord }}">0</b><span>In Discord</span></div>
374374
<div><b class="tnum" data-count="0" data-text="3 yrs">0</b><span>Shipping since</span></div>
375375
</div>
376376
<div class="tl rv">
@@ -421,7 +421,7 @@ <h2 class="rv mt1" style="max-width:22ch">Forty-eight thousand stars, and still
421421
employers reads as a customer logo wall, which is a claim we are not
422422
making; a sentence keeps it about the people, which is the true one. */}}
423423
<p class="kicker rv mt3">Who shows up</p>
424-
<h3 class="eco__h rv">{{ .Site.Params.contributors }} people have put code in this repository.</h3>
424+
<h3 class="eco__h rv">{{ lang.FormatNumberCustom 0 .Site.Data.stats.contributors }} people have put code in this repository.</h3>
425425
{{- $co := slice }}
426426
{{- range .Site.Data.ecosystem.contributors.companies }}{{ $co = $co | append (printf "<b>%s</b>" .name) }}{{ end }}
427427
{{- $ac := slice }}

website/layouts/partials/nav.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<a href="{{ .Site.Params.docsURL }}">Docs</a>
1414
</nav>
1515
<div class="topright">
16-
<a class="pill" href="{{ .Site.Params.github }}">&#9733; {{ .Site.Params.stars }}</a>
16+
<a class="pill" href="{{ .Site.Params.github }}">&#9733; {{ lang.FormatNumberCustom 0 .Site.Data.stats.stars }}</a>
1717
<a class="go-btn" href="{{ $home }}#start">Install</a>
1818
</div>
1919
</header>

0 commit comments

Comments
 (0)