Skip to content

Commit eff4c3c

Browse files
authored
Minimize Actions workflow list responses (#3047)
Return compact response types for workflow run and workflow job lists while retaining diagnostic, step, and runner metadata. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0eecbca7-7271-4a04-8d28-d952c27ed9c1
1 parent cdfa34e commit eff4c3c

4 files changed

Lines changed: 529 additions & 6 deletions

File tree

pkg/github/actions.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,7 @@ func listWorkflowRuns(ctx context.Context, client *github.Client, args map[strin
884884
}
885885

886886
defer func() { _ = resp.Body.Close() }()
887-
r, err := json.Marshal(workflowRuns)
887+
r, err := json.Marshal(convertToMinimalWorkflowRuns(workflowRuns))
888888
if err != nil {
889889
return nil, nil, fmt.Errorf("failed to marshal workflow runs: %w", err)
890890
}
@@ -919,7 +919,7 @@ func listWorkflowJobs(ctx context.Context, client *github.Client, args map[strin
919919
}
920920

921921
response := map[string]any{
922-
"jobs": workflowJobs,
922+
"jobs": convertToMinimalWorkflowJobs(workflowJobs),
923923
}
924924

925925
defer func() { _ = resp.Body.Close() }()

pkg/github/actions_minimal_test.go

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
package github
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
"time"
7+
8+
"github.com/google/go-github/v89/github"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestConvertToMinimalWorkflowRun(t *testing.T) {
14+
workflowRun := actionsTestWorkflowRun()
15+
16+
minimal := convertToMinimalWorkflowRun(workflowRun)
17+
18+
assert.Equal(t, workflowRun.GetID(), minimal.ID)
19+
assert.Equal(t, workflowRun.GetWorkflowID(), minimal.WorkflowID)
20+
assert.Equal(t, workflowRun.GetDisplayTitle(), minimal.DisplayTitle)
21+
assert.Equal(t, workflowRun.GetHeadSHA(), minimal.HeadSHA)
22+
assert.Equal(t, []int{42}, minimal.PullRequests)
23+
require.NotNil(t, minimal.HeadCommit)
24+
assert.Equal(t, "Reduce GitHub Actions response payloads", minimal.HeadCommit.Message)
25+
require.Len(t, minimal.ReferencedWorkflows, 1)
26+
assert.Equal(t, ".github/workflows/reusable-tests.yml", minimal.ReferencedWorkflows[0].Path)
27+
assert.Equal(t, "refs/tags/v3", minimal.ReferencedWorkflows[0].Ref)
28+
assert.Equal(t, "9f4f87d9790ab0f5c2c5ad2b74b886cab515a886", minimal.ReferencedWorkflows[0].SHA)
29+
require.NotNil(t, minimal.Actor)
30+
assert.Equal(t, "octocat", minimal.Actor.Login)
31+
require.NotNil(t, minimal.TriggeringActor)
32+
assert.Equal(t, "hubot", minimal.TriggeringActor.Login)
33+
34+
payload := marshalActionsObject(t, minimal)
35+
assert.NotContains(t, payload, "node_id")
36+
assert.NotContains(t, payload, "repository")
37+
assert.NotContains(t, payload, "head_repository")
38+
assert.NotContains(t, payload, "jobs_url")
39+
assert.NotContains(t, payload, "logs_url")
40+
assert.NotContains(t, payload, "artifacts_url")
41+
assert.Equal(t, map[string]any{
42+
"message": "Reduce GitHub Actions response payloads",
43+
}, payload["head_commit"])
44+
assert.Equal(t, []any{
45+
map[string]any{
46+
"path": ".github/workflows/reusable-tests.yml",
47+
"sha": "9f4f87d9790ab0f5c2c5ad2b74b886cab515a886",
48+
"ref": "refs/tags/v3",
49+
},
50+
}, payload["referenced_workflows"])
51+
}
52+
53+
func TestConvertToMinimalWorkflowJob(t *testing.T) {
54+
workflowJob := actionsTestWorkflowJob()
55+
56+
minimal := convertToMinimalWorkflowJob(workflowJob)
57+
58+
assert.Equal(t, workflowJob.GetID(), minimal.ID)
59+
assert.Equal(t, workflowJob.GetRunID(), minimal.RunID)
60+
assert.Equal(t, workflowJob.GetRunnerID(), minimal.RunnerID)
61+
assert.Equal(t, workflowJob.GetRunnerName(), minimal.RunnerName)
62+
assert.Equal(t, workflowJob.GetRunnerGroupID(), minimal.RunnerGroupID)
63+
assert.Equal(t, workflowJob.GetRunnerGroupName(), minimal.RunnerGroupName)
64+
assert.Equal(t, workflowJob.GetLabels(), minimal.Labels)
65+
require.Len(t, minimal.Steps, 2)
66+
assert.Equal(t, "Run tests", minimal.Steps[1].Name)
67+
assert.Equal(t, "failure", minimal.Steps[1].Conclusion)
68+
69+
payload := marshalActionsObject(t, minimal)
70+
assert.NotContains(t, payload, "node_id")
71+
assert.NotContains(t, payload, "url")
72+
assert.NotContains(t, payload, "run_url")
73+
assert.NotContains(t, payload, "check_run_url")
74+
assert.Equal(t, float64(1), payload["runner_id"])
75+
assert.Equal(t, float64(2), payload["runner_group_id"])
76+
assert.Equal(t, "GitHub Actions", payload["runner_group_name"])
77+
}
78+
79+
func TestConvertToMinimalActionsLists(t *testing.T) {
80+
t.Run("workflow runs", func(t *testing.T) {
81+
result := convertToMinimalWorkflowRuns(&github.WorkflowRuns{
82+
TotalCount: github.Ptr(2),
83+
WorkflowRuns: []*github.WorkflowRun{actionsTestWorkflowRun(), nil},
84+
})
85+
assert.Equal(t, 2, result.TotalCount)
86+
assert.Len(t, result.WorkflowRuns, 1)
87+
})
88+
89+
t.Run("workflow jobs", func(t *testing.T) {
90+
result := convertToMinimalWorkflowJobs(&github.Jobs{
91+
TotalCount: github.Ptr(2),
92+
Jobs: []*github.WorkflowJob{actionsTestWorkflowJob(), nil},
93+
})
94+
assert.Equal(t, 2, result.TotalCount)
95+
assert.Len(t, result.Jobs, 1)
96+
})
97+
98+
t.Run("nil workflow runs", func(t *testing.T) {
99+
result := convertToMinimalWorkflowRuns(nil)
100+
assert.NotNil(t, result.WorkflowRuns)
101+
assert.Empty(t, result.WorkflowRuns)
102+
})
103+
104+
t.Run("nil workflow jobs", func(t *testing.T) {
105+
result := convertToMinimalWorkflowJobs(nil)
106+
assert.NotNil(t, result.Jobs)
107+
assert.Empty(t, result.Jobs)
108+
})
109+
}
110+
111+
func actionsTestWorkflowRun() *github.WorkflowRun {
112+
repository := &github.Repository{
113+
ID: github.Ptr(int64(1296269)),
114+
NodeID: github.Ptr("MDEwOlJlcG9zaXRvcnkxMjk2MjY5"),
115+
Name: github.Ptr("octo-repo"),
116+
FullName: github.Ptr("octo-org/octo-repo"),
117+
Description: github.Ptr("A representative repository description included in the full API response."),
118+
HTMLURL: github.Ptr("https://github.com/octo-org/octo-repo"),
119+
URL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo"),
120+
CloneURL: github.Ptr("https://github.com/octo-org/octo-repo.git"),
121+
Language: github.Ptr("Go"),
122+
Topics: []string{"actions", "mcp", "automation"},
123+
}
124+
125+
return &github.WorkflowRun{
126+
ID: github.Ptr(int64(30433642)),
127+
Name: github.Ptr("CI"),
128+
NodeID: github.Ptr("MDEyOldvcmtmbG93IFJ1bjI2OTI4OQ=="),
129+
HeadBranch: github.Ptr("feature/minimal-actions"),
130+
HeadSHA: github.Ptr("acb5820ced9479c074f688cc328bf03f341a511d"),
131+
Path: github.Ptr(".github/workflows/ci.yml"),
132+
RunNumber: github.Ptr(562),
133+
RunAttempt: github.Ptr(2),
134+
Event: github.Ptr("pull_request"),
135+
DisplayTitle: github.Ptr("Reduce GitHub Actions response payloads"),
136+
Status: github.Ptr("completed"),
137+
Conclusion: github.Ptr("failure"),
138+
WorkflowID: github.Ptr(int64(161335)),
139+
CheckSuiteID: github.Ptr(int64(42)),
140+
CheckSuiteNodeID: github.Ptr("MDEwOkNoZWNrU3VpdGU0Mg=="),
141+
URL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642"),
142+
HTMLURL: github.Ptr("https://github.com/octo-org/octo-repo/actions/runs/30433642"),
143+
JobsURL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/jobs"),
144+
LogsURL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/logs"),
145+
CheckSuiteURL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/check-suites/42"),
146+
ArtifactsURL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/artifacts"),
147+
CancelURL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/cancel"),
148+
RerunURL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/rerun"),
149+
PreviousAttemptURL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/attempts/1"),
150+
WorkflowURL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/actions/workflows/161335"),
151+
Repository: repository,
152+
HeadRepository: repository,
153+
Actor: &github.User{
154+
Login: github.Ptr("octocat"),
155+
ID: github.Ptr(int64(1)),
156+
NodeID: github.Ptr("MDQ6VXNlcjE="),
157+
AvatarURL: github.Ptr("https://github.com/images/error/octocat_happy.gif"),
158+
HTMLURL: github.Ptr("https://github.com/octocat"),
159+
URL: github.Ptr("https://api.github.com/users/octocat"),
160+
Name: github.Ptr("The Octocat"),
161+
Bio: github.Ptr("A long biography that is not needed to identify the workflow run actor."),
162+
},
163+
TriggeringActor: &github.User{
164+
Login: github.Ptr("hubot"),
165+
ID: github.Ptr(int64(2)),
166+
HTMLURL: github.Ptr("https://github.com/hubot"),
167+
URL: github.Ptr("https://api.github.com/users/hubot"),
168+
},
169+
PullRequests: []*github.PullRequest{
170+
{
171+
ID: github.Ptr(int64(1001)),
172+
Number: github.Ptr(42),
173+
Title: github.Ptr("Reduce GitHub Actions response payloads"),
174+
Body: github.Ptr("A pull request body that is unnecessary in a workflow run response."),
175+
HTMLURL: github.Ptr("https://github.com/octo-org/octo-repo/pull/42"),
176+
Head: &github.PullRequestBranch{
177+
Ref: github.Ptr("feature/minimal-actions"),
178+
SHA: github.Ptr("acb5820ced9479c074f688cc328bf03f341a511d"),
179+
Repo: repository,
180+
},
181+
Base: &github.PullRequestBranch{
182+
Ref: github.Ptr("main"),
183+
SHA: github.Ptr("9a2f3ec"),
184+
Repo: repository,
185+
},
186+
},
187+
},
188+
HeadCommit: &github.HeadCommit{
189+
Message: github.Ptr("Reduce GitHub Actions response payloads"),
190+
URL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/commits/acb5820"),
191+
Author: &github.CommitAuthor{
192+
Name: github.Ptr("The Octocat"),
193+
Email: github.Ptr("octocat@example.com"),
194+
},
195+
},
196+
ReferencedWorkflows: []*github.ReferencedWorkflow{
197+
{
198+
Path: github.Ptr(".github/workflows/reusable-tests.yml"),
199+
SHA: github.Ptr("9f4f87d9790ab0f5c2c5ad2b74b886cab515a886"),
200+
Ref: github.Ptr("refs/tags/v3"),
201+
},
202+
nil,
203+
},
204+
CreatedAt: actionsTestTimestamp(),
205+
UpdatedAt: actionsTestTimestamp(),
206+
RunStartedAt: actionsTestTimestamp(),
207+
}
208+
}
209+
210+
func actionsTestWorkflowJob() *github.WorkflowJob {
211+
return &github.WorkflowJob{
212+
ID: github.Ptr(int64(399444496)),
213+
RunID: github.Ptr(int64(30433642)),
214+
RunURL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642"),
215+
NodeID: github.Ptr("MDEyOldvcmtmbG93IEpvYjM5OTQ0NDQ5Ng=="),
216+
HeadBranch: github.Ptr("feature/minimal-actions"),
217+
HeadSHA: github.Ptr("acb5820ced9479c074f688cc328bf03f341a511d"),
218+
URL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/actions/jobs/399444496"),
219+
HTMLURL: github.Ptr("https://github.com/octo-org/octo-repo/runs/399444496"),
220+
Status: github.Ptr("completed"),
221+
Conclusion: github.Ptr("failure"),
222+
CreatedAt: actionsTestTimestamp(),
223+
StartedAt: actionsTestTimestamp(),
224+
CompletedAt: actionsTestTimestamp(),
225+
Name: github.Ptr("test (ubuntu-latest, Go 1.24)"),
226+
CheckRunURL: github.Ptr("https://api.github.com/repos/octo-org/octo-repo/check-runs/399444496"),
227+
Labels: []string{"ubuntu-latest", "x64"},
228+
RunnerID: github.Ptr(int64(1)),
229+
RunnerName: github.Ptr("GitHub Actions 1"),
230+
RunnerGroupID: github.Ptr(int64(2)),
231+
RunnerGroupName: github.Ptr("GitHub Actions"),
232+
RunAttempt: github.Ptr(int64(2)),
233+
WorkflowName: github.Ptr("CI"),
234+
Steps: []*github.TaskStep{
235+
{
236+
Name: github.Ptr("Set up job"),
237+
Status: github.Ptr("completed"),
238+
Conclusion: github.Ptr("success"),
239+
Number: github.Ptr(int64(1)),
240+
StartedAt: actionsTestTimestamp(),
241+
CompletedAt: actionsTestTimestamp(),
242+
},
243+
{
244+
Name: github.Ptr("Run tests"),
245+
Status: github.Ptr("completed"),
246+
Conclusion: github.Ptr("failure"),
247+
Number: github.Ptr(int64(2)),
248+
StartedAt: actionsTestTimestamp(),
249+
CompletedAt: actionsTestTimestamp(),
250+
},
251+
},
252+
}
253+
}
254+
255+
func actionsTestTimestamp() *github.Timestamp {
256+
return &github.Timestamp{Time: time.Date(2026, time.August, 6, 10, 30, 0, 0, time.UTC)}
257+
}
258+
259+
func marshalActionsObject(t *testing.T, value any) map[string]any {
260+
t.Helper()
261+
data, err := json.Marshal(value)
262+
require.NoError(t, err)
263+
264+
var object map[string]any
265+
require.NoError(t, json.Unmarshal(data, &object))
266+
return object
267+
}

pkg/github/actions_test.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,12 @@ func Test_ActionsList_ListWorkflowRuns(t *testing.T) {
154154
require.False(t, result.IsError)
155155

156156
textContent := getTextResult(t, result)
157-
var response github.WorkflowRuns
157+
var response MinimalWorkflowRunsResult
158158
err = json.Unmarshal([]byte(textContent.Text), &response)
159159
require.NoError(t, err)
160-
assert.NotNil(t, response.TotalCount)
160+
assert.Equal(t, 1, response.TotalCount)
161+
require.Len(t, response.WorkflowRuns, 1)
162+
assert.Equal(t, int64(123), response.WorkflowRuns[0].ID)
161163
})
162164

163165
t.Run("list all workflow runs without resource_id", func(t *testing.T) {
@@ -202,13 +204,47 @@ func Test_ActionsList_ListWorkflowRuns(t *testing.T) {
202204
require.False(t, result.IsError)
203205

204206
textContent := getTextResult(t, result)
205-
var response github.WorkflowRuns
207+
var response MinimalWorkflowRunsResult
206208
err = json.Unmarshal([]byte(textContent.Text), &response)
207209
require.NoError(t, err)
208-
assert.Equal(t, 2, *response.TotalCount)
210+
assert.Equal(t, 2, response.TotalCount)
211+
assert.Len(t, response.WorkflowRuns, 2)
209212
})
210213
}
211214

215+
func Test_ActionsList_ListWorkflowJobs(t *testing.T) {
216+
toolDef := ActionsList(translations.NullTranslationHelper)
217+
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
218+
GetReposActionsRunsJobsByOwnerByRepoByRunID: mockResponse(t, http.StatusOK, &github.Jobs{
219+
TotalCount: github.Ptr(1),
220+
Jobs: []*github.WorkflowJob{actionsTestWorkflowJob()},
221+
}),
222+
})
223+
224+
client := mustNewGHClient(t, mockedClient)
225+
deps := BaseDeps{Client: client}
226+
handler := toolDef.Handler(deps)
227+
request := createMCPRequest(map[string]any{
228+
"method": "list_workflow_jobs",
229+
"owner": "owner",
230+
"repo": "repo",
231+
"resource_id": "30433642",
232+
})
233+
234+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
235+
require.NoError(t, err)
236+
require.False(t, result.IsError)
237+
238+
var response struct {
239+
Jobs MinimalWorkflowJobsResult `json:"jobs"`
240+
}
241+
require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &response))
242+
assert.Equal(t, 1, response.Jobs.TotalCount)
243+
require.Len(t, response.Jobs.Jobs, 1)
244+
assert.Equal(t, int64(399444496), response.Jobs.Jobs[0].ID)
245+
assert.Len(t, response.Jobs.Jobs[0].Steps, 2)
246+
}
247+
212248
func Test_ActionsGet(t *testing.T) {
213249
// Verify tool definition once
214250
toolDef := ActionsGet(translations.NullTranslationHelper)

0 commit comments

Comments
 (0)