diff --git a/.changeset/cozy-guests-bake.md b/.changeset/cozy-guests-bake.md
new file mode 100644
index 00000000..13bd16b0
--- /dev/null
+++ b/.changeset/cozy-guests-bake.md
@@ -0,0 +1,5 @@
+---
+'grafana-github-datasource': patch
+---
+
+Add deployments query type
diff --git a/docs/sources/query/_index.md b/docs/sources/query/_index.md
index c0c9b9c5..290ab570 100644
--- a/docs/sources/query/_index.md
+++ b/docs/sources/query/_index.md
@@ -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
@@ -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 |
\ No newline at end of file
+| 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 |
diff --git a/pkg/github/client/client.go b/pkg/github/client/client.go
index bdbcff00..7f50e63f 100644
--- a/pkg/github/client/client.go
+++ b/pkg/github/client/client.go
@@ -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)
diff --git a/pkg/github/codescanning_test.go b/pkg/github/codescanning_test.go
index 50f19f5e..0c6abbdc 100644
--- a/pkg/github/codescanning_test.go
+++ b/pkg/github/codescanning_test.go
@@ -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()
diff --git a/pkg/github/datasource.go b/pkg/github/datasource.go
index bd8a4196..95171985 100644
--- a/pkg/github/datasource.go
+++ b/pkg/github/datasource.go
@@ -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{
diff --git a/pkg/github/deployments.go b/pkg/github/deployments.go
new file mode 100644
index 00000000..b6367521
--- /dev/null
+++ b/pkg/github/deployments.go
@@ -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
+}
diff --git a/pkg/github/deployments_handler.go b/pkg/github/deployments_handler.go
new file mode 100644
index 00000000..f521f677
--- /dev/null
+++ b/pkg/github/deployments_handler.go
@@ -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
+}
diff --git a/pkg/github/deployments_test.go b/pkg/github/deployments_test.go
new file mode 100644
index 00000000..721089b8
--- /dev/null
+++ b/pkg/github/deployments_test.go
@@ -0,0 +1,329 @@
+package github
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ googlegithub "github.com/google/go-github/v81/github"
+ "github.com/grafana/github-datasource/pkg/models"
+ "github.com/grafana/grafana-plugin-sdk-go/backend"
+)
+
+type mockDeploymentsClient struct {
+ mockDeployments []*googlegithub.Deployment
+ mockResponse *googlegithub.Response
+ expectedOwner string
+ expectedRepo string
+ t *testing.T
+}
+
+func (m *mockDeploymentsClient) Query(ctx context.Context, q interface{}, variables map[string]interface{}) error {
+ return nil
+}
+
+func (m *mockDeploymentsClient) ListWorkflows(ctx context.Context, owner, repo string, opts *googlegithub.ListOptions) (*googlegithub.Workflows, *googlegithub.Response, error) {
+ return nil, nil, nil
+}
+
+func (m *mockDeploymentsClient) GetWorkflowUsage(ctx context.Context, owner, repo, workflow string, timeRange backend.TimeRange) (models.WorkflowUsage, error) {
+ return models.WorkflowUsage{}, nil
+}
+
+func (m *mockDeploymentsClient) GetWorkflowRuns(ctx context.Context, owner, repo, workflow string, branch string, timeRange backend.TimeRange) ([]*googlegithub.WorkflowRun, error) {
+ return nil, nil
+}
+
+func (m *mockDeploymentsClient) ListAlertsForRepo(ctx context.Context, owner, repo string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error) {
+ return nil, nil, nil
+}
+
+func (m *mockDeploymentsClient) ListAlertsForOrg(ctx context.Context, owner string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error) {
+ return nil, nil, nil
+}
+
+func (m *mockDeploymentsClient) ListDeployments(ctx context.Context, owner, repo string, opts *googlegithub.DeploymentsListOptions) ([]*googlegithub.Deployment, *googlegithub.Response, error) {
+ if owner != m.expectedOwner || repo != m.expectedRepo {
+ m.t.Errorf("Expected owner/repo to be %s/%s, got %s/%s", m.expectedOwner, m.expectedRepo, owner, repo)
+ }
+
+ return m.mockDeployments, m.mockResponse, nil
+}
+
+func TestGetAllDeployments(t *testing.T) {
+ var (
+ ctx = context.Background()
+ opts = models.ListDeploymentsOptions{
+ Repository: "grafana",
+ Owner: "grafana",
+ }
+ )
+
+ // Mock response data
+ mockDeployments := []*googlegithub.Deployment{
+ {
+ ID: googlegithub.Ptr(int64(1)),
+ SHA: googlegithub.Ptr("abc123"),
+ Ref: googlegithub.Ptr("main"),
+ Task: googlegithub.Ptr("deploy"),
+ Environment: googlegithub.Ptr("production"),
+ Description: googlegithub.Ptr("Test deployment"),
+ },
+ }
+ mockResponse := &googlegithub.Response{
+ NextPage: 0,
+ }
+
+ client := &mockDeploymentsClient{
+ mockDeployments: mockDeployments,
+ mockResponse: mockResponse,
+ expectedOwner: "grafana",
+ expectedRepo: "grafana",
+ t: t,
+ }
+
+ // Call the function
+ deployments, err := GetAllDeployments(ctx, client, opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Verify result
+ if len(deployments) != len(mockDeployments) {
+ t.Errorf("Expected %d deployments, got %d", len(mockDeployments), len(deployments))
+ }
+}
+
+func TestGetAllDeploymentsWithFilters(t *testing.T) {
+ var (
+ ctx = context.Background()
+ opts = models.ListDeploymentsOptions{
+ Repository: "grafana",
+ Owner: "grafana",
+ SHA: "abc123",
+ Ref: "main",
+ Task: "deploy",
+ Environment: "production",
+ }
+ )
+
+ // Mock response data
+ mockDeployments := []*googlegithub.Deployment{
+ {
+ ID: googlegithub.Ptr(int64(1)),
+ SHA: googlegithub.Ptr("abc123"),
+ Ref: googlegithub.Ptr("main"),
+ Task: googlegithub.Ptr("deploy"),
+ Environment: googlegithub.Ptr("production"),
+ },
+ }
+ mockResponse := &googlegithub.Response{
+ NextPage: 0,
+ }
+
+ client := &mockDeploymentsClient{
+ mockDeployments: mockDeployments,
+ mockResponse: mockResponse,
+ expectedOwner: "grafana",
+ expectedRepo: "grafana",
+ t: t,
+ }
+
+ // Call the function
+ deployments, err := GetAllDeployments(ctx, client, opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Verify result
+ if len(deployments) != len(mockDeployments) {
+ t.Errorf("Expected %d deployments, got %d", len(mockDeployments), len(deployments))
+ }
+}
+
+func TestGetAllDeploymentsEmptyOwnerOrRepo(t *testing.T) {
+ var (
+ ctx = context.Background()
+ opts = models.ListDeploymentsOptions{
+ Repository: "",
+ Owner: "",
+ }
+ )
+
+ client := &mockDeploymentsClient{
+ mockDeployments: []*googlegithub.Deployment{},
+ mockResponse: &googlegithub.Response{},
+ expectedOwner: "",
+ expectedRepo: "",
+ t: t,
+ }
+
+ // Call the function
+ deployments, err := GetAllDeployments(ctx, client, opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Should return nil when owner or repo is empty
+ if deployments != nil {
+ t.Errorf("Expected nil when owner or repo is empty, got %v", deployments)
+ }
+}
+
+func TestGetDeploymentsInRange(t *testing.T) {
+ var (
+ ctx = context.Background()
+ opts = models.ListDeploymentsOptions{
+ Repository: "grafana",
+ Owner: "grafana",
+ }
+ from = time.Now().Add(-30 * 24 * time.Hour)
+ to = time.Now()
+ )
+
+ // Create deployments with different timestamps
+ now := time.Now()
+ createdAt1 := &googlegithub.Timestamp{Time: now.Add(-10 * 24 * time.Hour)} // Within range
+ createdAt2 := &googlegithub.Timestamp{Time: now.Add(-40 * 24 * time.Hour)} // Outside range (too old)
+ createdAt3 := &googlegithub.Timestamp{Time: now.Add(1 * 24 * time.Hour)} // Outside range (future)
+
+ mockDeployments := []*googlegithub.Deployment{
+ {
+ ID: googlegithub.Ptr(int64(1)),
+ CreatedAt: createdAt1,
+ },
+ {
+ ID: googlegithub.Ptr(int64(2)),
+ CreatedAt: createdAt2,
+ },
+ {
+ ID: googlegithub.Ptr(int64(3)),
+ CreatedAt: createdAt3,
+ },
+ }
+ mockResponse := &googlegithub.Response{
+ NextPage: 0,
+ }
+
+ client := &mockDeploymentsClient{
+ mockDeployments: mockDeployments,
+ mockResponse: mockResponse,
+ expectedOwner: "grafana",
+ expectedRepo: "grafana",
+ t: t,
+ }
+
+ // Call the function
+ deployments, err := GetDeploymentsInRange(ctx, client, opts, from, to)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Should only return deployment 1 (within range)
+ if len(deployments) != 1 {
+ t.Errorf("Expected 1 deployment in range, got %d", len(deployments))
+ }
+
+ if deployments[0].GetID() != 1 {
+ t.Errorf("Expected deployment ID 1, got %d", deployments[0].GetID())
+ }
+}
+
+func TestDeploymentsWrapperFrames(t *testing.T) {
+ // Create test data
+ createdAt := &googlegithub.Timestamp{Time: time.Now().Add(-48 * time.Hour)}
+ updatedAt := &googlegithub.Timestamp{Time: time.Now().Add(-24 * time.Hour)}
+ creatorLogin := "username"
+
+ deployments := DeploymentsWrapper{
+ &googlegithub.Deployment{
+ ID: googlegithub.Ptr(int64(1)),
+ SHA: googlegithub.Ptr("abc123def456"),
+ Ref: googlegithub.Ptr("main"),
+ Task: googlegithub.Ptr("deploy"),
+ Environment: googlegithub.Ptr("production"),
+ Description: googlegithub.Ptr("Test deployment"),
+ Creator: &googlegithub.User{
+ Login: googlegithub.Ptr(creatorLogin),
+ },
+ CreatedAt: createdAt,
+ UpdatedAt: updatedAt,
+ URL: googlegithub.Ptr("https://api.github.com/repos/grafana/grafana/deployments/1"),
+ StatusesURL: googlegithub.Ptr("https://api.github.com/repos/grafana/grafana/deployments/1/statuses"),
+ },
+ &googlegithub.Deployment{
+ ID: googlegithub.Ptr(int64(2)),
+ SHA: googlegithub.Ptr("def456ghi789"),
+ Ref: googlegithub.Ptr("develop"),
+ Task: googlegithub.Ptr("deploy:migrations"),
+ Environment: googlegithub.Ptr("staging"),
+ Description: googlegithub.Ptr("Another deployment"),
+ Creator: nil, // Test nil creator
+ CreatedAt: createdAt,
+ UpdatedAt: updatedAt,
+ URL: googlegithub.Ptr("https://api.github.com/repos/grafana/grafana/deployments/2"),
+ StatusesURL: googlegithub.Ptr("https://api.github.com/repos/grafana/grafana/deployments/2/statuses"),
+ },
+ }
+
+ // Get data frames
+ frames := deployments.Frames()
+
+ // Verify frames
+ if len(frames) != 1 {
+ t.Fatalf("Expected 1 frame, got %d", len(frames))
+ }
+
+ frame := frames[0]
+ if frame.Name != "deployments" {
+ t.Errorf("Expected frame name to be 'deployments', got '%s'", frame.Name)
+ }
+
+ // Check number of rows
+ if frame.Rows() != 2 {
+ t.Errorf("Expected 2 rows, got %d", frame.Rows())
+ }
+
+ // Check fields
+ expectedFields := 11
+ if len(frame.Fields) != expectedFields {
+ t.Errorf("Expected %d fields, got %d", expectedFields, len(frame.Fields))
+ }
+
+ // Verify field names
+ expectedFieldNames := []string{"id", "sha", "ref", "task", "environment", "description", "creator", "created_at", "updated_at", "url", "statuses_url"}
+ for i, expectedName := range expectedFieldNames {
+ if i >= len(frame.Fields) {
+ t.Fatalf("Field index %d out of range", i)
+ }
+ if frame.Fields[i].Name != expectedName {
+ t.Errorf("Expected field name '%s' at index %d, got '%s'", expectedName, i, frame.Fields[i].Name)
+ }
+ }
+
+ // Verify first deployment data - ID field is *int64
+ idField := frame.Fields[0]
+ idValue := idField.At(0).(*int64)
+ if *idValue != int64(1) {
+ t.Errorf("Expected first deployment ID to be 1, got %d", *idValue)
+ }
+
+ // Verify creator field - first deployment has creator, second doesn't
+ creatorField := frame.Fields[6] // creator is at index 6
+ creatorValue0 := creatorField.At(0)
+ if creatorValue0 == nil {
+ t.Error("Expected first deployment to have a creator")
+ } else {
+ creatorStr := creatorValue0.(*string)
+ if *creatorStr != creatorLogin {
+ t.Errorf("Expected creator to be '%s', got '%s'", creatorLogin, *creatorStr)
+ }
+ }
+ creatorValue1 := creatorField.At(1)
+ if creatorValue1 != nil {
+ // The frame might store a nil pointer differently, so let's check if it's actually nil or an empty string pointer
+ if strPtr, ok := creatorValue1.(*string); ok && strPtr != nil {
+ t.Errorf("Expected second deployment to have nil creator, got '%s'", *strPtr)
+ }
+ }
+}
diff --git a/pkg/github/query_handler.go b/pkg/github/query_handler.go
index 0cf972c5..d520aca9 100644
--- a/pkg/github/query_handler.go
+++ b/pkg/github/query_handler.go
@@ -62,6 +62,7 @@ func GetQueryHandlers(s *QueryHandler) *datasource.QueryTypeMux {
mux.HandleFunc(models.QueryTypeWorkflowUsage, s.HandleWorkflowUsage)
mux.HandleFunc(models.QueryTypeWorkflowRuns, s.HandleWorkflowRuns)
mux.HandleFunc(models.QueryTypeCodeScanning, s.HandleCodeScanning)
+ mux.HandleFunc(models.QueryTypeDeployments, s.HandleDeployments)
return mux
}
diff --git a/pkg/models/client.go b/pkg/models/client.go
index 5c27005d..e4cef4ab 100644
--- a/pkg/models/client.go
+++ b/pkg/models/client.go
@@ -16,4 +16,5 @@ type Client interface {
GetWorkflowRuns(ctx context.Context, owner, repo, workflow string, branch string, timeRange backend.TimeRange) ([]*googlegithub.WorkflowRun, error)
ListAlertsForRepo(ctx context.Context, owner, repo string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error)
ListAlertsForOrg(ctx context.Context, owner string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error)
+ ListDeployments(ctx context.Context, owner, repo string, opts *googlegithub.DeploymentsListOptions) ([]*googlegithub.Deployment, *googlegithub.Response, error)
}
diff --git a/pkg/models/deployments.go b/pkg/models/deployments.go
new file mode 100644
index 00000000..72e0b4f9
--- /dev/null
+++ b/pkg/models/deployments.go
@@ -0,0 +1,22 @@
+package models
+
+// ListDeploymentsOptions are the available options when listing deployments
+type ListDeploymentsOptions struct {
+ // Repository is the name of the repository being queried (ex: grafana)
+ Repository string `json:"repository"`
+
+ // Owner is the owner of the repository (ex: grafana)
+ Owner string `json:"owner"`
+
+ // SHA is the SHA recorded at creation time to filter by
+ SHA string `json:"sha,omitempty"`
+
+ // Ref is the name of the ref (branch, tag, or SHA) to filter by
+ Ref string `json:"ref,omitempty"`
+
+ // Task is the name of the task (e.g., "deploy", "deploy:migrations") to filter by
+ Task string `json:"task,omitempty"`
+
+ // Environment is the name of the environment (e.g., "production", "staging") to filter by
+ Environment string `json:"environment,omitempty"`
+}
diff --git a/pkg/models/query.go b/pkg/models/query.go
index 88292805..04496845 100644
--- a/pkg/models/query.go
+++ b/pkg/models/query.go
@@ -43,6 +43,8 @@ const (
QueryTypeWorkflowRuns = "Workflow_Runs"
// QueryTypeCodeScanning is used when querying code scanning alerts for a repository
QueryTypeCodeScanning = "Code_Scanning"
+ // QueryTypeDeployments is used when querying deployments for a repository
+ QueryTypeDeployments = "Deployments"
)
// Query refers to the structure of a query built using the QueryEditor.
@@ -153,3 +155,9 @@ type CodeScanningQuery struct {
Query
Options CodeScanningOptions `json:"options"`
}
+
+// DeploymentsQuery is used when querying deployments for a repository
+type DeploymentsQuery struct {
+ Query
+ Options ListDeploymentsOptions `json:"options"`
+}
diff --git a/pkg/plugin/datasource.go b/pkg/plugin/datasource.go
index ee7ce469..72173934 100644
--- a/pkg/plugin/datasource.go
+++ b/pkg/plugin/datasource.go
@@ -28,6 +28,7 @@ type Datasource interface {
HandleWorkflowsQuery(context.Context, *models.WorkflowsQuery, backend.DataQuery) (dfutil.Framer, error)
HandleWorkflowUsageQuery(context.Context, *models.WorkflowUsageQuery, backend.DataQuery) (dfutil.Framer, error)
HandleWorkflowRunsQuery(context.Context, *models.WorkflowRunsQuery, backend.DataQuery) (dfutil.Framer, error)
+ HandleDeploymentsQuery(context.Context, *models.DeploymentsQuery, backend.DataQuery) (dfutil.Framer, error)
CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error)
QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error)
}
diff --git a/pkg/plugin/datasource_caching.go b/pkg/plugin/datasource_caching.go
index 1c19970f..a78f5d30 100644
--- a/pkg/plugin/datasource_caching.go
+++ b/pkg/plugin/datasource_caching.go
@@ -262,6 +262,16 @@ func (c *CachedDatasource) HandleWorkflowRunsQuery(ctx context.Context, q *model
return c.saveCache(req, f, err)
}
+// HandleDeploymentsQuery is the cache wrapper for the deployments query handler
+func (c *CachedDatasource) HandleDeploymentsQuery(ctx context.Context, q *models.DeploymentsQuery, req backend.DataQuery) (dfutil.Framer, error) {
+ if value, err := c.getCache(req); err == nil {
+ return value, err
+ }
+
+ f, err := c.datasource.HandleDeploymentsQuery(ctx, q, req)
+ return c.saveCache(req, f, err)
+}
+
// CheckHealth forwards the request to the datasource and does not perform any caching
func (c *CachedDatasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
return c.datasource.CheckHealth(ctx, req)
diff --git a/pkg/testutil/client.go b/pkg/testutil/client.go
index 1a4cf2da..0bf03cd4 100644
--- a/pkg/testutil/client.go
+++ b/pkg/testutil/client.go
@@ -76,3 +76,8 @@ func (c *TestClient) ListAlertsForRepo(ctx context.Context, owner, repo string,
func (c *TestClient) ListAlertsForOrg(ctx context.Context, owner string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error) {
panic("unimplemented")
}
+
+// ListDeployments is not implemented because it is not being used in tests at the moment.
+func (c *TestClient) ListDeployments(ctx context.Context, owner, repo string, opts *googlegithub.DeploymentsListOptions) ([]*googlegithub.Deployment, *googlegithub.Response, error) {
+ panic("unimplemented")
+}
diff --git a/src/constants.ts b/src/constants.ts
index 4f86d8ef..c7fbeb96 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -20,6 +20,7 @@ export enum QueryType {
Workflows = 'Workflows',
Workflow_Usage = 'Workflow_Usage',
Workflow_Runs = 'Workflow_Runs',
+ Deployments = 'Deployments',
}
export const DefaultQueryType = QueryType.Issues;
diff --git a/src/types/query.ts b/src/types/query.ts
index 1694020f..7faf2805 100644
--- a/src/types/query.ts
+++ b/src/types/query.ts
@@ -20,7 +20,8 @@ export interface GitHubQuery extends Indexable, DataQuery, RepositoryOptions {
| ProjectsOptions
| WorkflowsOptions
| WorkflowUsageOptions
- | WorkflowRunsOptions;
+ | WorkflowRunsOptions
+ | DeploymentsOptions;
}
export interface Label {
@@ -83,6 +84,13 @@ export interface WorkflowRunsOptions extends Indexable {
branch?: string;
}
+export interface DeploymentsOptions extends Indexable {
+ sha?: string;
+ ref?: string;
+ task?: string;
+ environment?: string;
+}
+
export interface PackagesOptions extends Indexable {
names?: string;
packageType?: PackageType;
diff --git a/src/views/QueryEditor.tsx b/src/views/QueryEditor.tsx
index f94f60cd..2bc064d1 100644
--- a/src/views/QueryEditor.tsx
+++ b/src/views/QueryEditor.tsx
@@ -23,6 +23,7 @@ import QueryEditorWorkflows from './QueryEditorWorkflows';
import QueryEditorWorkflowUsage from './QueryEditorWorkflowUsage';
import QueryEditorWorkflowRuns from './QueryEditorWorkflowRuns';
import QueryEditorCodeScanning from './QueryEditorCodeScanning';
+import QueryEditorDeployments from './QueryEditorDeployments';
import { QueryType, DefaultQueryType } from '../constants';
import type { GitHubQuery } from '../types/query';
import type { GitHubDataSourceOptions } from '../types/config';
@@ -117,6 +118,11 @@ const queryEditors: {
),
},
+ [QueryType.Deployments]: {
+ component: (props: Props, onChange: (val: any) => void) => (
+
+ ),
+ },
};
/* eslint-enable react/display-name */
diff --git a/src/views/QueryEditorDeployments.tsx b/src/views/QueryEditorDeployments.tsx
new file mode 100644
index 00000000..db6f2af6
--- /dev/null
+++ b/src/views/QueryEditorDeployments.tsx
@@ -0,0 +1,92 @@
+import React, { useState } from 'react';
+import { Input, InlineField } from '@grafana/ui';
+import { RightColumnWidth, LeftColumnWidth } from './QueryEditor';
+import type { DeploymentsOptions } from 'types/query';
+
+interface Props extends DeploymentsOptions {
+ onChange: (value: DeploymentsOptions) => void;
+}
+
+const QueryEditorDeployments = (props: Props) => {
+ const { sha: initialSha, ref: initialRef, task: initialTask, environment: initialEnvironment } = props;
+ const [sha, setSha] = useState(initialSha);
+ // eslint-disable-next-line react-hooks/refs -- 'ref' is a prop name from DeploymentsOptions, not a React ref
+ const [gitRef, setGitRef] = useState(initialRef);
+ const [task, setTask] = useState(initialTask);
+ const [environment, setEnvironment] = useState(initialEnvironment);
+
+ return (
+ <>
+
+ setSha(el.currentTarget.value)}
+ onBlur={(el) =>
+ props.onChange({
+ ...props,
+ sha: el.currentTarget.value || undefined,
+ })
+ }
+ />
+
+
+ setGitRef(el.currentTarget.value)}
+ onBlur={(el) =>
+ props.onChange({
+ ...props,
+ ref: el.currentTarget.value || undefined,
+ })
+ }
+ />
+
+
+ setTask(el.currentTarget.value)}
+ onBlur={(el) =>
+ props.onChange({
+ ...props,
+ task: el.currentTarget.value || undefined,
+ })
+ }
+ />
+
+
+ setEnvironment(el.currentTarget.value)}
+ onBlur={(el) =>
+ props.onChange({
+ ...props,
+ environment: el.currentTarget.value || undefined,
+ })
+ }
+ />
+
+ >
+ );
+};
+
+export default QueryEditorDeployments;