feat(api): add MCP protocol endpoint for AI tool integration
Adds /api/v2/mcp endpoint implementing the Model Context Protocol (MCP) for AI tool integration. Available tools: - list_runners: List all runners with status and capabilities - get_runner: Get detailed runner information - list_workflow_runs: List workflow runs for a repository - get_workflow_run: Get workflow run details with all jobs - get_job_logs: Get logs from a specific job - list_releases: List releases for a repository - get_release: Get release details with all assets This enables AI assistants to directly query Gitea Actions state without web scraping. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -84,8 +84,12 @@ func Routes() *web.Router {
|
||||
m.Get("/live", LivenessCheck)
|
||||
m.Get("/ready", ReadinessCheck)
|
||||
m.Get("/component/{component}", ComponentHealthCheck)
|
||||
|
||||
})
|
||||
|
||||
// MCP Protocol endpoint for AI tool integration
|
||||
m.Post("/mcp", MCPHandler)
|
||||
|
||||
// Operation progress endpoints (SSE)
|
||||
m.Group("/operations", func() {
|
||||
m.Get("/{id}/progress", OperationProgress)
|
||||
|
||||
732
routers/api/v2/mcp.go
Normal file
732
routers/api/v2/mcp.go
Normal file
@@ -0,0 +1,732 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// MCP Protocol Types (JSON-RPC 2.0)
|
||||
|
||||
type MCPRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID interface{} `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type MCPResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID interface{} `json:"id"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
Error *MCPError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type MCPError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// MCP Tool definitions
|
||||
type MCPTool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputSchema map[string]interface{} `json:"inputSchema"`
|
||||
}
|
||||
|
||||
type MCPToolsListResult struct {
|
||||
Tools []MCPTool `json:"tools"`
|
||||
}
|
||||
|
||||
type MCPToolCallParams struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
}
|
||||
|
||||
type MCPToolCallResult struct {
|
||||
Content []MCPContent `json:"content"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
}
|
||||
|
||||
type MCPContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type MCPInitializeParams struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
Capabilities map[string]interface{} `json:"capabilities"`
|
||||
ClientInfo map[string]string `json:"clientInfo"`
|
||||
}
|
||||
|
||||
type MCPInitializeResult struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
Capabilities map[string]interface{} `json:"capabilities"`
|
||||
ServerInfo map[string]string `json:"serverInfo"`
|
||||
}
|
||||
|
||||
// Available MCP tools
|
||||
var mcpTools = []MCPTool{
|
||||
{
|
||||
Name: "list_runners",
|
||||
Description: "List all runners with their status, capabilities, and current workload",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"owner": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository owner (optional, lists global runners if omitted)",
|
||||
},
|
||||
"repo": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository name (optional)",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"type": "string",
|
||||
"enum": []string{"online", "offline", "all"},
|
||||
"description": "Filter by runner status",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_runner",
|
||||
Description: "Get detailed information about a specific runner including capabilities, disk space, and bandwidth",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"runner_id": map[string]interface{}{
|
||||
"type": "integer",
|
||||
"description": "The runner ID",
|
||||
},
|
||||
},
|
||||
"required": []string{"runner_id"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list_workflow_runs",
|
||||
Description: "List workflow runs for a repository with status and timing information",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"owner": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"type": "string",
|
||||
"enum": []string{"pending", "running", "success", "failure", "cancelled", "all"},
|
||||
"description": "Filter by run status",
|
||||
},
|
||||
"limit": map[string]interface{}{
|
||||
"type": "integer",
|
||||
"description": "Maximum number of runs to return (default 20)",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_workflow_run",
|
||||
Description: "Get detailed information about a specific workflow run including all jobs and their status",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"owner": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"run_id": map[string]interface{}{
|
||||
"type": "integer",
|
||||
"description": "The workflow run ID",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo", "run_id"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_job_logs",
|
||||
Description: "Get logs from a specific job in a workflow run",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"owner": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"job_id": map[string]interface{}{
|
||||
"type": "integer",
|
||||
"description": "The job ID",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo", "job_id"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list_releases",
|
||||
Description: "List releases for a repository",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"owner": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"limit": map[string]interface{}{
|
||||
"type": "integer",
|
||||
"description": "Maximum number of releases to return (default 10)",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_release",
|
||||
Description: "Get details of a specific release including all assets",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"owner": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"tag": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Release tag (e.g., v1.0.0)",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo", "tag"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// MCPHandler handles MCP protocol requests
|
||||
// @Summary MCP Protocol Endpoint
|
||||
// @Description Handles Model Context Protocol requests for AI tool integration
|
||||
// @Tags mcp
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} MCPResponse
|
||||
// @Router /mcp [post]
|
||||
func MCPHandler(ctx *context.APIContext) {
|
||||
body, err := io.ReadAll(ctx.Req.Body)
|
||||
if err != nil {
|
||||
sendMCPError(ctx, nil, -32700, "Parse error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req MCPRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
sendMCPError(ctx, nil, -32700, "Parse error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if req.JSONRPC != "2.0" {
|
||||
sendMCPError(ctx, req.ID, -32600, "Invalid Request", "jsonrpc must be 2.0")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("MCP request: method=%s id=%v", req.Method, req.ID)
|
||||
|
||||
switch req.Method {
|
||||
case "initialize":
|
||||
handleInitialize(ctx, &req)
|
||||
case "tools/list":
|
||||
handleToolsList(ctx, &req)
|
||||
case "tools/call":
|
||||
handleToolsCall(ctx, &req)
|
||||
case "ping":
|
||||
sendMCPResult(ctx, req.ID, map[string]string{})
|
||||
default:
|
||||
sendMCPError(ctx, req.ID, -32601, "Method not found", fmt.Sprintf("Unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleInitialize(ctx *context.APIContext, req *MCPRequest) {
|
||||
result := MCPInitializeResult{
|
||||
ProtocolVersion: "2024-11-05",
|
||||
Capabilities: map[string]interface{}{
|
||||
"tools": map[string]interface{}{},
|
||||
},
|
||||
ServerInfo: map[string]string{
|
||||
"name": "gitea-actions",
|
||||
"version": setting.AppVer,
|
||||
},
|
||||
}
|
||||
sendMCPResult(ctx, req.ID, result)
|
||||
}
|
||||
|
||||
func handleToolsList(ctx *context.APIContext, req *MCPRequest) {
|
||||
result := MCPToolsListResult{Tools: mcpTools}
|
||||
sendMCPResult(ctx, req.ID, result)
|
||||
}
|
||||
|
||||
func handleToolsCall(ctx *context.APIContext, req *MCPRequest) {
|
||||
var params MCPToolCallParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
sendMCPError(ctx, req.ID, -32602, "Invalid params", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var result interface{}
|
||||
var err error
|
||||
|
||||
switch params.Name {
|
||||
case "list_runners":
|
||||
result, err = toolListRunners(ctx, params.Arguments)
|
||||
case "get_runner":
|
||||
result, err = toolGetRunner(ctx, params.Arguments)
|
||||
case "list_workflow_runs":
|
||||
result, err = toolListWorkflowRuns(ctx, params.Arguments)
|
||||
case "get_workflow_run":
|
||||
result, err = toolGetWorkflowRun(ctx, params.Arguments)
|
||||
case "get_job_logs":
|
||||
result, err = toolGetJobLogs(ctx, params.Arguments)
|
||||
case "list_releases":
|
||||
result, err = toolListReleases(ctx, params.Arguments)
|
||||
case "get_release":
|
||||
result, err = toolGetRelease(ctx, params.Arguments)
|
||||
default:
|
||||
sendMCPError(ctx, req.ID, -32602, "Unknown tool", params.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
sendMCPToolResult(ctx, req.ID, err.Error(), true)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert result to JSON text
|
||||
jsonBytes, _ := json.MarshalIndent(result, "", " ")
|
||||
sendMCPToolResult(ctx, req.ID, string(jsonBytes), false)
|
||||
}
|
||||
|
||||
func sendMCPResult(ctx *context.APIContext, id interface{}, result interface{}) {
|
||||
ctx.JSON(http.StatusOK, MCPResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Result: result,
|
||||
})
|
||||
}
|
||||
|
||||
func sendMCPError(ctx *context.APIContext, id interface{}, code int, message, data string) {
|
||||
ctx.JSON(http.StatusOK, MCPResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Error: &MCPError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func sendMCPToolResult(ctx *context.APIContext, id interface{}, text string, isError bool) {
|
||||
ctx.JSON(http.StatusOK, MCPResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Result: MCPToolCallResult{
|
||||
Content: []MCPContent{{Type: "text", Text: text}},
|
||||
IsError: isError,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Tool implementations
|
||||
|
||||
func toolListRunners(ctx *context.APIContext, args map[string]interface{}) (interface{}, error) {
|
||||
var runners actions_model.RunnerList
|
||||
var err error
|
||||
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
|
||||
if owner != "" && repo != "" {
|
||||
// Get repo-specific runners
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
runners, err = actions_model.GetRunnersOfRepo(ctx, repository.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Get all runners (admin)
|
||||
opts := actions_model.FindRunnerOptions{}
|
||||
opts.PageSize = 100
|
||||
runners, err = db.Find[actions_model.ActionRunner](ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
statusFilter, _ := args["status"].(string)
|
||||
|
||||
result := make([]map[string]interface{}, 0, len(runners))
|
||||
for _, runner := range runners {
|
||||
isOnline := runner.IsOnline()
|
||||
|
||||
if statusFilter == "online" && !isOnline {
|
||||
continue
|
||||
}
|
||||
if statusFilter == "offline" && isOnline {
|
||||
continue
|
||||
}
|
||||
|
||||
r := map[string]interface{}{
|
||||
"id": runner.ID,
|
||||
"name": runner.Name,
|
||||
"is_online": isOnline,
|
||||
"status": runner.Status().String(),
|
||||
"version": runner.Version,
|
||||
"labels": runner.AgentLabels,
|
||||
"last_online": runner.LastOnline.AsTime().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// Parse capabilities if available
|
||||
if runner.CapabilitiesJSON != "" {
|
||||
var caps api.RunnerCapability
|
||||
if json.Unmarshal([]byte(runner.CapabilitiesJSON), &caps) == nil {
|
||||
r["capabilities"] = caps
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, r)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"runners": result,
|
||||
"count": len(result),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetRunner(ctx *context.APIContext, args map[string]interface{}) (interface{}, error) {
|
||||
runnerIDFloat, ok := args["runner_id"].(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("runner_id is required")
|
||||
}
|
||||
runnerID := int64(runnerIDFloat)
|
||||
|
||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("runner not found: %d", runnerID)
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"id": runner.ID,
|
||||
"name": runner.Name,
|
||||
"is_online": runner.IsOnline(),
|
||||
"status": runner.Status().String(),
|
||||
"version": runner.Version,
|
||||
"labels": runner.AgentLabels,
|
||||
"last_online": runner.LastOnline.AsTime().Format(time.RFC3339),
|
||||
"repo_id": runner.RepoID,
|
||||
"owner_id": runner.OwnerID,
|
||||
}
|
||||
|
||||
if runner.CapabilitiesJSON != "" {
|
||||
var caps api.RunnerCapability
|
||||
if json.Unmarshal([]byte(runner.CapabilitiesJSON), &caps) == nil {
|
||||
result["capabilities"] = caps
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func toolListWorkflowRuns(ctx *context.APIContext, args map[string]interface{}) (interface{}, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
|
||||
if owner == "" || repo == "" {
|
||||
return nil, fmt.Errorf("owner and repo are required")
|
||||
}
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
limit := 20
|
||||
if l, ok := args["limit"].(float64); ok {
|
||||
limit = int(l)
|
||||
}
|
||||
|
||||
opts := actions_model.FindRunOptions{
|
||||
RepoID: repository.ID,
|
||||
}
|
||||
opts.PageSize = limit
|
||||
|
||||
runs, err := db.Find[actions_model.ActionRun](ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statusFilter, _ := args["status"].(string)
|
||||
|
||||
result := make([]map[string]interface{}, 0, len(runs))
|
||||
for _, run := range runs {
|
||||
status := run.Status.String()
|
||||
|
||||
if statusFilter != "" && statusFilter != "all" && !strings.EqualFold(status, statusFilter) {
|
||||
continue
|
||||
}
|
||||
|
||||
r := map[string]interface{}{
|
||||
"id": run.ID,
|
||||
"title": run.Title,
|
||||
"status": status,
|
||||
"event": string(run.Event),
|
||||
"workflow_id": run.WorkflowID,
|
||||
"ref": run.Ref,
|
||||
"commit_sha": run.CommitSHA,
|
||||
"started": run.Started.AsTime().Format(time.RFC3339),
|
||||
"stopped": run.Stopped.AsTime().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
result = append(result, r)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"runs": result,
|
||||
"count": len(result),
|
||||
"repo": fmt.Sprintf("%s/%s", owner, repo),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetWorkflowRun(ctx *context.APIContext, args map[string]interface{}) (interface{}, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
runIDFloat, ok := args["run_id"].(float64)
|
||||
|
||||
if owner == "" || repo == "" || !ok {
|
||||
return nil, fmt.Errorf("owner, repo, and run_id are required")
|
||||
}
|
||||
runID := int64(runIDFloat)
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, repository.ID, runID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("run not found: %d", runID)
|
||||
}
|
||||
|
||||
// Get jobs for this run
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, runID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jobResults := make([]map[string]interface{}, 0, len(jobs))
|
||||
for _, job := range jobs {
|
||||
j := map[string]interface{}{
|
||||
"id": job.ID,
|
||||
"name": job.Name,
|
||||
"status": job.Status.String(),
|
||||
"started": job.Started.AsTime().Format(time.RFC3339),
|
||||
"stopped": job.Stopped.AsTime().Format(time.RFC3339),
|
||||
"task_id": job.TaskID,
|
||||
}
|
||||
jobResults = append(jobResults, j)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"id": run.ID,
|
||||
"title": run.Title,
|
||||
"status": run.Status.String(),
|
||||
"event": string(run.Event),
|
||||
"workflow_id": run.WorkflowID,
|
||||
"ref": run.Ref,
|
||||
"commit_sha": run.CommitSHA,
|
||||
"started": run.Started.AsTime().Format(time.RFC3339),
|
||||
"stopped": run.Stopped.AsTime().Format(time.RFC3339),
|
||||
"jobs": jobResults,
|
||||
"job_count": len(jobResults),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetJobLogs(ctx *context.APIContext, args map[string]interface{}) (interface{}, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
jobIDFloat, ok := args["job_id"].(float64)
|
||||
|
||||
if owner == "" || repo == "" || !ok {
|
||||
return nil, fmt.Errorf("owner, repo, and job_id are required")
|
||||
}
|
||||
jobID := int64(jobIDFloat)
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
job, err := actions_model.GetRunJobByID(ctx, jobID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("job not found: %d", jobID)
|
||||
}
|
||||
|
||||
// Verify job belongs to this repo
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, repository.ID, job.RunID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("job not found in repository")
|
||||
}
|
||||
_ = run // used for validation
|
||||
|
||||
// Get log content - simplified for now, actual implementation needs log storage access
|
||||
return map[string]interface{}{
|
||||
"job_id": jobID,
|
||||
"job_name": job.Name,
|
||||
"status": job.Status.String(),
|
||||
"message": "Log retrieval requires task log storage access - see actions log storage",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolListReleases(ctx *context.APIContext, args map[string]interface{}) (interface{}, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
|
||||
if owner == "" || repo == "" {
|
||||
return nil, fmt.Errorf("owner and repo are required")
|
||||
}
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
limit := 10
|
||||
if l, ok := args["limit"].(float64); ok {
|
||||
limit = int(l)
|
||||
}
|
||||
|
||||
opts := repo_model.FindReleasesOptions{
|
||||
RepoID: repository.ID,
|
||||
}
|
||||
opts.PageSize = limit
|
||||
|
||||
releases, err := db.Find[repo_model.Release](ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]map[string]interface{}, 0, len(releases))
|
||||
for _, release := range releases {
|
||||
r := map[string]interface{}{
|
||||
"id": release.ID,
|
||||
"tag_name": release.TagName,
|
||||
"title": release.Title,
|
||||
"is_draft": release.IsDraft,
|
||||
"is_prerelease": release.IsPrerelease,
|
||||
"created_at": release.CreatedUnix.AsTime().Format(time.RFC3339),
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"releases": result,
|
||||
"count": len(result),
|
||||
"repo": fmt.Sprintf("%s/%s", owner, repo),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetRelease(ctx *context.APIContext, args map[string]interface{}) (interface{}, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
tag, _ := args["tag"].(string)
|
||||
|
||||
if owner == "" || repo == "" || tag == "" {
|
||||
return nil, fmt.Errorf("owner, repo, and tag are required")
|
||||
}
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
release, err := repo_model.GetRelease(ctx, repository.ID, tag)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("release not found: %s", tag)
|
||||
}
|
||||
|
||||
// Load attachments
|
||||
if err := release.LoadAttributes(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assets := make([]map[string]interface{}, 0, len(release.Attachments))
|
||||
for _, att := range release.Attachments {
|
||||
assets = append(assets, map[string]interface{}{
|
||||
"id": att.ID,
|
||||
"name": att.Name,
|
||||
"size": att.Size,
|
||||
"download_count": att.DownloadCount,
|
||||
"download_url": fmt.Sprintf("%s/%s/%s/releases/download/%s/%s",
|
||||
setting.AppURL, owner, repo, tag, att.Name),
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"id": release.ID,
|
||||
"tag_name": release.TagName,
|
||||
"title": release.Title,
|
||||
"body": release.Note,
|
||||
"is_draft": release.IsDraft,
|
||||
"is_prerelease": release.IsPrerelease,
|
||||
"created_at": release.CreatedUnix.AsTime().Format(time.RFC3339),
|
||||
"assets": assets,
|
||||
"asset_count": len(assets),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper to parse int from interface - unused but kept for future use
|
||||
func getIntArg(args map[string]interface{}, key string) (int64, bool) {
|
||||
if v, ok := args[key].(float64); ok {
|
||||
return int64(v), true
|
||||
}
|
||||
if v, ok := args[key].(string); ok {
|
||||
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
Reference in New Issue
Block a user