385 lines
10 KiB
Go
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
|
|
}
|