Skip to content

Commit deb66ce

Browse files
committed
feat: add support for running workflows by ID and filename in GitHub Actions tools
- Introduced a new tool, RunWorkflowByFileName, to allow users to run workflows using the workflow filename. - Updated the existing RunWorkflow tool to accept a numeric workflow ID instead of a filename. - Enhanced tests to cover scenarios for both running workflows by ID and filename, including error handling for missing parameters. - Improved tool descriptions for clarity and usability.
1 parent 1339249 commit deb66ce

File tree

3 files changed

+185
-10
lines changed

3 files changed

+185
-10
lines changed

pkg/github/actions.go

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,24 +229,24 @@ func ListWorkflowRuns(getClient GetClientFn, t translations.TranslationHelperFun
229229
}
230230
}
231231

232-
// RunWorkflow creates a tool to run an Actions workflow
232+
// RunWorkflow creates a tool to run an Actions workflow by workflow ID
233233
func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
234234
return mcp.NewTool("run_workflow",
235-
mcp.WithDescription(t("TOOL_RUN_WORKFLOW_DESCRIPTION", "Run an Actions workflow")),
235+
mcp.WithDescription(t("TOOL_RUN_WORKFLOW_DESCRIPTION", "Run an Actions workflow by workflow ID")),
236236
mcp.WithToolAnnotation(mcp.ToolAnnotation{
237237
ReadOnlyHint: ToBoolPtr(false),
238238
}),
239239
mcp.WithString("owner",
240240
mcp.Required(),
241-
mcp.Description("The account owner of the repository. The name is not case sensitive."),
241+
mcp.Description("Repository owner"),
242242
),
243243
mcp.WithString("repo",
244244
mcp.Required(),
245245
mcp.Description("Repository name"),
246246
),
247-
mcp.WithString("workflow_file",
247+
mcp.WithNumber("workflow_id",
248248
mcp.Required(),
249-
mcp.Description("The workflow ID or workflow file name"),
249+
mcp.Description("The workflow ID (numeric identifier)"),
250250
),
251251
mcp.WithString("ref",
252252
mcp.Required(),
@@ -265,10 +265,11 @@ func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (t
265265
if err != nil {
266266
return mcp.NewToolResultError(err.Error()), nil
267267
}
268-
workflowFile, err := RequiredParam[string](request, "workflow_file")
268+
workflowIDInt, err := RequiredInt(request, "workflow_id")
269269
if err != nil {
270270
return mcp.NewToolResultError(err.Error()), nil
271271
}
272+
workflowID := int64(workflowIDInt)
272273
ref, err := RequiredParam[string](request, "ref")
273274
if err != nil {
274275
return mcp.NewToolResultError(err.Error()), nil
@@ -292,15 +293,17 @@ func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (t
292293
Inputs: inputs,
293294
}
294295

295-
resp, err := client.Actions.CreateWorkflowDispatchEventByFileName(ctx, owner, repo, workflowFile, event)
296+
// Convert workflow ID to string format for the API call
297+
workflowIDStr := fmt.Sprintf("%d", workflowID)
298+
resp, err := client.Actions.CreateWorkflowDispatchEventByFileName(ctx, owner, repo, workflowIDStr, event)
296299
if err != nil {
297300
return nil, fmt.Errorf("failed to run workflow: %w", err)
298301
}
299302
defer func() { _ = resp.Body.Close() }()
300303

301304
result := map[string]any{
302305
"message": "Workflow run has been queued",
303-
"workflow": workflowFile,
306+
"workflow_id": workflowID,
304307
"ref": ref,
305308
"inputs": inputs,
306309
"status": resp.Status,
@@ -316,6 +319,93 @@ func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (t
316319
}
317320
}
318321

322+
// RunWorkflowByFileName creates a tool to run an Actions workflow by filename
323+
func RunWorkflowByFileName(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
324+
return mcp.NewTool("run_workflow_by_filename",
325+
mcp.WithDescription(t("TOOL_RUN_WORKFLOW_BY_FILENAME_DESCRIPTION", "Run an Actions workflow by workflow filename")),
326+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
327+
ReadOnlyHint: ToBoolPtr(false),
328+
}),
329+
mcp.WithString("owner",
330+
mcp.Required(),
331+
mcp.Description("Repository owner"),
332+
),
333+
mcp.WithString("repo",
334+
mcp.Required(),
335+
mcp.Description("Repository name"),
336+
),
337+
mcp.WithString("workflow_file",
338+
mcp.Required(),
339+
mcp.Description("The workflow file name (e.g., main.yml, ci.yaml)"),
340+
),
341+
mcp.WithString("ref",
342+
mcp.Required(),
343+
mcp.Description("The git reference for the workflow. The reference can be a branch or tag name."),
344+
),
345+
mcp.WithObject("inputs",
346+
mcp.Description("Inputs the workflow accepts"),
347+
),
348+
),
349+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
350+
owner, err := RequiredParam[string](request, "owner")
351+
if err != nil {
352+
return mcp.NewToolResultError(err.Error()), nil
353+
}
354+
repo, err := RequiredParam[string](request, "repo")
355+
if err != nil {
356+
return mcp.NewToolResultError(err.Error()), nil
357+
}
358+
workflowFile, err := RequiredParam[string](request, "workflow_file")
359+
if err != nil {
360+
return mcp.NewToolResultError(err.Error()), nil
361+
}
362+
ref, err := RequiredParam[string](request, "ref")
363+
if err != nil {
364+
return mcp.NewToolResultError(err.Error()), nil
365+
}
366+
367+
// Get optional inputs parameter
368+
var inputs map[string]interface{}
369+
if requestInputs, ok := request.GetArguments()["inputs"]; ok {
370+
if inputsMap, ok := requestInputs.(map[string]interface{}); ok {
371+
inputs = inputsMap
372+
}
373+
}
374+
375+
client, err := getClient(ctx)
376+
if err != nil {
377+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
378+
}
379+
380+
event := github.CreateWorkflowDispatchEventRequest{
381+
Ref: ref,
382+
Inputs: inputs,
383+
}
384+
385+
resp, err := client.Actions.CreateWorkflowDispatchEventByFileName(ctx, owner, repo, workflowFile, event)
386+
if err != nil {
387+
return nil, fmt.Errorf("failed to run workflow: %w", err)
388+
}
389+
defer func() { _ = resp.Body.Close() }()
390+
391+
result := map[string]any{
392+
"message": "Workflow run has been queued",
393+
"workflow_file": workflowFile,
394+
"ref": ref,
395+
"inputs": inputs,
396+
"status": resp.Status,
397+
"status_code": resp.StatusCode,
398+
}
399+
400+
r, err := json.Marshal(result)
401+
if err != nil {
402+
return nil, fmt.Errorf("failed to marshal response: %w", err)
403+
}
404+
405+
return mcp.NewToolResultText(string(r)), nil
406+
}
407+
}
408+
319409
// GetWorkflowRun creates a tool to get details of a specific workflow run
320410
func GetWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
321411
return mcp.NewTool("get_workflow_run",

pkg/github/actions_test.go

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,90 @@ func Test_RunWorkflow(t *testing.T) {
134134
assert.NotEmpty(t, tool.Description)
135135
assert.Contains(t, tool.InputSchema.Properties, "owner")
136136
assert.Contains(t, tool.InputSchema.Properties, "repo")
137+
assert.Contains(t, tool.InputSchema.Properties, "workflow_id")
138+
assert.Contains(t, tool.InputSchema.Properties, "ref")
139+
assert.Contains(t, tool.InputSchema.Properties, "inputs")
140+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "workflow_id", "ref"})
141+
142+
tests := []struct {
143+
name string
144+
mockedClient *http.Client
145+
requestArgs map[string]any
146+
expectError bool
147+
expectedErrMsg string
148+
}{
149+
{
150+
name: "successful workflow run",
151+
mockedClient: mock.NewMockedHTTPClient(
152+
mock.WithRequestMatchHandler(
153+
mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId,
154+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
155+
w.WriteHeader(http.StatusNoContent)
156+
}),
157+
),
158+
),
159+
requestArgs: map[string]any{
160+
"owner": "owner",
161+
"repo": "repo",
162+
"workflow_id": float64(12345),
163+
"ref": "main",
164+
},
165+
expectError: false,
166+
},
167+
{
168+
name: "missing required parameter workflow_id",
169+
mockedClient: mock.NewMockedHTTPClient(),
170+
requestArgs: map[string]any{
171+
"owner": "owner",
172+
"repo": "repo",
173+
"ref": "main",
174+
},
175+
expectError: true,
176+
expectedErrMsg: "missing required parameter: workflow_id",
177+
},
178+
}
179+
180+
for _, tc := range tests {
181+
t.Run(tc.name, func(t *testing.T) {
182+
// Setup client with mock
183+
client := github.NewClient(tc.mockedClient)
184+
_, handler := RunWorkflow(stubGetClientFn(client), translations.NullTranslationHelper)
185+
186+
// Create call request
187+
request := createMCPRequest(tc.requestArgs)
188+
189+
// Call handler
190+
result, err := handler(context.Background(), request)
191+
192+
require.NoError(t, err)
193+
require.Equal(t, tc.expectError, result.IsError)
194+
195+
// Parse the result and get the text content if no error
196+
textContent := getTextResult(t, result)
197+
198+
if tc.expectedErrMsg != "" {
199+
assert.Equal(t, tc.expectedErrMsg, textContent.Text)
200+
return
201+
}
202+
203+
// Unmarshal and verify the result
204+
var response map[string]any
205+
err = json.Unmarshal([]byte(textContent.Text), &response)
206+
require.NoError(t, err)
207+
assert.Equal(t, "Workflow run has been queued", response["message"])
208+
})
209+
}
210+
}
211+
212+
func Test_RunWorkflowByFileName(t *testing.T) {
213+
// Verify tool definition once
214+
mockClient := github.NewClient(nil)
215+
tool, _ := RunWorkflowByFileName(stubGetClientFn(mockClient), translations.NullTranslationHelper)
216+
217+
assert.Equal(t, "run_workflow_by_filename", tool.Name)
218+
assert.NotEmpty(t, tool.Description)
219+
assert.Contains(t, tool.InputSchema.Properties, "owner")
220+
assert.Contains(t, tool.InputSchema.Properties, "repo")
137221
assert.Contains(t, tool.InputSchema.Properties, "workflow_file")
138222
assert.Contains(t, tool.InputSchema.Properties, "ref")
139223
assert.Contains(t, tool.InputSchema.Properties, "inputs")
@@ -147,7 +231,7 @@ func Test_RunWorkflow(t *testing.T) {
147231
expectedErrMsg string
148232
}{
149233
{
150-
name: "successful workflow run",
234+
name: "successful workflow run by filename",
151235
mockedClient: mock.NewMockedHTTPClient(
152236
mock.WithRequestMatchHandler(
153237
mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId,
@@ -181,7 +265,7 @@ func Test_RunWorkflow(t *testing.T) {
181265
t.Run(tc.name, func(t *testing.T) {
182266
// Setup client with mock
183267
client := github.NewClient(tc.mockedClient)
184-
_, handler := RunWorkflow(stubGetClientFn(client), translations.NullTranslationHelper)
268+
_, handler := RunWorkflowByFileName(stubGetClientFn(client), translations.NullTranslationHelper)
185269

186270
// Create call request
187271
request := createMCPRequest(tc.requestArgs)

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
125125
).
126126
AddWriteTools(
127127
toolsets.NewServerTool(RunWorkflow(getClient, t)),
128+
toolsets.NewServerTool(RunWorkflowByFileName(getClient, t)),
128129
toolsets.NewServerTool(RerunWorkflowRun(getClient, t)),
129130
toolsets.NewServerTool(RerunFailedJobs(getClient, t)),
130131
toolsets.NewServerTool(CancelWorkflowRun(getClient, t)),

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy