Skip to content
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
30 changes: 30 additions & 0 deletions pkg/api/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,36 @@ func CIGetRun(ctx context.Context, token, orgID, runID string) (*civ1.GetRunResp
return resp.Msg, nil
}

// CIGetJobAttemptMetrics returns CPU and memory samples for a CI job attempt.
func CIGetJobAttemptMetrics(ctx context.Context, token, orgID, attemptID string) (*civ1.GetJobAttemptMetricsResponse, error) {
client := newCIServiceClient()
resp, err := client.GetJobAttemptMetrics(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ1.GetJobAttemptMetricsRequest{AttemptId: attemptID}), token, orgID))
if err != nil {
return nil, err
}
return resp.Msg, nil
}

// CIGetJobMetrics returns per-attempt CPU and memory metric summaries for a CI job.
func CIGetJobMetrics(ctx context.Context, token, orgID, jobID string) (*civ1.GetJobMetricsResponse, error) {
client := newCIServiceClient()
resp, err := client.GetJobMetrics(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ1.GetJobMetricsRequest{JobId: jobID}), token, orgID))
if err != nil {
return nil, err
}
return resp.Msg, nil
}

// CIGetRunMetrics returns workflow/job/attempt CPU and memory metric summaries for a CI run.
func CIGetRunMetrics(ctx context.Context, token, orgID, runID string) (*civ1.GetRunMetricsResponse, error) {
client := newCIServiceClient()
resp, err := client.GetRunMetrics(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ1.GetRunMetricsRequest{RunId: runID}), token, orgID))
if err != nil {
return nil, err
}
return resp.Msg, nil
}

// CIGetJobAttemptLogs returns all log lines for a job attempt, paginating through all pages.
func CIGetJobAttemptLogs(ctx context.Context, token, orgID, attemptID string) ([]*civ1.LogLine, error) {
client := newCIServiceClient()
Expand Down
52 changes: 52 additions & 0 deletions pkg/api/ci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,30 @@ func (h ciServiceTestHandler) GetWorkflow(_ context.Context, req *connect.Reques
return connect.NewResponse(&civ1.GetWorkflowResponse{WorkflowId: req.Msg.WorkflowId, OrgId: "org-123"}), nil
}

func (h ciServiceTestHandler) GetJobAttemptMetrics(_ context.Context, req *connect.Request[civ1.GetJobAttemptMetricsRequest]) (*connect.Response[civ1.GetJobAttemptMetricsResponse], error) {
assertAuthAndOrg(h.t, req.Header())
if req.Msg.AttemptId != "attempt-123" {
h.t.Fatalf("AttemptId = %q, want attempt-123", req.Msg.AttemptId)
}
return connect.NewResponse(&civ1.GetJobAttemptMetricsResponse{SnapshotAt: "2026-05-03T12:00:00Z"}), nil
}

func (h ciServiceTestHandler) GetJobMetrics(_ context.Context, req *connect.Request[civ1.GetJobMetricsRequest]) (*connect.Response[civ1.GetJobMetricsResponse], error) {
assertAuthAndOrg(h.t, req.Header())
if req.Msg.JobId != "job-123" {
h.t.Fatalf("JobId = %q, want job-123", req.Msg.JobId)
}
return connect.NewResponse(&civ1.GetJobMetricsResponse{SnapshotAt: "2026-05-03T12:00:00Z"}), nil
}

func (h ciServiceTestHandler) GetRunMetrics(_ context.Context, req *connect.Request[civ1.GetRunMetricsRequest]) (*connect.Response[civ1.GetRunMetricsResponse], error) {
assertAuthAndOrg(h.t, req.Header())
if req.Msg.RunId != "run-123" {
h.t.Fatalf("RunId = %q, want run-123", req.Msg.RunId)
}
return connect.NewResponse(&civ1.GetRunMetricsResponse{SnapshotAt: "2026-05-03T12:00:00Z"}), nil
}

func (h ciServiceTestHandler) GetJobAttemptLogs(context.Context, *connect.Request[civ1.GetJobAttemptLogsRequest]) (*connect.Response[civ1.GetJobAttemptLogsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, nil)
}
Expand Down Expand Up @@ -136,6 +160,34 @@ func TestCIGetWorkflowWrapper(t *testing.T) {
})
}

func TestCIMetricsWrappers(t *testing.T) {
withTestCIService(t, func() {
attemptResp, err := CIGetJobAttemptMetrics(context.Background(), "token-123", "org-123", "attempt-123")
if err != nil {
t.Fatalf("CIGetJobAttemptMetrics returned error: %v", err)
}
if attemptResp.SnapshotAt == "" {
t.Fatal("CIGetJobAttemptMetrics returned empty snapshot")
}

jobResp, err := CIGetJobMetrics(context.Background(), "token-123", "org-123", "job-123")
if err != nil {
t.Fatalf("CIGetJobMetrics returned error: %v", err)
}
if jobResp.SnapshotAt == "" {
t.Fatal("CIGetJobMetrics returned empty snapshot")
}

runResp, err := CIGetRunMetrics(context.Background(), "token-123", "org-123", "run-123")
if err != nil {
t.Fatalf("CIGetRunMetrics returned error: %v", err)
}
if runResp.SnapshotAt == "" {
t.Fatal("CIGetRunMetrics returned empty snapshot")
}
})
}

func withTestCIService(t *testing.T, fn func()) {
t.Helper()

Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/ci/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func NewCmdCI() *cobra.Command {
cmd.AddCommand(NewCmdCancel())
cmd.AddCommand(NewCmdDispatch())
cmd.AddCommand(NewCmdLogs())
cmd.AddCommand(NewCmdMetrics())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New "metrics" command missing from registration guard test

Low Severity

The new metrics subcommand is registered in NewCmdCI but not added to the wanted slice in TestCICommandRegistration in ci_test.go. That test's comment explicitly says "New verbs should be added to the wanted slice below." Without the entry, a future refactor that accidentally drops cmd.AddCommand(NewCmdMetrics()) won't be caught by the guard test.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4f9d544. Configure here.

cmd.AddCommand(NewCmdMigrate())
cmd.AddCommand(NewCmdRerun())
cmd.AddCommand(NewCmdRetry())
Expand Down
Loading
Loading