gitea源码

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