Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions docs/tables/github_user_contribution_stats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: "Steampipe Table: github_user_contribution_stats - Query GitHub user contributions summary using SQL"
description: "Query GitHub user contribution summaries and calendar data from the GraphQL ContributionsCollection."
folder: "User"
---

## Table: github_user_contribution_stats - Query GitHub user contributions summary using SQL

The `github_user_contribution_stats` table provides access to GitHub's ContributionsCollection data for a user, including total contribution counts and the contribution calendar (weeks/days). This makes it possible to build dashboards and reports similar to a user's public contribution graph.

## Table Usage Guide

The table is scoped to a single user per query. Optionally specify `from_date` and `to_date` to constrain the contribution window, and `max_repositories` to control how many repositories are returned for commit contributions by repository.

## Important Notes

- You must specify the `login` column in the `where` clause.
- The `commit_contributions_by_repository` field returns at most 100 repositories (default 100).

## Examples

### Get contribution summary for a user

```sql+postgres
select
total_commit_contributions,
total_issue_contributions,
total_pull_request_contributions,
total_pull_request_review_contributions,
total_repositories_with_contributed_commits
from
github_user_contribution_stats
where
login = 'octocat';
```

```sql+sqlite
select
total_commit_contributions,
total_issue_contributions,
total_pull_request_contributions,
total_pull_request_review_contributions,
total_repositories_with_contributed_commits
from
github_user_contribution_stats
where
login = 'octocat';
```

### Get contribution calendar for a date range

```sql+postgres
select
contribution_calendar
from
github_user_contribution_stats
where
login = 'octocat'
and from_date = '2025-01-01'
and to_date = '2025-12-31';
```

```sql+sqlite
select
contribution_calendar
from
github_user_contribution_stats
where
login = 'octocat'
and from_date = '2025-01-01'
and to_date = '2025-12-31';
```

### Limit repositories in commit contributions breakdown

```sql+postgres
select
commit_contributions_by_repository
from
github_user_contribution_stats
where
login = 'octocat'
and max_repositories = 100;
```

```sql+sqlite
select
commit_contributions_by_repository
from
github_user_contribution_stats
where
login = 'octocat'
and max_repositories = 100;
```
39 changes: 39 additions & 0 deletions github/contribution_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package github

import (
"context"
"fmt"
"slices"

"github.com/shurcooL/githubv4"
"github.com/turbot/steampipe-plugin-github/github/models"
"github.com/turbot/steampipe-plugin-sdk/v5/plugin"
)

func appendContributionColumnIncludes(m *map[string]interface{}, cols []string) {
(*m)["includeContributionCalendar"] = githubv4.Boolean(slices.Contains(cols, "contribution_calendar"))
(*m)["includeCommitContributionsByRepository"] = githubv4.Boolean(slices.Contains(cols, "commit_contributions_by_repository"))
}

func extractContributionsCollectionFromHydrateItem(h *plugin.HydrateData) (models.ContributionsCollection, error) {
if collection, ok := h.Item.(models.ContributionsCollection); ok {
return collection, nil
}
return models.ContributionsCollection{}, fmt.Errorf("unable to parse hydrate item %v as ContributionsCollection", h.Item)
}

func contributionHydrateCalendar(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
collection, err := extractContributionsCollectionFromHydrateItem(h)
if err != nil {
return nil, err
}
return collection.ContributionCalendar, nil
}

func contributionHydrateCommitContributionsByRepository(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
collection, err := extractContributionsCollectionFromHydrateItem(h)
if err != nil {
return nil, err
}
return collection.CommitContributionsByRepository, nil
}
41 changes: 41 additions & 0 deletions github/models/contribution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package models

import "github.com/shurcooL/githubv4"

type ContributionCalendar struct {
TotalContributions int `graphql:"totalContributions" json:"total_contributions"`
Weeks []ContributionWeek `graphql:"weeks" json:"weeks"`
}

type ContributionWeek struct {
ContributionDays []ContributionDay `graphql:"contributionDays" json:"contribution_days"`
FirstDay githubv4.Date `graphql:"firstDay" json:"first_day"`
}

type ContributionDay struct {
Color string `graphql:"color" json:"color"`
ContributionCount int `graphql:"contributionCount" json:"contribution_count"`
ContributionLevel githubv4.ContributionLevel `graphql:"contributionLevel" json:"contribution_level"`
Date githubv4.Date `graphql:"date" json:"date"`
Weekday int `graphql:"weekday" json:"weekday"`
}

type CommitContributionsByRepository struct {
Repository struct {
NameWithOwner string `graphql:"nameWithOwner" json:"name_with_owner"`
Url string `graphql:"url" json:"url"`
} `graphql:"repository" json:"repository"`
Contributions struct {
TotalCount int `graphql:"totalCount" json:"total_count"`
} `graphql:"contributions" json:"contributions"`
}

type ContributionsCollection struct {
TotalCommitContributions int `graphql:"totalCommitContributions" json:"total_commit_contributions"`
TotalIssueContributions int `graphql:"totalIssueContributions" json:"total_issue_contributions"`
TotalPullRequestContributions int `graphql:"totalPullRequestContributions" json:"total_pull_request_contributions"`
TotalPullRequestReviewContributions int `graphql:"totalPullRequestReviewContributions" json:"total_pull_request_review_contributions"`
TotalRepositoriesWithContributedCommits int `graphql:"totalRepositoriesWithContributedCommits" json:"total_repositories_with_contributed_commits"`
ContributionCalendar ContributionCalendar `graphql:"contributionCalendar @include(if:$includeContributionCalendar)" json:"contribution_calendar,omitempty"`
CommitContributionsByRepository []CommitContributionsByRepository `graphql:"commitContributionsByRepository(maxRepositories: $maxRepositories) @include(if:$includeCommitContributionsByRepository)" json:"commit_contributions_by_repository,omitempty"`
}
1 change: 1 addition & 0 deletions github/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func Plugin(ctx context.Context) *plugin.Plugin {
"github_traffic_view_weekly": tableGitHubTrafficViewWeekly(),
"github_tree": tableGitHubTree(),
"github_user": tableGitHubUser(),
"github_user_contribution_stats": tableGitHubUserContributionStats(),
"github_workflow": tableGitHubWorkflow(),
},
}
Expand Down
113 changes: 113 additions & 0 deletions github/table_github_user_contribution_stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package github

import (
"context"
"fmt"
"strings"

"github.com/shurcooL/githubv4"
"github.com/turbot/steampipe-plugin-github/github/models"
"github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto"
"github.com/turbot/steampipe-plugin-sdk/v5/plugin"
"github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform"
)

const maxCommitContributionsRepositories = 100

func tableGitHubUserContributionStats() *plugin.Table {
return &plugin.Table{
Name: "github_user_contribution_stats",
Description: "Contribution summary and calendar data for a GitHub user.",
List: &plugin.ListConfig{
KeyColumns: []*plugin.KeyColumn{
{Name: "login", Require: plugin.Required},
{Name: "from_date", Require: plugin.Optional, Operators: []string{"="}},
{Name: "to_date", Require: plugin.Optional, Operators: []string{"="}},
{Name: "max_repositories", Require: plugin.Optional},
},
ShouldIgnoreError: isNotFoundError([]string{"404"}),
Hydrate: tableGitHubUserContributionStatsList,
},
Columns: commonColumns([]*plugin.Column{
{Name: "login", Type: proto.ColumnType_STRING, Description: "The login name of the user.", Transform: transform.FromQual("login")},
{Name: "from_date", Type: proto.ColumnType_TIMESTAMP, Description: "Start date for the contribution window.", Transform: transform.FromQual("from_date")},
{Name: "to_date", Type: proto.ColumnType_TIMESTAMP, Description: "End date for the contribution window.", Transform: transform.FromQual("to_date")},
{Name: "max_repositories", Type: proto.ColumnType_INT, Description: "Maximum repositories returned for commit contributions by repository.", Transform: transform.FromQual("max_repositories")},
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The max_repositories column is populated via transform.FromQual("max_repositories"), so it will be NULL unless the user explicitly provides the qualifier. However, the table applies a default of 100 when the qualifier is omitted, so the column description (and docs) are misleading about the effective value used. Consider returning the effective maxRepositories value (defaulting to 100) or updating the column description to clarify it only reflects the provided qualifier.

Suggested change
{Name: "max_repositories", Type: proto.ColumnType_INT, Description: "Maximum repositories returned for commit contributions by repository.", Transform: transform.FromQual("max_repositories")},
{Name: "max_repositories", Type: proto.ColumnType_INT, Description: "Maximum repositories returned for commit contributions by repository. This column reflects only the provided qualifier value; when omitted, a default of 100 is used internally and this column will be NULL.", Transform: transform.FromQual("max_repositories")},

Copilot uses AI. Check for mistakes.
{Name: "total_commit_contributions", Type: proto.ColumnType_INT, Description: "Total count of commit contributions.", Transform: transform.FromField("TotalCommitContributions")},
{Name: "total_issue_contributions", Type: proto.ColumnType_INT, Description: "Total count of issue contributions.", Transform: transform.FromField("TotalIssueContributions")},
{Name: "total_pull_request_contributions", Type: proto.ColumnType_INT, Description: "Total count of pull request contributions.", Transform: transform.FromField("TotalPullRequestContributions")},
{Name: "total_pull_request_review_contributions", Type: proto.ColumnType_INT, Description: "Total count of pull request review contributions.", Transform: transform.FromField("TotalPullRequestReviewContributions")},
{Name: "total_repositories_with_contributed_commits", Type: proto.ColumnType_INT, Description: "Total count of repositories with contributed commits.", Transform: transform.FromField("TotalRepositoriesWithContributedCommits")},
{Name: "contribution_calendar", Type: proto.ColumnType_JSON, Description: "Contribution calendar with weeks and days.", Hydrate: contributionHydrateCalendar, Transform: transform.FromValue().NullIfZero()},
{Name: "commit_contributions_by_repository", Type: proto.ColumnType_JSON, Description: "Commit contributions aggregated by repository.", Hydrate: contributionHydrateCommitContributionsByRepository, Transform: transform.FromValue().NullIfZero()},
}),
}
}

func tableGitHubUserContributionStatsList(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) {
login := d.EqualsQuals["login"].GetStringValue()

var fromDate *githubv4.DateTime
var toDate *githubv4.DateTime
maxRepositories := maxCommitContributionsRepositories

if d.EqualsQuals["from_date"] != nil {
fromTime := d.EqualsQuals["from_date"].GetTimestampValue().AsTime()
fromDate = githubv4.NewDateTime(githubv4.DateTime{Time: fromTime})
}

if d.EqualsQuals["to_date"] != nil {
toTime := d.EqualsQuals["to_date"].GetTimestampValue().AsTime()
toDate = githubv4.NewDateTime(githubv4.DateTime{Time: toTime})
}

if d.EqualsQuals["max_repositories"] != nil {
maxRepositories = int(d.EqualsQuals["max_repositories"].GetInt64Value())
}

if maxRepositories <= 0 {
return nil, fmt.Errorf("invalid value for 'max_repositories' must be greater than 0")
}

if maxRepositories > maxCommitContributionsRepositories {
return nil, fmt.Errorf("invalid value for 'max_repositories' must be <= %d", maxCommitContributionsRepositories)
}

var query struct {
RateLimit models.RateLimit
User struct {
ContributionsCollection models.ContributionsCollection `graphql:"contributionsCollection(from: $from, to: $to)"`
} `graphql:"user(login: $login)"`
}

variables := map[string]interface{}{
"login": githubv4.String(login),
"from": (*githubv4.DateTime)(nil),
"to": (*githubv4.DateTime)(nil),
"maxRepositories": githubv4.Int(maxRepositories),
}

if fromDate != nil {
variables["from"] = fromDate
}
if toDate != nil {
variables["to"] = toDate
}

appendContributionColumnIncludes(&variables, d.QueryContext.Columns)

client := connectV4(ctx, d)
err := client.Query(ctx, &query, variables)
plugin.Logger(ctx).Debug(rateLimitLogString("github_user_contribution_stats", &query.RateLimit))
if err != nil {
plugin.Logger(ctx).Error("github_user_contribution_stats", "api_error", err)
if strings.Contains(err.Error(), "Could not resolve to a User with the login of") {
return nil, nil
}
return nil, err
}

d.StreamListItem(ctx, query.User.ContributionsCollection)

return nil, nil
}
Loading