Split each contribution year into quarterly windows. commitContributionsByRepository caps at 100 repos per query and drops the remainder without signalling it, which a prolific year hits: 2026 returned exactly 100 where the quarterly union returns 108. Warn when a window still comes back at the ceiling instead of reporting a complete list. Add -include-org-repos / include_org_repos so org-owned repos the user administers count toward stars, repo count, repos-per-language and top-starred. Off by default, since enabling it moves those totals for every existing caller. Commit-driven cards already covered org repos through the seed list. Org repos carrying only read or write access are dropped via viewerPermission. Rename Profile.PublicRepos to RepoCount and drop "public" from the card label: the count tracks the fetch filters and includes private repos whenever the token can see them.
14 KiB
ghstats
Generate SVG cards summarizing a GitHub user's profile — written in Go.
ghstats is a single-binary CLI (and a GitHub Action wrapping it) that fetches
data for a GitHub user and writes a themed set of SVGs you can embed in your
profile README.
Marketplace listing: ghstats-cards · Source: tiennm99/ghstats
Cards rendered:
| # | Card | What it shows |
|---|---|---|
| 0 | Profile details | Login (Name) title + Octicon-labelled rows for company, location, link, join date (with age), followers/following, repo count |
| 1 | Repos per language | Donut + legend: how many owned non-fork repos use each language as primary |
| 2 | Most commit language (last year) | Donut + legend: last-year commits byte-weighted across each repo's language breakdown |
| 3 | Stats | Star, commit (lifetime + last-year), PR, issue, PR-review, contributed-to totals |
| 4 | Productive time (last year) | 24-hour bar chart with axes, title includes UTC±N.NN |
| 5 | Productive weekday (last year) | 7-bar day-of-week chart, peak day highlighted |
| 6 | Contributions (last year) | Smooth monthly area chart, Y-axis mirrored both sides, mm/yy labels |
| 7 | Contributions heatmap | Classic 7×53 calendar grid with theme-derived intensity ramp and legend |
| 8 | Top starred repos | Top 5 owned non-fork repos by ⭐, language dot + proportional bar |
| 9 | Streak | Current streak, longest streak, active days / total days with date ranges |
| 10 | Most commit language (all time) | Same as #2 but over lifetime commits |
| 11 | Productive time (all time) | Same as #4 but over lifetime commits |
| 12 | Productive weekday (all time) | Same as #5 but over lifetime commits |
| 13 | Contributions (all time) | Area chart across every active year, auto-thinned x-axis labels |
| 14 | Contributions by year | One bar per active year, peak year highlighted |
| 15 | Records (all time) | Six personal-best rows: peak day, peak month, first contribution, lifetime active days, account age, languages used |
Preview — dracula theme
Live render against the author's profile, committed by .github/workflows/demo.yml on every push to main. Rendered with start_of_week: monday so the heatmap rows and weekday bars start on Mon, and include_org_repos on so repos under the author's orgs count toward the repo and language totals. Other 64 themes in the demo gallery.
In the wild
- tiennm99/tiennm99 — author's profile README, refreshed daily via
tiennm99/ghstats@v1. Two-per-row layout, dracula theme.
Use as a GitHub Action (recommended)
Drop this in .github/workflows/ghstats.yml in your profile repo (the one
named after your username):
name: ghstats
on:
schedule:
- cron: "0 0 * * *" # daily
workflow_dispatch:
permissions:
contents: write
jobs:
cards:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: tiennm99/ghstats@v1
with:
user: ${{ github.repository_owner }}
token: ${{ secrets.GHSTATS_TOKEN }} # classic PAT with read:user + repo
themes: dracula,github_dark,tokyonight
tz: Asia/Saigon
include_forks: "true"
include_private: "true"
commit_changes: "true"
Then embed the cards in your README.md:
















Action inputs
| Input | Default | Description |
|---|---|---|
user |
— | GitHub username (required) |
token |
${{ github.token }} |
PAT with read:user + repo for private repo stats |
out |
output |
Output directory |
themes |
dracula |
Comma-separated theme ids, or all |
tz |
UTC |
IANA tz for the productive-time card (e.g. Asia/Saigon) |
start_of_week |
sunday |
First day of week for heatmap rows and weekday bars (sunday…saturday) |
top_repos |
0 |
Optional cap on seed repos probed for commit history (0 = unlimited) |
commits_per_repo |
500 |
Max commits sampled per repo (covers last-year and all-time aggregates) |
include_forks |
true |
Include forked repos in stats and commit probing |
include_private |
true |
Include private repos (requires PAT with repo scope; silently no-op otherwise) |
include_org_repos |
false |
Count org-owned repos you administer toward stars, repo count, languages, top-starred (needs read:org) |
commit_changes |
false |
Commit generated cards back to the repo |
commit_message |
chore: update ghstats cards |
Commit message |
commit_branch |
(current ref) | Target branch for auto-commit |
author_name |
github-actions[bot] |
Commit author |
author_email |
…@users.noreply.github.com |
Commit email |
Use as a CLI
go install github.com/tiennm99/ghstats@latest
Or build from source:
git clone https://github.com/tiennm99/ghstats
cd ghstats
go build -o ghstats .
Then:
export GITHUB_TOKEN=ghp_xxx
ghstats -user tiennm99 -themes dracula,github_dark -tz Asia/Saigon -out output
| Flag | Default | Description |
|---|---|---|
-user |
(required) | GitHub username |
-token |
$GITHUB_TOKEN |
Personal access token |
-out |
output |
Output directory (<out>/<theme>/…svg) |
-themes |
dracula |
Comma-separated theme ids, or all |
-tz |
Local |
IANA timezone for productive-time cards |
-start-of-week |
sunday |
First day of week for heatmap rows and weekday bars (sunday…saturday) |
-top-repos |
0 |
Optional cap on seed repos probed (0 = unlimited) |
-commits-per-repo |
500 |
Max commits sampled per repo |
-include-forks |
true |
Include forked repos in the stats |
-include-private |
true |
Include private repos (requires repo PAT scope; silently no-op otherwise) |
-include-org-repos |
false |
Count org-owned repos you administer toward stars, repo count, languages, top-starred |
-list-themes |
Print available theme ids and exit |
How attribution works
Repo sampling uses a seed list built from contributionsCollection.commitContributionsByRepository, unioned across every active contribution year. This catches every repo you've committed in — not just your top-starred ones. Each year is queried a quarter at a time: the API caps that field at 100 repos per query and drops the rest without saying so, which a prolific year hits easily.
Which repos count where. The commit-driven cards (most-commit-language, productive time, productive weekday, and everything derived from the contribution calendar) cover repos in any namespace you committed to — your own, your orgs', and upstream repos you sent PRs to. The repo-driven cards (stars, repo count, repos-per-language, top-starred) look only at repos you own. Set include_org_repos / -include-org-repos to also count org-owned repos where your permission is ADMIN; org repos you merely have read or write access to are never counted.
Commit-to-language is byte-weighted: each commit credits every language in the repo, proportional to linguist's byte share. A commit to a 60% Go / 40% Python repo adds 0.6 to Go and 0.4 to Python, regardless of which file was touched. Caveats:
- Linguist excludes prose (Markdown, AsciiDoc, reST) from byte counts, so heavily-Markdown repos skew toward whatever small code fraction linguist did detect.
- For per-file accuracy, a future
-accurate-languagesmode is planned (per-commit REST + go-enry).
Cost per run (current defaults, typical user):
- ~1 profile query + ~4 queries per active year + ~50 commit-history pages ≈ 80-100 GraphQL calls.
- Zero REST calls. Well under the 5000 points/hr budget.
Themes
Run ghstats -list-themes for the full list (65 themes ported from
github-profile-summary-cards). Built-ins include default, dark, dracula,
github, github_dark, tokyonight, onedark, nord_dark, nord_bright,
gruvbox, radical, synthwave, monokai, solarized, solarized_dark,
transparent, and more. Preview every one against real profile data in the
demo gallery.
Output
output/
dracula/
profile-details.svg
repos-per-language.svg
most-commit-language.svg
stats.svg
productive-time.svg
productive-weekday.svg
contributions.svg
contributions-heatmap.svg
top-starred-repos.svg
streak.svg
most-commit-language-all-time.svg
productive-time-all-time.svg
productive-weekday-all-time.svg
contributions-all-time.svg
contributions-by-year.svg
records.svg
output/ is entirely gitignored — it's regenerated on each run. For a
reference render of every card in every theme, see the CI-built
demo/ gallery instead.
Tokens & permissions
The default ${{ github.token }} can read public user data but will not see
your private-repo commits. For accurate stats, create a classic personal
access token with read:user and repo, save it as a repo secret (e.g.
GHSTATS_TOKEN), and pass it via the token input. include_private
defaults to true so those commits are counted automatically once the token
has repo scope; pass include_private: "false" if you want to keep private
work out of the rendered cards even when the token can see it.
Enabling include_org_repos additionally needs read:org on the token, and
SSO authorization for any org that enforces it — otherwise those repos stay
invisible and the input silently changes nothing.
Credits & inspiration
- github-profile-summary-cards by @vn7n24fzkq — card layout, chart styles, theme palette, Octicon selection, and output structure.
License
Apache-2.0 — see LICENSE.