Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit7c62774

Browse files
authored
Add tail logs option (#615)
1 parent5904a03 commit7c62774

File tree

3 files changed

+97
-13
lines changed

3 files changed

+97
-13
lines changed

‎README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
456456
-`repo`: Repository name (string, required)
457457
-`return_content`: Returns actual log content instead of URLs (boolean, optional)
458458
-`run_id`: Workflow run ID (required when using failed_only) (number, optional)
459+
-`tail_lines`: Number of lines to return from the end of the log (number, optional)
459460

460461
-**get_workflow_run** - Get workflow run
461462
-`owner`: Repository owner (string, required)

‎pkg/github/actions.go

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,10 @@ func GetJobLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (to
584584
mcp.WithBoolean("return_content",
585585
mcp.Description("Returns actual log content instead of URLs"),
586586
),
587+
mcp.WithNumber("tail_lines",
588+
mcp.Description("Number of lines to return from the end of the log"),
589+
mcp.DefaultNumber(500),
590+
),
587591
),
588592
func(ctx context.Context,request mcp.CallToolRequest) (*mcp.CallToolResult,error) {
589593
owner,err:=RequiredParam[string](request,"owner")
@@ -612,6 +616,14 @@ func GetJobLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (to
612616
iferr!=nil {
613617
returnmcp.NewToolResultError(err.Error()),nil
614618
}
619+
tailLines,err:=OptionalIntParam(request,"tail_lines")
620+
iferr!=nil {
621+
returnmcp.NewToolResultError(err.Error()),nil
622+
}
623+
// Default to 500 lines if not specified
624+
iftailLines==0 {
625+
tailLines=500
626+
}
615627

616628
client,err:=getClient(ctx)
617629
iferr!=nil {
@@ -628,18 +640,18 @@ func GetJobLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (to
628640

629641
iffailedOnly&&runID>0 {
630642
// Handle failed-only mode: get logs for all failed jobs in the workflow run
631-
returnhandleFailedJobLogs(ctx,client,owner,repo,int64(runID),returnContent)
643+
returnhandleFailedJobLogs(ctx,client,owner,repo,int64(runID),returnContent,tailLines)
632644
}elseifjobID>0 {
633645
// Handle single job mode
634-
returnhandleSingleJobLogs(ctx,client,owner,repo,int64(jobID),returnContent)
646+
returnhandleSingleJobLogs(ctx,client,owner,repo,int64(jobID),returnContent,tailLines)
635647
}
636648

637649
returnmcp.NewToolResultError("Either job_id must be provided for single job logs, or run_id with failed_only=true for failed job logs"),nil
638650
}
639651
}
640652

641653
// handleFailedJobLogs gets logs for all failed jobs in a workflow run
642-
funchandleFailedJobLogs(ctx context.Context,client*github.Client,owner,repostring,runIDint64,returnContentbool) (*mcp.CallToolResult,error) {
654+
funchandleFailedJobLogs(ctx context.Context,client*github.Client,owner,repostring,runIDint64,returnContentbool,tailLinesint) (*mcp.CallToolResult,error) {
643655
// First, get all jobs for the workflow run
644656
jobs,resp,err:=client.Actions.ListWorkflowJobs(ctx,owner,repo,runID,&github.ListWorkflowJobsOptions{
645657
Filter:"latest",
@@ -671,7 +683,7 @@ func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo
671683
// Collect logs for all failed jobs
672684
varlogResults []map[string]any
673685
for_,job:=rangefailedJobs {
674-
jobResult,resp,err:=getJobLogData(ctx,client,owner,repo,job.GetID(),job.GetName(),returnContent)
686+
jobResult,resp,err:=getJobLogData(ctx,client,owner,repo,job.GetID(),job.GetName(),returnContent,tailLines)
675687
iferr!=nil {
676688
// Continue with other jobs even if one fails
677689
jobResult=map[string]any{
@@ -704,8 +716,8 @@ func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo
704716
}
705717

706718
// handleSingleJobLogs gets logs for a single job
707-
funchandleSingleJobLogs(ctx context.Context,client*github.Client,owner,repostring,jobIDint64,returnContentbool) (*mcp.CallToolResult,error) {
708-
jobResult,resp,err:=getJobLogData(ctx,client,owner,repo,jobID,"",returnContent)
719+
funchandleSingleJobLogs(ctx context.Context,client*github.Client,owner,repostring,jobIDint64,returnContentbool,tailLinesint) (*mcp.CallToolResult,error) {
720+
jobResult,resp,err:=getJobLogData(ctx,client,owner,repo,jobID,"",returnContent,tailLines)
709721
iferr!=nil {
710722
returnghErrors.NewGitHubAPIErrorResponse(ctx,"failed to get job logs",resp,err),nil
711723
}
@@ -719,7 +731,7 @@ func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo
719731
}
720732

721733
// getJobLogData retrieves log data for a single job, either as URL or content
722-
funcgetJobLogData(ctx context.Context,client*github.Client,owner,repostring,jobIDint64,jobNamestring,returnContentbool) (map[string]any,*github.Response,error) {
734+
funcgetJobLogData(ctx context.Context,client*github.Client,owner,repostring,jobIDint64,jobNamestring,returnContentbool,tailLinesint) (map[string]any,*github.Response,error) {
723735
// Get the download URL for the job logs
724736
url,resp,err:=client.Actions.GetWorkflowJobLogs(ctx,owner,repo,jobID,1)
725737
iferr!=nil {
@@ -736,7 +748,7 @@ func getJobLogData(ctx context.Context, client *github.Client, owner, repo strin
736748

737749
ifreturnContent {
738750
// Download and return the actual log content
739-
content,httpResp,err:=downloadLogContent(url.String())//nolint:bodyclose // Response body is closed in downloadLogContent, but we need to return httpResp
751+
content,originalLength,httpResp,err:=downloadLogContent(url.String(),tailLines)//nolint:bodyclose // Response body is closed in downloadLogContent, but we need to return httpResp
740752
iferr!=nil {
741753
// To keep the return value consistent wrap the response as a GitHub Response
742754
ghRes:=&github.Response{
@@ -746,6 +758,7 @@ func getJobLogData(ctx context.Context, client *github.Client, owner, repo strin
746758
}
747759
result["logs_content"]=content
748760
result["message"]="Job logs content retrieved successfully"
761+
result["original_length"]=originalLength
749762
}else {
750763
// Return just the URL
751764
result["logs_url"]=url.String()
@@ -757,25 +770,46 @@ func getJobLogData(ctx context.Context, client *github.Client, owner, repo strin
757770
}
758771

759772
// downloadLogContent downloads the actual log content from a GitHub logs URL
760-
funcdownloadLogContent(logURLstring) (string,*http.Response,error) {
773+
funcdownloadLogContent(logURLstring,tailLinesint) (string,int,*http.Response,error) {
761774
httpResp,err:=http.Get(logURL)//nolint:gosec // URLs are provided by GitHub API and are safe
762775
iferr!=nil {
763-
return"",httpResp,fmt.Errorf("failed to download logs: %w",err)
776+
return"",0,httpResp,fmt.Errorf("failed to download logs: %w",err)
764777
}
765778
deferfunc() {_=httpResp.Body.Close() }()
766779

767780
ifhttpResp.StatusCode!=http.StatusOK {
768-
return"",httpResp,fmt.Errorf("failed to download logs: HTTP %d",httpResp.StatusCode)
781+
return"",0,httpResp,fmt.Errorf("failed to download logs: HTTP %d",httpResp.StatusCode)
769782
}
770783

771784
content,err:=io.ReadAll(httpResp.Body)
772785
iferr!=nil {
773-
return"",httpResp,fmt.Errorf("failed to read log content: %w",err)
786+
return"",0,httpResp,fmt.Errorf("failed to read log content: %w",err)
774787
}
775788

776789
// Clean up and format the log content for better readability
777790
logContent:=strings.TrimSpace(string(content))
778-
returnlogContent,httpResp,nil
791+
792+
trimmedContent,lineCount:=trimContent(logContent,tailLines)
793+
returntrimmedContent,lineCount,httpResp,nil
794+
}
795+
796+
// trimContent trims the content to a maximum length and returns the trimmed content and an original length
797+
functrimContent(contentstring,tailLinesint) (string,int) {
798+
// Truncate to tail_lines if specified
799+
lineCount:=0
800+
iftailLines>0 {
801+
802+
// Count backwards to find the nth newline from the end
803+
fori:=len(content)-1;i>=0&&lineCount<tailLines;i-- {
804+
ifcontent[i]=='\n' {
805+
lineCount++
806+
iflineCount==tailLines {
807+
content=content[i+1:]
808+
}
809+
}
810+
}
811+
}
812+
returncontent,lineCount
779813
}
780814

781815
// RerunWorkflowRun creates a tool to re-run an entire workflow run

‎pkg/github/actions_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,3 +1095,52 @@ func Test_GetJobLogs_WithContentReturn(t *testing.T) {
10951095
assert.Equal(t,"Job logs content retrieved successfully",response["message"])
10961096
assert.NotContains(t,response,"logs_url")// Should not have URL when returning content
10971097
}
1098+
1099+
funcTest_GetJobLogs_WithContentReturnAndTailLines(t*testing.T) {
1100+
// Test the return_content functionality with a mock HTTP server
1101+
logContent:="2023-01-01T10:00:00.000Z Starting job...\n2023-01-01T10:00:01.000Z Running tests...\n2023-01-01T10:00:02.000Z Job completed successfully"
1102+
expectedLogContent:="2023-01-01T10:00:02.000Z Job completed successfully"
1103+
1104+
// Create a test server to serve log content
1105+
testServer:=httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
1106+
w.WriteHeader(http.StatusOK)
1107+
_,_=w.Write([]byte(logContent))
1108+
}))
1109+
defertestServer.Close()
1110+
1111+
mockedClient:=mock.NewMockedHTTPClient(
1112+
mock.WithRequestMatchHandler(
1113+
mock.GetReposActionsJobsLogsByOwnerByRepoByJobId,
1114+
http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
1115+
w.Header().Set("Location",testServer.URL)
1116+
w.WriteHeader(http.StatusFound)
1117+
}),
1118+
),
1119+
)
1120+
1121+
client:=github.NewClient(mockedClient)
1122+
_,handler:=GetJobLogs(stubGetClientFn(client),translations.NullTranslationHelper)
1123+
1124+
request:=createMCPRequest(map[string]any{
1125+
"owner":"owner",
1126+
"repo":"repo",
1127+
"job_id":float64(123),
1128+
"return_content":true,
1129+
"tail_lines":float64(1),// Requesting last 1 line
1130+
})
1131+
1132+
result,err:=handler(context.Background(),request)
1133+
require.NoError(t,err)
1134+
require.False(t,result.IsError)
1135+
1136+
textContent:=getTextResult(t,result)
1137+
varresponsemap[string]any
1138+
err=json.Unmarshal([]byte(textContent.Text),&response)
1139+
require.NoError(t,err)
1140+
1141+
assert.Equal(t,float64(123),response["job_id"])
1142+
assert.Equal(t,float64(1),response["original_length"])
1143+
assert.Equal(t,expectedLogContent,response["logs_content"])
1144+
assert.Equal(t,"Job logs content retrieved successfully",response["message"])
1145+
assert.NotContains(t,response,"logs_url")// Should not have URL when returning content
1146+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp