Automatically syncs your GitHub contribution data daily and publishes aggregated statistics as JSON. Private repository details (names, URLs, languages) are stripped from the public output — only commit counts and line changes are included.
Full sync data is stored encrypted (AES-256-GCM) in Git LFS for delta syncs. The aggregated stats are committed as plain JSON.
Workflows are disabled by default on forked repositories. Go to the Actions tab of your fork and click "I understand my workflows, go ahead and enable them".
Go to Settings > Secrets and variables > Actions and add:
| Secret | Description | How to generate |
|---|---|---|
GH_PAT |
Personal Access Token with repo and read:org scopes |
Create a PAT |
ENCRYPTION_KEY |
32-byte hex key for encrypting sync data | openssl rand -hex 32 |
Go to Actions > Setup User Branch > Run workflow. This creates a branch named after your GitHub username and commits a default config.json.
Scheduled workflows are disabled by default on forks, even after enabling Actions in step 2. Go to Actions > Sync GitHub Stats and click "Enable workflow" to activate the daily cron.
The sync workflow runs daily at midnight UTC on your user branch. You can also trigger it manually from Actions > Sync GitHub Stats > Run workflow.
Your aggregated stats will be available at:
https://raw.githubusercontent.com/<you>/github-stats/<you>/data/stats.json
Create or edit config.json on your user branch (see config.schema.json for the full schema):
{
"$schema": "./config.schema.json",
"timeZone": "America/New_York",
"concurrency": 10,
"maxRetries": 2,
"pageSize": 50,
"rateLimitGracePeriod": 1000,
"skip": {
"organizations": ["org-to-skip"],
"repositories": ["owner/repo-to-skip"]
},
"exclude": [
"owner/public-repo",
"sha256:a1b2c3..."
]
}| Field | Description |
|---|---|
timeZone |
IANA time zone for grouping commits by date and hour (default: UTC) |
concurrency |
Number of concurrent API requests during sync |
maxRetries |
Maximum retries for failed API requests |
pageSize |
Number of items per page for GraphQL pagination (default: 50) |
rateLimitGracePeriod |
Grace period in ms added when waiting for rate limit reset (default: 1000) |
skip.organizations |
Organizations to skip entirely during sync |
skip.repositories |
Repositories to skip entirely during sync (owner/repo) |
exclude |
Repositories to exclude from aggregated stats but still sync. Use owner/repo for public repos or sha256:<hash> for private repos where the hash is echo -n "owner/repo" | sha256sum |
The data/stats.json file has this structure:
{
user: {
username: string;
avatarUrl: string;
url: string;
};
organizations: {
[name: string]: { avatarUrl: string; url: string };
};
languageColors: { [language: string]: string };
repositories: {
name?: string; // only for public repos
url?: string; // only for public repos
languages?: string[]; // only for public repos
commitsPerDate: {
[date: string]: { // yyyy-MM-dd
commitCount: number;
additions: number;
deletions: number;
changedFiles: number;
};
};
commitsPerHour: {
[weekdayHour: string]: { // "ddd, hh" e.g. "Tue, 09"
commitCount: number;
additions: number;
deletions: number;
changedFiles: number;
};
};
}[];
}Dates and hours are grouped using the configured timeZone (defaults to UTC). Private repositories appear as entries with only commitsPerDate and commitsPerHour — no identifying information is included.
-
Sync (
src/sync.ts): Decrypts previous data, fetches new contributions via get-all-github-contributions, saves snapshots every 60 seconds (survives timeouts), encrypts and writes the full data. -
Stats (
src/stats.ts): Reads the encrypted data, checks for new contributions, aggregates per-repo daily stats, strips private repo metadata, writesstats.json. Only triggers a commit when there is actual new data. -
Encryption: AES-256-GCM with a random IV per write. The encrypted file is stored in Git LFS. Only the GitHub Actions workflow can decrypt it using the
ENCRYPTION_KEYsecret.