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 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package actions
  4. import (
  5. "archive/zip"
  6. "compress/gzip"
  7. "context"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "net/url"
  13. "strings"
  14. "time"
  15. actions_model "code.gitea.io/gitea/models/actions"
  16. "code.gitea.io/gitea/models/db"
  17. repo_model "code.gitea.io/gitea/models/repo"
  18. "code.gitea.io/gitea/models/unit"
  19. "code.gitea.io/gitea/modules/actions"
  20. "code.gitea.io/gitea/modules/base"
  21. context_module "code.gitea.io/gitea/modules/context"
  22. "code.gitea.io/gitea/modules/storage"
  23. "code.gitea.io/gitea/modules/timeutil"
  24. "code.gitea.io/gitea/modules/util"
  25. "code.gitea.io/gitea/modules/web"
  26. actions_service "code.gitea.io/gitea/services/actions"
  27. "xorm.io/builder"
  28. )
  29. func View(ctx *context_module.Context) {
  30. ctx.Data["PageIsActions"] = true
  31. runIndex := ctx.ParamsInt64("run")
  32. jobIndex := ctx.ParamsInt64("job")
  33. ctx.Data["RunIndex"] = runIndex
  34. ctx.Data["JobIndex"] = jobIndex
  35. ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
  36. if getRunJobs(ctx, runIndex, jobIndex); ctx.Written() {
  37. return
  38. }
  39. ctx.HTML(http.StatusOK, tplViewActions)
  40. }
  41. type ViewRequest struct {
  42. LogCursors []struct {
  43. Step int `json:"step"`
  44. Cursor int64 `json:"cursor"`
  45. Expanded bool `json:"expanded"`
  46. } `json:"logCursors"`
  47. }
  48. type ViewResponse struct {
  49. State struct {
  50. Run struct {
  51. Link string `json:"link"`
  52. Title string `json:"title"`
  53. Status string `json:"status"`
  54. CanCancel bool `json:"canCancel"`
  55. CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
  56. CanRerun bool `json:"canRerun"`
  57. Done bool `json:"done"`
  58. Jobs []*ViewJob `json:"jobs"`
  59. Commit ViewCommit `json:"commit"`
  60. } `json:"run"`
  61. CurrentJob struct {
  62. Title string `json:"title"`
  63. Detail string `json:"detail"`
  64. Steps []*ViewJobStep `json:"steps"`
  65. } `json:"currentJob"`
  66. } `json:"state"`
  67. Logs struct {
  68. StepsLog []*ViewStepLog `json:"stepsLog"`
  69. } `json:"logs"`
  70. }
  71. type ViewJob struct {
  72. ID int64 `json:"id"`
  73. Name string `json:"name"`
  74. Status string `json:"status"`
  75. CanRerun bool `json:"canRerun"`
  76. Duration string `json:"duration"`
  77. }
  78. type ViewCommit struct {
  79. LocaleCommit string `json:"localeCommit"`
  80. LocalePushedBy string `json:"localePushedBy"`
  81. ShortSha string `json:"shortSHA"`
  82. Link string `json:"link"`
  83. Pusher ViewUser `json:"pusher"`
  84. Branch ViewBranch `json:"branch"`
  85. }
  86. type ViewUser struct {
  87. DisplayName string `json:"displayName"`
  88. Link string `json:"link"`
  89. }
  90. type ViewBranch struct {
  91. Name string `json:"name"`
  92. Link string `json:"link"`
  93. }
  94. type ViewJobStep struct {
  95. Summary string `json:"summary"`
  96. Duration string `json:"duration"`
  97. Status string `json:"status"`
  98. }
  99. type ViewStepLog struct {
  100. Step int `json:"step"`
  101. Cursor int64 `json:"cursor"`
  102. Lines []*ViewStepLogLine `json:"lines"`
  103. Started int64 `json:"started"`
  104. }
  105. type ViewStepLogLine struct {
  106. Index int64 `json:"index"`
  107. Message string `json:"message"`
  108. Timestamp float64 `json:"timestamp"`
  109. }
  110. func ViewPost(ctx *context_module.Context) {
  111. req := web.GetForm(ctx).(*ViewRequest)
  112. runIndex := ctx.ParamsInt64("run")
  113. jobIndex := ctx.ParamsInt64("job")
  114. current, jobs := getRunJobs(ctx, runIndex, jobIndex)
  115. if ctx.Written() {
  116. return
  117. }
  118. run := current.Run
  119. if err := run.LoadAttributes(ctx); err != nil {
  120. ctx.Error(http.StatusInternalServerError, err.Error())
  121. return
  122. }
  123. resp := &ViewResponse{}
  124. resp.State.Run.Title = run.Title
  125. resp.State.Run.Link = run.Link()
  126. resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
  127. resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
  128. resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
  129. resp.State.Run.Done = run.Status.IsDone()
  130. resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
  131. resp.State.Run.Status = run.Status.String()
  132. for _, v := range jobs {
  133. resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{
  134. ID: v.ID,
  135. Name: v.Name,
  136. Status: v.Status.String(),
  137. CanRerun: v.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions),
  138. Duration: v.Duration().String(),
  139. })
  140. }
  141. pusher := ViewUser{
  142. DisplayName: run.TriggerUser.GetDisplayName(),
  143. Link: run.TriggerUser.HomeLink(),
  144. }
  145. branch := ViewBranch{
  146. Name: run.PrettyRef(),
  147. Link: run.RefLink(),
  148. }
  149. resp.State.Run.Commit = ViewCommit{
  150. LocaleCommit: ctx.Tr("actions.runs.commit"),
  151. LocalePushedBy: ctx.Tr("actions.runs.pushed_by"),
  152. ShortSha: base.ShortSha(run.CommitSHA),
  153. Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
  154. Pusher: pusher,
  155. Branch: branch,
  156. }
  157. var task *actions_model.ActionTask
  158. if current.TaskID > 0 {
  159. var err error
  160. task, err = actions_model.GetTaskByID(ctx, current.TaskID)
  161. if err != nil {
  162. ctx.Error(http.StatusInternalServerError, err.Error())
  163. return
  164. }
  165. task.Job = current
  166. if err := task.LoadAttributes(ctx); err != nil {
  167. ctx.Error(http.StatusInternalServerError, err.Error())
  168. return
  169. }
  170. }
  171. resp.State.CurrentJob.Title = current.Name
  172. resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
  173. if run.NeedApproval {
  174. resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc")
  175. }
  176. resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
  177. resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
  178. if task != nil {
  179. steps := actions.FullSteps(task)
  180. for _, v := range steps {
  181. resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &ViewJobStep{
  182. Summary: v.Name,
  183. Duration: v.Duration().String(),
  184. Status: v.Status.String(),
  185. })
  186. }
  187. for _, cursor := range req.LogCursors {
  188. if !cursor.Expanded {
  189. continue
  190. }
  191. step := steps[cursor.Step]
  192. logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json
  193. index := step.LogIndex + cursor.Cursor
  194. validCursor := cursor.Cursor >= 0 &&
  195. // !(cursor.Cursor < step.LogLength) when the frontend tries to fetch next line before it's ready.
  196. // So return the same cursor and empty lines to let the frontend retry.
  197. cursor.Cursor < step.LogLength &&
  198. // !(index < task.LogIndexes[index]) when task data is older than step data.
  199. // It can be fixed by making sure write/read tasks and steps in the same transaction,
  200. // but it's easier to just treat it as fetching the next line before it's ready.
  201. index < int64(len(task.LogIndexes))
  202. if validCursor {
  203. length := step.LogLength - cursor.Cursor
  204. offset := task.LogIndexes[index]
  205. var err error
  206. logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
  207. if err != nil {
  208. ctx.Error(http.StatusInternalServerError, err.Error())
  209. return
  210. }
  211. for i, row := range logRows {
  212. logLines = append(logLines, &ViewStepLogLine{
  213. Index: cursor.Cursor + int64(i) + 1, // start at 1
  214. Message: row.Content,
  215. Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second),
  216. })
  217. }
  218. }
  219. resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{
  220. Step: cursor.Step,
  221. Cursor: cursor.Cursor + int64(len(logLines)),
  222. Lines: logLines,
  223. Started: int64(step.Started),
  224. })
  225. }
  226. }
  227. ctx.JSON(http.StatusOK, resp)
  228. }
  229. // Rerun will rerun jobs in the given run
  230. // jobIndex = 0 means rerun all jobs
  231. func Rerun(ctx *context_module.Context) {
  232. runIndex := ctx.ParamsInt64("run")
  233. jobIndex := ctx.ParamsInt64("job")
  234. run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
  235. if err != nil {
  236. ctx.Error(http.StatusInternalServerError, err.Error())
  237. return
  238. }
  239. // can not rerun job when workflow is disabled
  240. cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
  241. cfg := cfgUnit.ActionsConfig()
  242. if cfg.IsWorkflowDisabled(run.WorkflowID) {
  243. ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled"))
  244. return
  245. }
  246. job, jobs := getRunJobs(ctx, runIndex, jobIndex)
  247. if ctx.Written() {
  248. return
  249. }
  250. if jobIndex != 0 {
  251. jobs = []*actions_model.ActionRunJob{job}
  252. }
  253. for _, j := range jobs {
  254. if err := rerunJob(ctx, j); err != nil {
  255. ctx.Error(http.StatusInternalServerError, err.Error())
  256. return
  257. }
  258. }
  259. ctx.JSON(http.StatusOK, struct{}{})
  260. }
  261. func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) error {
  262. status := job.Status
  263. if !status.IsDone() {
  264. return nil
  265. }
  266. job.TaskID = 0
  267. job.Status = actions_model.StatusWaiting
  268. job.Started = 0
  269. job.Stopped = 0
  270. if err := db.WithTx(ctx, func(ctx context.Context) error {
  271. _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
  272. return err
  273. }); err != nil {
  274. return err
  275. }
  276. actions_service.CreateCommitStatus(ctx, job)
  277. return nil
  278. }
  279. func Logs(ctx *context_module.Context) {
  280. runIndex := ctx.ParamsInt64("run")
  281. jobIndex := ctx.ParamsInt64("job")
  282. job, _ := getRunJobs(ctx, runIndex, jobIndex)
  283. if ctx.Written() {
  284. return
  285. }
  286. if job.TaskID == 0 {
  287. ctx.Error(http.StatusNotFound, "job is not started")
  288. return
  289. }
  290. err := job.LoadRun(ctx)
  291. if err != nil {
  292. ctx.Error(http.StatusInternalServerError, err.Error())
  293. return
  294. }
  295. task, err := actions_model.GetTaskByID(ctx, job.TaskID)
  296. if err != nil {
  297. ctx.Error(http.StatusInternalServerError, err.Error())
  298. return
  299. }
  300. if task.LogExpired {
  301. ctx.Error(http.StatusNotFound, "logs have been cleaned up")
  302. return
  303. }
  304. reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
  305. if err != nil {
  306. ctx.Error(http.StatusInternalServerError, err.Error())
  307. return
  308. }
  309. defer reader.Close()
  310. workflowName := job.Run.WorkflowID
  311. if p := strings.Index(workflowName, "."); p > 0 {
  312. workflowName = workflowName[0:p]
  313. }
  314. ctx.ServeContent(reader, &context_module.ServeHeaderOptions{
  315. Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID),
  316. ContentLength: &task.LogSize,
  317. ContentType: "text/plain",
  318. ContentTypeCharset: "utf-8",
  319. Disposition: "attachment",
  320. })
  321. }
  322. func Cancel(ctx *context_module.Context) {
  323. runIndex := ctx.ParamsInt64("run")
  324. _, jobs := getRunJobs(ctx, runIndex, -1)
  325. if ctx.Written() {
  326. return
  327. }
  328. if err := db.WithTx(ctx, func(ctx context.Context) error {
  329. for _, job := range jobs {
  330. status := job.Status
  331. if status.IsDone() {
  332. continue
  333. }
  334. if job.TaskID == 0 {
  335. job.Status = actions_model.StatusCancelled
  336. job.Stopped = timeutil.TimeStampNow()
  337. n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
  338. if err != nil {
  339. return err
  340. }
  341. if n == 0 {
  342. return fmt.Errorf("job has changed, try again")
  343. }
  344. continue
  345. }
  346. if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
  347. return err
  348. }
  349. }
  350. return nil
  351. }); err != nil {
  352. ctx.Error(http.StatusInternalServerError, err.Error())
  353. return
  354. }
  355. actions_service.CreateCommitStatus(ctx, jobs...)
  356. ctx.JSON(http.StatusOK, struct{}{})
  357. }
  358. func Approve(ctx *context_module.Context) {
  359. runIndex := ctx.ParamsInt64("run")
  360. current, jobs := getRunJobs(ctx, runIndex, -1)
  361. if ctx.Written() {
  362. return
  363. }
  364. run := current.Run
  365. doer := ctx.Doer
  366. if err := db.WithTx(ctx, func(ctx context.Context) error {
  367. run.NeedApproval = false
  368. run.ApprovedBy = doer.ID
  369. if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
  370. return err
  371. }
  372. for _, job := range jobs {
  373. if len(job.Needs) == 0 && job.Status.IsBlocked() {
  374. job.Status = actions_model.StatusWaiting
  375. _, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
  376. if err != nil {
  377. return err
  378. }
  379. }
  380. }
  381. return nil
  382. }); err != nil {
  383. ctx.Error(http.StatusInternalServerError, err.Error())
  384. return
  385. }
  386. actions_service.CreateCommitStatus(ctx, jobs...)
  387. ctx.JSON(http.StatusOK, struct{}{})
  388. }
  389. // getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
  390. // Any error will be written to the ctx.
  391. // It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
  392. func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
  393. run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
  394. if err != nil {
  395. if errors.Is(err, util.ErrNotExist) {
  396. ctx.Error(http.StatusNotFound, err.Error())
  397. return nil, nil
  398. }
  399. ctx.Error(http.StatusInternalServerError, err.Error())
  400. return nil, nil
  401. }
  402. run.Repo = ctx.Repo.Repository
  403. jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
  404. if err != nil {
  405. ctx.Error(http.StatusInternalServerError, err.Error())
  406. return nil, nil
  407. }
  408. if len(jobs) == 0 {
  409. ctx.Error(http.StatusNotFound, err.Error())
  410. return nil, nil
  411. }
  412. for _, v := range jobs {
  413. v.Run = run
  414. }
  415. if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
  416. return jobs[jobIndex], jobs
  417. }
  418. return jobs[0], jobs
  419. }
  420. type ArtifactsViewResponse struct {
  421. Artifacts []*ArtifactsViewItem `json:"artifacts"`
  422. }
  423. type ArtifactsViewItem struct {
  424. Name string `json:"name"`
  425. Size int64 `json:"size"`
  426. Status string `json:"status"`
  427. }
  428. func ArtifactsView(ctx *context_module.Context) {
  429. runIndex := ctx.ParamsInt64("run")
  430. run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
  431. if err != nil {
  432. if errors.Is(err, util.ErrNotExist) {
  433. ctx.Error(http.StatusNotFound, err.Error())
  434. return
  435. }
  436. ctx.Error(http.StatusInternalServerError, err.Error())
  437. return
  438. }
  439. artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, run.ID)
  440. if err != nil {
  441. ctx.Error(http.StatusInternalServerError, err.Error())
  442. return
  443. }
  444. artifactsResponse := ArtifactsViewResponse{
  445. Artifacts: make([]*ArtifactsViewItem, 0, len(artifacts)),
  446. }
  447. for _, art := range artifacts {
  448. status := "completed"
  449. if art.Status == int64(actions_model.ArtifactStatusExpired) {
  450. status = "expired"
  451. }
  452. artifactsResponse.Artifacts = append(artifactsResponse.Artifacts, &ArtifactsViewItem{
  453. Name: art.ArtifactName,
  454. Size: art.FileSize,
  455. Status: status,
  456. })
  457. }
  458. ctx.JSON(http.StatusOK, artifactsResponse)
  459. }
  460. func ArtifactsDownloadView(ctx *context_module.Context) {
  461. runIndex := ctx.ParamsInt64("run")
  462. artifactName := ctx.Params("artifact_name")
  463. run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
  464. if err != nil {
  465. if errors.Is(err, util.ErrNotExist) {
  466. ctx.Error(http.StatusNotFound, err.Error())
  467. return
  468. }
  469. ctx.Error(http.StatusInternalServerError, err.Error())
  470. return
  471. }
  472. artifacts, err := actions_model.ListArtifactsByRunIDAndName(ctx, run.ID, artifactName)
  473. if err != nil {
  474. ctx.Error(http.StatusInternalServerError, err.Error())
  475. return
  476. }
  477. if len(artifacts) == 0 {
  478. ctx.Error(http.StatusNotFound, "artifact not found")
  479. return
  480. }
  481. ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
  482. writer := zip.NewWriter(ctx.Resp)
  483. defer writer.Close()
  484. for _, art := range artifacts {
  485. f, err := storage.ActionsArtifacts.Open(art.StoragePath)
  486. if err != nil {
  487. ctx.Error(http.StatusInternalServerError, err.Error())
  488. return
  489. }
  490. var r io.ReadCloser
  491. if art.ContentEncoding == "gzip" {
  492. r, err = gzip.NewReader(f)
  493. if err != nil {
  494. ctx.Error(http.StatusInternalServerError, err.Error())
  495. return
  496. }
  497. } else {
  498. r = f
  499. }
  500. defer r.Close()
  501. w, err := writer.Create(art.ArtifactPath)
  502. if err != nil {
  503. ctx.Error(http.StatusInternalServerError, err.Error())
  504. return
  505. }
  506. if _, err := io.Copy(w, r); err != nil {
  507. ctx.Error(http.StatusInternalServerError, err.Error())
  508. return
  509. }
  510. }
  511. }
  512. func DisableWorkflowFile(ctx *context_module.Context) {
  513. disableOrEnableWorkflowFile(ctx, false)
  514. }
  515. func EnableWorkflowFile(ctx *context_module.Context) {
  516. disableOrEnableWorkflowFile(ctx, true)
  517. }
  518. func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
  519. workflow := ctx.FormString("workflow")
  520. if len(workflow) == 0 {
  521. ctx.ServerError("workflow", nil)
  522. return
  523. }
  524. cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
  525. cfg := cfgUnit.ActionsConfig()
  526. if isEnable {
  527. cfg.EnableWorkflow(workflow)
  528. } else {
  529. cfg.DisableWorkflow(workflow)
  530. }
  531. if err := repo_model.UpdateRepoUnit(cfgUnit); err != nil {
  532. ctx.ServerError("UpdateRepoUnit", err)
  533. return
  534. }
  535. if isEnable {
  536. ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow))
  537. } else {
  538. ctx.Flash.Success(ctx.Tr("actions.workflow.disable_success", workflow))
  539. }
  540. redirectURL := fmt.Sprintf("%s/actions?workflow=%s&actor=%s&status=%s", ctx.Repo.RepoLink, url.QueryEscape(workflow),
  541. url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
  542. ctx.JSONRedirect(redirectURL)
  543. }