Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add github_code_owner table from .CODEOWNERS file in a repository #200

Merged
Merged
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
90 changes: 90 additions & 0 deletions docs/tables/github_code_owner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Table: github_code_owner

All of your code owners rules defined in your repository, in CODEOWNERS files.

The `github_code_owner` table can be used to query information about **ANY** repository, and **you must specify which repository** in the where or join clause (`where repository_full_name=`, `join github_code_owner on repository_full_name=`).

## Examples

### Get All your CodeOwners rules about a specific repository

```sql
select
line,
repository_full_name,
pattern,
users,
teams,
pre_comments,
line_comment
from
github_code_owner
where
repository_full_name = 'github/docs'
order by
line asc
```

**CODEOWNERS file content**

```
# Order is important. The LAST matching pattern has the MOST precedence.
# gitignore style patterns are used, not globs.
# https://docs.github.com/articles/about-codeowners
# https://git-scm.com/docs/gitignore

# Engineering
*.js @github/docs-engineering
*.ts @github/docs-engineering
*.tsx @github/docs-engineering
/.github/ @github/docs-engineering
/script/ @github/docs-engineering
/includes/ @github/docs-engineering
/lib/search/popular-pages.json @github/docs-engineering
Dockerfile @github/docs-engineering
package-lock.json @github/docs-engineering
package.json @github/docs-engineering

# Localization
/.github/actions-scripts/msft-create-translation-batch-pr.js @github/docs-engineering
/.github/workflows/msft-create-translation-batch-pr.yml @github/docs-engineering
/translations/ @Octomerger

# Site Policy
/content/site-policy/ @github/site-policy-admins

# Content strategy
/contributing/content-markup-reference.md @github/docs-content-strategy
/contributing/content-style-guide.md @github/docs-content-strategy
/contributing/content-model.md @github/docs-content-strategy
/contributing/content-style-guide.md @github/docs-content-strategy
/contributing/content-templates.md @github/docs-content-strategy

# Requires review of #actions-oidc-integration, docs-engineering/issues/1506
content/actions/deployment/security-hardening-your-deployments/** @github/oidc
```

**ResultSet**

| line | repository\_full\_name | pattern | users | teams | pre\_comments | line\_comment |
| ---- | ---------------------- | ------------------------------------------------------------------- | ----------------- | ----------------------------------- | ---------------------------------------------------------------------------------- | ------------- |
| 7 | github/docs | \*.js | <null> | \["@github/docs-engineering"\] | \["# Engineering"\] | |
| 8 | github/docs | \*.ts | <null> | \["@github/docs-engineering"\] | \["# Engineering"\] | |
| 9 | github/docs | \*.tsx | <null> | \["@github/docs-engineering"\] | \["# Engineering"\] | |
| 10 | github/docs | /.github/ | <null> | \["@github/docs-engineering"\] | \["# Engineering"\] | |
| 11 | github/docs | /script/ | <null> | \["@github/docs-engineering"\] | \["# Engineering"\] | |
| 12 | github/docs | /includes/ | <null> | \["@github/docs-engineering"\] | \["# Engineering"\] | |
| 13 | github/docs | /lib/search/popular-pages.json | <null> | \["@github/docs-engineering"\] | \["# Engineering"\] | |
| 14 | github/docs | Dockerfile | <null> | \["@github/docs-engineering"\] | \["# Engineering"\] | |
| 15 | github/docs | package-lock.json | <null> | \["@github/docs-engineering"\] | \["# Engineering"\] | |
| 16 | github/docs | package.json | <null> | \["@github/docs-engineering"\] | \["# Engineering"\] | |
| 19 | github/docs | /.github/actions-scripts/msft-create-translation-batch-pr.js | <null> | \["@github/docs-engineering"\] | \["# Localization"\] | |
| 20 | github/docs | /.github/workflows/msft-create-translation-batch-pr.yml | <null> | \["@github/docs-engineering"\] | \["# Localization"\] | |
| 21 | github/docs | /translations/ | \["@Octomerger"\] | <null> | \["# Localization"\] | |
| 24 | github/docs | /content/site-policy/ | <null> | \["@github/site-policy-admins"\] | \["# Site Policy"\] | |
| 27 | github/docs | /contributing/content-markup-reference.md | <null> | \["@github/docs-content-strategy"\] | \["# Content strategy"\] | |
| 28 | github/docs | /contributing/content-style-guide.md | <null> | \["@github/docs-content-strategy"\] | \["# Content strategy"\] | |
| 29 | github/docs | /contributing/content-model.md | <null> | \["@github/docs-content-strategy"\] | \["# Content strategy"\] | |
| 30 | github/docs | /contributing/content-style-guide.md | <null> | \["@github/docs-content-strategy"\] | \["# Content strategy"\] | |
| 31 | github/docs | /contributing/content-templates.md | <null> | \["@github/docs-content-strategy"\] | \["# Content strategy"\] | |
| 34 | github/docs | content/actions/deployment/security-hardening-your-deployments/\*\* | <null> | \["@github/oidc"\] | \["# Requires review of #actions-oidc-integration, docs-engineering/issues/1506"\] |
3 changes: 2 additions & 1 deletion github/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func Plugin(ctx context.Context) *plugin.Plugin {
"github_branch": tableGitHubBranch(ctx),
"github_commit": tableGitHubCommit(ctx),
"github_community_profile": tableGitHubCommunityProfile(ctx),
"github_code_owner": tableGitHubCodeOwner(),
"github_gist": tableGitHubGist(),
"github_gitignore": tableGitHubGitignore(),
"github_issue": tableGitHubIssue(),
Expand Down Expand Up @@ -63,4 +64,4 @@ func Plugin(ctx context.Context) *plugin.Plugin {
},
}
return p
}
}
171 changes: 171 additions & 0 deletions github/table_github_code_owner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package github

import (
"context"
"fmt"
"github.com/google/go-github/v45/github"
"github.com/turbot/steampipe-plugin-sdk/v4/plugin/transform"
"strings"

"github.com/turbot/steampipe-plugin-sdk/v4/grpc/proto"
"github.com/turbot/steampipe-plugin-sdk/v4/plugin"
)

//// TABLE DEFINITION

func gitHubCodeOwnerColumns() []*plugin.Column {
return []*plugin.Column{
cbruno10 marked this conversation as resolved.
Show resolved Hide resolved
// Top columns
{Name: "repository_full_name", Type: proto.ColumnType_STRING, Description: "The full name of the repository, including the owner and repo name."},
// Other columns
{Name: "line", Type: proto.ColumnType_INT, Description: "The rule's line number in the CODEOWNERS file.", Transform: transform.FromField("LineNumber")},
{Name: "pattern", Type: proto.ColumnType_STRING, Description: "The pattern used to identify what code a team, or an individual is responsible for"},
{Name: "users", Type: proto.ColumnType_JSON, Description: "Users responsible for code in the repo"},
{Name: "teams", Type: proto.ColumnType_JSON, Description: "Teams responsible for code in the repo"},
{Name: "pre_comments", Type: proto.ColumnType_JSON, Description: "Specifies the comments added above a key."},
{Name: "line_comment", Type: proto.ColumnType_STRING, Description: "Specifies the comment following the node and before empty lines."},
}
}

//// TABLE DEFINITION

func tableGitHubCodeOwner() *plugin.Table {
return &plugin.Table{
Name: "github_code_owner",
Description: "Individuals or teams that are responsible for code in a repository.",
List: &plugin.ListConfig{
Hydrate: tableGitHubCodeOwnerList,
ShouldIgnoreError: isNotFoundError([]string{"404"}),
KeyColumns: plugin.SingleColumn("repository_full_name"),
},
Columns: gitHubCodeOwnerColumns(),
}
}

// // LIST FUNCTION
type CodeOwnerRule struct {
LineNumber int
Pattern string
Users []string
Teams []string
PreComments []string
LineComment string
}

func tableGitHubCodeOwnerList(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
plugin.Logger(ctx).Trace("tableGitHubCodeOwnerList")
repoFullName := d.KeyColumnQuals["repository_full_name"].GetStringValue()
owner, repoName := parseRepoFullName(repoFullName)

type CodeOwnerRuleResponse struct {
RepositoryFullName string
LineNumber int
Pattern string
Users []string
Teams []string
PreComments []string
LineComment string
}
getCodeOwners := func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
var fileContent *github.RepositoryContent
var err error

client := connect(ctx, d)
opt := &github.RepositoryContentGetOptions{}
// stop on the first found CODEOWNERS file.
// NOTE : a repository can have multiple CODEOWNERS files, even if it's invalid
// In that case, GitHub uses precedence over these files in the following order : .github/CODEOWNERS, CODEOWNERS, docs/CODEOWNERS
cbruno10 marked this conversation as resolved.
Show resolved Hide resolved
var paths = []string{".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"}
for _, path := range paths {
fileContent, _, _, err = client.Repositories.GetContents(ctx, owner, repoName, path, opt)
if err == nil {
break
}
// HTTP 404 is the only tolerated HTTP error code (if it's different, it means something is wrong with your rights or your repository)
if err.(*github.ErrorResponse).Response.StatusCode != 404 {
return nil, fmt.Errorf("Downloading file \"%s\" : %s", path, err.(*github.ErrorResponse).Response.Status)
}
}

// no CODEOWNERS file
if err != nil {
cbruno10 marked this conversation as resolved.
Show resolved Hide resolved
return []CodeOwnerRuleResponse{}, err
}

decodedContent, err := fileContent.GetContent()
if err != nil {
return []CodeOwnerRuleResponse{}, err
}

return decodeCodeOwnerFileContent(decodedContent), err
}

codeOwnersElements, err := retryHydrate(ctx, d, h, getCodeOwners)
if err != nil {
return nil, err
}

for _, codeOwner := range codeOwnersElements.([]*CodeOwnerRule) {
if codeOwner != nil {
d.StreamListItem(ctx, CodeOwnerRuleResponse{
RepositoryFullName: repoFullName,
LineNumber: codeOwner.LineNumber,
Pattern: codeOwner.Pattern,
Users: codeOwner.Users,
Teams: codeOwner.Teams,
PreComments: codeOwner.PreComments,
LineComment: codeOwner.LineComment,
})
}
}
return nil, nil
}

func decodeCodeOwnerFileContent(content string) []*CodeOwnerRule {
var codeOwnerRules []*CodeOwnerRule

var comments []string
for i, line := range strings.Split(content, "\n") {
lineNumber := i + 1
// if line is empty, consider the codeblock end
if len(line) == 0 {
comments = []string{}
continue
}
// code block with comments
if strings.HasPrefix(line, "#") {
comments = append(comments, line)
continue
}
// code owners rule
// if line is empty, consider the codeblock end
rule := strings.SplitN(line, " ", 2)
if len(rule) < 2 {
comments = []string{}
continue
}

var pattern, lineComment string
pattern = rule[0]

// line comment
ownersAndComment := strings.SplitN(rule[1], "#", 2)
if len(ownersAndComment) == 2 && len(ownersAndComment[1]) > 0 {
lineComment = ownersAndComment[1]
} else {
ownersAndComment = []string{rule[1]}
}

// owners computing
var users, teams []string
for _, owner := range strings.Split(strings.TrimSpace(ownersAndComment[0]), " ") {
if strings.Index(owner, "/") > 0 {
teams = append(teams, owner)
} else {
users = append(users, owner)
}
}
codeOwnerRules = append(codeOwnerRules, &CodeOwnerRule{LineNumber: lineNumber, Pattern: pattern, Users: users, Teams: teams, PreComments: comments, LineComment: lineComment})
}
return codeOwnerRules
}