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.

run.go 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package actions
  4. import (
  5. "context"
  6. "fmt"
  7. "slices"
  8. "strings"
  9. "time"
  10. "code.gitea.io/gitea/models/db"
  11. repo_model "code.gitea.io/gitea/models/repo"
  12. user_model "code.gitea.io/gitea/models/user"
  13. "code.gitea.io/gitea/modules/git"
  14. "code.gitea.io/gitea/modules/json"
  15. api "code.gitea.io/gitea/modules/structs"
  16. "code.gitea.io/gitea/modules/timeutil"
  17. "code.gitea.io/gitea/modules/util"
  18. webhook_module "code.gitea.io/gitea/modules/webhook"
  19. "github.com/nektos/act/pkg/jobparser"
  20. "xorm.io/builder"
  21. )
  22. // ActionRun represents a run of a workflow file
  23. type ActionRun struct {
  24. ID int64
  25. Title string
  26. RepoID int64 `xorm:"index unique(repo_index)"`
  27. Repo *repo_model.Repository `xorm:"-"`
  28. OwnerID int64 `xorm:"index"`
  29. WorkflowID string `xorm:"index"` // the name of workflow file
  30. Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository
  31. TriggerUserID int64 `xorm:"index"`
  32. TriggerUser *user_model.User `xorm:"-"`
  33. ScheduleID int64
  34. Ref string `xorm:"index"` // the commit/tag/… that caused the run
  35. CommitSHA string
  36. IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
  37. NeedApproval bool // may need approval if it's a fork pull request
  38. ApprovedBy int64 `xorm:"index"` // who approved
  39. Event webhook_module.HookEventType // the webhook event that causes the workflow to run
  40. EventPayload string `xorm:"LONGTEXT"`
  41. TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow
  42. Status Status `xorm:"index"`
  43. Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed
  44. Started timeutil.TimeStamp
  45. Stopped timeutil.TimeStamp
  46. Created timeutil.TimeStamp `xorm:"created"`
  47. Updated timeutil.TimeStamp `xorm:"updated"`
  48. }
  49. func init() {
  50. db.RegisterModel(new(ActionRun))
  51. db.RegisterModel(new(ActionRunIndex))
  52. }
  53. func (run *ActionRun) HTMLURL() string {
  54. if run.Repo == nil {
  55. return ""
  56. }
  57. return fmt.Sprintf("%s/actions/runs/%d", run.Repo.HTMLURL(), run.Index)
  58. }
  59. func (run *ActionRun) Link() string {
  60. if run.Repo == nil {
  61. return ""
  62. }
  63. return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index)
  64. }
  65. // RefLink return the url of run's ref
  66. func (run *ActionRun) RefLink() string {
  67. refName := git.RefName(run.Ref)
  68. if refName.IsPull() {
  69. return run.Repo.Link() + "/pulls/" + refName.ShortName()
  70. }
  71. return git.RefURL(run.Repo.Link(), run.Ref)
  72. }
  73. // PrettyRef return #id for pull ref or ShortName for others
  74. func (run *ActionRun) PrettyRef() string {
  75. refName := git.RefName(run.Ref)
  76. if refName.IsPull() {
  77. return "#" + strings.TrimSuffix(strings.TrimPrefix(run.Ref, git.PullPrefix), "/head")
  78. }
  79. return refName.ShortName()
  80. }
  81. // LoadAttributes load Repo TriggerUser if not loaded
  82. func (run *ActionRun) LoadAttributes(ctx context.Context) error {
  83. if run == nil {
  84. return nil
  85. }
  86. if run.Repo == nil {
  87. repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID)
  88. if err != nil {
  89. return err
  90. }
  91. run.Repo = repo
  92. }
  93. if err := run.Repo.LoadAttributes(ctx); err != nil {
  94. return err
  95. }
  96. if run.TriggerUser == nil {
  97. u, err := user_model.GetPossibleUserByID(ctx, run.TriggerUserID)
  98. if err != nil {
  99. return err
  100. }
  101. run.TriggerUser = u
  102. }
  103. return nil
  104. }
  105. func (run *ActionRun) Duration() time.Duration {
  106. return calculateDuration(run.Started, run.Stopped, run.Status)
  107. }
  108. func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) {
  109. if run.Event == webhook_module.HookEventPush {
  110. var payload api.PushPayload
  111. if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
  112. return nil, err
  113. }
  114. return &payload, nil
  115. }
  116. return nil, fmt.Errorf("event %s is not a push event", run.Event)
  117. }
  118. func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) {
  119. if run.Event == webhook_module.HookEventPullRequest || run.Event == webhook_module.HookEventPullRequestSync {
  120. var payload api.PullRequestPayload
  121. if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
  122. return nil, err
  123. }
  124. return &payload, nil
  125. }
  126. return nil, fmt.Errorf("event %s is not a pull request event", run.Event)
  127. }
  128. func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
  129. _, err := db.GetEngine(ctx).ID(repo.ID).
  130. SetExpr("num_action_runs",
  131. builder.Select("count(*)").From("action_run").
  132. Where(builder.Eq{"repo_id": repo.ID}),
  133. ).
  134. SetExpr("num_closed_action_runs",
  135. builder.Select("count(*)").From("action_run").
  136. Where(builder.Eq{
  137. "repo_id": repo.ID,
  138. }.And(
  139. builder.In("status",
  140. StatusSuccess,
  141. StatusFailure,
  142. StatusCancelled,
  143. StatusSkipped,
  144. ),
  145. ),
  146. ),
  147. ).
  148. Update(repo)
  149. return err
  150. }
  151. // CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow.
  152. func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
  153. // Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'.
  154. runs, total, err := FindRuns(ctx, FindRunOptions{
  155. RepoID: repoID,
  156. Ref: ref,
  157. WorkflowID: workflowID,
  158. TriggerEvent: event,
  159. Status: []Status{StatusRunning, StatusWaiting},
  160. })
  161. if err != nil {
  162. return err
  163. }
  164. // If there are no runs found, there's no need to proceed with cancellation, so return nil.
  165. if total == 0 {
  166. return nil
  167. }
  168. // Iterate over each found run and cancel its associated jobs.
  169. for _, run := range runs {
  170. // Find all jobs associated with the current run.
  171. jobs, _, err := FindRunJobs(ctx, FindRunJobOptions{
  172. RunID: run.ID,
  173. })
  174. if err != nil {
  175. return err
  176. }
  177. // Iterate over each job and attempt to cancel it.
  178. for _, job := range jobs {
  179. // Skip jobs that are already in a terminal state (completed, cancelled, etc.).
  180. status := job.Status
  181. if status.IsDone() {
  182. continue
  183. }
  184. // If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it.
  185. if job.TaskID == 0 {
  186. job.Status = StatusCancelled
  187. job.Stopped = timeutil.TimeStampNow()
  188. // Update the job's status and stopped time in the database.
  189. n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
  190. if err != nil {
  191. return err
  192. }
  193. // If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again.
  194. if n == 0 {
  195. return fmt.Errorf("job has changed, try again")
  196. }
  197. // Continue with the next job.
  198. continue
  199. }
  200. // If the job has an associated task, try to stop the task, effectively cancelling the job.
  201. if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil {
  202. return err
  203. }
  204. }
  205. }
  206. // Return nil to indicate successful cancellation of all running and waiting jobs.
  207. return nil
  208. }
  209. // InsertRun inserts a run
  210. func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error {
  211. ctx, commiter, err := db.TxContext(ctx)
  212. if err != nil {
  213. return err
  214. }
  215. defer commiter.Close()
  216. index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID)
  217. if err != nil {
  218. return err
  219. }
  220. run.Index = index
  221. if err := db.Insert(ctx, run); err != nil {
  222. return err
  223. }
  224. if run.Repo == nil {
  225. repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID)
  226. if err != nil {
  227. return err
  228. }
  229. run.Repo = repo
  230. }
  231. if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil {
  232. return err
  233. }
  234. runJobs := make([]*ActionRunJob, 0, len(jobs))
  235. var hasWaiting bool
  236. for _, v := range jobs {
  237. id, job := v.Job()
  238. needs := job.Needs()
  239. if err := v.SetJob(id, job.EraseNeeds()); err != nil {
  240. return err
  241. }
  242. payload, _ := v.Marshal()
  243. status := StatusWaiting
  244. if len(needs) > 0 || run.NeedApproval {
  245. status = StatusBlocked
  246. } else {
  247. hasWaiting = true
  248. }
  249. job.Name, _ = util.SplitStringAtByteN(job.Name, 255)
  250. runJobs = append(runJobs, &ActionRunJob{
  251. RunID: run.ID,
  252. RepoID: run.RepoID,
  253. OwnerID: run.OwnerID,
  254. CommitSHA: run.CommitSHA,
  255. IsForkPullRequest: run.IsForkPullRequest,
  256. Name: job.Name,
  257. WorkflowPayload: payload,
  258. JobID: id,
  259. Needs: needs,
  260. RunsOn: job.RunsOn(),
  261. Status: status,
  262. })
  263. }
  264. if err := db.Insert(ctx, runJobs); err != nil {
  265. return err
  266. }
  267. // if there is a job in the waiting status, increase tasks version.
  268. if hasWaiting {
  269. if err := IncreaseTaskVersion(ctx, run.OwnerID, run.RepoID); err != nil {
  270. return err
  271. }
  272. }
  273. return commiter.Commit()
  274. }
  275. func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) {
  276. var run ActionRun
  277. has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run)
  278. if err != nil {
  279. return nil, err
  280. } else if !has {
  281. return nil, fmt.Errorf("run with id %d: %w", id, util.ErrNotExist)
  282. }
  283. return &run, nil
  284. }
  285. func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error) {
  286. run := &ActionRun{
  287. RepoID: repoID,
  288. Index: index,
  289. }
  290. has, err := db.GetEngine(ctx).Get(run)
  291. if err != nil {
  292. return nil, err
  293. } else if !has {
  294. return nil, fmt.Errorf("run with index %d %d: %w", repoID, index, util.ErrNotExist)
  295. }
  296. return run, nil
  297. }
  298. // UpdateRun updates a run.
  299. // It requires the inputted run has Version set.
  300. // It will return error if the version is not matched (it means the run has been changed after loaded).
  301. func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
  302. sess := db.GetEngine(ctx).ID(run.ID)
  303. if len(cols) > 0 {
  304. sess.Cols(cols...)
  305. }
  306. affected, err := sess.Update(run)
  307. if err != nil {
  308. return err
  309. }
  310. if affected == 0 {
  311. return fmt.Errorf("run has changed")
  312. // It's impossible that the run is not found, since Gitea never deletes runs.
  313. }
  314. if run.Status != 0 || slices.Contains(cols, "status") {
  315. if run.RepoID == 0 {
  316. run, err = GetRunByID(ctx, run.ID)
  317. if err != nil {
  318. return err
  319. }
  320. }
  321. if run.Repo == nil {
  322. repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID)
  323. if err != nil {
  324. return err
  325. }
  326. run.Repo = repo
  327. }
  328. if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil {
  329. return err
  330. }
  331. }
  332. return nil
  333. }
  334. type ActionRunIndex db.ResourceIndex