forgejo-mcp/operation/actions/workflow_runs.go
Christoph Görn dd9116fbca
fix: 🚨 clear golangci-lint v2 backlog (closes bead forgejo-mcp-hc9)
- errorlint: fmt.Errorf %v→%w across all handlers (errors now wrap the
  underlying SDK/HTTP error for errors.Is/As); errors.Is for
  context.Canceled in cmd
- staticcheck quickfixes: Fprintf over WriteString(Sprintf), tagged
  switches, omit inferred 'any'; QF1001 resolved by hoisting hex checks
  into isHex predicates (normalizeColor, validateSHA)
- govet: reflect.Ptr → reflect.Pointer in pkg/ptr
- errcheck: explicit '_ =' for fs.Parse (ExitOnError) and deferred
  zap Sync
- repair four test assertions the autofix broke (it dropped the
  're.Code != X' half of combined type-assert checks, inverting the
  test); all errors.As sites use 'var re *resource.ResourceError'

golangci-lint: 0 issues. staticcheck, vet, tests: clean.
2026-06-11 00:03:36 +02:00

205 lines
5.4 KiB
Go

package actions
import (
"context"
"errors"
"fmt"
"strings"
"codeberg.org/goern/forgejo-mcp/v2/operation/params"
"codeberg.org/goern/forgejo-mcp/v2/pkg/forgejo"
"codeberg.org/goern/forgejo-mcp/v2/pkg/log"
"codeberg.org/goern/forgejo-mcp/v2/pkg/to"
forgejo_sdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3"
"github.com/mark3labs/mcp-go/mcp"
)
const (
ListWorkflowRunsToolName = "list_workflow_runs"
GetWorkflowRunToolName = "get_workflow_run"
)
var (
ListWorkflowRunsTool = mcp.NewTool(
ListWorkflowRunsToolName,
mcp.WithDescription("List workflow runs for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
mcp.WithString("status", mcp.Description(params.Status)),
mcp.WithString("event", mcp.Description(params.Event)),
mcp.WithNumber("run_number", mcp.Description(params.RunNumber)),
mcp.WithString("head_sha", mcp.Description(params.HeadSHA)),
mcp.WithNumber("page", mcp.Description(params.Page), mcp.DefaultNumber(1), mcp.Min(1)),
mcp.WithNumber("limit", mcp.Description(params.Limit), mcp.DefaultNumber(30), mcp.Min(1)),
)
GetWorkflowRunTool = mcp.NewTool(
GetWorkflowRunToolName,
mcp.WithDescription("Get details of a specific workflow run"),
mcp.WithString("owner", mcp.Required(), mcp.Description(params.Owner)),
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
mcp.WithNumber("run_id", mcp.Required(), mcp.Description(params.RunID)),
)
)
func ListWorkflowRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListWorkflowRunsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required"))
}
status, _ := req.GetArguments()["status"].(string)
event, _ := req.GetArguments()["event"].(string)
headSHA, _ := req.GetArguments()["head_sha"].(string)
var runNumber int64
if rn, err := to.Float64(req.GetArguments()["run_number"]); err == nil {
runNumber = int64(rn)
}
page := 1
if p, err := to.Float64(req.GetArguments()["page"]); err == nil {
page = int(p)
}
limit := 30
if l, err := to.Float64(req.GetArguments()["limit"]); err == nil {
limit = int(l)
}
opt := forgejo_sdk.ListActionRunsOption{
ListOptions: forgejo_sdk.ListOptions{
Page: page,
PageSize: limit,
},
Status: status,
Event: event,
RunNumber: runNumber,
HeadSHA: headSHA,
}
client, err := forgejo.Client(ctx)
if err != nil {
return to.ErrorResult(err)
}
resp, _, err := client.ListRepoActionRuns(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("failed to list workflow runs: %w", err))
}
if len(resp.WorkflowRuns) == 0 {
return to.TextResult(fmt.Sprintf("No workflow runs found for %s/%s", owner, repo))
}
var sb strings.Builder
fmt.Fprintf(&sb, "Workflow Runs for %s/%s (total: %d):\n\n", owner, repo, resp.TotalCount)
for _, run := range resp.WorkflowRuns {
duration := ""
if !run.Started.IsZero() && !run.Stopped.IsZero() {
duration = fmt.Sprintf(" | Duration: %s", run.Stopped.Sub(run.Started).Round(1e9).String())
}
fmt.Fprintf(&sb, "#%d - %s\n", run.ID, run.Title)
fmt.Fprintf(&sb, " Status: %s | Event: %s | SHA: %.7s%s\n",
run.Status, run.Event, run.CommitSHA, duration)
if !run.Started.IsZero() {
fmt.Fprintf(&sb, " Started: %s\n", run.Started.Format("2006-01-02 15:04:05"))
}
fmt.Fprintf(&sb, " URL: %s\n\n", run.HTMLURL)
}
return to.TextResult(sb.String())
}
func GetWorkflowRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetWorkflowRunFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required"))
}
runIDFloat, err := to.Float64(req.GetArguments()["run_id"])
if err != nil {
return to.ErrorResult(errors.New("run_id is required"))
}
runID := int64(runIDFloat)
if runID <= 0 {
return to.ErrorResult(errors.New("run_id must be a positive integer"))
}
client, err := forgejo.Client(ctx)
if err != nil {
return to.ErrorResult(err)
}
run, _, err := client.GetRepoActionRun(owner, repo, runID)
if err != nil {
return to.ErrorResult(fmt.Errorf("failed to get workflow run: %w", err))
}
duration := "N/A"
if !run.Started.IsZero() && !run.Stopped.IsZero() {
duration = run.Stopped.Sub(run.Started).Round(1e9).String()
}
triggerUser := "unknown"
if run.TriggerUser != nil {
triggerUser = run.TriggerUser.UserName
}
createdStr := "N/A"
if !run.Created.IsZero() {
createdStr = run.Created.Format("2006-01-02 15:04:05")
}
startedStr := "Not started"
if !run.Started.IsZero() {
startedStr = run.Started.Format("2006-01-02 15:04:05")
}
stoppedStr := "Not stopped"
if !run.Stopped.IsZero() {
stoppedStr = run.Stopped.Format("2006-01-02 15:04:05")
}
result := fmt.Sprintf(`Workflow Run #%d
Title: %s
Status: %s
Event: %s
Ref: %s
Commit SHA: %s
Triggered by: %s
Created: %s
Started: %s
Stopped: %s
Duration: %s
URL: %s`,
run.ID,
run.Title,
run.Status,
run.Event,
run.PrettyRef,
run.CommitSHA,
triggerUser,
createdStr,
startedStr,
stoppedStr,
duration,
run.HTMLURL,
)
return to.TextResult(result)
}