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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package actions
  4. import (
  5. "context"
  6. "fmt"
  7. "strings"
  8. actions_model "code.gitea.io/gitea/models/actions"
  9. issues_model "code.gitea.io/gitea/models/issues"
  10. packages_model "code.gitea.io/gitea/models/packages"
  11. access_model "code.gitea.io/gitea/models/perm/access"
  12. repo_model "code.gitea.io/gitea/models/repo"
  13. unit_model "code.gitea.io/gitea/models/unit"
  14. user_model "code.gitea.io/gitea/models/user"
  15. actions_module "code.gitea.io/gitea/modules/actions"
  16. "code.gitea.io/gitea/modules/git"
  17. "code.gitea.io/gitea/modules/json"
  18. "code.gitea.io/gitea/modules/log"
  19. api "code.gitea.io/gitea/modules/structs"
  20. webhook_module "code.gitea.io/gitea/modules/webhook"
  21. "code.gitea.io/gitea/services/convert"
  22. "github.com/nektos/act/pkg/jobparser"
  23. )
  24. var methodCtxKey struct{}
  25. // withMethod sets the notification method that this context currently executes.
  26. // Used for debugging/ troubleshooting purposes.
  27. func withMethod(ctx context.Context, method string) context.Context {
  28. // don't overwrite
  29. if v := ctx.Value(methodCtxKey); v != nil {
  30. if _, ok := v.(string); ok {
  31. return ctx
  32. }
  33. }
  34. return context.WithValue(ctx, methodCtxKey, method)
  35. }
  36. // getMethod gets the notification method that this context currently executes.
  37. // Default: "notify"
  38. // Used for debugging/ troubleshooting purposes.
  39. func getMethod(ctx context.Context) string {
  40. if v := ctx.Value(methodCtxKey); v != nil {
  41. if s, ok := v.(string); ok {
  42. return s
  43. }
  44. }
  45. return "notify"
  46. }
  47. type notifyInput struct {
  48. // required
  49. Repo *repo_model.Repository
  50. Doer *user_model.User
  51. Event webhook_module.HookEventType
  52. // optional
  53. Ref string
  54. Payload api.Payloader
  55. PullRequest *issues_model.PullRequest
  56. }
  57. func newNotifyInput(repo *repo_model.Repository, doer *user_model.User, event webhook_module.HookEventType) *notifyInput {
  58. return &notifyInput{
  59. Repo: repo,
  60. Doer: doer,
  61. Event: event,
  62. }
  63. }
  64. func (input *notifyInput) WithDoer(doer *user_model.User) *notifyInput {
  65. input.Doer = doer
  66. return input
  67. }
  68. func (input *notifyInput) WithRef(ref string) *notifyInput {
  69. input.Ref = ref
  70. return input
  71. }
  72. func (input *notifyInput) WithPayload(payload api.Payloader) *notifyInput {
  73. input.Payload = payload
  74. return input
  75. }
  76. func (input *notifyInput) WithPullRequest(pr *issues_model.PullRequest) *notifyInput {
  77. input.PullRequest = pr
  78. if input.Ref == "" {
  79. input.Ref = pr.GetGitRefName()
  80. }
  81. return input
  82. }
  83. func (input *notifyInput) Notify(ctx context.Context) {
  84. log.Trace("execute %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
  85. if err := notify(ctx, input); err != nil {
  86. log.Error("an error occurred while executing the %s actions method: %v", getMethod(ctx), err)
  87. }
  88. }
  89. func notify(ctx context.Context, input *notifyInput) error {
  90. if input.Doer.IsActions() {
  91. // avoiding triggering cyclically, for example:
  92. // a comment of an issue will trigger the runner to add a new comment as reply,
  93. // and the new comment will trigger the runner again.
  94. log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
  95. return nil
  96. }
  97. if unit_model.TypeActions.UnitGlobalDisabled() {
  98. return nil
  99. }
  100. if err := input.Repo.LoadUnits(ctx); err != nil {
  101. return fmt.Errorf("repo.LoadUnits: %w", err)
  102. } else if !input.Repo.UnitEnabled(ctx, unit_model.TypeActions) {
  103. return nil
  104. }
  105. gitRepo, err := git.OpenRepository(context.Background(), input.Repo.RepoPath())
  106. if err != nil {
  107. return fmt.Errorf("git.OpenRepository: %w", err)
  108. }
  109. defer gitRepo.Close()
  110. ref := input.Ref
  111. if input.Event == webhook_module.HookEventDelete {
  112. // The event is deleting a reference, so it will fail to get the commit for a deleted reference.
  113. // Set ref to empty string to fall back to the default branch.
  114. ref = ""
  115. }
  116. if ref == "" {
  117. ref = input.Repo.DefaultBranch
  118. }
  119. // Get the commit object for the ref
  120. commit, err := gitRepo.GetCommit(ref)
  121. if err != nil {
  122. return fmt.Errorf("gitRepo.GetCommit: %w", err)
  123. }
  124. workflows, err := actions_module.DetectWorkflows(commit, input.Event, input.Payload)
  125. if err != nil {
  126. return fmt.Errorf("DetectWorkflows: %w", err)
  127. }
  128. if len(workflows) == 0 {
  129. log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
  130. return nil
  131. }
  132. p, err := json.Marshal(input.Payload)
  133. if err != nil {
  134. return fmt.Errorf("json.Marshal: %w", err)
  135. }
  136. isForkPullRequest := false
  137. if pr := input.PullRequest; pr != nil {
  138. switch pr.Flow {
  139. case issues_model.PullRequestFlowGithub:
  140. isForkPullRequest = pr.IsFromFork()
  141. case issues_model.PullRequestFlowAGit:
  142. // There is no fork concept in agit flow, anyone with read permission can push refs/for/<target-branch>/<topic-branch> to the repo.
  143. // So we can treat it as a fork pull request because it may be from an untrusted user
  144. isForkPullRequest = true
  145. default:
  146. // unknown flow, assume it's a fork pull request to be safe
  147. isForkPullRequest = true
  148. }
  149. }
  150. for id, content := range workflows {
  151. run := &actions_model.ActionRun{
  152. Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
  153. RepoID: input.Repo.ID,
  154. OwnerID: input.Repo.OwnerID,
  155. WorkflowID: id,
  156. TriggerUserID: input.Doer.ID,
  157. Ref: ref,
  158. CommitSHA: commit.ID.String(),
  159. IsForkPullRequest: isForkPullRequest,
  160. Event: input.Event,
  161. EventPayload: string(p),
  162. Status: actions_model.StatusWaiting,
  163. }
  164. if need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer); err != nil {
  165. log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err)
  166. continue
  167. } else {
  168. run.NeedApproval = need
  169. }
  170. jobs, err := jobparser.Parse(content)
  171. if err != nil {
  172. log.Error("jobparser.Parse: %v", err)
  173. continue
  174. }
  175. if err := actions_model.InsertRun(ctx, run, jobs); err != nil {
  176. log.Error("InsertRun: %v", err)
  177. continue
  178. }
  179. if jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID}); err != nil {
  180. log.Error("FindRunJobs: %v", err)
  181. } else {
  182. CreateCommitStatus(ctx, jobs...)
  183. }
  184. }
  185. return nil
  186. }
  187. func newNotifyInputFromIssue(issue *issues_model.Issue, event webhook_module.HookEventType) *notifyInput {
  188. return newNotifyInput(issue.Repo, issue.Poster, event)
  189. }
  190. func notifyRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release, action api.HookReleaseAction) {
  191. if err := rel.LoadAttributes(ctx); err != nil {
  192. log.Error("LoadAttributes: %v", err)
  193. return
  194. }
  195. permission, _ := access_model.GetUserRepoPermission(ctx, rel.Repo, doer)
  196. newNotifyInput(rel.Repo, doer, webhook_module.HookEventRelease).
  197. WithRef(git.RefNameFromTag(rel.TagName).String()).
  198. WithPayload(&api.ReleasePayload{
  199. Action: action,
  200. Release: convert.ToAPIRelease(ctx, rel.Repo, rel),
  201. Repository: convert.ToRepo(ctx, rel.Repo, permission),
  202. Sender: convert.ToUser(ctx, doer, nil),
  203. }).
  204. Notify(ctx)
  205. }
  206. func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_model.PackageDescriptor, action api.HookPackageAction) {
  207. if pd.Repository == nil {
  208. // When a package is uploaded to an organization, it could trigger an event to notify.
  209. // So the repository could be nil, however, actions can't support that yet.
  210. // See https://github.com/go-gitea/gitea/pull/17940
  211. return
  212. }
  213. apiPackage, err := convert.ToPackage(ctx, pd, sender)
  214. if err != nil {
  215. log.Error("Error converting package: %v", err)
  216. return
  217. }
  218. newNotifyInput(pd.Repository, sender, webhook_module.HookEventPackage).
  219. WithPayload(&api.PackagePayload{
  220. Action: action,
  221. Package: apiPackage,
  222. Sender: convert.ToUser(ctx, sender, nil),
  223. }).
  224. Notify(ctx)
  225. }
  226. func ifNeedApproval(ctx context.Context, run *actions_model.ActionRun, repo *repo_model.Repository, user *user_model.User) (bool, error) {
  227. // don't need approval if it's not a fork PR
  228. if !run.IsForkPullRequest {
  229. return false, nil
  230. }
  231. // always need approval if the user is restricted
  232. if user.IsRestricted {
  233. log.Trace("need approval because user %d is restricted", user.ID)
  234. return true, nil
  235. }
  236. // don't need approval if the user can write
  237. if perm, err := access_model.GetUserRepoPermission(ctx, repo, user); err != nil {
  238. return false, fmt.Errorf("GetUserRepoPermission: %w", err)
  239. } else if perm.CanWrite(unit_model.TypeActions) {
  240. log.Trace("do not need approval because user %d can write", user.ID)
  241. return false, nil
  242. }
  243. // don't need approval if the user has been approved before
  244. if count, err := actions_model.CountRuns(ctx, actions_model.FindRunOptions{
  245. RepoID: repo.ID,
  246. TriggerUserID: user.ID,
  247. Approved: true,
  248. }); err != nil {
  249. return false, fmt.Errorf("CountRuns: %w", err)
  250. } else if count > 0 {
  251. log.Trace("do not need approval because user %d has been approved before", user.ID)
  252. return false, nil
  253. }
  254. // otherwise, need approval
  255. log.Trace("need approval because it's the first time user %d triggered actions", user.ID)
  256. return true, nil
  257. }