Files
devstar/routers/web/repo/actions/debug.go
2025-11-27 10:26:09 +08:00

385 lines
10 KiB
Go

// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"encoding/json"
"fmt"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/log"
actions_service "code.gitea.io/gitea/services/actions"
context_module "code.gitea.io/gitea/services/context"
)
// Debug shows the debug page
func Debug(ctx *context_module.Context) {
ctx.Data["PageIsActions"] = true
workflowID := ctx.FormString("workflow")
if workflowID == "" {
ctx.NotFound(nil)
return
}
// Get workflow content
content, err := actions_service.GetDebugSessionWorkflowContent(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID)
if err != nil {
ctx.ServerError("GetDebugSessionWorkflowContent", err)
return
}
// Create a new debug session
debugSession := &actions_model.DebugSession{
RepoID: ctx.Repo.Repository.ID,
CreatorID: ctx.Doer.ID,
WorkflowID: workflowID,
WorkflowContent: content,
}
if err := actions_model.CreateDebugSession(ctx, debugSession); err != nil {
ctx.ServerError("CreateDebugSession", err)
return
}
ctx.Data["DebugSessionID"] = debugSession.ID
ctx.Data["WorkflowID"] = workflowID
ctx.Data["WorkflowContent"] = content
ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
ctx.Data["DebugAPIURL"] = ctx.Repo.RepoLink + "/actions/debug-api"
ctx.HTML(http.StatusOK, "repo/actions/debug")
}
// DebugRunRequest represents the request to run a debug workflow
type DebugRunRequest struct {
WorkflowContent string `json:"workflow_content"`
Ref string `json:"ref"`
Event string `json:"event"`
Inputs map[string]string `json:"inputs"`
Env map[string]string `json:"env"`
}
// DebugRunResponse represents the response of running a debug workflow
type DebugRunResponse struct {
RunIndex int64 `json:"run_index"`
Success bool `json:"success"`
Message string `json:"message"`
}
// APIDebugRun handles running a debug workflow
func APIDebugRun(ctx *context_module.Context) {
log.Info("APIDebugRun called")
debugSessionID := ctx.PathParamInt64("debugSessionID")
log.Info("Debug session ID: %d", debugSessionID)
// Verify the debug session belongs to this repo
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
if err != nil {
log.Error("GetDebugSession error: %v", err)
ctx.JSON(http.StatusNotFound, map[string]string{
"error": "debug session not found",
})
return
}
if debugSession.RepoID != ctx.Repo.Repository.ID {
log.Error("Repo mismatch: debug session repo %d, current repo %d", debugSession.RepoID, ctx.Repo.Repository.ID)
ctx.JSON(http.StatusForbidden, map[string]string{
"error": "forbidden",
})
return
}
var req DebugRunRequest
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
log.Error("JSON decode error: %v", err)
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid request body: " + err.Error(),
})
return
}
log.Info("Received debug run request: content_len=%d, ref=%s, event=%s", len(req.WorkflowContent), req.Ref, req.Event)
if req.Inputs == nil {
req.Inputs = make(map[string]string)
}
if req.Env == nil {
req.Env = make(map[string]string)
}
if req.WorkflowContent == "" {
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "workflow content is required",
})
return
}
if req.Event == "" {
req.Event = "push"
}
// Run debug workflow
runIndex, err := actions_service.DebugWorkflow(ctx, ctx.Doer, ctx.Repo.Repository,
ctx.Repo.GitRepo, debugSessionID, req.WorkflowContent, req.Ref,
req.Event, req.Inputs, req.Env)
if err != nil {
log.Error("DebugWorkflow error: %v", err)
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": fmt.Sprintf("failed to run debug workflow: %v", err),
})
return
}
log.Info("Debug workflow started successfully: run_index=%d", runIndex)
ctx.JSON(http.StatusOK, DebugRunResponse{
RunIndex: runIndex,
Success: true,
Message: "Debug workflow started",
})
}
// APIDebugSession returns the debug session status
func APIDebugSession(ctx *context_module.Context) {
debugSessionID := ctx.PathParamInt64("debugSessionID")
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
if err != nil {
ctx.JSON(http.StatusNotFound, map[string]string{
"error": "debug session not found",
})
return
}
if debugSession.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(http.StatusForbidden, map[string]string{
"error": "forbidden",
})
return
}
response := map[string]interface{}{
"id": debugSession.ID,
"workflow_id": debugSession.WorkflowID,
"status": debugSession.Status,
"run_id": debugSession.RunID,
"workflow_content": debugSession.WorkflowContent,
"error_msg": debugSession.ErrorMsg,
"created_unix": debugSession.CreatedUnix,
"updated_unix": debugSession.UpdatedUnix,
}
ctx.JSON(http.StatusOK, response)
}
// APIDebugSessionUpdate updates the debug session workflow content
func APIDebugSessionUpdate(ctx *context_module.Context) {
debugSessionID := ctx.PathParamInt64("debugSessionID")
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
if err != nil {
ctx.JSON(http.StatusNotFound, map[string]string{
"error": "debug session not found",
})
return
}
if debugSession.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(http.StatusForbidden, map[string]string{
"error": "forbidden",
})
return
}
if debugSession.Status != actions_model.DebugSessionStatusDraft {
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "debug session is not in draft status",
})
return
}
content := ctx.FormString("workflow_content")
if content == "" {
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "workflow content is required",
})
return
}
debugSession.WorkflowContent = content
if err := actions_model.UpdateDebugSession(ctx, debugSession); err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to update debug session",
})
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"success": true,
"message": "debug session updated",
})
}
// APIDebugSessionDelete deletes a debug session
func APIDebugSessionDelete(ctx *context_module.Context) {
debugSessionID := ctx.PathParamInt64("debugSessionID")
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
if err != nil {
ctx.JSON(http.StatusNotFound, map[string]string{
"error": "debug session not found",
})
return
}
if debugSession.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(http.StatusForbidden, map[string]string{
"error": "forbidden",
})
return
}
if err := actions_model.DeleteDebugSession(ctx, debugSessionID); err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to delete debug session",
})
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"success": true,
"message": "debug session deleted",
})
}
// APIDebugLogs returns the logs for a debug workflow run
func APIDebugLogs(ctx *context_module.Context) {
debugSessionID := ctx.PathParamInt64("debugSessionID")
debugSession, err := actions_model.GetDebugSession(ctx, debugSessionID)
if err != nil {
ctx.JSON(http.StatusNotFound, map[string]string{
"error": "debug session not found",
})
return
}
if debugSession.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(http.StatusForbidden, map[string]string{
"error": "forbidden",
})
return
}
if debugSession.RunID == 0 {
ctx.JSON(http.StatusOK, map[string]interface{}{
"status": "not_started",
"logs": "",
})
return
}
// Get the run using GetRunByRepoAndID
run, err := actions_model.GetRunByRepoAndID(ctx, debugSession.RepoID, debugSession.RunID)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to get run",
})
return
}
// Get jobs for this run
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to get jobs",
})
return
}
if len(jobs) == 0 {
ctx.JSON(http.StatusOK, map[string]interface{}{
"status": run.Status.String(),
"logs": "No jobs found",
})
return
}
// Collect logs from all jobs
var allLogs string
for _, job := range jobs {
allLogs += fmt.Sprintf("=== Job: %s (Status: %s) ===\n", job.Name, job.Status.String())
if job.TaskID > 0 {
task, err := actions_model.GetTaskByID(ctx, job.TaskID)
if err != nil {
log.Error("Failed to get task %d: %v", job.TaskID, err)
continue
}
task.Job = job
if err := task.LoadAttributes(ctx); err != nil {
log.Error("Failed to load task attributes: %v", err)
continue
}
// Get logs for this task
jobLogs, err := getTaskLogs(ctx, task)
if err != nil {
log.Error("Failed to get task logs: %v", err)
allLogs += fmt.Sprintf("Error getting logs: %v\n", err)
} else {
allLogs += jobLogs
}
} else {
allLogs += "Task not started yet\n"
}
allLogs += "\n"
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"status": run.Status.String(),
"logs": allLogs,
"run_index": run.Index,
"run_link": run.Link(),
})
}
// getTaskLogs retrieves all logs for a task
func getTaskLogs(ctx *context_module.Context, task *actions_model.ActionTask) (string, error) {
if task.LogExpired {
return "Logs have expired\n", nil
}
steps := actions.FullSteps(task)
var allLogs string
for i, step := range steps {
allLogs += fmt.Sprintf("\n--- Step %d: %s (Status: %s) ---\n", i+1, step.Name, step.Status.String())
if step.LogLength == 0 {
allLogs += "(No output)\n"
continue
}
// Read logs for this step
offset := task.LogIndexes[step.LogIndex]
logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, step.LogLength)
if err != nil {
return "", fmt.Errorf("failed to read logs: %w", err)
}
for _, row := range logRows {
allLogs += row.Content + "\n"
}
}
return allLogs, nil
}