Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
70 changes: 70 additions & 0 deletions docs/tables/github_user_contribution_stats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
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.

**Important Notes**
- You must specify the `login` column in the `where` clause.
Comment thread
salekseev marked this conversation as resolved.

## 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';
```
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
95 changes: 95 additions & 0 deletions github/table_github_user_contribution_stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package github

import (
"context"
"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"
)

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{"="}},
},
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: "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

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})
}

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(100),
}
Comment thread
salekseev marked this conversation as resolved.

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