Skip to content

feat: [HWT16]: Add get_pipeline_failure_logs tool for retrieving Harness pipeline failure logs with Git context #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions client/dto/execution_graph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package dto

// ExecutionGraphResponse represents the execution graph response
type ExecutionGraphResponse struct {
RootNodeID string `json:"rootNodeId,omitempty"`
NodeMap map[string]ExecutionNode `json:"nodeMap,omitempty"`
}

// ExecutionNode represents a node in the execution graph
type ExecutionNode struct {
UUID string `json:"uuid,omitempty"`
SetupID string `json:"setupId,omitempty"`
Name string `json:"name,omitempty"`
Identifier string `json:"identifier,omitempty"`
BaseFqn string `json:"baseFqn,omitempty"`
Status string `json:"status,omitempty"`
StepParameters map[string]interface{} `json:"stepParameters,omitempty"`
LogBaseKey string `json:"logBaseKey,omitempty"`
ExecutableResponses []ExecutableResponse `json:"executableResponses,omitempty"`
}

// ExecutableResponse represents the executable response in a node
type ExecutableResponse struct {
Async ExecutableAsync `json:"async,omitempty"`
Child *ChildResponse `json:"child,omitempty"`
}

// ChildResponse represents child node information
type ChildResponse struct {
ChildNodeID string `json:"childNodeId,omitempty"`
LogKeys []string `json:"logKeys,omitempty"`
}

// ExecutableAsync contains async information including log keys
type ExecutableAsync struct {
LogKeys []string `json:"logKeys,omitempty"`
CallbackIds []string `json:"callbackIds,omitempty"`
Status string `json:"status,omitempty"`
}

// FailedNodeInfo contains information about a failed node
type FailedNodeInfo struct {
StageID string `json:"stageId,omitempty"`
StepID string `json:"stepId,omitempty"`
FailureMessage string `json:"failureMessage,omitempty"`
NodeID string `json:"nodeId,omitempty"`
}

// GitContext contains git-related information for a pipeline execution
type GitContext struct {
RepoURL string `json:"repoUrl,omitempty"`
Branch string `json:"branch,omitempty"`
Tag string `json:"tag,omitempty"`
CommitHash string `json:"commitHash,omitempty"`
CommitMessage string `json:"commitMessage,omitempty"`
}

// FailureLogResponse represents the response for pipeline failure logs
type FailureLogResponse struct {
PipelineID string `json:"pipelineId,omitempty"`
ExecutionID string `json:"executionId,omitempty"`
Status string `json:"status,omitempty"`
GitContext GitContext `json:"gitContext,omitempty"`
Failures []FailureDetails `json:"failures,omitempty"`
}

// FailureDetails contains details about a specific failure
type FailureDetails struct {
Stage string `json:"stage,omitempty"`
Step string `json:"step,omitempty"`
Message string `json:"message,omitempty"`
Logs string `json:"logs,omitempty"`
}
41 changes: 21 additions & 20 deletions client/dto/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,26 +134,27 @@ type PipelineExecutionResponse struct {

// PipelineExecution represents a pipeline execution
type PipelineExecution struct {
PipelineIdentifier string `json:"pipelineIdentifier,omitempty"`
ProjectIdentifier string `json:"projectIdentifier,omitempty"`
OrgIdentifier string `json:"orgIdentifier,omitempty"`
PlanExecutionId string `json:"planExecutionId,omitempty"`
Name string `json:"name,omitempty"`
Status string `json:"status,omitempty"`
FailureInfo ExecutionFailureInfo `json:"failureInfo,omitempty"`
StartTs int64 `json:"startTs,omitempty"`
EndTs int64 `json:"endTs,omitempty"`
CreatedAt int64 `json:"createdAt,omitempty"`
ConnectorRef string `json:"connectorRef,omitempty"`
SuccessfulStagesCount int `json:"successfulStagesCount,omitempty"`
FailedStagesCount int `json:"failedStagesCount,omitempty"`
RunningStagesCount int `json:"runningStagesCount,omitempty"`
TotalStagesRunningCount int `json:"totalStagesRunningCount,omitempty"`
StagesExecuted []string `json:"stagesExecuted,omitempty"`
AbortedBy User `json:"abortedBy,omitempty"`
QueuedType string `json:"queuedType,omitempty"`
RunSequence int32 `json:"runSequence,omitempty"`
ShouldUseSimplifiedBaseKey bool `json:"shouldUseSimplifiedBaseKey,omitempty"`
PipelineIdentifier string `json:"pipelineIdentifier,omitempty"`
ProjectIdentifier string `json:"projectIdentifier,omitempty"`
OrgIdentifier string `json:"orgIdentifier,omitempty"`
PlanExecutionId string `json:"planExecutionId,omitempty"`
Name string `json:"name,omitempty"`
Status string `json:"status,omitempty"`
FailureInfo ExecutionFailureInfo `json:"failureInfo,omitempty"`
StartTs int64 `json:"startTs,omitempty"`
EndTs int64 `json:"endTs,omitempty"`
CreatedAt int64 `json:"createdAt,omitempty"`
ConnectorRef string `json:"connectorRef,omitempty"`
SuccessfulStagesCount int `json:"successfulStagesCount,omitempty"`
FailedStagesCount int `json:"failedStagesCount,omitempty"`
RunningStagesCount int `json:"runningStagesCount,omitempty"`
TotalStagesRunningCount int `json:"totalStagesRunningCount,omitempty"`
StagesExecuted []string `json:"stagesExecuted,omitempty"`
AbortedBy User `json:"abortedBy,omitempty"`
QueuedType string `json:"queuedType,omitempty"`
RunSequence int32 `json:"runSequence,omitempty"`
ShouldUseSimplifiedBaseKey bool `json:"shouldUseSimplifiedBaseKey,omitempty"`
ModuleInfo map[string]interface{} `json:"moduleInfo,omitempty"`
}

// ExecutionFailureInfo represents the failure information of a pipeline execution
Expand Down
96 changes: 95 additions & 1 deletion client/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ package client
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"

"github.com/harness/harness-mcp/client/dto"
)

const (
logDownloadPath = "log-service/blob/download"
logDownloadPath = "log-service/blob/download"
logServiceTokenPath = "gateway/log-service/token"
logServiceBlobPath = "gateway/log-service/blob"
)

// LogService handles operations related to pipeline logs
Expand Down Expand Up @@ -60,3 +65,92 @@ func (l *LogService) DownloadLogs(ctx context.Context, scope dto.Scope, planExec

return response.Link, nil
}

// GetLogServiceToken retrieves a token for accessing the log serviceAdd commentMore actions
func (l *LogService) GetLogServiceToken(ctx context.Context, accountID string) (string, error) {
// Prepare query parameters
params := make(map[string]string)
params["accountID"] = accountID

// Create URL with parameters
url := *l.Client.BaseURL
url.Path = logServiceTokenPath
q := url.Query()
for k, v := range params {
q.Add(k, v)
}
url.RawQuery = q.Encode()

// Create request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}

// Add auth header using the auth provider
k, v, err := l.Client.AuthProvider.GetHeader(ctx)
if err != nil {
return "", fmt.Errorf("failed to get auth header: %w", err)
}
req.Header.Set(k, v)

// Make the request
resp, err := l.Client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to get log service token: %w", err)
}
defer resp.Body.Close()

// Read the raw response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}

tokenString := string(body)
if err != nil {
return "", fmt.Errorf("failed to get log service token: %w", err)
}

return tokenString, nil
}

// GetStepLogs retrieves logs for a specific step using the log key
func (l *LogService) GetStepLogs(ctx context.Context, accountID, orgID, projectID, pipelineID, logKey, token string) (string, error) {
// The log service uses a different authentication mechanism with a token
// rather than the API key, so we need to construct the URL and make the request manually
baseURL := l.Client.BaseURL.String()
logURL, err := url.Parse(fmt.Sprintf("%s/%s", baseURL, logServiceBlobPath))
if err != nil {
return "", fmt.Errorf("failed to parse log URL: %w", err)
}

// Add query parameters
query := logURL.Query()
query.Add("accountID", accountID)
query.Add("orgId", orgID)
query.Add("projectId", projectID)
query.Add("pipelineId", pipelineID)
query.Add("X-Harness-Token", token)
query.Add("key", logKey)
logURL.RawQuery = query.Encode()

// Make the HTTP request
resp, err := http.Get(logURL.String())
if err != nil {
return "", fmt.Errorf("failed to get logs: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

// Read the response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}

return string(body), nil
}
21 changes: 21 additions & 0 deletions client/pipelines.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const (
pipelineExecutionPath = "pipeline/api/pipelines/execution/url"
pipelineExecutionGetPath = "pipeline/api/pipelines/execution/v2/%s"
pipelineExecutionSummaryPath = "pipeline/api/pipelines/execution/summary"
pipelineExecutionGraphPath = "pipeline/api/pipelines/execution/getExecutionGraph/%s"
)

type PipelineService struct {
Expand Down Expand Up @@ -194,3 +195,23 @@ func (p *PipelineService) FetchExecutionURL(

return urlResponse.Data, nil
}

// GetExecutionGraph retrieves the execution graph for a specific pipeline executionAdd commentMore actions
func (p *PipelineService) GetExecutionGraph(ctx context.Context, scope dto.Scope, planExecutionID string) (*dto.ExecutionGraphResponse, error) {
path := fmt.Sprintf(pipelineExecutionGraphPath, planExecutionID)

// Prepare query parameters
params := make(map[string]string)
addScope(scope, params)

// Initialize the response object
response := &dto.Entity[dto.ExecutionGraphResponse]{}

// Make the GET request
err := p.Client.Get(ctx, path, params, nil, response)
if err != nil {
return nil, fmt.Errorf("failed to get execution graph: %w", err)
}

return &response.Data, nil
}
Loading