Skip to content

Commit 9b377a4

Browse files
authored
Merge pull request #249 from buildkite/feat_build_job_filtering
feat(builds): push job state filtering to API via go-buildkite v4.17.0
2 parents 74d4b12 + 8e7537c commit 9b377a4

4 files changed

Lines changed: 47 additions & 41 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.25.0
55
require (
66
github.com/alecthomas/kong v1.14.0
77
github.com/buildkite/buildkite-logs v0.8.0
8-
github.com/buildkite/go-buildkite/v4 v4.16.0
8+
github.com/buildkite/go-buildkite/v4 v4.17.0
99
github.com/google/jsonschema-go v0.4.2
1010
github.com/mattn/go-isatty v0.0.20
1111
github.com/microcosm-cc/bluemonday v1.0.27

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
7676
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
7777
github.com/buildkite/buildkite-logs v0.8.0 h1:Zp+lIZDD4Ny3/fnGjAkR3gjDNG68sSqQKv/DnTKzCrw=
7878
github.com/buildkite/buildkite-logs v0.8.0/go.mod h1:32+BbDpjJAzL3yH/qkr8OTkMtlXPUFWlkp34NcM43dM=
79-
github.com/buildkite/go-buildkite/v4 v4.16.0 h1:uRZmOg6zfZOCpak1tizzlv9pq8Syt7WmeEb0Ov7r1NE=
80-
github.com/buildkite/go-buildkite/v4 v4.16.0/go.mod h1:8+7GiWBKwEPAWoZnRU/kpNCt46j1iVH8kFMMbD4YDfc=
79+
github.com/buildkite/go-buildkite/v4 v4.17.0 h1:OYy9PG5A15K4Up2dkZgvXP7esAqzQskA0VGXvciRUNQ=
80+
github.com/buildkite/go-buildkite/v4 v4.17.0/go.mod h1:8+7GiWBKwEPAWoZnRU/kpNCt46j1iVH8kFMMbD4YDfc=
8181
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
8282
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
8383
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=

pkg/buildkite/builds.go

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ type BuildSummary struct {
3636
Message string `json:"message"`
3737
WebURL string `json:"web_url"`
3838
CreatedAt *buildkite.Timestamp `json:"created_at"`
39-
JobsTotal int `json:"jobs_total"`
4039
}
4140

4241
// JobEntry represents a lightweight job reference with just enough info to identify and filter jobs
@@ -106,12 +105,11 @@ func summarizeBuild(build buildkite.Build) BuildSummary {
106105
Message: build.Message,
107106
WebURL: build.WebURL,
108107
CreatedAt: build.CreatedAt,
109-
JobsTotal: len(build.Jobs),
110108
}
111109
}
112110

113111
// detailBuild converts a buildkite.Build to BuildDetail with job summary
114-
// filteredJobs is used for job_summary stats, while build.Jobs is used for jobs_total
112+
// filteredJobs is used for job_summary stats and lightweight job entries
115113
func detailBuild(build buildkite.Build, filteredJobs []buildkite.Job) BuildDetail {
116114
summary := summarizeBuild(build)
117115

@@ -134,7 +132,7 @@ func detailBuild(build buildkite.Build, filteredJobs []buildkite.Job) BuildDetai
134132
}
135133

136134
return BuildDetail{
137-
BuildSummary: summary, // jobs_total reflects ALL jobs (unfiltered)
135+
BuildSummary: summary,
138136
Source: build.Source,
139137
Author: build.Author,
140138
StartedAt: build.StartedAt,
@@ -345,33 +343,23 @@ func GetBuild() (mcp.Tool, mcp.ToolHandlerFor[GetBuildArgs, any], []string) {
345343
IncludeTestEngine: true,
346344
}
347345

346+
// Push job state filtering down to the API
347+
if args.JobState != "" {
348+
states := strings.Split(args.JobState, ",")
349+
jobStates := make([]string, len(states))
350+
for i, state := range states {
351+
jobStates[i] = strings.TrimSpace(state)
352+
}
353+
options.JobStates = jobStates
354+
}
355+
348356
deps := DepsFromContext(ctx)
349357
build, _, err := deps.BuildsClient.Get(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, options)
350358
if err != nil {
351359
return handleBuildkiteError(err)
352360
}
353361

354-
// Parse job states filter
355-
var requestedStates map[string]bool
356-
if args.JobState != "" {
357-
states := strings.Split(args.JobState, ",")
358-
requestedStates = make(map[string]bool, len(states))
359-
for _, state := range states {
360-
requestedStates[strings.TrimSpace(state)] = true
361-
}
362-
}
363-
364-
// Filter jobs if states specified
365362
jobs := build.Jobs
366-
if requestedStates != nil {
367-
filteredJobs := make([]buildkite.Job, 0)
368-
for _, job := range build.Jobs {
369-
if job.State != "" && requestedStates[job.State] {
370-
filteredJobs = append(filteredJobs, job)
371-
}
372-
}
373-
jobs = filteredJobs
374-
}
375363

376364
// Strip agent details if not requested
377365
if !args.IncludeAgent && len(jobs) > 0 {

pkg/buildkite/builds_test.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func TestGetBuildDefault(t *testing.T) {
104104
assert.Contains(textContent.Text, `"state":"running"`)
105105
assert.Contains(textContent.Text, `"total":0`)
106106
assert.Contains(textContent.Text, `"by_state":{}`)
107-
assert.Contains(textContent.Text, `"jobs_total":0`)
107+
assert.NotContains(textContent.Text, `"jobs_total"`)
108108
}
109109

110110
func TestGetBuildWithJobSummary(t *testing.T) {
@@ -204,7 +204,7 @@ func TestListBuilds(t *testing.T) {
204204
assert.Contains(textContent.Text, `"id":"123"`)
205205
assert.Contains(textContent.Text, `"number":1`)
206206
assert.Contains(textContent.Text, `"state":"running"`)
207-
assert.Contains(textContent.Text, `"jobs_total":0`)
207+
assert.NotContains(textContent.Text, `"jobs_total"`)
208208

209209
// Verify default pagination parameters - new defaults
210210
assert.NotNil(capturedOptions)
@@ -554,18 +554,38 @@ func TestCreateBuild(t *testing.T) {
554554
func TestGetBuildWithJobStateFilter(t *testing.T) {
555555
assert := require.New(t)
556556

557+
// Mock returns jobs matching the requested states (simulating API-side filtering)
557558
client := &MockBuildsClient{
558559
GetFunc: func(ctx context.Context, org string, pipeline string, id string, opt *buildkite.BuildGetOptions) (buildkite.Build, *buildkite.Response, error) {
560+
// Simulate API-side job state filtering
561+
allJobs := []buildkite.Job{
562+
{ID: "job1", Name: "Test 1", State: "passed", Agent: buildkite.Agent{ID: "agent1", Name: "Agent 1"}},
563+
{ID: "job2", Name: "Test 2", State: "failed", Agent: buildkite.Agent{ID: "agent2", Name: "Agent 2"}},
564+
{ID: "job3", Name: "Test 3", State: "failed", Agent: buildkite.Agent{ID: "agent3", Name: "Agent 3"}},
565+
{ID: "job4", Name: "Test 4", State: "broken", Agent: buildkite.Agent{ID: "agent4", Name: "Agent 4"}},
566+
}
567+
568+
// Filter jobs based on requested states (simulating what the API does)
569+
var jobs []buildkite.Job
570+
if len(opt.JobStates) > 0 {
571+
stateSet := make(map[string]bool, len(opt.JobStates))
572+
for _, s := range opt.JobStates {
573+
stateSet[s] = true
574+
}
575+
for _, job := range allJobs {
576+
if stateSet[job.State] {
577+
jobs = append(jobs, job)
578+
}
579+
}
580+
} else {
581+
jobs = allJobs
582+
}
583+
559584
return buildkite.Build{
560585
ID: "123",
561586
Number: 1,
562587
State: "failed",
563-
Jobs: []buildkite.Job{
564-
{ID: "job1", Name: "Test 1", State: "passed", Agent: buildkite.Agent{ID: "agent1", Name: "Agent 1"}},
565-
{ID: "job2", Name: "Test 2", State: "failed", Agent: buildkite.Agent{ID: "agent2", Name: "Agent 2"}},
566-
{ID: "job3", Name: "Test 3", State: "failed", Agent: buildkite.Agent{ID: "agent3", Name: "Agent 3"}},
567-
{ID: "job4", Name: "Test 4", State: "broken", Agent: buildkite.Agent{ID: "agent4", Name: "Agent 4"}},
568-
},
588+
Jobs: jobs,
569589
}, &buildkite.Response{
570590
Response: &http.Response{StatusCode: 200},
571591
}, nil
@@ -696,15 +716,16 @@ func TestGetBuildDetailedWithJobStateFilter(t *testing.T) {
696716

697717
client := &MockBuildsClient{
698718
GetFunc: func(ctx context.Context, org string, pipeline string, id string, opt *buildkite.BuildGetOptions) (buildkite.Build, *buildkite.Response, error) {
719+
assert.Equal([]string{"failed"}, opt.JobStates, "JobStates should be passed to the API")
720+
721+
// API returns only the filtered jobs
699722
return buildkite.Build{
700723
ID: "123",
701724
Number: 1,
702725
State: "failed",
703726
Jobs: []buildkite.Job{
704-
{ID: "job1", Name: "Test 1", State: "passed"},
705727
{ID: "job2", Name: "Test 2", State: "failed"},
706728
{ID: "job3", Name: "Test 3", State: "failed"},
707-
{ID: "job4", Name: "Test 4", State: "broken"},
708729
},
709730
}, &buildkite.Response{
710731
Response: &http.Response{StatusCode: 200},
@@ -729,17 +750,14 @@ func TestGetBuildDetailedWithJobStateFilter(t *testing.T) {
729750
assert.NoError(err)
730751

731752
textContent := getTextResult(t, result)
732-
// jobs_total should reflect ALL jobs (4)
733-
assert.Contains(textContent.Text, `"jobs_total":4`)
753+
assert.NotContains(textContent.Text, `"jobs_total"`)
734754
// job_summary.total should reflect filtered jobs (2)
735755
assert.Contains(textContent.Text, `"total":2`)
736756
// job_summary.by_state should only show failed count
737757
assert.Contains(textContent.Text, `"failed":2`)
738758
// jobs array should contain only the filtered entries
739759
assert.Contains(textContent.Text, `{"id":"job2","name":"Test 2","state":"failed"}`)
740760
assert.Contains(textContent.Text, `{"id":"job3","name":"Test 3","state":"failed"}`)
741-
assert.NotContains(textContent.Text, `{"id":"job1"`)
742-
assert.NotContains(textContent.Text, `{"id":"job4"`)
743761
}
744762

745763
func TestGetBuildInvalidDetailLevel(t *testing.T) {

0 commit comments

Comments
 (0)