You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

view.go 7.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package actions
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "net/http"
  9. "time"
  10. actions_model "code.gitea.io/gitea/models/actions"
  11. "code.gitea.io/gitea/models/db"
  12. "code.gitea.io/gitea/models/unit"
  13. "code.gitea.io/gitea/modules/actions"
  14. context_module "code.gitea.io/gitea/modules/context"
  15. "code.gitea.io/gitea/modules/timeutil"
  16. "code.gitea.io/gitea/modules/util"
  17. "code.gitea.io/gitea/modules/web"
  18. actions_service "code.gitea.io/gitea/services/actions"
  19. "xorm.io/builder"
  20. )
  21. func View(ctx *context_module.Context) {
  22. ctx.Data["PageIsActions"] = true
  23. runIndex := ctx.ParamsInt64("run")
  24. jobIndex := ctx.ParamsInt64("job")
  25. ctx.Data["RunIndex"] = runIndex
  26. ctx.Data["JobIndex"] = jobIndex
  27. ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
  28. if getRunJobs(ctx, runIndex, jobIndex); ctx.Written() {
  29. return
  30. }
  31. ctx.HTML(http.StatusOK, tplViewActions)
  32. }
  33. type ViewRequest struct {
  34. LogCursors []struct {
  35. Step int `json:"step"`
  36. Cursor int64 `json:"cursor"`
  37. Expanded bool `json:"expanded"`
  38. } `json:"logCursors"`
  39. }
  40. type ViewResponse struct {
  41. State struct {
  42. Run struct {
  43. HTMLURL string `json:"htmlurl"`
  44. Title string `json:"title"`
  45. CanCancel bool `json:"canCancel"`
  46. Done bool `json:"done"`
  47. Jobs []*ViewJob `json:"jobs"`
  48. } `json:"run"`
  49. CurrentJob struct {
  50. Title string `json:"title"`
  51. Detail string `json:"detail"`
  52. Steps []*ViewJobStep `json:"steps"`
  53. } `json:"currentJob"`
  54. } `json:"state"`
  55. Logs struct {
  56. StepsLog []*ViewStepLog `json:"stepsLog"`
  57. } `json:"logs"`
  58. }
  59. type ViewJob struct {
  60. ID int64 `json:"id"`
  61. Name string `json:"name"`
  62. Status string `json:"status"`
  63. CanRerun bool `json:"canRerun"`
  64. }
  65. type ViewJobStep struct {
  66. Summary string `json:"summary"`
  67. Duration string `json:"duration"`
  68. Status string `json:"status"`
  69. }
  70. type ViewStepLog struct {
  71. Step int `json:"step"`
  72. Cursor int64 `json:"cursor"`
  73. Lines []*ViewStepLogLine `json:"lines"`
  74. }
  75. type ViewStepLogLine struct {
  76. Index int64 `json:"index"`
  77. Message string `json:"message"`
  78. Timestamp float64 `json:"timestamp"`
  79. }
  80. func ViewPost(ctx *context_module.Context) {
  81. req := web.GetForm(ctx).(*ViewRequest)
  82. runIndex := ctx.ParamsInt64("run")
  83. jobIndex := ctx.ParamsInt64("job")
  84. current, jobs := getRunJobs(ctx, runIndex, jobIndex)
  85. if ctx.Written() {
  86. return
  87. }
  88. run := current.Run
  89. resp := &ViewResponse{}
  90. resp.State.Run.Title = run.Title
  91. resp.State.Run.HTMLURL = run.HTMLURL()
  92. resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
  93. resp.State.Run.Done = run.Status.IsDone()
  94. resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
  95. for _, v := range jobs {
  96. resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{
  97. ID: v.ID,
  98. Name: v.Name,
  99. Status: v.Status.String(),
  100. CanRerun: v.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions),
  101. })
  102. }
  103. var task *actions_model.ActionTask
  104. if current.TaskID > 0 {
  105. var err error
  106. task, err = actions_model.GetTaskByID(ctx, current.TaskID)
  107. if err != nil {
  108. ctx.Error(http.StatusInternalServerError, err.Error())
  109. return
  110. }
  111. task.Job = current
  112. if err := task.LoadAttributes(ctx); err != nil {
  113. ctx.Error(http.StatusInternalServerError, err.Error())
  114. return
  115. }
  116. }
  117. resp.State.CurrentJob.Title = current.Name
  118. resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
  119. resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
  120. resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
  121. if task != nil {
  122. steps := actions.FullSteps(task)
  123. for _, v := range steps {
  124. resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &ViewJobStep{
  125. Summary: v.Name,
  126. Duration: v.Duration().String(),
  127. Status: v.Status.String(),
  128. })
  129. }
  130. for _, cursor := range req.LogCursors {
  131. if !cursor.Expanded {
  132. continue
  133. }
  134. step := steps[cursor.Step]
  135. logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json
  136. if c := cursor.Cursor; c < step.LogLength && c >= 0 {
  137. index := step.LogIndex + c
  138. length := step.LogLength - cursor.Cursor
  139. offset := task.LogIndexes[index]
  140. var err error
  141. logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
  142. if err != nil {
  143. ctx.Error(http.StatusInternalServerError, err.Error())
  144. return
  145. }
  146. for i, row := range logRows {
  147. logLines = append(logLines, &ViewStepLogLine{
  148. Index: cursor.Cursor + int64(i) + 1, // start at 1
  149. Message: row.Content,
  150. Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second),
  151. })
  152. }
  153. }
  154. resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{
  155. Step: cursor.Step,
  156. Cursor: cursor.Cursor + int64(len(logLines)),
  157. Lines: logLines,
  158. })
  159. }
  160. }
  161. ctx.JSON(http.StatusOK, resp)
  162. }
  163. func Rerun(ctx *context_module.Context) {
  164. runIndex := ctx.ParamsInt64("run")
  165. jobIndex := ctx.ParamsInt64("job")
  166. job, _ := getRunJobs(ctx, runIndex, jobIndex)
  167. if ctx.Written() {
  168. return
  169. }
  170. status := job.Status
  171. if !status.IsDone() {
  172. ctx.JSON(http.StatusOK, struct{}{})
  173. return
  174. }
  175. job.TaskID = 0
  176. job.Status = actions_model.StatusWaiting
  177. job.Started = 0
  178. job.Stopped = 0
  179. if err := db.WithTx(ctx, func(ctx context.Context) error {
  180. if _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped"); err != nil {
  181. return err
  182. }
  183. return actions_service.CreateCommitStatus(ctx, job)
  184. }); err != nil {
  185. ctx.Error(http.StatusInternalServerError, err.Error())
  186. return
  187. }
  188. ctx.JSON(http.StatusOK, struct{}{})
  189. }
  190. func Cancel(ctx *context_module.Context) {
  191. runIndex := ctx.ParamsInt64("run")
  192. _, jobs := getRunJobs(ctx, runIndex, -1)
  193. if ctx.Written() {
  194. return
  195. }
  196. if err := db.WithTx(ctx, func(ctx context.Context) error {
  197. for _, job := range jobs {
  198. status := job.Status
  199. if status.IsDone() {
  200. continue
  201. }
  202. if job.TaskID == 0 {
  203. job.Status = actions_model.StatusCancelled
  204. job.Stopped = timeutil.TimeStampNow()
  205. n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
  206. if err != nil {
  207. return err
  208. }
  209. if n == 0 {
  210. return fmt.Errorf("job has changed, try again")
  211. }
  212. continue
  213. }
  214. if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
  215. return err
  216. }
  217. if err := actions_service.CreateCommitStatus(ctx, job); err != nil {
  218. return err
  219. }
  220. }
  221. return nil
  222. }); err != nil {
  223. ctx.Error(http.StatusInternalServerError, err.Error())
  224. return
  225. }
  226. ctx.JSON(http.StatusOK, struct{}{})
  227. }
  228. // getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
  229. // Any error will be written to the ctx.
  230. // It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
  231. func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
  232. run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
  233. if err != nil {
  234. if errors.Is(err, util.ErrNotExist) {
  235. ctx.Error(http.StatusNotFound, err.Error())
  236. return nil, nil
  237. }
  238. ctx.Error(http.StatusInternalServerError, err.Error())
  239. return nil, nil
  240. }
  241. run.Repo = ctx.Repo.Repository
  242. jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
  243. if err != nil {
  244. ctx.Error(http.StatusInternalServerError, err.Error())
  245. return nil, nil
  246. }
  247. if len(jobs) == 0 {
  248. ctx.Error(http.StatusNotFound, err.Error())
  249. return nil, nil
  250. }
  251. for _, v := range jobs {
  252. v.Run = run
  253. }
  254. if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
  255. return jobs[jobIndex], jobs
  256. }
  257. return jobs[0], jobs
  258. }