Skip to content

Commit 8e7537c

Browse files
committed
feat(builds): push job state filtering to API via go-buildkite v4.17.0
Upgrade go-buildkite to v4.17.0 and use BuildGetOptions.JobStates to filter jobs server-side instead of fetching all jobs and filtering in-memory. Also removes the computed jobs_total field from BuildSummary as it has no API equivalent and would misrepresent totals when filtering is applied.
1 parent 2bda64a commit 8e7537c

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
@@ -34,7 +34,6 @@ type BuildSummary struct {
3434
Message string `json:"message"`
3535
WebURL string `json:"web_url"`
3636
CreatedAt *buildkite.Timestamp `json:"created_at"`
37-
JobsTotal int `json:"jobs_total"`
3837
}
3938

4039
// JobEntry represents a lightweight job reference with just enough info to identify and filter jobs
@@ -104,12 +103,11 @@ func summarizeBuild(build buildkite.Build) BuildSummary {
104103
Message: build.Message,
105104
WebURL: build.WebURL,
106105
CreatedAt: build.CreatedAt,
107-
JobsTotal: len(build.Jobs),
108106
}
109107
}
110108

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

@@ -132,7 +130,7 @@ func detailBuild(build buildkite.Build, filteredJobs []buildkite.Job) BuildDetai
132130
}
133131

134132
return BuildDetail{
135-
BuildSummary: summary, // jobs_total reflects ALL jobs (unfiltered)
133+
BuildSummary: summary,
136134
Source: build.Source,
137135
Author: build.Author,
138136
StartedAt: build.StartedAt,
@@ -343,33 +341,23 @@ func GetBuild() (mcp.Tool, mcp.ToolHandlerFor[GetBuildArgs, any], []string) {
343341
IncludeTestEngine: true,
344342
}
345343

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

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

374362
// Strip agent details if not requested
375363
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
@@ -87,7 +87,7 @@ func TestGetBuildDefault(t *testing.T) {
8787
assert.Contains(textContent.Text, `"state":"running"`)
8888
assert.Contains(textContent.Text, `"total":0`)
8989
assert.Contains(textContent.Text, `"by_state":{}`)
90-
assert.Contains(textContent.Text, `"jobs_total":0`)
90+
assert.NotContains(textContent.Text, `"jobs_total"`)
9191
}
9292

9393
func TestGetBuildWithJobSummary(t *testing.T) {
@@ -187,7 +187,7 @@ func TestListBuilds(t *testing.T) {
187187
assert.Contains(textContent.Text, `"id":"123"`)
188188
assert.Contains(textContent.Text, `"number":1`)
189189
assert.Contains(textContent.Text, `"state":"running"`)
190-
assert.Contains(textContent.Text, `"jobs_total":0`)
190+
assert.NotContains(textContent.Text, `"jobs_total"`)
191191

192192
// Verify default pagination parameters - new defaults
193193
assert.NotNil(capturedOptions)
@@ -537,18 +537,38 @@ func TestCreateBuild(t *testing.T) {
537537
func TestGetBuildWithJobStateFilter(t *testing.T) {
538538
assert := require.New(t)
539539

540+
// Mock returns jobs matching the requested states (simulating API-side filtering)
540541
client := &MockBuildsClient{
541542
GetFunc: func(ctx context.Context, org string, pipeline string, id string, opt *buildkite.BuildGetOptions) (buildkite.Build, *buildkite.Response, error) {
543+
// Simulate API-side job state filtering
544+
allJobs := []buildkite.Job{
545+
{ID: "job1", Name: "Test 1", State: "passed", Agent: buildkite.Agent{ID: "agent1", Name: "Agent 1"}},
546+
{ID: "job2", Name: "Test 2", State: "failed", Agent: buildkite.Agent{ID: "agent2", Name: "Agent 2"}},
547+
{ID: "job3", Name: "Test 3", State: "failed", Agent: buildkite.Agent{ID: "agent3", Name: "Agent 3"}},
548+
{ID: "job4", Name: "Test 4", State: "broken", Agent: buildkite.Agent{ID: "agent4", Name: "Agent 4"}},
549+
}
550+
551+
// Filter jobs based on requested states (simulating what the API does)
552+
var jobs []buildkite.Job
553+
if len(opt.JobStates) > 0 {
554+
stateSet := make(map[string]bool, len(opt.JobStates))
555+
for _, s := range opt.JobStates {
556+
stateSet[s] = true
557+
}
558+
for _, job := range allJobs {
559+
if stateSet[job.State] {
560+
jobs = append(jobs, job)
561+
}
562+
}
563+
} else {
564+
jobs = allJobs
565+
}
566+
542567
return buildkite.Build{
543568
ID: "123",
544569
Number: 1,
545570
State: "failed",
546-
Jobs: []buildkite.Job{
547-
{ID: "job1", Name: "Test 1", State: "passed", Agent: buildkite.Agent{ID: "agent1", Name: "Agent 1"}},
548-
{ID: "job2", Name: "Test 2", State: "failed", Agent: buildkite.Agent{ID: "agent2", Name: "Agent 2"}},
549-
{ID: "job3", Name: "Test 3", State: "failed", Agent: buildkite.Agent{ID: "agent3", Name: "Agent 3"}},
550-
{ID: "job4", Name: "Test 4", State: "broken", Agent: buildkite.Agent{ID: "agent4", Name: "Agent 4"}},
551-
},
571+
Jobs: jobs,
552572
}, &buildkite.Response{
553573
Response: &http.Response{StatusCode: 200},
554574
}, nil
@@ -679,15 +699,16 @@ func TestGetBuildDetailedWithJobStateFilter(t *testing.T) {
679699

680700
client := &MockBuildsClient{
681701
GetFunc: func(ctx context.Context, org string, pipeline string, id string, opt *buildkite.BuildGetOptions) (buildkite.Build, *buildkite.Response, error) {
702+
assert.Equal([]string{"failed"}, opt.JobStates, "JobStates should be passed to the API")
703+
704+
// API returns only the filtered jobs
682705
return buildkite.Build{
683706
ID: "123",
684707
Number: 1,
685708
State: "failed",
686709
Jobs: []buildkite.Job{
687-
{ID: "job1", Name: "Test 1", State: "passed"},
688710
{ID: "job2", Name: "Test 2", State: "failed"},
689711
{ID: "job3", Name: "Test 3", State: "failed"},
690-
{ID: "job4", Name: "Test 4", State: "broken"},
691712
},
692713
}, &buildkite.Response{
693714
Response: &http.Response{StatusCode: 200},
@@ -712,17 +733,14 @@ func TestGetBuildDetailedWithJobStateFilter(t *testing.T) {
712733
assert.NoError(err)
713734

714735
textContent := getTextResult(t, result)
715-
// jobs_total should reflect ALL jobs (4)
716-
assert.Contains(textContent.Text, `"jobs_total":4`)
736+
assert.NotContains(textContent.Text, `"jobs_total"`)
717737
// job_summary.total should reflect filtered jobs (2)
718738
assert.Contains(textContent.Text, `"total":2`)
719739
// job_summary.by_state should only show failed count
720740
assert.Contains(textContent.Text, `"failed":2`)
721741
// jobs array should contain only the filtered entries
722742
assert.Contains(textContent.Text, `{"id":"job2","name":"Test 2","state":"failed"}`)
723743
assert.Contains(textContent.Text, `{"id":"job3","name":"Test 3","state":"failed"}`)
724-
assert.NotContains(textContent.Text, `{"id":"job1"`)
725-
assert.NotContains(textContent.Text, `{"id":"job4"`)
726744
}
727745

728746
func TestGetBuildInvalidDetailLevel(t *testing.T) {

0 commit comments

Comments
 (0)