123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481 |
- // Copyright 2022 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package actions
-
- import (
- "bytes"
- "context"
- "fmt"
- "strings"
-
- actions_model "code.gitea.io/gitea/models/actions"
- issues_model "code.gitea.io/gitea/models/issues"
- packages_model "code.gitea.io/gitea/models/packages"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- unit_model "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- actions_module "code.gitea.io/gitea/modules/actions"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/convert"
-
- "github.com/nektos/act/pkg/jobparser"
- "github.com/nektos/act/pkg/model"
- )
-
- var methodCtxKey struct{}
-
- // withMethod sets the notification method that this context currently executes.
- // Used for debugging/ troubleshooting purposes.
- func withMethod(ctx context.Context, method string) context.Context {
- // don't overwrite
- if v := ctx.Value(methodCtxKey); v != nil {
- if _, ok := v.(string); ok {
- return ctx
- }
- }
- return context.WithValue(ctx, methodCtxKey, method)
- }
-
- // getMethod gets the notification method that this context currently executes.
- // Default: "notify"
- // Used for debugging/ troubleshooting purposes.
- func getMethod(ctx context.Context) string {
- if v := ctx.Value(methodCtxKey); v != nil {
- if s, ok := v.(string); ok {
- return s
- }
- }
- return "notify"
- }
-
- type notifyInput struct {
- // required
- Repo *repo_model.Repository
- Doer *user_model.User
- Event webhook_module.HookEventType
-
- // optional
- Ref string
- Payload api.Payloader
- PullRequest *issues_model.PullRequest
- }
-
- func newNotifyInput(repo *repo_model.Repository, doer *user_model.User, event webhook_module.HookEventType) *notifyInput {
- return ¬ifyInput{
- Repo: repo,
- Doer: doer,
- Event: event,
- }
- }
-
- func (input *notifyInput) WithDoer(doer *user_model.User) *notifyInput {
- input.Doer = doer
- return input
- }
-
- func (input *notifyInput) WithRef(ref string) *notifyInput {
- input.Ref = ref
- return input
- }
-
- func (input *notifyInput) WithPayload(payload api.Payloader) *notifyInput {
- input.Payload = payload
- return input
- }
-
- func (input *notifyInput) WithPullRequest(pr *issues_model.PullRequest) *notifyInput {
- input.PullRequest = pr
- if input.Ref == "" {
- input.Ref = pr.GetGitRefName()
- }
- return input
- }
-
- func (input *notifyInput) Notify(ctx context.Context) {
- log.Trace("execute %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
-
- if err := notify(ctx, input); err != nil {
- log.Error("an error occurred while executing the %s actions method: %v", getMethod(ctx), err)
- }
- }
-
- func notify(ctx context.Context, input *notifyInput) error {
- if input.Doer.IsActions() {
- // avoiding triggering cyclically, for example:
- // a comment of an issue will trigger the runner to add a new comment as reply,
- // and the new comment will trigger the runner again.
- log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
- return nil
- }
- if unit_model.TypeActions.UnitGlobalDisabled() {
- if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
- log.Error("CleanRepoScheduleTasks: %v", err)
- }
- return nil
- }
- if err := input.Repo.LoadUnits(ctx); err != nil {
- return fmt.Errorf("repo.LoadUnits: %w", err)
- } else if !input.Repo.UnitEnabled(ctx, unit_model.TypeActions) {
- return nil
- }
-
- gitRepo, err := git.OpenRepository(context.Background(), input.Repo.RepoPath())
- if err != nil {
- return fmt.Errorf("git.OpenRepository: %w", err)
- }
- defer gitRepo.Close()
-
- ref := input.Ref
- if input.Event == webhook_module.HookEventDelete {
- // The event is deleting a reference, so it will fail to get the commit for a deleted reference.
- // Set ref to empty string to fall back to the default branch.
- ref = ""
- }
- if ref == "" {
- ref = input.Repo.DefaultBranch
- }
-
- // Get the commit object for the ref
- commit, err := gitRepo.GetCommit(ref)
- if err != nil {
- return fmt.Errorf("gitRepo.GetCommit: %w", err)
- }
-
- var detectedWorkflows []*actions_module.DetectedWorkflow
- actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
- workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit,
- input.Event,
- input.Payload,
- input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch,
- )
- if err != nil {
- return fmt.Errorf("DetectWorkflows: %w", err)
- }
-
- log.Trace("repo %s with commit %s event %s find %d workflows and %d schedules",
- input.Repo.RepoPath(),
- commit.ID,
- input.Event,
- len(workflows),
- len(schedules),
- )
-
- for _, wf := range workflows {
- if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
- log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName)
- continue
- }
-
- if wf.TriggerEvent.Name != actions_module.GithubEventPullRequestTarget {
- detectedWorkflows = append(detectedWorkflows, wf)
- }
- }
-
- if input.PullRequest != nil {
- // detect pull_request_target workflows
- baseRef := git.BranchPrefix + input.PullRequest.BaseBranch
- baseCommit, err := gitRepo.GetCommit(baseRef)
- if err != nil {
- return fmt.Errorf("gitRepo.GetCommit: %w", err)
- }
- baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload, false)
- if err != nil {
- return fmt.Errorf("DetectWorkflows: %w", err)
- }
- if len(baseWorkflows) == 0 {
- log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.RepoPath(), baseCommit.ID)
- } else {
- for _, wf := range baseWorkflows {
- if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget {
- detectedWorkflows = append(detectedWorkflows, wf)
- }
- }
- }
- }
-
- if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
- return err
- }
-
- return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
- }
-
- func handleWorkflows(
- ctx context.Context,
- detectedWorkflows []*actions_module.DetectedWorkflow,
- commit *git.Commit,
- input *notifyInput,
- ref string,
- ) error {
- if len(detectedWorkflows) == 0 {
- log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
- return nil
- }
-
- p, err := json.Marshal(input.Payload)
- if err != nil {
- return fmt.Errorf("json.Marshal: %w", err)
- }
-
- isForkPullRequest := false
- if pr := input.PullRequest; pr != nil {
- switch pr.Flow {
- case issues_model.PullRequestFlowGithub:
- isForkPullRequest = pr.IsFromFork()
- case issues_model.PullRequestFlowAGit:
- // There is no fork concept in agit flow, anyone with read permission can push refs/for/<target-branch>/<topic-branch> to the repo.
- // So we can treat it as a fork pull request because it may be from an untrusted user
- isForkPullRequest = true
- default:
- // unknown flow, assume it's a fork pull request to be safe
- isForkPullRequest = true
- }
- }
-
- for _, dwf := range detectedWorkflows {
- run := &actions_model.ActionRun{
- Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
- RepoID: input.Repo.ID,
- OwnerID: input.Repo.OwnerID,
- WorkflowID: dwf.EntryName,
- TriggerUserID: input.Doer.ID,
- Ref: ref,
- CommitSHA: commit.ID.String(),
- IsForkPullRequest: isForkPullRequest,
- Event: input.Event,
- EventPayload: string(p),
- TriggerEvent: dwf.TriggerEvent.Name,
- Status: actions_model.StatusWaiting,
- }
- if need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer); err != nil {
- log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err)
- continue
- } else {
- run.NeedApproval = need
- }
-
- jobs, err := jobparser.Parse(dwf.Content)
- if err != nil {
- log.Error("jobparser.Parse: %v", err)
- continue
- }
-
- // cancel running jobs if the event is push
- if run.Event == webhook_module.HookEventPush {
- // cancel running jobs of the same workflow
- if err := actions_model.CancelRunningJobs(
- ctx,
- run.RepoID,
- run.Ref,
- run.WorkflowID,
- run.Event,
- ); err != nil {
- log.Error("CancelRunningJobs: %v", err)
- }
- }
-
- if err := actions_model.InsertRun(ctx, run, jobs); err != nil {
- log.Error("InsertRun: %v", err)
- continue
- }
-
- alljobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID})
- if err != nil {
- log.Error("FindRunJobs: %v", err)
- continue
- }
- CreateCommitStatus(ctx, alljobs...)
- }
- return nil
- }
-
- func newNotifyInputFromIssue(issue *issues_model.Issue, event webhook_module.HookEventType) *notifyInput {
- return newNotifyInput(issue.Repo, issue.Poster, event)
- }
-
- func notifyRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release, action api.HookReleaseAction) {
- if err := rel.LoadAttributes(ctx); err != nil {
- log.Error("LoadAttributes: %v", err)
- return
- }
-
- permission, _ := access_model.GetUserRepoPermission(ctx, rel.Repo, doer)
-
- newNotifyInput(rel.Repo, doer, webhook_module.HookEventRelease).
- WithRef(git.RefNameFromTag(rel.TagName).String()).
- WithPayload(&api.ReleasePayload{
- Action: action,
- Release: convert.ToAPIRelease(ctx, rel.Repo, rel),
- Repository: convert.ToRepo(ctx, rel.Repo, permission),
- Sender: convert.ToUser(ctx, doer, nil),
- }).
- Notify(ctx)
- }
-
- func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_model.PackageDescriptor, action api.HookPackageAction) {
- if pd.Repository == nil {
- // When a package is uploaded to an organization, it could trigger an event to notify.
- // So the repository could be nil, however, actions can't support that yet.
- // See https://github.com/go-gitea/gitea/pull/17940
- return
- }
-
- apiPackage, err := convert.ToPackage(ctx, pd, sender)
- if err != nil {
- log.Error("Error converting package: %v", err)
- return
- }
-
- newNotifyInput(pd.Repository, sender, webhook_module.HookEventPackage).
- WithPayload(&api.PackagePayload{
- Action: action,
- Package: apiPackage,
- Sender: convert.ToUser(ctx, sender, nil),
- }).
- Notify(ctx)
- }
-
- func ifNeedApproval(ctx context.Context, run *actions_model.ActionRun, repo *repo_model.Repository, user *user_model.User) (bool, error) {
- // 1. don't need approval if it's not a fork PR
- // 2. don't need approval if the event is `pull_request_target` since the workflow will run in the context of base branch
- // see https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
- if !run.IsForkPullRequest || run.TriggerEvent == actions_module.GithubEventPullRequestTarget {
- return false, nil
- }
-
- // always need approval if the user is restricted
- if user.IsRestricted {
- log.Trace("need approval because user %d is restricted", user.ID)
- return true, nil
- }
-
- // don't need approval if the user can write
- if perm, err := access_model.GetUserRepoPermission(ctx, repo, user); err != nil {
- return false, fmt.Errorf("GetUserRepoPermission: %w", err)
- } else if perm.CanWrite(unit_model.TypeActions) {
- log.Trace("do not need approval because user %d can write", user.ID)
- return false, nil
- }
-
- // don't need approval if the user has been approved before
- if count, err := actions_model.CountRuns(ctx, actions_model.FindRunOptions{
- RepoID: repo.ID,
- TriggerUserID: user.ID,
- Approved: true,
- }); err != nil {
- return false, fmt.Errorf("CountRuns: %w", err)
- } else if count > 0 {
- log.Trace("do not need approval because user %d has been approved before", user.ID)
- return false, nil
- }
-
- // otherwise, need approval
- log.Trace("need approval because it's the first time user %d triggered actions", user.ID)
- return true, nil
- }
-
- func handleSchedules(
- ctx context.Context,
- detectedWorkflows []*actions_module.DetectedWorkflow,
- commit *git.Commit,
- input *notifyInput,
- ref string,
- ) error {
- branch, err := commit.GetBranchName()
- if err != nil {
- return err
- }
- if branch != input.Repo.DefaultBranch {
- log.Trace("commit branch is not default branch in repo")
- return nil
- }
-
- if count, err := actions_model.CountSchedules(ctx, actions_model.FindScheduleOptions{RepoID: input.Repo.ID}); err != nil {
- log.Error("CountSchedules: %v", err)
- return err
- } else if count > 0 {
- if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
- log.Error("CleanRepoScheduleTasks: %v", err)
- }
- }
-
- if len(detectedWorkflows) == 0 {
- log.Trace("repo %s with commit %s couldn't find schedules", input.Repo.RepoPath(), commit.ID)
- return nil
- }
-
- p, err := json.Marshal(input.Payload)
- if err != nil {
- return fmt.Errorf("json.Marshal: %w", err)
- }
-
- crons := make([]*actions_model.ActionSchedule, 0, len(detectedWorkflows))
- for _, dwf := range detectedWorkflows {
- // Check cron job condition. Only working in default branch
- workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content))
- if err != nil {
- log.Error("ReadWorkflow: %v", err)
- continue
- }
- schedules := workflow.OnSchedule()
- if len(schedules) == 0 {
- log.Warn("no schedule event")
- continue
- }
-
- run := &actions_model.ActionSchedule{
- Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
- RepoID: input.Repo.ID,
- OwnerID: input.Repo.OwnerID,
- WorkflowID: dwf.EntryName,
- TriggerUserID: input.Doer.ID,
- Ref: ref,
- CommitSHA: commit.ID.String(),
- Event: input.Event,
- EventPayload: string(p),
- Specs: schedules,
- Content: dwf.Content,
- }
- crons = append(crons, run)
- }
-
- return actions_model.CreateScheduleTask(ctx, crons)
- }
-
- // DetectAndHandleSchedules detects the schedule workflows on the default branch and create schedule tasks
- func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) error {
- gitRepo, err := git.OpenRepository(context.Background(), repo.RepoPath())
- if err != nil {
- return fmt.Errorf("git.OpenRepository: %w", err)
- }
- defer gitRepo.Close()
-
- // Only detect schedule workflows on the default branch
- commit, err := gitRepo.GetCommit(repo.DefaultBranch)
- if err != nil {
- return fmt.Errorf("gitRepo.GetCommit: %w", err)
- }
- scheduleWorkflows, err := actions_module.DetectScheduledWorkflows(gitRepo, commit)
- if err != nil {
- return fmt.Errorf("detect schedule workflows: %w", err)
- }
- if len(scheduleWorkflows) == 0 {
- return nil
- }
-
- // We need a notifyInput to call handleSchedules
- // Here we use the commit author as the Doer of the notifyInput
- commitUser, err := user_model.GetUserByEmail(ctx, commit.Author.Email)
- if err != nil {
- return fmt.Errorf("get user by email: %w", err)
- }
- notifyInput := newNotifyInput(repo, commitUser, webhook_module.HookEventSchedule)
-
- return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch)
- }
|