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
5 changes: 5 additions & 0 deletions .changeset/rare-planes-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'grafana-github-datasource': minor
---

adds a Check Runs query type that lists the check runs reported against a commit, branch, or tag, including status, conclusion, and duration
43 changes: 43 additions & 0 deletions docs/sources/query-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This document describes the query types and response fields available in the Git
Select a query type from the **Query Type** drop-down in the query editor:

- [**Branches**](#branches): List branches for a repository, with optional name filtering.
- [**Check runs**](#check-runs): List check runs for a commit, branch, or tag, including status, conclusion, and duration.
- [**Code scanning**](#code-scanning): Query code scanning alerts for a repository or organization.
- [**Commit files**](#commit-files): List files changed in a specific commit.
- [**Commits**](#commits): Retrieve a list of commits for a branch or ref within a repository, including commit message, author, and timestamp.
Expand Down Expand Up @@ -203,6 +204,48 @@ The response includes one row per changed file:
| status | Change type: `added`, `modified`, `renamed`, etc. |
| previous_filename | Original path for renamed files |

### Check runs

List check runs for a git reference. Check runs are the individual checks reported against a commit by GitHub Apps, such as GitHub Actions jobs, and include the status, conclusion, and how long each check took.

{{< admonition type="note" >}}
The reference can be a commit SHA, a branch formatted as `heads/<branch name>`, or a tag formatted as `tags/<tag name>`.
{{< /admonition >}}

#### Query options

| Name | Description | Required |
|------|-------------|----------|
| Owner | The GitHub user or organization that owns the repository | Yes |
| Repository | The name of the repository | Yes |
| Ref | The commit SHA, branch, or tag to list check runs for | Yes |
| Check name | Only return check runs with this exact name | No |
| Status | Only return check runs with the given status: `queued`, `in_progress`, or `completed` | No |
| Filter | `latest` returns the most recent check run for each name, `all` returns every check run. Defaults to `latest` | No |

#### Response

The response includes one row per check run:

| Name | Description |
|------|-------------|
| id | Unique identifier of the check run |
| name | Name of the check run |
| head_sha | SHA of the commit the check run is associated with |
| status | Current status: `queued`, `in_progress`, or `completed` |
| conclusion | Result of a completed check run, such as `success`, `failure`, `neutral`, `cancelled`, `skipped`, `timed_out`, or `action_required` |
| started_at | Time the check run started |
| completed_at | Time the check run completed, empty while it is still running |
| duration_seconds | How long the check run took in seconds, empty while it is still running |
| html_url | URL of the check run on GitHub |
| details_url | URL of the external site with the full check run details |
| app_name | Name of the GitHub App that created the check run |
| app_slug | Slug of the GitHub App that created the check run |
| check_suite_id | Identifier of the check suite the check run belongs to |
| output_title | Title of the check run output |
| output_summary | Summary of the check run output |
| annotations_count | Number of annotations attached to the check run |

### Contributors

Get a list of contributors to an organization or repository.
Expand Down
128 changes: 128 additions & 0 deletions pkg/github/checkruns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package github

import (
"context"
"fmt"
"time"

googlegithub "github.com/google/go-github/v84/github"
"github.com/grafana/grafana-plugin-sdk-go/data"

"github.com/grafana/github-datasource/pkg/models"
)

// CheckRunsWrapper is a list of check runs returned by the GitHub API
type CheckRunsWrapper []*googlegithub.CheckRun

// Frames converts the list of check runs to a Grafana DataFrame
func (checkRuns CheckRunsWrapper) Frames() data.Frames {
frame := data.NewFrame(
"check_runs",
data.NewField("id", nil, []int64{}),
data.NewField("name", nil, []string{}),
data.NewField("head_sha", nil, []string{}),
data.NewField("status", nil, []string{}),
data.NewField("conclusion", nil, []string{}),
data.NewField("started_at", nil, []*time.Time{}),
data.NewField("completed_at", nil, []*time.Time{}),
data.NewField("duration_seconds", nil, []*int64{}),
data.NewField("html_url", nil, []string{}),
data.NewField("details_url", nil, []string{}),
data.NewField("app_name", nil, []string{}),
data.NewField("app_slug", nil, []string{}),
data.NewField("check_suite_id", nil, []int64{}),
data.NewField("output_title", nil, []string{}),
data.NewField("output_summary", nil, []string{}),
data.NewField("annotations_count", nil, []int64{}),
)

for _, checkRun := range checkRuns {
startedAt := checkRunTimestamp(checkRun.StartedAt)
completedAt := checkRunTimestamp(checkRun.CompletedAt)

frame.AppendRow(
checkRun.GetID(),
checkRun.GetName(),
checkRun.GetHeadSHA(),
checkRun.GetStatus(),
checkRun.GetConclusion(),
startedAt,
completedAt,
checkRunDuration(startedAt, completedAt),
checkRun.GetHTMLURL(),
checkRun.GetDetailsURL(),
checkRun.GetApp().GetName(),
checkRun.GetApp().GetSlug(),
checkRun.GetCheckSuite().GetID(),
checkRun.GetOutput().GetTitle(),
checkRun.GetOutput().GetSummary(),
int64(checkRun.GetOutput().GetAnnotationsCount()),
)
}

frame.Meta = &data.FrameMeta{PreferredVisualization: data.VisTypeTable}
return data.Frames{frame}
}

// checkRunTimestamp converts an optional GitHub timestamp into a nullable time
func checkRunTimestamp(ts *googlegithub.Timestamp) *time.Time {
if ts == nil || ts.IsZero() {
return nil
}
return nullableTime(ts.Time)
}

// checkRunDuration returns how long a check run took in seconds.
// It returns nil when the check run has not completed yet, or when either timestamp is missing.
func checkRunDuration(startedAt, completedAt *time.Time) *int64 {
if startedAt == nil || completedAt == nil {
return nil
}
seconds := int64(completedAt.Sub(*startedAt).Seconds())
return &seconds
}

// GetCheckRuns lists the check runs for a git reference in a repository, handling pagination.
// GET /repos/{owner}/{repo}/commits/{ref}/check-runs
// https://docs.github.com/en/rest/checks/runs#list-check-runs-for-a-git-reference
func GetCheckRuns(ctx context.Context, client models.Client, opts models.CheckRunsOptions) (CheckRunsWrapper, error) {
if opts.Owner == "" || opts.Repository == "" || opts.Ref == "" {
return nil, nil
}

listOpts := &googlegithub.ListCheckRunsOptions{
ListOptions: googlegithub.ListOptions{PerPage: 100},
}
if opts.CheckName != "" {
listOpts.CheckName = &opts.CheckName
}
if opts.Status != "" {
listOpts.Status = &opts.Status
}
if opts.Filter != "" {
listOpts.Filter = &opts.Filter
}

var checkRuns []*googlegithub.CheckRun
page := 1

for {
listOpts.ListOptions.Page = page

results, resp, err := client.ListCheckRunsForRef(ctx, opts.Owner, opts.Repository, opts.Ref, listOpts)
if err != nil {
return nil, fmt.Errorf("listing check runs: owner=%s repo=%s ref=%s page=%d: %w", opts.Owner, opts.Repository, opts.Ref, page, err)
}

if results != nil {
checkRuns = append(checkRuns, results.CheckRuns...)
}

if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
}

return CheckRunsWrapper(checkRuns), nil
}
25 changes: 25 additions & 0 deletions pkg/github/checkruns_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package github

import (
"context"

"github.com/grafana/grafana-plugin-sdk-go/backend"

"github.com/grafana/github-datasource/pkg/dfutil"
"github.com/grafana/github-datasource/pkg/models"
)

func (s *QueryHandler) handleCheckRunsRequests(ctx context.Context, q backend.DataQuery) backend.DataResponse {
query := &models.CheckRunsQuery{}
if err := UnmarshalQuery(q.JSON, query); err != nil {
return *err
}
return dfutil.FrameResponseWithError(s.Datasource.HandleCheckRunsQuery(ctx, query, q))
}

// HandleCheckRuns handles the plugin query for github check runs
func (s *QueryHandler) HandleCheckRuns(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
return &backend.QueryDataResponse{
Responses: processQueries(ctx, req, s.handleCheckRunsRequests),
}, nil
}
Loading