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/cozy-guests-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'grafana-github-datasource': patch
---

Add deployments query type
52 changes: 51 additions & 1 deletion docs/sources/query/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ The data source supports the following query types, which you can select from th
- [**Workflows**](#workflows): List GitHub Actions workflows defined in a repository.
- [**Workflow usage**](#workflow-usage): Retrieve usage statistics for a workflow, such as run counts and durations.
- [**Workflow runs**](#workflow-runs): List runs for a specific workflow, including status, conclusion, and timing information.
- [**Deployments**](#deployments): List deployments for a repository, including environment, ref, and task information.

### Commits

Expand Down Expand Up @@ -665,4 +666,53 @@ Show all completed runs for the `Levitate` workflow in the `grafana/grafana` rep
| conclusion | Final conclusion of the workflow run, can be: `success`, `failure`, `neutral`, `cancelled`, `skipped`, `timed_out`, or `action_required` |
| event | Event that triggered the workflow run (e.g., `push`, `pull_request`) - see [Events that trigger workflows](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows) |
| workflow_id | Unique identifier for the workflow definition |
| run_number | The run number for this workflow run in the repository |
| run_number | The run number for this workflow run in the repository |
### Deployments

List deployments for a repository, including environment, ref, and task information. Deployments track deployment requests for specific refs (branches, tags, or SHAs) to different environments.

#### Query options

| Name | Description | Required |
| ---------- | ------------------------------------------------------------------------------- | -------- |
| Owner | GitHub user or organization that owns the repository | Yes |
| Repository | Name of the repository | Yes |
| SHA | Filter deployments by the SHA recorded at creation time | No |
| Ref | Filter by ref name (branch, tag, or SHA) | No |
| Task | Filter by task name (e.g., "deploy", "deploy:migrations") | No |
| Environment | Filter by environment name (e.g., "production", "staging", "qa") | No |

##### Sample queries

Show all deployments for the `grafana/grafana` repository:

- Owner: `grafana`
- Repository: `grafana`

Show all deployments to the production environment:

- Owner: `grafana`
- Repository: `grafana`
- Environment: `production`

Show all deployments for a specific branch:

- Owner: `grafana`
- Repository: `grafana`
- Ref: `main`

#### Response

| Name | Description |
| ------------ | ------------------------------------------------------------------------ |
| id | Unique identifier for the deployment |
| sha | SHA of the commit that was deployed |
| ref | Ref (branch, tag, or SHA) that was deployed |
| task | Task name (e.g., "deploy", "deploy:migrations") |
| environment | Environment name (e.g., "production", "staging") |
| description | Description of the deployment |
| creator | GitHub handle of the user who created the deployment |
| created_at | When the deployment was created: YYYY-MM-DD HH:MM:SS |
| updated_at | When the deployment was last updated: YYYY-MM-DD HH:MM:SS |
| url | API URL for the deployment |
| statuses_url | API URL for the deployment statuses |
9 changes: 9 additions & 0 deletions pkg/github/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ func (client *Client) ListAlertsForOrg(ctx context.Context, owner string, opts *
return alerts, resp, err
}

// ListDeployments sends a request to the GitHub rest API to list the deployments in a specific repository.
func (client *Client) ListDeployments(ctx context.Context, owner, repo string, opts *googlegithub.DeploymentsListOptions) ([]*googlegithub.Deployment, *googlegithub.Response, error) {
deployments, resp, err := client.restClient.Repositories.ListDeployments(ctx, owner, repo, opts)
if err != nil {
return nil, nil, addErrorSourceToError(err, resp)
}
return deployments, resp, err
}

// GetWorkflowUsage returns the workflow usage for a specific workflow.
func (client *Client) GetWorkflowUsage(ctx context.Context, owner, repo, workflow string, timeRange backend.TimeRange) (models.WorkflowUsage, error) {
actors := make(map[string]struct{}, 0)
Expand Down
4 changes: 4 additions & 0 deletions pkg/github/codescanning_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ func (m *mockClient) ListAlertsForOrg(ctx context.Context, owner string, opts *g
return m.mockAlerts, m.mockResponse, nil
}

func (m *mockClient) ListDeployments(ctx context.Context, owner, repo string, opts *googlegithub.DeploymentsListOptions) ([]*googlegithub.Deployment, *googlegithub.Response, error) {
return nil, nil, nil
}

func TestGetCodeScanningAlerts(t *testing.T) {
var (
ctx = context.Background()
Expand Down
17 changes: 17 additions & 0 deletions pkg/github/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,23 @@ func (d *Datasource) HandleWorkflowRunsQuery(ctx context.Context, query *models.
return GetWorkflowRuns(ctx, d.client, opt, req.TimeRange)
}

// HandleDeploymentsQuery is the query handler for listing GitHub Deployments
func (d *Datasource) HandleDeploymentsQuery(ctx context.Context, query *models.DeploymentsQuery, req backend.DataQuery) (dfutil.Framer, error) {
opt := models.ListDeploymentsOptions{
Repository: query.Repository,
Owner: query.Owner,
SHA: query.Options.SHA,
Ref: query.Options.Ref,
Task: query.Options.Task,
Environment: query.Options.Environment,
}

if req.TimeRange.From.Unix() <= 0 && req.TimeRange.To.Unix() <= 0 {
return GetAllDeployments(ctx, d.client, opt)
}
return GetDeploymentsInRange(ctx, d.client, opt, req.TimeRange.From, req.TimeRange.To)
}

// CheckHealth is the health check for GitHub
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
_, err := GetAllRepositories(ctx, d.client, models.ListRepositoriesOptions{
Expand Down
124 changes: 124 additions & 0 deletions pkg/github/deployments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package github

import (
"context"
"fmt"
"time"

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

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

// DeploymentsWrapper is a list of GitHub deployments
type DeploymentsWrapper []*googlegithub.Deployment

// Frames converts the list of deployments to a Grafana DataFrame
func (deployments DeploymentsWrapper) Frames() data.Frames {
frame := data.NewFrame(
"deployments",
data.NewField("id", nil, []*int64{}),
data.NewField("sha", nil, []*string{}),
data.NewField("ref", nil, []*string{}),
data.NewField("task", nil, []*string{}),
data.NewField("environment", nil, []*string{}),
data.NewField("description", nil, []*string{}),
data.NewField("creator", nil, []*string{}),
data.NewField("created_at", nil, []*time.Time{}),
data.NewField("updated_at", nil, []*time.Time{}),
data.NewField("url", nil, []*string{}),
data.NewField("statuses_url", nil, []*string{}),
)

for _, deployment := range deployments {
var creator *string
if deployment.Creator != nil {
creatorLogin := deployment.Creator.GetLogin()
if creatorLogin != "" {
creator = &creatorLogin
}
}

frame.AppendRow(
deployment.ID,
deployment.SHA,
deployment.Ref,
deployment.Task,
deployment.Environment,
deployment.Description,
creator,
deployment.CreatedAt.GetTime(),
deployment.UpdatedAt.GetTime(),
deployment.URL,
deployment.StatusesURL,
)
}

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

// GetAllDeployments retrieves every deployment from a repository
func GetAllDeployments(ctx context.Context, client models.Client, opts models.ListDeploymentsOptions) (DeploymentsWrapper, error) {
if opts.Owner == "" || opts.Repository == "" {
return nil, nil
}

deployments := []*googlegithub.Deployment{}

// Build the list options with filters
listOpts := &googlegithub.DeploymentsListOptions{
ListOptions: googlegithub.ListOptions{PerPage: 100},
}

if opts.SHA != "" {
listOpts.SHA = opts.SHA
}
if opts.Ref != "" {
listOpts.Ref = opts.Ref
}
if opts.Task != "" {
listOpts.Task = opts.Task
}
if opts.Environment != "" {
listOpts.Environment = opts.Environment
}

page := 1
for page != 0 {
listOpts.Page = page
deploymentsPage, resp, err := client.ListDeployments(ctx, opts.Owner, opts.Repository, listOpts)
if err != nil {
return nil, fmt.Errorf("listing deployments: opts=%+v %w", opts, err)
}

deployments = append(deployments, deploymentsPage...)

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

return DeploymentsWrapper(deployments), nil
}

// GetDeploymentsInRange retrieves every deployment from the repository and then returns the ones that fall within the given time range.
func GetDeploymentsInRange(ctx context.Context, client models.Client, opts models.ListDeploymentsOptions, from time.Time, to time.Time) (DeploymentsWrapper, error) {
deployments, err := GetAllDeployments(ctx, client, opts)
if err != nil {
return nil, err
}

filtered := []*googlegithub.Deployment{}

for _, deployment := range deployments {
createdAt := deployment.CreatedAt.GetTime()
if createdAt != nil && !createdAt.Before(from) && !createdAt.After(to) {
filtered = append(filtered, deployment)
}
}

return DeploymentsWrapper(filtered), nil
}
24 changes: 24 additions & 0 deletions pkg/github/deployments_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package github

import (
"context"

"github.com/grafana/github-datasource/pkg/dfutil"
"github.com/grafana/github-datasource/pkg/models"
"github.com/grafana/grafana-plugin-sdk-go/backend"
)

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

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