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
209 changes: 209 additions & 0 deletions docs/tables/github_project_v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
---
title: "Steampipe Table: github_project_v2 - Query GitHub Projects (V2) using SQL"
description: "Allows users to query GitHub Projects (V2), providing insights into the organization-level projects used to plan and track work."
folder: "Project"
---

# Table: github_project_v2 - Query GitHub Projects (V2) using SQL

GitHub Projects (V2) is GitHub's flexible, table and board based tool for planning and tracking work across issues and pull requests. It allows teams to organize work items, track status, and view progress across one or more repositories using customizable views, fields, and workflows.

## Table Usage Guide

The `github_project_v2` table provides insights into the ProjectsV2 owned by a GitHub organization. As a project manager or developer, explore project-specific details through this table, including title, description, visibility, status, linked repositories, and linked teams. Utilize it to uncover information about projects, such as which ones are public, which repositories and teams are associated with them, and when they were last updated.

To query this table using a [fine-grained access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token), the following permissions are required:
- Organization permissions:
- Projects (Read-only): Required to access all columns.

**Important Notes**
- You must specify the `organization` column in a `where` or `join` clause to query the table.

## Examples

### List the projects in an organization
Explore the title, state, and visibility of the ProjectsV2 owned by a specific GitHub organization to get an overview of ongoing work.

```sql+postgres
select
organization,
number,
title,
state,
is_public,
created_at
from
github_project_v2
where
organization = 'turbot';
```

```sql+sqlite
select
organization,
number,
title,
state,
is_public,
created_at
from
github_project_v2
where
organization = 'turbot';
```

### List open projects in an organization
Identify the projects that are still open in a specific organization, to help focus attention on active planning boards.

```sql+postgres
select
organization,
number,
title,
created_at,
updated_at
from
github_project_v2
where
organization = 'turbot'
and
state = 'open';
```

```sql+sqlite
select
organization,
number,
title,
created_at,
updated_at
from
github_project_v2
where
organization = 'turbot'
and state = 'open';
```

### Get a specific project by number
Retrieve the details of a single project using its project number, useful when you already know which project you want to inspect.

```sql+postgres
select
number,
title,
description,
owner,
creator
from
github_project_v2
where
organization = 'turbot'
and
number = 1;
```

```sql+sqlite
select
number,
title,
description,
owner,
creator
from
github_project_v2
where
organization = 'turbot'
and number = 1;
```

### List projects updated in the last 30 days
Discover the projects that have had recent activity, useful for tracking which planning boards are actively being maintained.

```sql+postgres
select
number,
title,
updated_at
from
github_project_v2
where
organization = 'turbot'
and
updated_at >= now() - interval '30 days'
order by
updated_at desc;
```

```sql+sqlite
select
number,
title,
updated_at
from
github_project_v2
where
organization = 'turbot'
and updated_at >= datetime('now', '-30 days')
order by
updated_at desc;
```

### List repositories and teams linked to each project
Explore which repositories and teams are linked to each project, to understand the scope of collaboration around a project.

```sql+postgres
select
number,
title,
repositories,
repositories_total_count,
teams,
teams_total_count
from
github_project_v2
where
organization = 'turbot';
```

```sql+sqlite
select
number,
title,
repositories,
repositories_total_count,
teams,
teams_total_count
from
github_project_v2
where
organization = 'turbot';
```

### List the latest status update for each project
Explore the most recent status update posted on each project, useful for quickly checking the reported health and progress of a project.

```sql+postgres
select
number,
title,
latest_status_update ->> 'status' as status,
latest_status_update ->> 'body' as body,
latest_status_update ->> 'created_at' as reported_at
from
github_project_v2
where
organization = 'turbot';
```

```sql+sqlite
select
number,
title,
json_extract(latest_status_update, '$.status') as status,
json_extract(latest_status_update, '$.body') as body,
json_extract(latest_status_update, '$.created_at') as reported_at
from
github_project_v2
where
organization = 'turbot';
```
33 changes: 33 additions & 0 deletions github/issue_pr_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ func appendIssueColumnIncludes(m *map[string]interface{}, cols []string) {
(*m)["includeIssueNodeId"] = githubv4.Boolean(slices.Contains(cols, "node_id"))
(*m)["includeIssueId"] = githubv4.Boolean(slices.Contains(cols, "id"))
(*m)["includeIssueIsReadByUser"] = githubv4.Boolean(slices.Contains(cols, "is_read_by_user"))
(*m)["includeIssueProjectItems"] = githubv4.Boolean(slices.Contains(cols, "project_items") || slices.Contains(cols, "project_items_total_count"))
(*m)["includeIssueProjectsV2"] = githubv4.Boolean(slices.Contains(cols, "projects_v2_total_count"))
}

func issueHydrateIsReadByUser(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
Expand Down Expand Up @@ -472,6 +474,37 @@ func issueHydrateLabels(_ context.Context, _ *plugin.QueryData, h *plugin.Hydrat
return issue.Labels.Nodes, nil
}

func issueHydrateProjectItems(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
issue, err := extractIssueFromHydrateItem(h)
if err != nil {
return nil, err
}
if len(issue.ProjectItems.Nodes) == 0 {
return []string{}, nil
}
nodeIds := make([]string, len(issue.ProjectItems.Nodes))
for i, item := range issue.ProjectItems.Nodes {
nodeIds[i] = item.Project.Id
}
return nodeIds, nil
}

func issueHydrateProjectItemsTotalCount(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
issue, err := extractIssueFromHydrateItem(h)
if err != nil {
return nil, err
}
return issue.ProjectItems.TotalCount, nil
}

func issueHydrateProjectsV2TotalCount(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
issue, err := extractIssueFromHydrateItem(h)
if err != nil {
return nil, err
}
return issue.ProjectsV2.TotalCount, nil
}

func extractIssueCommentFromHydrateItem(h *plugin.HydrateData) (models.IssueComment, error) {
if issueComment, ok := h.Item.(models.IssueComment); ok {
return issueComment, nil
Expand Down
11 changes: 11 additions & 0 deletions github/models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ type Issue struct {
TotalCount int
Nodes []BaseUser
} `graphql:"assignees(first: 10) @include(if:$includeIssueAssignees)" json:"assignees"`
ProjectItems struct {
TotalCount int
Nodes []struct {
Project struct {
Id string `graphql:"id" json:"id"`
} `json:"project"`
}
} `graphql:"projectItems(first: 100) @include(if:$includeIssueProjectItems)" json:"project_items"`
ProjectsV2 struct {
TotalCount int
} `graphql:"projectsV2 @include(if:$includeIssueProjectsV2)" json:"projects_v2"`
}

type IssueTemplate struct {
Expand Down
32 changes: 31 additions & 1 deletion github/models/misc.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package models

import (
"strings"
"time"

"github.com/shurcooL/githubv4"
Expand All @@ -18,6 +19,35 @@ func (t NullableTime) MarshalJSON() ([]byte, error) {
}
}

// NullableDate wraps time.Time to support unmarshalling the GraphQL `Date`
// scalar (e.g. "2024-08-11"), which does not include a time-of-day or
// timezone component like `DateTime`/`ISO8601DateTime` scalars do.
type NullableDate struct {
time.Time
}

func (d *NullableDate) UnmarshalJSON(data []byte) error {
s := strings.Trim(string(data), `"`)
if s == "" || s == "null" {
d.Time = time.Time{}
return nil
}

parsed, err := time.Parse("2006-01-02", s)
if err != nil {
return err
}
d.Time = parsed
return nil
}

func (d NullableDate) MarshalJSON() ([]byte, error) {
if d.IsZero() {
return []byte("null"), nil
}
return []byte(`"` + d.Time.Format("2006-01-02") + `"`), nil
}

type NameSlug struct {
Name string `json:"name"`
Slug string `json:"slug"`
Expand Down Expand Up @@ -55,7 +85,7 @@ type SponsorsListing struct {
FullDescription string `json:"full_description"`
IsPublic bool `json:"is_public"`
Name string `json:"name"`
NextPayoutDate time.Time `json:"next_payout_date"`
NextPayoutDate NullableDate `json:"next_payout_date"`
ResidenceCountryOrRegion string `json:"residence_country_or_region"`
ShortDescription string `json:"short_description"`
Slug string `json:"slug"`
Expand Down
61 changes: 61 additions & 0 deletions github/models/project_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package models

// ProjectV2Owner represents the owner of a project, which can be an Organization or User.
type ProjectV2Owner struct {
TypeName string `graphql:"type: __typename" json:"type"`
Organization struct {
Id int `graphql:"id: databaseId" json:"id"`
Login string `json:"login"`
} `graphql:"... on Organization" json:"organization,omitempty"`
User struct {
Id int `graphql:"id: databaseId" json:"id"`
Login string `json:"login"`
} `graphql:"... on User" json:"user,omitempty"`
}

// ProjectV2StatusUpdate represents a single status update on a project.
type ProjectV2StatusUpdate struct {
Id string `graphql:"id: fullDatabaseId" json:"id"`
NodeId string `graphql:"nodeId: id" json:"node_id"`
Status string `json:"status"`
Body string `json:"body"`
StartDate string `json:"start_date"`
TargetDate string `json:"target_date"`
CreatedAt NullableTime `json:"created_at"`
UpdatedAt NullableTime `json:"updated_at"`
Creator Actor `json:"creator"`
}

type ProjectV2 struct {
Id string `graphql:"id: fullDatabaseId @include(if:$includeId)" json:"id"`
NodeId string `graphql:"nodeId: id @include(if:$includeNodeId)" json:"node_id"`
Number int `json:"number"`
Owner ProjectV2Owner `graphql:"owner @include(if:$includeOwner)" json:"owner,omitempty"`
Creator Actor `graphql:"creator @include(if:$includeCreator)" json:"creator,omitempty"`
Title string `graphql:"title @include(if:$includeTitle)" json:"title"`
Description string `graphql:"description: shortDescription @include(if:$includeDescription)" json:"description"`
IsPublic bool `graphql:"public @include(if:$includeIsPublic)" json:"public"`
ClosedAt NullableTime `graphql:"closedAt @include(if:$includeClosedAt)" json:"closed_at"`
CreatedAt NullableTime `graphql:"createdAt @include(if:$includeCreatedAt)" json:"created_at"`
UpdatedAt NullableTime `graphql:"updatedAt @include(if:$includeUpdatedAt)" json:"updated_at"`
Closed bool `graphql:"closed @include(if:$includeState)" json:"closed"`
LatestStatusUpdate struct {
Nodes []ProjectV2StatusUpdate
} `graphql:"statusUpdates(last: 1) @include(if:$includeLatestStatusUpdate)" json:"latest_status_update"`
IsTemplate bool `graphql:"template @include(if:$includeIsTemplate)" json:"template"`
Readme string `graphql:"readme @include(if:$includeReadme)" json:"readme"`
ResourcePath string `graphql:"resourcePath @include(if:$includeResourcePath)" json:"resource_path"`
Url string `graphql:"url @include(if:$includeUrl)" json:"url"`
Repositories struct {
TotalCount int
Nodes []struct {
NameWithOwner string `json:"name_with_owner"`
}
} `graphql:"repositories(first: 100) @include(if:$includeRepositories)" json:"repositories"`
Teams struct {
TotalCount int
Nodes []struct {
Slug string `json:"slug"`
}
} `graphql:"teams(first: 100) @include(if:$includeTeams)" json:"teams"`
}
1 change: 1 addition & 0 deletions github/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func Plugin(ctx context.Context) *plugin.Plugin {
"github_organization_ruleset": tableGitHubOrganizationRuleset(),
"github_package": tableGitHubPackage(),
"github_package_version": tableGitHubPackageVersion(),
"github_project_v2": tableGitHubProjectV2(),
"github_pull_request": tableGitHubPullRequest(),
"github_pull_request_comment": tableGitHubPullRequestComment(),
"github_pull_request_review": tableGitHubPullRequestReview(),
Expand Down
Loading