// Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package actions import ( "context" "errors" "fmt" "net/http" "time" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/actions" context_module "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" actions_service "code.gitea.io/gitea/services/actions" "xorm.io/builder" ) func View(ctx *context_module.Context) { ctx.Data["PageIsActions"] = true runIndex := ctx.ParamsInt64("run") jobIndex := ctx.ParamsInt64("job") ctx.Data["RunIndex"] = runIndex ctx.Data["JobIndex"] = jobIndex ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions" if getRunJobs(ctx, runIndex, jobIndex); ctx.Written() { return } ctx.HTML(http.StatusOK, tplViewActions) } type ViewRequest struct { LogCursors []struct { Step int `json:"step"` Cursor int64 `json:"cursor"` Expanded bool `json:"expanded"` } `json:"logCursors"` } type ViewResponse struct { State struct { Run struct { Link string `json:"link"` Title string `json:"title"` CanCancel bool `json:"canCancel"` CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve Done bool `json:"done"` Jobs []*ViewJob `json:"jobs"` } `json:"run"` CurrentJob struct { Title string `json:"title"` Detail string `json:"detail"` Steps []*ViewJobStep `json:"steps"` } `json:"currentJob"` } `json:"state"` Logs struct { StepsLog []*ViewStepLog `json:"stepsLog"` } `json:"logs"` } type ViewJob struct { ID int64 `json:"id"` Name string `json:"name"` Status string `json:"status"` CanRerun bool `json:"canRerun"` } type ViewJobStep struct { Summary string `json:"summary"` Duration string `json:"duration"` Status string `json:"status"` } type ViewStepLog struct { Step int `json:"step"` Cursor int64 `json:"cursor"` Lines []*ViewStepLogLine `json:"lines"` } type ViewStepLogLine struct { Index int64 `json:"index"` Message string `json:"message"` Timestamp float64 `json:"timestamp"` } func ViewPost(ctx *context_module.Context) { req := web.GetForm(ctx).(*ViewRequest) runIndex := ctx.ParamsInt64("run") jobIndex := ctx.ParamsInt64("job") current, jobs := getRunJobs(ctx, runIndex, jobIndex) if ctx.Written() { return } run := current.Run resp := &ViewResponse{} resp.State.Run.Title = run.Title resp.State.Run.Link = run.Link() resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.Done = run.Status.IsDone() resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json for _, v := range jobs { resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{ ID: v.ID, Name: v.Name, Status: v.Status.String(), CanRerun: v.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions), }) } var task *actions_model.ActionTask if current.TaskID > 0 { var err error task, err = actions_model.GetTaskByID(ctx, current.TaskID) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return } task.Job = current if err := task.LoadAttributes(ctx); err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return } } resp.State.CurrentJob.Title = current.Name resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale) if run.NeedApproval { resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc") } resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json if task != nil { steps := actions.FullSteps(task) for _, v := range steps { resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &ViewJobStep{ Summary: v.Name, Duration: v.Duration().String(), Status: v.Status.String(), }) } for _, cursor := range req.LogCursors { if !cursor.Expanded { continue } step := steps[cursor.Step] logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json if c := cursor.Cursor; c < step.LogLength && c >= 0 { index := step.LogIndex + c length := step.LogLength - cursor.Cursor offset := task.LogIndexes[index] var err error logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return } for i, row := range logRows { logLines = append(logLines, &ViewStepLogLine{ Index: cursor.Cursor + int64(i) + 1, // start at 1 Message: row.Content, Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second), }) } } resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{ Step: cursor.Step, Cursor: cursor.Cursor + int64(len(logLines)), Lines: logLines, }) } } ctx.JSON(http.StatusOK, resp) } func Rerun(ctx *context_module.Context) { runIndex := ctx.ParamsInt64("run") jobIndex := ctx.ParamsInt64("job") job, _ := getRunJobs(ctx, runIndex, jobIndex) if ctx.Written() { return } status := job.Status if !status.IsDone() { ctx.JSON(http.StatusOK, struct{}{}) return } job.TaskID = 0 job.Status = actions_model.StatusWaiting job.Started = 0 job.Stopped = 0 if err := db.WithTx(ctx, func(ctx context.Context) error { if _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped"); err != nil { return err } return actions_service.CreateCommitStatus(ctx, job) }); err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return } ctx.JSON(http.StatusOK, struct{}{}) } func Cancel(ctx *context_module.Context) { runIndex := ctx.ParamsInt64("run") _, jobs := getRunJobs(ctx, runIndex, -1) if ctx.Written() { return } if err := db.WithTx(ctx, func(ctx context.Context) error { for _, job := range jobs { status := job.Status if status.IsDone() { continue } if job.TaskID == 0 { job.Status = actions_model.StatusCancelled job.Stopped = timeutil.TimeStampNow() n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") if err != nil { return err } if n == 0 { return fmt.Errorf("job has changed, try again") } continue } if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil { return err } if err := actions_service.CreateCommitStatus(ctx, job); err != nil { return err } } return nil }); err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return } ctx.JSON(http.StatusOK, struct{}{}) } func Approve(ctx *context_module.Context) { runIndex := ctx.ParamsInt64("run") current, jobs := getRunJobs(ctx, runIndex, -1) if ctx.Written() { return } run := current.Run doer := ctx.Doer if err := db.WithTx(ctx, func(ctx context.Context) error { run.NeedApproval = false run.ApprovedBy = doer.ID if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil { return err } for _, job := range jobs { if len(job.Needs) == 0 && job.Status.IsBlocked() { job.Status = actions_model.StatusWaiting _, err := actions_model.UpdateRunJob(ctx, job, nil, "status") if err != nil { return err } } } return nil }); err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return } ctx.JSON(http.StatusOK, struct{}{}) } // getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs. // Any error will be written to the ctx. // It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0. func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) { run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.Error(http.StatusNotFound, err.Error()) return nil, nil } ctx.Error(http.StatusInternalServerError, err.Error()) return nil, nil } run.Repo = ctx.Repo.Repository jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return nil, nil } if len(jobs) == 0 { ctx.Error(http.StatusNotFound, err.Error()) return nil, nil } for _, v := range jobs { v.Run = run } if jobIndex >= 0 && jobIndex < int64(len(jobs)) { return jobs[jobIndex], jobs } return jobs[0], jobs }