I Built a Tool to Track My Open Source Contributions

I Built a Tool to Track My Open Source Contributions

Source: Dev.to

The Problem ## The Approach ## Architecture Decision: Library First ## Handling GitHub's Rate Limits ## The Output ## Using It ## Installation ## Basic Usage ## Automating with GitHub Actions ## Displaying on My Website ## What I Learned ## What's Next ## Resources 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. 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” So I created gh-oss-stats The core insight is to use Github’s search query author:USERNAME type:pr is:merged -user:USERNAME This find all pull requets: Request looks like this: output looks like this: From there, it's a matter of: I built this as a Go library with a CLI wrapper, not just a CLI tool. The core logic lives in an importable package: This means I can use the same code in: 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. Running the tool produces JSON like this: This feeds directly into my website's contributions section. I run this weekly via GitHub Actions to keep my website updated automatically: Now my website always has fresh data without any manual work. 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: The JSON is the contract — however you want to display it is up to you. 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. I'm planning to build a companion service gh-oss-badge that generates SVG badges you can embed in your GitHub profile README: 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 Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: https://api.github.com/search/issues?q=author:mabd-dev+type:pr+is:merged+-user:mabd-dev Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: https://api.github.com/search/issues?q=author:mabd-dev+type:pr+is:merged+-user:mabd-dev CODE_BLOCK: https://api.github.com/search/issues?q=author:mabd-dev+type:pr+is:merged+-user:mabd-dev CODE_BLOCK: { "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", }, ... ] } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "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", }, ... ] } CODE_BLOCK: { "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", }, ... ] } CODE_BLOCK: 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") Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 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") CODE_BLOCK: 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") CODE_BLOCK: { "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" }, ... ] } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "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" }, ... ] } CODE_BLOCK: { "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" }, ... ] } CODE_BLOCK: go install github.com/gh-oss-tools/gh-oss-stats/cmd/gh-oss-stats@latest Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: go install github.com/gh-oss-tools/gh-oss-stats/cmd/gh-oss-stats@latest CODE_BLOCK: go install github.com/gh-oss-tools/gh-oss-stats/cmd/gh-oss-stats@latest COMMAND_BLOCK: # 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 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # 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 COMMAND_BLOCK: # 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 COMMAND_BLOCK: 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 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: 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 COMMAND_BLOCK: 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 CODE_BLOCK: ![OSS Stats](https://oss-badge.example.com/mabd-dev.svg) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: ![OSS Stats](https://oss-badge.example.com/mabd-dev.svg) CODE_BLOCK: ![OSS Stats](https://oss-badge.example.com/mabd-dev.svg) - 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 - 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. - Fetching PR details (commits, additions, deletions) - Enriching with repo metadata (stars, description) - Aggregating into useful statistics - 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. - 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 - Loop through contributions array - Display repo name, stars, PR count - Show totals from summary - Link to the actual repos - 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