Skip to content

Commit

Permalink
Add support for workflow runs data
Browse files Browse the repository at this point in the history
  • Loading branch information
lehmanju committed Dec 4, 2024
1 parent 54a5cd0 commit fa1e825
Show file tree
Hide file tree
Showing 16 changed files with 251 additions and 3 deletions.
28 changes: 26 additions & 2 deletions pkg/github/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (client *Client) GetWorkflowUsage(ctx context.Context, owner, repo, workflo
}
var workflowRuns []*googlegithub.WorkflowRun
var err error
workflowRuns, page, err = client.getWorkflowRuns(ctx, owner, repo, workflow, timeRange, page)
workflowRuns, page, err = client.getWorkflowRuns(ctx, owner, repo, workflow, "", timeRange, page)
if err != nil {
return models.WorkflowUsage{}, fmt.Errorf("fetching workflow runs: %w", err)
}
Expand Down Expand Up @@ -280,7 +280,29 @@ func (client *Client) getWorkflowUsage(ctx context.Context, owner, repo string,
return client.restClient.Actions.GetWorkflowUsageByFileName(ctx, owner, repo, workflow)
}

func (client *Client) getWorkflowRuns(ctx context.Context, owner, repo, workflow string, timeRange backend.TimeRange, page int) ([]*googlegithub.WorkflowRun, int, error) {
func (client *Client) GetWorkflowRuns(ctx context.Context, owner, repo, workflow string, branch string, timeRange backend.TimeRange) ([]*googlegithub.WorkflowRun, error) {
workflowRuns := []*googlegithub.WorkflowRun{}

page := 1
for {
if page == 0 {
break
}

workflowRunsPage, nextPage, err := client.getWorkflowRuns(ctx, owner, repo, workflow, branch, timeRange, page)
if err != nil {
return nil, fmt.Errorf("fetching workflow runs: %w", err)
}

workflowRuns = append(workflowRuns, workflowRunsPage...)

page = nextPage
}

return workflowRuns, nil
}

func (client *Client) getWorkflowRuns(ctx context.Context, owner, repo, workflow string, branch string, timeRange backend.TimeRange, page int) ([]*googlegithub.WorkflowRun, int, error) {
workflowID, _ := strconv.ParseInt(workflow, 10, 64)

workflowRuns := []*googlegithub.WorkflowRun{}
Expand All @@ -298,11 +320,13 @@ func (client *Client) getWorkflowRuns(ctx context.Context, owner, repo, workflow
runs, response, err = client.restClient.Actions.ListWorkflowRunsByID(ctx, owner, repo, workflowID, &googlegithub.ListWorkflowRunsOptions{
Created: created,
ListOptions: googlegithub.ListOptions{Page: page, PerPage: 100},
Branch: branch,
})
} else {
runs, response, err = client.restClient.Actions.ListWorkflowRunsByFileName(ctx, owner, repo, workflow, &googlegithub.ListWorkflowRunsOptions{
Created: created,
ListOptions: googlegithub.ListOptions{Page: page, PerPage: 100},
Branch: branch,
})
}

Expand Down
12 changes: 12 additions & 0 deletions pkg/github/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,18 @@ func (d *Datasource) HandleWorkflowUsageQuery(ctx context.Context, query *models
return GetWorkflowUsage(ctx, d.client, opt, req.TimeRange)
}

// HandleWorkflowRunsQuery is the query handler for listing workflow runs of a GitHub repository
func (d *Datasource) HandleWorkflowRunsQuery(ctx context.Context, query *models.WorkflowRunsQuery, req backend.DataQuery) (dfutil.Framer, error) {
opt := models.WorkflowRunsOptions{
Repository: query.Repository,
Owner: query.Owner,
Workflow: query.Options.Workflow,
Branch: query.Options.Branch,
}

return GetWorkflowRuns(ctx, d.client, opt, req.TimeRange)
}

// 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
1 change: 1 addition & 0 deletions pkg/github/query_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func GetQueryHandlers(s *QueryHandler) *datasource.QueryTypeMux {
mux.HandleFunc(models.QueryTypeStargazers, s.HandleStargazers)
mux.HandleFunc(models.QueryTypeWorkflows, s.HandleWorkflows)
mux.HandleFunc(models.QueryTypeWorkflowUsage, s.HandleWorkflowUsage)
mux.HandleFunc(models.QueryTypeWorkflowRuns, s.HandleWorkflowRuns)

return mux
}
59 changes: 59 additions & 0 deletions pkg/github/workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,62 @@ func GetWorkflowUsage(ctx context.Context, client models.Client, opts models.Wor

return WorkflowUsageWrapper(data), nil
}

// WorkflowRunsWrapper is a list of GitHub workflow runs
type WorkflowRunsWrapper []*googlegithub.WorkflowRun

// Frames converts the list of workflow runs to a Grafana DataFrame
func (workflowRuns WorkflowRunsWrapper) Frames() data.Frames {
frame := data.NewFrame(
"workflow_run",
data.NewField("id", nil, []*int64{}),
data.NewField("name", nil, []*string{}),
data.NewField("head_branch", nil, []*string{}),
data.NewField("head_sha", nil, []*string{}),
data.NewField("created_at", nil, []*time.Time{}),
data.NewField("updated_at", nil, []*time.Time{}),
data.NewField("html_url", nil, []*string{}),
data.NewField("url", nil, []*string{}),
data.NewField("status", nil, []*string{}),
data.NewField("conclusion", nil, []*string{}),
data.NewField("event", nil, []*string{}),
data.NewField("workflow_id", nil, []*int64{}),
data.NewField("run_number", nil, []int64{}),
)

for _, workflowRun := range workflowRuns {
frame.InsertRow(
0,
workflowRun.ID,
workflowRun.Name,
workflowRun.HeadBranch,
workflowRun.HeadSHA,
workflowRun.CreatedAt.GetTime(),
workflowRun.UpdatedAt.GetTime(),
workflowRun.HTMLURL,
workflowRun.URL,
workflowRun.Status,
workflowRun.Conclusion,
workflowRun.Event,
workflowRun.WorkflowID,
int64(*workflowRun.RunNumber),
)
}

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

// GetWorkflowRuns gets all workflows runs for a GitHub repository and workflow
func GetWorkflowRuns(ctx context.Context, client models.Client, opts models.WorkflowRunsOptions, timeRange backend.TimeRange) (WorkflowRunsWrapper, error) {
if opts.Owner == "" || opts.Repository == "" {
return nil, nil
}

workflowRuns, err := client.GetWorkflowRuns(ctx, opts.Owner, opts.Repository, opts.Workflow, opts.Branch, timeRange)
if err != nil {
return nil, fmt.Errorf("listing workflows: opts=%+v %w", opts, err)
}

return WorkflowRunsWrapper(workflowRuns), nil
}
16 changes: 16 additions & 0 deletions pkg/github/workflows_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,19 @@ func (s *QueryHandler) HandleWorkflowUsage(ctx context.Context, req *backend.Que
Responses: processQueries(ctx, req, s.handleWorkflowUsageQuery),
}, nil
}

func (s *QueryHandler) handleWorkflowRunsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse {
query := &models.WorkflowRunsQuery{}
if err := UnmarshalQuery(q.JSON, query); err != nil {
return *err
}

return dfutil.FrameResponseWithError(s.Datasource.HandleWorkflowRunsQuery(ctx, query, q))
}

// HandleWorkflowRuns handles the plugin query for GitHub workflows
func (s *QueryHandler) HandleWorkflowRuns(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
return &backend.QueryDataResponse{
Responses: processQueries(ctx, req, s.handleWorkflowRunsQuery),
}, nil
}
39 changes: 39 additions & 0 deletions pkg/github/workflows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,42 @@ func TestWorkflowUsageDataframe(t *testing.T) {

testutil.CheckGoldenFramer(t, "workflowUsage", usage)
}

func TestWorkflowRunsDataFrame(t *testing.T) {
t.Parallel()

createdAt1, err := time.Parse("2006-Jan-02", "2013-Feb-01")
assert.NoError(t, err)

updatedAt1, err := time.Parse("2006-Jan-02", "2013-Feb-02")
assert.NoError(t, err)

createdAt2, err := time.Parse("2006-Jan-02", "2013-Feb-03")
assert.NoError(t, err)

updatedAt2, err := time.Parse("2006-Jan-02", "2013-Feb-04")
assert.NoError(t, err)

workflowRuns := WorkflowRunsWrapper([]*googlegithub.WorkflowRun{
{
ID: ptr(int64(1)),
Name: ptr("name_1"),
HeadBranch: ptr("head_branch_1"),
HeadSHA: ptr("head_sha_1"),
CreatedAt: &googlegithub.Timestamp{Time: createdAt1},
UpdatedAt: &googlegithub.Timestamp{Time: updatedAt1},
HTMLURL: ptr("html_url_1"),
},
{
ID: ptr(int64(2)),
Name: ptr("name_2"),
HeadBranch: ptr("head_branch_2"),
HeadSHA: ptr("head_sha_2"),
CreatedAt: &googlegithub.Timestamp{Time: createdAt2},
UpdatedAt: &googlegithub.Timestamp{Time: updatedAt2},
HTMLURL: ptr("html_url_2"),
},
})

testutil.CheckGoldenFramer(t, "workflowRuns", workflowRuns)
}
1 change: 1 addition & 0 deletions pkg/models/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ type Client interface {
Query(ctx context.Context, q interface{}, variables map[string]interface{}) error
ListWorkflows(ctx context.Context, owner, repo string, opts *googlegithub.ListOptions) (*googlegithub.Workflows, *googlegithub.Response, error)
GetWorkflowUsage(ctx context.Context, owner, repo, workflow string, timeRange backend.TimeRange) (WorkflowUsage, error)
GetWorkflowRuns(ctx context.Context, owner, repo, workflow string, branch string, timeRange backend.TimeRange) ([]*googlegithub.WorkflowRun, error)
}
8 changes: 8 additions & 0 deletions pkg/models/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const (
QueryTypeWorkflows = "Workflows"
// QueryTypeWorkflowUsage is used when querying a specific workflow usage
QueryTypeWorkflowUsage = "Workflow_Usage"
// QueryTypeWorkflowRuns is used when querying workflow runs for a repository
QueryTypeWorkflowRuns = "Workflow_Runs"
)

// Query refers to the structure of a query built using the QueryEditor.
Expand Down Expand Up @@ -129,3 +131,9 @@ type WorkflowUsageQuery struct {
Query
Options WorkflowUsageOptions `json:"options"`
}

// WorkflowRunsQuery is used when querying workflow runs for a repository
type WorkflowRunsQuery struct {
Query
Options WorkflowRunsOptions `json:"options"`
}
5 changes: 5 additions & 0 deletions pkg/models/workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ type WorkflowUsageOptions struct {

// Workflow is the id or the workflow file name.
Workflow string `json:"workflow"`

// Branch is the branch to filter the runs by.
Branch string `json:"branch"`
}

type WorkflowRunsOptions = WorkflowUsageOptions

// WorkflowUsage contains a specific workflow usage information.
type WorkflowUsage struct {
CostUSD float64
Expand Down
1 change: 1 addition & 0 deletions pkg/plugin/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Datasource interface {
HandleStargazersQuery(context.Context, *models.StargazersQuery, backend.DataQuery) (dfutil.Framer, error)
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)
CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error)
QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error)
}
10 changes: 10 additions & 0 deletions pkg/plugin/datasource_caching.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,16 @@ func (c *CachedDatasource) HandleWorkflowUsageQuery(ctx context.Context, q *mode
return c.saveCache(req, f, err)
}

// HandleWorkflowRunsQuery is the cache wrapper for the workflows runs query handler
func (c *CachedDatasource) HandleWorkflowRunsQuery(ctx context.Context, q *models.WorkflowRunsQuery, req backend.DataQuery) (dfutil.Framer, error) {
if value, err := c.getCache(req); err == nil {
return value, err
}

f, err := c.datasource.HandleWorkflowRunsQuery(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)
Expand Down
5 changes: 5 additions & 0 deletions pkg/testutil/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,8 @@ func (c *TestClient) ListWorkflows(ctx context.Context, owner, repo string, opts
func (c *TestClient) GetWorkflowUsage(ctx context.Context, owner, repo, workflow string, timeRange backend.TimeRange) (models.WorkflowUsage, error) {
panic("unimplemented")
}

// GetWorkflowRuns is not implemented because it is not being used at the moment.
func (c *TestClient) GetWorkflowRuns(ctx context.Context, owner, repo, workflow string, branch string, timeRange backend.TimeRange) ([]*googlegithub.WorkflowRun, error) {
panic("unimplemented")
}
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export enum QueryType {
Stargazers = 'Stargazers',
Workflows = 'Workflows',
Workflow_Usage = 'Workflow_Usage',
Workflow_Runs = 'Workflow_Runs',
}

export const DefaultQueryType = QueryType.Issues;
Expand Down
8 changes: 7 additions & 1 deletion src/types/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export interface GitHubQuery extends Indexable, DataQuery, RepositoryOptions {
| ContributorsOptions
| ProjectsOptions
| WorkflowsOptions
| WorkflowUsageOptions;
| WorkflowUsageOptions
| WorkflowRunsOptions;
}

export interface Label {
Expand Down Expand Up @@ -66,6 +67,11 @@ export interface WorkflowUsageOptions extends Indexable {
workflowID?: number;
}

export interface WorkflowRunsOptions extends Indexable {
workflowID?: string;
branch?: string;
}

export interface PackagesOptions extends Indexable {
names?: string;
packageType?: PackageType;
Expand Down
6 changes: 6 additions & 0 deletions src/views/QueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import QueryEditorVulnerabilities from './QueryEditorVulnerabilities';
import QueryEditorProjects from './QueryEditorProjects';
import QueryEditorWorkflows from './QueryEditorWorkflows';
import QueryEditorWorkflowUsage from './QueryEditorWorkflowUsage';
import QueryEditorWorkflowRuns from './QueryEditorWorkflowRuns';
import { QueryType, DefaultQueryType } from '../constants';
import type { GitHubQuery } from '../types/query';
import type { GitHubDataSourceOptions } from '../types/config';
Expand Down Expand Up @@ -101,6 +102,11 @@ const queryEditors: {
<QueryEditorWorkflowUsage {...(props.query.options || {})} onChange={onChange} />
),
},
[QueryType.Workflow_Runs]: {
component: (props: Props, onChange: (val: any) => void) => (
<QueryEditorWorkflowRuns {...(props.query.options || {})} onChange={onChange} />
),
},
};

/* eslint-enable react/display-name */
Expand Down
54 changes: 54 additions & 0 deletions src/views/QueryEditorWorkflowRuns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { Input, InlineField } from '@grafana/ui';
import { RightColumnWidth, LeftColumnWidth } from './QueryEditor';
import type { WorkflowRunsOptions } from 'types/query';

interface Props extends WorkflowRunsOptions {
onChange: (value: WorkflowRunsOptions) => void;
}

const QueryEditorWorkflowRuns = (props: Props) => {
const [workflow, setWorkflow] = useState<string | undefined>(props.workflow);
const [branch, setBranch] = useState<string | undefined>(props.branch);

return (
<>
<InlineField
labelWidth={LeftColumnWidth * 2}
label="Workflow"
tooltip="The workflow id number or file name (e.g my-workflow.yml)"
>
<Input
value={workflow}
width={RightColumnWidth * 2 + LeftColumnWidth}
onChange={(el) => setWorkflow(el.currentTarget.value)}
onBlur={(el) =>
props.onChange({
...props,
workflow: el.currentTarget.value,
})
}
/>
</InlineField>
<InlineField
labelWidth={LeftColumnWidth * 2}
label="Branch"
tooltip="The branch to filter on (can be left empty)"
>
<Input
value={branch}
width={RightColumnWidth * 2 + LeftColumnWidth}
onChange={(el) => setBranch(el.currentTarget.value)}
onBlur={(el) =>
props.onChange({
...props,
branch: el.currentTarget.value,
})
}
/>
</InlineField>
</>
);
};

export default QueryEditorWorkflowRuns;

0 comments on commit fa1e825

Please sign in to comment.