mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2025-12-24 01:02:43 +00:00
# Add Gitea Actions support (secrets, variables, workflows, runs, jobs, logs)
## Summary
This PR adds comprehensive support for Gitea Actions API to the MCP server, enabling users to manage Actions secrets, variables, workflows, runs, jobs, and logs through the Model Context Protocol interface.
## New Features
### Actions Secrets (Repository & Organization Level)
- `list_repo_action_secrets` - List repository secrets (metadata only, values never exposed)
- `upsert_repo_action_secret` - Create or update a repository secret
- `delete_repo_action_secret` - Delete a repository secret
- `list_org_action_secrets` - List organization secrets
- `upsert_org_action_secret` - Create or update an organization secret
- `delete_org_action_secret` - Delete an organization secret
### Actions Variables (Repository & Organization Level)
- `list_repo_action_variables` - List repository variables
- `get_repo_action_variable` - Get a specific repository variable
- `create_repo_action_variable` - Create a repository variable
- `update_repo_action_variable` - Update a repository variable
- `delete_repo_action_variable` - Delete a repository variable
- `list_org_action_variables` - List organization variables
- `get_org_action_variable` - Get a specific organization variable
- `create_org_action_variable` - Create an organization variable
- `update_org_action_variable` - Update an organization variable
- `delete_org_action_variable` - Delete an organization variable
### Actions Workflows
- `list_repo_action_workflows` - List repository workflows
- `get_repo_action_workflow` - Get a specific workflow by ID
- `dispatch_repo_action_workflow` - Trigger (dispatch) a workflow run with optional inputs
### Actions Runs
- `list_repo_action_runs` - List workflow runs with optional status filtering
- `get_repo_action_run` - Get a specific run by ID
- `cancel_repo_action_run` - Cancel a running workflow
- `rerun_repo_action_run` - Rerun a workflow (with fallback routes for version compatibility)
### Actions Jobs
- `list_repo_action_jobs` - List all jobs in a repository
- `list_repo_action_run_jobs` - List jobs for a specific workflow run
### Actions Job Logs
- `get_repo_action_job_log_preview` - Get log preview with tail/limit support (chat-friendly)
- `download_repo_action_job_log` - Download full job logs to file (default: `~/.gitea-mcp/artifacts/actions-logs/`)
## Implementation Details
### Architecture
- Follows existing codebase patterns: new `operation/actions/` package with tools registered via `Tool.RegisterRead/Write()`
- Uses Gitea SDK (`code.gitea.io/sdk/gitea v0.22.1`) where endpoints are available
- Shared REST helper (`pkg/gitea/rest.go`) for endpoints not yet in SDK (workflows, runs, jobs, logs)
### Security
- **Secrets never expose values**: List/get operations return only safe metadata (name, description, created_at)
- Request-scoped token support: HTTP Bearer tokens properly respected (fixes issue where wiki REST calls were hardcoding `flag.Token`)
### Compatibility
- Fallback route logic for dispatch/rerun endpoints (handles Gitea version differences)
- Clear error messages when endpoints aren't available, referencing Gitea 1.24 API docs
- Graceful handling of 404/405 responses for unsupported endpoints
### Testing
- Unit tests for REST helper token precedence
- Unit tests for log truncation/formatting helpers
- All existing tests pass
## Files Changed
- **New**: `operation/actions/*` - Complete Actions module (secrets, variables, runs, logs)
- **New**: `pkg/gitea/rest.go` - Shared REST helper with token context support
- **New**: `pkg/gitea/rest_test.go` - Tests for REST helper
- **Modified**: `operation/operation.go` - Register Actions tools
- **Modified**: `operation/wiki/wiki.go` - Refactored to use shared REST helper (removed hardcoded token)
- **Modified**: `README.md` - Added all new tools to documentation
## Testing
```bash
# All tests pass
go test ./...
# Build succeeds
make build
```
## Example Usage
```python
# List repository secrets
mcp.call_tool("list_repo_action_secrets", {"owner": "user", "repo": "myrepo"})
# Trigger a workflow
mcp.call_tool("dispatch_repo_action_workflow", {
"owner": "user",
"repo": "myrepo",
"workflow_id": 123,
"ref": "main",
"inputs": {"deploy_env": "production"}
})
# Get job log preview (last 100 lines)
mcp.call_tool("get_repo_action_job_log_preview", {
"owner": "user",
"repo": "myrepo",
"job_id": 456,
"tail_lines": 100
})
```
## Breaking Changes
None - this is a purely additive change.
## Related Issues
Fixes #[issue-number] (if applicable)
## Checklist
- [x] Code follows existing patterns and conventions
- [x] All tests pass
- [x] Documentation updated (README.md)
- [x] No breaking changes
- [x] Security considerations addressed (secrets never expose values)
- [x] Error handling implemented with clear messages
- [x] Version compatibility considered (fallback routes)
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/110
Reviewed-by: hiifong <f@f.style>
Co-authored-by: Shawn Anderson <sanderson@eye-catcher.com>
Co-committed-by: Shawn Anderson <sanderson@eye-catcher.com>
469 lines
17 KiB
Go
469 lines
17 KiB
Go
package actions
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/mark3labs/mcp-go/server"
|
|
)
|
|
|
|
const (
|
|
ListRepoActionWorkflowsToolName = "list_repo_action_workflows"
|
|
GetRepoActionWorkflowToolName = "get_repo_action_workflow"
|
|
DispatchRepoActionWorkflowToolName = "dispatch_repo_action_workflow"
|
|
|
|
ListRepoActionRunsToolName = "list_repo_action_runs"
|
|
GetRepoActionRunToolName = "get_repo_action_run"
|
|
CancelRepoActionRunToolName = "cancel_repo_action_run"
|
|
RerunRepoActionRunToolName = "rerun_repo_action_run"
|
|
|
|
ListRepoActionJobsToolName = "list_repo_action_jobs"
|
|
ListRepoActionRunJobsToolName = "list_repo_action_run_jobs"
|
|
)
|
|
|
|
var (
|
|
ListRepoActionWorkflowsTool = mcp.NewTool(
|
|
ListRepoActionWorkflowsToolName,
|
|
mcp.WithDescription("List repository Actions workflows"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)),
|
|
)
|
|
|
|
GetRepoActionWorkflowTool = mcp.NewTool(
|
|
GetRepoActionWorkflowToolName,
|
|
mcp.WithDescription("Get a repository Actions workflow by ID"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("workflow_id", mcp.Required(), mcp.Description("workflow ID")),
|
|
)
|
|
|
|
DispatchRepoActionWorkflowTool = mcp.NewTool(
|
|
DispatchRepoActionWorkflowToolName,
|
|
mcp.WithDescription("Trigger (dispatch) a repository Actions workflow"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("workflow_id", mcp.Required(), mcp.Description("workflow ID")),
|
|
mcp.WithString("ref", mcp.Required(), mcp.Description("git ref (branch or tag)")),
|
|
mcp.WithObject("inputs", mcp.Description("workflow inputs object")),
|
|
)
|
|
|
|
ListRepoActionRunsTool = mcp.NewTool(
|
|
ListRepoActionRunsToolName,
|
|
mcp.WithDescription("List repository Actions workflow runs"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)),
|
|
mcp.WithString("status", mcp.Description("optional status filter")),
|
|
)
|
|
|
|
GetRepoActionRunTool = mcp.NewTool(
|
|
GetRepoActionRunToolName,
|
|
mcp.WithDescription("Get a repository Actions run by ID"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")),
|
|
)
|
|
|
|
CancelRepoActionRunTool = mcp.NewTool(
|
|
CancelRepoActionRunToolName,
|
|
mcp.WithDescription("Cancel a repository Actions run"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")),
|
|
)
|
|
|
|
RerunRepoActionRunTool = mcp.NewTool(
|
|
RerunRepoActionRunToolName,
|
|
mcp.WithDescription("Rerun a repository Actions run"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")),
|
|
)
|
|
|
|
ListRepoActionJobsTool = mcp.NewTool(
|
|
ListRepoActionJobsToolName,
|
|
mcp.WithDescription("List repository Actions jobs"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)),
|
|
mcp.WithString("status", mcp.Description("optional status filter")),
|
|
)
|
|
|
|
ListRepoActionRunJobsTool = mcp.NewTool(
|
|
ListRepoActionRunJobsToolName,
|
|
mcp.WithDescription("List Actions jobs for a specific run"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")),
|
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)),
|
|
)
|
|
)
|
|
|
|
func init() {
|
|
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionWorkflowsTool, Handler: ListRepoActionWorkflowsFn})
|
|
Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionWorkflowTool, Handler: GetRepoActionWorkflowFn})
|
|
Tool.RegisterWrite(server.ServerTool{Tool: DispatchRepoActionWorkflowTool, Handler: DispatchRepoActionWorkflowFn})
|
|
|
|
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionRunsTool, Handler: ListRepoActionRunsFn})
|
|
Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionRunTool, Handler: GetRepoActionRunFn})
|
|
Tool.RegisterWrite(server.ServerTool{Tool: CancelRepoActionRunTool, Handler: CancelRepoActionRunFn})
|
|
Tool.RegisterWrite(server.ServerTool{Tool: RerunRepoActionRunTool, Handler: RerunRepoActionRunFn})
|
|
|
|
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionJobsTool, Handler: ListRepoActionJobsFn})
|
|
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionRunJobsTool, Handler: ListRepoActionRunJobsFn})
|
|
}
|
|
|
|
func doJSONWithFallback(ctx context.Context, method string, paths []string, query url.Values, body any, respOut any) (string, int, error) {
|
|
var lastErr error
|
|
for _, p := range paths {
|
|
status, err := gitea.DoJSON(ctx, method, p, query, body, respOut)
|
|
if err == nil {
|
|
return p, status, nil
|
|
}
|
|
lastErr = err
|
|
var httpErr *gitea.HTTPError
|
|
if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) {
|
|
continue
|
|
}
|
|
return p, status, err
|
|
}
|
|
return "", 0, lastErr
|
|
}
|
|
|
|
func ListRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called ListRepoActionWorkflowsFn")
|
|
owner, ok := req.GetArguments()["owner"].(string)
|
|
if !ok || owner == "" {
|
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
|
}
|
|
repo, ok := req.GetArguments()["repo"].(string)
|
|
if !ok || repo == "" {
|
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
|
}
|
|
page, _ := req.GetArguments()["page"].(float64)
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
|
if pageSize <= 0 {
|
|
pageSize = 50
|
|
}
|
|
query := url.Values{}
|
|
query.Set("page", fmt.Sprintf("%d", int(page)))
|
|
query.Set("limit", fmt.Sprintf("%d", int(pageSize)))
|
|
|
|
var result any
|
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
|
[]string{
|
|
fmt.Sprintf("repos/%s/%s/actions/workflows", url.PathEscape(owner), url.PathEscape(repo)),
|
|
},
|
|
query, nil, &result,
|
|
)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("list action workflows err: %v", err))
|
|
}
|
|
return to.TextResult(result)
|
|
}
|
|
|
|
func GetRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called GetRepoActionWorkflowFn")
|
|
owner, ok := req.GetArguments()["owner"].(string)
|
|
if !ok || owner == "" {
|
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
|
}
|
|
repo, ok := req.GetArguments()["repo"].(string)
|
|
if !ok || repo == "" {
|
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
|
}
|
|
workflowID, ok := req.GetArguments()["workflow_id"].(float64)
|
|
if !ok || workflowID <= 0 {
|
|
return to.ErrorResult(fmt.Errorf("workflow_id is required"))
|
|
}
|
|
|
|
var result any
|
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
|
[]string{
|
|
fmt.Sprintf("repos/%s/%s/actions/workflows/%d", url.PathEscape(owner), url.PathEscape(repo), int64(workflowID)),
|
|
},
|
|
nil, nil, &result,
|
|
)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get action workflow err: %v", err))
|
|
}
|
|
return to.TextResult(result)
|
|
}
|
|
|
|
func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called DispatchRepoActionWorkflowFn")
|
|
owner, ok := req.GetArguments()["owner"].(string)
|
|
if !ok || owner == "" {
|
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
|
}
|
|
repo, ok := req.GetArguments()["repo"].(string)
|
|
if !ok || repo == "" {
|
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
|
}
|
|
workflowID, ok := req.GetArguments()["workflow_id"].(float64)
|
|
if !ok || workflowID <= 0 {
|
|
return to.ErrorResult(fmt.Errorf("workflow_id is required"))
|
|
}
|
|
ref, ok := req.GetArguments()["ref"].(string)
|
|
if !ok || ref == "" {
|
|
return to.ErrorResult(fmt.Errorf("ref is required"))
|
|
}
|
|
|
|
var inputs map[string]any
|
|
if raw, exists := req.GetArguments()["inputs"]; exists {
|
|
if m, ok := raw.(map[string]any); ok {
|
|
inputs = m
|
|
} else if m, ok := raw.(map[string]interface{}); ok {
|
|
inputs = make(map[string]any, len(m))
|
|
for k, v := range m {
|
|
inputs[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
body := map[string]any{
|
|
"ref": ref,
|
|
}
|
|
if inputs != nil {
|
|
body["inputs"] = inputs
|
|
}
|
|
|
|
_, _, err := doJSONWithFallback(ctx, "POST",
|
|
[]string{
|
|
fmt.Sprintf("repos/%s/%s/actions/workflows/%d/dispatches", url.PathEscape(owner), url.PathEscape(repo), int64(workflowID)),
|
|
fmt.Sprintf("repos/%s/%s/actions/workflows/%d/dispatch", url.PathEscape(owner), url.PathEscape(repo), int64(workflowID)),
|
|
},
|
|
nil, body, nil,
|
|
)
|
|
if err != nil {
|
|
var httpErr *gitea.HTTPError
|
|
if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) {
|
|
return to.ErrorResult(fmt.Errorf("workflow dispatch not supported on this Gitea version (endpoint returned %d). Check https://docs.gitea.com/api/1.24/ for available Actions endpoints", httpErr.StatusCode))
|
|
}
|
|
return to.ErrorResult(fmt.Errorf("dispatch action workflow err: %v", err))
|
|
}
|
|
return to.TextResult(map[string]any{"message": "workflow dispatched"})
|
|
}
|
|
|
|
func ListRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called ListRepoActionRunsFn")
|
|
owner, ok := req.GetArguments()["owner"].(string)
|
|
if !ok || owner == "" {
|
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
|
}
|
|
repo, ok := req.GetArguments()["repo"].(string)
|
|
if !ok || repo == "" {
|
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
|
}
|
|
page, _ := req.GetArguments()["page"].(float64)
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
|
if pageSize <= 0 {
|
|
pageSize = 50
|
|
}
|
|
statusFilter, _ := req.GetArguments()["status"].(string)
|
|
|
|
query := url.Values{}
|
|
query.Set("page", fmt.Sprintf("%d", int(page)))
|
|
query.Set("limit", fmt.Sprintf("%d", int(pageSize)))
|
|
if statusFilter != "" {
|
|
query.Set("status", statusFilter)
|
|
}
|
|
|
|
var result any
|
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
|
[]string{
|
|
fmt.Sprintf("repos/%s/%s/actions/runs", url.PathEscape(owner), url.PathEscape(repo)),
|
|
},
|
|
query, nil, &result,
|
|
)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("list action runs err: %v", err))
|
|
}
|
|
return to.TextResult(result)
|
|
}
|
|
|
|
func GetRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called GetRepoActionRunFn")
|
|
owner, ok := req.GetArguments()["owner"].(string)
|
|
if !ok || owner == "" {
|
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
|
}
|
|
repo, ok := req.GetArguments()["repo"].(string)
|
|
if !ok || repo == "" {
|
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
|
}
|
|
runID, ok := req.GetArguments()["run_id"].(float64)
|
|
if !ok || runID <= 0 {
|
|
return to.ErrorResult(fmt.Errorf("run_id is required"))
|
|
}
|
|
|
|
var result any
|
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
|
[]string{
|
|
fmt.Sprintf("repos/%s/%s/actions/runs/%d", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
|
|
},
|
|
nil, nil, &result,
|
|
)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get action run err: %v", err))
|
|
}
|
|
return to.TextResult(result)
|
|
}
|
|
|
|
func CancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called CancelRepoActionRunFn")
|
|
owner, ok := req.GetArguments()["owner"].(string)
|
|
if !ok || owner == "" {
|
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
|
}
|
|
repo, ok := req.GetArguments()["repo"].(string)
|
|
if !ok || repo == "" {
|
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
|
}
|
|
runID, ok := req.GetArguments()["run_id"].(float64)
|
|
if !ok || runID <= 0 {
|
|
return to.ErrorResult(fmt.Errorf("run_id is required"))
|
|
}
|
|
|
|
_, _, err := doJSONWithFallback(ctx, "POST",
|
|
[]string{
|
|
fmt.Sprintf("repos/%s/%s/actions/runs/%d/cancel", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
|
|
},
|
|
nil, nil, nil,
|
|
)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("cancel action run err: %v", err))
|
|
}
|
|
return to.TextResult(map[string]any{"message": "run cancellation requested"})
|
|
}
|
|
|
|
func RerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called RerunRepoActionRunFn")
|
|
owner, ok := req.GetArguments()["owner"].(string)
|
|
if !ok || owner == "" {
|
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
|
}
|
|
repo, ok := req.GetArguments()["repo"].(string)
|
|
if !ok || repo == "" {
|
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
|
}
|
|
runID, ok := req.GetArguments()["run_id"].(float64)
|
|
if !ok || runID <= 0 {
|
|
return to.ErrorResult(fmt.Errorf("run_id is required"))
|
|
}
|
|
|
|
_, _, err := doJSONWithFallback(ctx, "POST",
|
|
[]string{
|
|
fmt.Sprintf("repos/%s/%s/actions/runs/%d/rerun", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
|
|
fmt.Sprintf("repos/%s/%s/actions/runs/%d/rerun-failed-jobs", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
|
|
},
|
|
nil, nil, nil,
|
|
)
|
|
if err != nil {
|
|
var httpErr *gitea.HTTPError
|
|
if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) {
|
|
return to.ErrorResult(fmt.Errorf("workflow rerun not supported on this Gitea version (endpoint returned %d). Check https://docs.gitea.com/api/1.24/ for available Actions endpoints", httpErr.StatusCode))
|
|
}
|
|
return to.ErrorResult(fmt.Errorf("rerun action run err: %v", err))
|
|
}
|
|
return to.TextResult(map[string]any{"message": "run rerun requested"})
|
|
}
|
|
|
|
func ListRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called ListRepoActionJobsFn")
|
|
owner, ok := req.GetArguments()["owner"].(string)
|
|
if !ok || owner == "" {
|
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
|
}
|
|
repo, ok := req.GetArguments()["repo"].(string)
|
|
if !ok || repo == "" {
|
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
|
}
|
|
page, _ := req.GetArguments()["page"].(float64)
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
|
if pageSize <= 0 {
|
|
pageSize = 50
|
|
}
|
|
statusFilter, _ := req.GetArguments()["status"].(string)
|
|
|
|
query := url.Values{}
|
|
query.Set("page", fmt.Sprintf("%d", int(page)))
|
|
query.Set("limit", fmt.Sprintf("%d", int(pageSize)))
|
|
if statusFilter != "" {
|
|
query.Set("status", statusFilter)
|
|
}
|
|
|
|
var result any
|
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
|
[]string{
|
|
fmt.Sprintf("repos/%s/%s/actions/jobs", url.PathEscape(owner), url.PathEscape(repo)),
|
|
},
|
|
query, nil, &result,
|
|
)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("list action jobs err: %v", err))
|
|
}
|
|
return to.TextResult(result)
|
|
}
|
|
|
|
func ListRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called ListRepoActionRunJobsFn")
|
|
owner, ok := req.GetArguments()["owner"].(string)
|
|
if !ok || owner == "" {
|
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
|
}
|
|
repo, ok := req.GetArguments()["repo"].(string)
|
|
if !ok || repo == "" {
|
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
|
}
|
|
runID, ok := req.GetArguments()["run_id"].(float64)
|
|
if !ok || runID <= 0 {
|
|
return to.ErrorResult(fmt.Errorf("run_id is required"))
|
|
}
|
|
page, _ := req.GetArguments()["page"].(float64)
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
|
if pageSize <= 0 {
|
|
pageSize = 50
|
|
}
|
|
|
|
query := url.Values{}
|
|
query.Set("page", fmt.Sprintf("%d", int(page)))
|
|
query.Set("limit", fmt.Sprintf("%d", int(pageSize)))
|
|
|
|
var result any
|
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
|
[]string{
|
|
fmt.Sprintf("repos/%s/%s/actions/runs/%d/jobs", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
|
|
},
|
|
query, nil, &result,
|
|
)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("list action run jobs err: %v", err))
|
|
}
|
|
return to.TextResult(result)
|
|
}
|