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.

notifier_helper.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package actions
  4. import (
  5. "bytes"
  6. "context"
  7. "fmt"
  8. "strings"
  9. actions_model "code.gitea.io/gitea/models/actions"
  10. issues_model "code.gitea.io/gitea/models/issues"
  11. packages_model "code.gitea.io/gitea/models/packages"
  12. access_model "code.gitea.io/gitea/models/perm/access"
  13. repo_model "code.gitea.io/gitea/models/repo"
  14. unit_model "code.gitea.io/gitea/models/unit"
  15. user_model "code.gitea.io/gitea/models/user"
  16. actions_module "code.gitea.io/gitea/modules/actions"
  17. "code.gitea.io/gitea/modules/git"
  18. "code.gitea.io/gitea/modules/json"
  19. "code.gitea.io/gitea/modules/log"
  20. api "code.gitea.io/gitea/modules/structs"
  21. webhook_module "code.gitea.io/gitea/modules/webhook"
  22. "code.gitea.io/gitea/services/convert"
  23. "github.com/nektos/act/pkg/jobparser"
  24. "github.com/nektos/act/pkg/model"
  25. )
  26. var methodCtxKey struct{}
  27. // withMethod sets the notification method that this context currently executes.
  28. // Used for debugging/ troubleshooting purposes.
  29. func withMethod(ctx context.Context, method string) context.Context {
  30. // don't overwrite
  31. if v := ctx.Value(methodCtxKey); v != nil {
  32. if _, ok := v.(string); ok {
  33. return ctx
  34. }
  35. }
  36. return context.WithValue(ctx, methodCtxKey, method)
  37. }
  38. // getMethod gets the notification method that this context currently executes.
  39. // Default: "notify"
  40. // Used for debugging/ troubleshooting purposes.
  41. func getMethod(ctx context.Context) string {
  42. if v := ctx.Value(methodCtxKey); v != nil {
  43. if s, ok := v.(string); ok {
  44. return s
  45. }
  46. }
  47. return "notify"
  48. }
  49. type notifyInput struct {
  50. // required
  51. Repo *repo_model.Repository
  52. Doer *user_model.User
  53. Event webhook_module.HookEventType
  54. // optional
  55. Ref string
  56. Payload api.Payloader
  57. PullRequest *issues_model.PullRequest
  58. }
  59. func newNotifyInput(repo *repo_model.Repository, doer *user_model.User, event webhook_module.HookEventType) *notifyInput {
  60. return &notifyInput{
  61. Repo: repo,
  62. Doer: doer,
  63. Event: event,
  64. }
  65. }
  66. func (input *notifyInput) WithDoer(doer *user_model.User) *notifyInput {
  67. input.Doer = doer
  68. return input
  69. }
  70. func (input *notifyInput) WithRef(ref string) *notifyInput {
  71. input.Ref = ref
  72. return input
  73. }
  74. func (input *notifyInput) WithPayload(payload api.Payloader) *notifyInput {
  75. input.Payload = payload
  76. return input
  77. }
  78. func (input *notifyInput) WithPullRequest(pr *issues_model.PullRequest) *notifyInput {
  79. input.PullRequest = pr
  80. if input.Ref == "" {
  81. input.Ref = pr.GetGitRefName()
  82. }
  83. return input
  84. }
  85. func (input *notifyInput) Notify(ctx context.Context) {
  86. log.Trace("execute %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
  87. if err := notify(ctx, input); err != nil {
  88. log.Error("an error occurred while executing the %s actions method: %v", getMethod(ctx), err)
  89. }
  90. }
  91. func notify(ctx context.Context, input *notifyInput) error {
  92. if input.Doer.IsActions() {
  93. // avoiding triggering cyclically, for example:
  94. // a comment of an issue will trigger the runner to add a new comment as reply,
  95. // and the new comment will trigger the runner again.
  96. log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
  97. return nil
  98. }
  99. if unit_model.TypeActions.UnitGlobalDisabled() {
  100. if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
  101. log.Error("CleanRepoScheduleTasks: %v", err)
  102. }
  103. return nil
  104. }
  105. if err := input.Repo.LoadUnits(ctx); err != nil {
  106. return fmt.Errorf("repo.LoadUnits: %w", err)
  107. } else if !input.Repo.UnitEnabled(ctx, unit_model.TypeActions) {
  108. return nil
  109. }
  110. gitRepo, err := git.OpenRepository(context.Background(), input.Repo.RepoPath())
  111. if err != nil {
  112. return fmt.Errorf("git.OpenRepository: %w", err)
  113. }
  114. defer gitRepo.Close()
  115. ref := input.Ref
  116. if input.Event == webhook_module.HookEventDelete {
  117. // The event is deleting a reference, so it will fail to get the commit for a deleted reference.
  118. // Set ref to empty string to fall back to the default branch.
  119. ref = ""
  120. }
  121. if ref == "" {
  122. ref = input.Repo.DefaultBranch
  123. }
  124. // Get the commit object for the ref
  125. commit, err := gitRepo.GetCommit(ref)
  126. if err != nil {
  127. return fmt.Errorf("gitRepo.GetCommit: %w", err)
  128. }
  129. var detectedWorkflows []*actions_module.DetectedWorkflow
  130. actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
  131. workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit,
  132. input.Event,
  133. input.Payload,
  134. input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch,
  135. )
  136. if err != nil {
  137. return fmt.Errorf("DetectWorkflows: %w", err)
  138. }
  139. log.Trace("repo %s with commit %s event %s find %d workflows and %d schedules",
  140. input.Repo.RepoPath(),
  141. commit.ID,
  142. input.Event,
  143. len(workflows),
  144. len(schedules),
  145. )
  146. for _, wf := range workflows {
  147. if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
  148. log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName)
  149. continue
  150. }
  151. if wf.TriggerEvent.Name != actions_module.GithubEventPullRequestTarget {
  152. detectedWorkflows = append(detectedWorkflows, wf)
  153. }
  154. }
  155. if input.PullRequest != nil {
  156. // detect pull_request_target workflows
  157. baseRef := git.BranchPrefix + input.PullRequest.BaseBranch
  158. baseCommit, err := gitRepo.GetCommit(baseRef)
  159. if err != nil {
  160. return fmt.Errorf("gitRepo.GetCommit: %w", err)
  161. }
  162. baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload, false)
  163. if err != nil {
  164. return fmt.Errorf("DetectWorkflows: %w", err)
  165. }
  166. if len(baseWorkflows) == 0 {
  167. log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.RepoPath(), baseCommit.ID)
  168. } else {
  169. for _, wf := range baseWorkflows {
  170. if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget {
  171. detectedWorkflows = append(detectedWorkflows, wf)
  172. }
  173. }
  174. }
  175. }
  176. if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
  177. return err
  178. }
  179. return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
  180. }
  181. func handleWorkflows(
  182. ctx context.Context,
  183. detectedWorkflows []*actions_module.DetectedWorkflow,
  184. commit *git.Commit,
  185. input *notifyInput,
  186. ref string,
  187. ) error {
  188. if len(detectedWorkflows) == 0 {
  189. log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
  190. return nil
  191. }
  192. p, err := json.Marshal(input.Payload)
  193. if err != nil {
  194. return fmt.Errorf("json.Marshal: %w", err)
  195. }
  196. isForkPullRequest := false
  197. if pr := input.PullRequest; pr != nil {
  198. switch pr.Flow {
  199. case issues_model.PullRequestFlowGithub:
  200. isForkPullRequest = pr.IsFromFork()
  201. case issues_model.PullRequestFlowAGit:
  202. // There is no fork concept in agit flow, anyone with read permission can push refs/for/<target-branch>/<topic-branch> to the repo.
  203. // So we can treat it as a fork pull request because it may be from an untrusted user
  204. isForkPullRequest = true
  205. default:
  206. // unknown flow, assume it's a fork pull request to be safe
  207. isForkPullRequest = true
  208. }
  209. }
  210. for _, dwf := range detectedWorkflows {
  211. run := &actions_model.ActionRun{
  212. Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
  213. RepoID: input.Repo.ID,
  214. OwnerID: input.Repo.OwnerID,
  215. WorkflowID: dwf.EntryName,
  216. TriggerUserID: input.Doer.ID,
  217. Ref: ref,
  218. CommitSHA: commit.ID.String(),
  219. IsForkPullRequest: isForkPullRequest,
  220. Event: input.Event,
  221. EventPayload: string(p),
  222. TriggerEvent: dwf.TriggerEvent.Name,
  223. Status: actions_model.StatusWaiting,
  224. }
  225. if need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer); err != nil {
  226. log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err)
  227. continue
  228. } else {
  229. run.NeedApproval = need
  230. }
  231. jobs, err := jobparser.Parse(dwf.Content)
  232. if err != nil {
  233. log.Error("jobparser.Parse: %v", err)
  234. continue
  235. }
  236. // cancel running jobs if the event is push
  237. if run.Event == webhook_module.HookEventPush {
  238. // cancel running jobs of the same workflow
  239. if err := actions_model.CancelRunningJobs(
  240. ctx,
  241. run.RepoID,
  242. run.Ref,
  243. run.WorkflowID,
  244. run.Event,
  245. ); err != nil {
  246. log.Error("CancelRunningJobs: %v", err)
  247. }
  248. }
  249. if err := actions_model.InsertRun(ctx, run, jobs); err != nil {
  250. log.Error("InsertRun: %v", err)
  251. continue
  252. }
  253. alljobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID})
  254. if err != nil {
  255. log.Error("FindRunJobs: %v", err)
  256. continue
  257. }
  258. CreateCommitStatus(ctx, alljobs...)
  259. }
  260. return nil
  261. }
  262. func newNotifyInputFromIssue(issue *issues_model.Issue, event webhook_module.HookEventType) *notifyInput {
  263. return newNotifyInput(issue.Repo, issue.Poster, event)
  264. }
  265. func notifyRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release, action api.HookReleaseAction) {
  266. if err := rel.LoadAttributes(ctx); err != nil {
  267. log.Error("LoadAttributes: %v", err)
  268. return
  269. }
  270. permission, _ := access_model.GetUserRepoPermission(ctx, rel.Repo, doer)
  271. newNotifyInput(rel.Repo, doer, webhook_module.HookEventRelease).
  272. WithRef(git.RefNameFromTag(rel.TagName).String()).
  273. WithPayload(&api.ReleasePayload{
  274. Action: action,
  275. Release: convert.ToAPIRelease(ctx, rel.Repo, rel),
  276. Repository: convert.ToRepo(ctx, rel.Repo, permission),
  277. Sender: convert.ToUser(ctx, doer, nil),
  278. }).
  279. Notify(ctx)
  280. }
  281. func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_model.PackageDescriptor, action api.HookPackageAction) {
  282. if pd.Repository == nil {
  283. // When a package is uploaded to an organization, it could trigger an event to notify.
  284. // So the repository could be nil, however, actions can't support that yet.
  285. // See https://github.com/go-gitea/gitea/pull/17940
  286. return
  287. }
  288. apiPackage, err := convert.ToPackage(ctx, pd, sender)
  289. if err != nil {
  290. log.Error("Error converting package: %v", err)
  291. return
  292. }
  293. newNotifyInput(pd.Repository, sender, webhook_module.HookEventPackage).
  294. WithPayload(&api.PackagePayload{
  295. Action: action,
  296. Package: apiPackage,
  297. Sender: convert.ToUser(ctx, sender, nil),
  298. }).
  299. Notify(ctx)
  300. }
  301. func ifNeedApproval(ctx context.Context, run *actions_model.ActionRun, repo *repo_model.Repository, user *user_model.User) (bool, error) {
  302. // 1. don't need approval if it's not a fork PR
  303. // 2. don't need approval if the event is `pull_request_target` since the workflow will run in the context of base branch
  304. // see https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
  305. if !run.IsForkPullRequest || run.TriggerEvent == actions_module.GithubEventPullRequestTarget {
  306. return false, nil
  307. }
  308. // always need approval if the user is restricted
  309. if user.IsRestricted {
  310. log.Trace("need approval because user %d is restricted", user.ID)
  311. return true, nil
  312. }
  313. // don't need approval if the user can write
  314. if perm, err := access_model.GetUserRepoPermission(ctx, repo, user); err != nil {
  315. return false, fmt.Errorf("GetUserRepoPermission: %w", err)
  316. } else if perm.CanWrite(unit_model.TypeActions) {
  317. log.Trace("do not need approval because user %d can write", user.ID)
  318. return false, nil
  319. }
  320. // don't need approval if the user has been approved before
  321. if count, err := actions_model.CountRuns(ctx, actions_model.FindRunOptions{
  322. RepoID: repo.ID,
  323. TriggerUserID: user.ID,
  324. Approved: true,
  325. }); err != nil {
  326. return false, fmt.Errorf("CountRuns: %w", err)
  327. } else if count > 0 {
  328. log.Trace("do not need approval because user %d has been approved before", user.ID)
  329. return false, nil
  330. }
  331. // otherwise, need approval
  332. log.Trace("need approval because it's the first time user %d triggered actions", user.ID)
  333. return true, nil
  334. }
  335. func handleSchedules(
  336. ctx context.Context,
  337. detectedWorkflows []*actions_module.DetectedWorkflow,
  338. commit *git.Commit,
  339. input *notifyInput,
  340. ref string,
  341. ) error {
  342. branch, err := commit.GetBranchName()
  343. if err != nil {
  344. return err
  345. }
  346. if branch != input.Repo.DefaultBranch {
  347. log.Trace("commit branch is not default branch in repo")
  348. return nil
  349. }
  350. if count, err := actions_model.CountSchedules(ctx, actions_model.FindScheduleOptions{RepoID: input.Repo.ID}); err != nil {
  351. log.Error("CountSchedules: %v", err)
  352. return err
  353. } else if count > 0 {
  354. if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
  355. log.Error("CleanRepoScheduleTasks: %v", err)
  356. }
  357. }
  358. if len(detectedWorkflows) == 0 {
  359. log.Trace("repo %s with commit %s couldn't find schedules", input.Repo.RepoPath(), commit.ID)
  360. return nil
  361. }
  362. p, err := json.Marshal(input.Payload)
  363. if err != nil {
  364. return fmt.Errorf("json.Marshal: %w", err)
  365. }
  366. crons := make([]*actions_model.ActionSchedule, 0, len(detectedWorkflows))
  367. for _, dwf := range detectedWorkflows {
  368. // Check cron job condition. Only working in default branch
  369. workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content))
  370. if err != nil {
  371. log.Error("ReadWorkflow: %v", err)
  372. continue
  373. }
  374. schedules := workflow.OnSchedule()
  375. if len(schedules) == 0 {
  376. log.Warn("no schedule event")
  377. continue
  378. }
  379. run := &actions_model.ActionSchedule{
  380. Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
  381. RepoID: input.Repo.ID,
  382. OwnerID: input.Repo.OwnerID,
  383. WorkflowID: dwf.EntryName,
  384. TriggerUserID: input.Doer.ID,
  385. Ref: ref,
  386. CommitSHA: commit.ID.String(),
  387. Event: input.Event,
  388. EventPayload: string(p),
  389. Specs: schedules,
  390. Content: dwf.Content,
  391. }
  392. crons = append(crons, run)
  393. }
  394. return actions_model.CreateScheduleTask(ctx, crons)
  395. }
  396. // DetectAndHandleSchedules detects the schedule workflows on the default branch and create schedule tasks
  397. func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) error {
  398. gitRepo, err := git.OpenRepository(context.Background(), repo.RepoPath())
  399. if err != nil {
  400. return fmt.Errorf("git.OpenRepository: %w", err)
  401. }
  402. defer gitRepo.Close()
  403. // Only detect schedule workflows on the default branch
  404. commit, err := gitRepo.GetCommit(repo.DefaultBranch)
  405. if err != nil {
  406. return fmt.Errorf("gitRepo.GetCommit: %w", err)
  407. }
  408. scheduleWorkflows, err := actions_module.DetectScheduledWorkflows(gitRepo, commit)
  409. if err != nil {
  410. return fmt.Errorf("detect schedule workflows: %w", err)
  411. }
  412. if len(scheduleWorkflows) == 0 {
  413. return nil
  414. }
  415. // We need a notifyInput to call handleSchedules
  416. // Here we use the commit author as the Doer of the notifyInput
  417. commitUser, err := user_model.GetUserByEmail(ctx, commit.Author.Email)
  418. if err != nil {
  419. return fmt.Errorf("get user by email: %w", err)
  420. }
  421. notifyInput := newNotifyInput(repo, commitUser, webhook_module.HookEventSchedule)
  422. return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch)
  423. }