- deployment-guide.md: embed example now lists all 14 cards (was 9). - codebase-summary.md: FetchProductive row includes Weekday / WeekdayAllTime; shared helpers list adds renderWeekday, renderHeatmap, mixHex/parseHex. - system-architecture.md: per-commit fetch diagram shows the weekday bucket alongside the hour bucket.
6.5 KiB
Deployment Guide
Three consumption paths: GitHub Action, prebuilt binaries, go install.
1. GitHub Action (recommended for README auto-updates)
Workflow template
File: .github/workflows/ghstats.yml in your profile repo.
name: ghstats
on:
schedule:
- cron: "0 0 * * *" # daily at 00:00 UTC
workflow_dispatch:
permissions:
contents: write # needed for commit_changes
jobs:
cards:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: tiennm99/ghstats@v1
with:
user: ${{ github.repository_owner }}
token: ${{ secrets.GHSTATS_TOKEN }}
themes: dracula,github_dark,tokyonight
tz: Asia/Saigon
include_forks: "false"
include_private: "false"
commit_changes: "true"
Required secrets
GHSTATS_TOKEN: a classic personal access token with at minimum:
| Scope | Needed for |
|---|---|
read:user |
Basic profile fields, contribution calendar |
repo |
Only if include_private: "true" |
Fine-grained PATs and the default ${{ github.token }} lack the introspection scope for contribution calendars in many orgs, so a classic PAT is recommended.
Create one at https://github.com/settings/tokens → "Generate new token (classic)" → select read:user (+ repo if needed) → save as repo secret GHSTATS_TOKEN.
Embedding in README















The Action commits SVGs to output/<theme>/ on the default branch. GitHub serves them from the raw URL the README references.
2. Prebuilt binaries
Each tag under v* publishes:
- Linux
amd64,arm64 - macOS
amd64,arm64 - Windows
amd64
Released via .github/workflows/release.yml which matrixes GOOS × GOARCH, strips symbols (-ldflags="-s -w"), and uploads tar.gz / zip to the GitHub Release.
Install:
# Linux x86_64 example
curl -L https://github.com/tiennm99/ghstats/releases/latest/download/ghstats_linux_amd64.tar.gz \
| tar xz
./ghstats -user YOUR_USERNAME
3. go install
go install github.com/tiennm99/ghstats@latest
Requires Go 1.26+. Puts the binary in $(go env GOPATH)/bin.
Docker image
Published to ghcr.io/tiennm99/ghstats:<tag> on each v* release via .github/workflows/release.yml (buildx, multi-tag: exact version, major.minor, major, latest).
The Action itself uses a runner-built image by default (image: Dockerfile in action.yml). To switch to the pre-built image for faster cold starts, edit action.yml:
runs:
using: docker
image: docker://ghcr.io/tiennm99/ghstats:v1
Release process
- Tag:
git tag -a v1.2.0 -m "..." && git push origin v1.2.0. release.ymlrunsgo vet+go testas a gate before the docker and binaries jobs. If tests fail, no artifacts ship.- On green, GHCR push + cross-platform binary artifacts happen automatically.
- The
update-major-tagjob force-moves the floating major tag (e.g.v1) to this release's commit after test + docker + binaries all pass. Consumers pinned totiennm99/ghstats@v1pick up the release on their next Action run without a workflow edit. - Docker base images and third-party actions are SHA-pinned (with version comments) so mutable-tag changes upstream can't rewrite a released image.
- Marketplace publishing (one-time per repo): GitHub only exposes the
"Publish this Action to the GitHub Marketplace" toggle on the Release
web UI — there is no CLI flag. Open the newly created release at
https://github.com/tiennm99/ghstats/releases/tag/vX.Y.Z/edit, tick the marketplace checkbox, accept the terms, and re-publish. Subsequent releases inherit marketplace visibility automatically. The Marketplace listing name isghstats-cards(set inaction.yml) because the bareghstatsis already taken on the Marketplace.
Rollback
- Revert the tag:
git push --delete origin v1.2.0, delete GitHub release, delete GHCR tag. - Users pinned to
@v1keep working because the previous patch is still tagged.
Rate limit considerations
| Scenario | GraphQL calls per run | Notes |
|---|---|---|
| Typical user, defaults | 15–40 | Well under 5000 pts/hr |
| Active user (8 years, 30+ seed repos) | 40–80 | Still comfortable |
-include-private=true with 100+ work repos |
80–200 | Fine for daily cron |
| Adversarial user with 500+ committed repos/year | Capped by maxRepositories: 100 per year query |
Long tail drops silently |
No REST calls today. Future -accurate-languages mode will push toward 1000+ REST per run; schedule that mode less frequently (weekly, not daily).
The client auto-handles rate-limit responses: on 429 or 403 with X-RateLimit-Remaining: 0, it sleeps up to 5 minutes (honoring Retry-After / X-RateLimit-Reset) and retries once. A reset window longer than 5 min surfaces as an error so CI can reschedule instead of burning runner time. Use the -timeout flag (default 30m) to cap total fetch duration; SIGINT/SIGTERM cancels in-flight requests cleanly.
Troubleshooting
| Symptom | Check |
|---|---|
| "error: fetch profile: graphql: Could not resolve to a User" | Username typo |
| "http 401" | Token expired or lacks read:user |
| "rate limit resets in 42m (>5m0s max wait)" | Client refused to sleep through a long window; reschedule the Action |
| "http 403" on non-rate-limit path | PAT scope too narrow |
| Blank contribution chart | User has 0 contributions in their window; expected |
| Private repo data missing | -include-private=true not set, or PAT lacks repo |
| Nothing committed by the Action | Check permissions: contents: write in the workflow |