Github contributions graph is great at showing activity, but it does not answer the question: what open source projects I have contributed to.
I wanted to display open source projects I have contributed to on my personal website. I can do this manually. However, this can get annoying and add extra thing to remember.
I want to show projects I contributed to, how many PR’s merged, lines of code contributed. Github does not surface this easily, so i built a tool to do so.
The Problem
If you contribute to external repositories (projects you don’t own) Github buries this info. You can get it manually by searching your PR’s, but their is no API endpoint that says “give me all this user’s contributions to external repositories”
I wanted:
- List of external projects contributed to
- Number of merged PR’s per project
- Commit count and number of lines added/removed (per project)
- JSON output I can feed to my website
So I created gh-oss-stats
The Approach
The core insight is to use Github’s search query
author:USERNAME type:pr is:merged -user:USERNAME
This find all pull requets:
- Authored by you (
author:USERNAME) - That are PR’s not issues (
type:pr) - That are merged (
is:merged) - For repos you don’t own (
-user:USERNAME) That's your OSS contribution history in one query.
Request looks like this:
https://api.github.com/search/issues?q=author:mabd-dev+type:pr+is:merged+-user:mabd-dev
output looks like this:
{
"total_count": 20,
"incomplete_results": false,
"items": [
{
{
"url": "https://api.github.com/repos/qamarelsafadi/JetpackComposeTracker/issues/9",
"repository_url": "https://api.github.com/repos/qamarelsafadi/JetpackComposeTracker",
"labels_url": "https://api.github.com/repos/qamarelsafadi/JetpackComposeTracker/issues/9/labels{/name}",
"comments_url": "https://api.github.com/repos/qamarelsafadi/JetpackComposeTracker/issues/9/comments",
"events_url": "https://api.github.com/repos/qamarelsafadi/JetpackComposeTracker/issues/9/events",
"html_url": "https://github.com/qamarelsafadi/JetpackComposeTracker/pull/9",
"id": 3204496021,
"node_id": "PR_kwDONQBujs6diLmP",
"number": 9,
"title": "🔧 Refactor: Add Global Theme Support for UI Customization",
"user": {...},
"labels": [...],
"state": "closed",
},
...
]
}
From there, it's a matter of:
- Fetching PR details (commits, additions, deletions)
- Enriching with repo metadata (stars, description)
- Aggregating into useful statistics
Architecture Decision: Library First
I built this as a Go library with a CLI wrapper, not just a CLI tool. The core logic lives in an importable package:
import "github.com/gh-oss-tools/gh-oss-stats/pkg/ossstats"
client := ossstats.New(
ossstats.WithToken(os.Getenv("GITHUB_TOKEN")),
ossstats.WithLOC(true), LOC: lines of code
)
stats, err := client.GetContributions(ctx, "mabd-dev")
This means I can use the same code in:
- The CLI tool (for local use)
- GitHub Actions (automated updates)
- A future badge service (SVG generation)
- Anywhere else I need this data The CLI is just a thin wrapper that parses flags and calls the library.
Handling GitHub's Rate Limits
GitHub's API has limits: 5,000 requests/hour for authenticated users, but only 60 requests/hour for the Search API. For someone with many contributions, you can burn through this quickly.
The tool implements:
- Exponential backoff on rate limit errors
- 2-second delays between search API calls
- Controlled concurrency (5 parallel requests for PR details)
- Partial results if rate limited mid-fetch
The Output
Running the tool produces JSON like this:
{
"username": "mabd-dev",
"generatedAt": "2025-12-21T06:46:57.823990311Z",
"summary": {
"totalProjects": 7,
"totalPRsMerged": 17,
"totalCommits": 58,
"totalAdditions": 1270,
"totalDeletions": 594
},
"contributions": [
{
"repo": "qamarelsafadi/JetpackComposeTracker",
"owner": "qamarelsafadi",
"repoName": "JetpackComposeTracker",
"description": "This is a tool to track you recomposition state in real-time !",
"repoURL": "https://github.com/qamarelsafadi/JetpackComposeTracker",
"stars": 94,
"prsMerged": 2,
"commits": 14,
"additions": 181,
"deletions": 78,
"firstContribution": "2025-06-14T20:55:24Z",
"lastContribution": "2025-07-21T21:39:53Z"
},
...
]
}
This feeds directly into my website's contributions section.
Using It
Installation
go install github.com/gh-oss-tools/gh-oss-stats/cmd/gh-oss-stats@latest
Basic Usage
# Set your GitHub token
export GITHUB_TOKEN=ghp_xxxxxxxxxxxx
# Run it
gh-oss-stats --user YOUR_USERNAME
# Save to file
gh-oss-stats --user YOUR_USERNAME -o contributions.json
Automating with GitHub Actions
I run this weekly via GitHub Actions to keep my website updated automatically:
name: Update OSS Contributions
on:
schedule:
- cron: '0 0 * * 0' # Weekly on Sunday
workflow_dispatch: # Manual trigger
permissions:
contents: write
jobs:
update-stats:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Install gh-oss-stats
run: go install github.com/gh-oss-tools/gh-oss-stats/cmd/gh-oss-stats@latest
- name: Fetch contributions
env:
GITHUB_TOKEN: ${{ secrets.GH_OSS_TOKEN }}
run: |
gh-oss-stats \
--user YOUR_USERNAME \
--exclude-orgs="your-org" \
-o data/contributions.json
- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add data/contributions.json
if ! git diff --staged --quiet; then
git commit -m "Update OSS contributions"
git push
fi
Now my website always has fresh data without any manual work.
Displaying on My Website
On mabd.dev, I read the JSON file and render it. The exact implementation depends on your stack, but the data structure makes it straightforward:
- Loop through
contributionsarray - Display repo name, stars, PR count
- Show totals from
summary - Link to the actual repos
The JSON is the contract — however you want to display it is up to you.
What I Learned
GitHub's Search API is powerful but quirky. The -user: exclusion syntax does not exclude repos you own on your organization. I had to do custom logic to detect that.
Library-first design pays off. Building the core as an importable package meant the CLI came together in under an hour. It also means future tools (like a badge service) can reuse 100% of the logic.
What's Next
I'm planning to build a companion service gh-oss-badge that generates SVG badges you can embed in your GitHub profile README:
markdown

Same data, different presentation. The library-first architecture means this service will just import gh-oss-stats/pkg/ossstats and add an HTTP layer on top.
If you want to track your own OSS contributions, give gh-oss-stats a try. It's open source (naturally), and contributions are welcome
Resources
- Github api docs: https://docs.github.com/en/rest?apiVersion=2022-11-28
- Github api rate limit: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28
- Authenticating to rest api: https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28
Top comments (0)