a42a838843
Follow #25229 At present, when the trigger event is `pull_request_target`, the `ref` and `sha` of `ActionRun` are set according to the base branch of the pull request. This makes it impossible for us to find the head branch of the `ActionRun` directly. In this PR, the `ref` and `sha` will always be set to the head branch and they will be changed to the base branch when generating the task context.
331 lines
10 KiB
Go
331 lines
10 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"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"
|
|
)
|
|
|
|
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() {
|
|
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
|
|
workflows, err := actions_module.DetectWorkflows(commit, input.Event, input.Payload)
|
|
if err != nil {
|
|
return fmt.Errorf("DetectWorkflows: %w", err)
|
|
}
|
|
if len(workflows) == 0 {
|
|
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
|
|
} else {
|
|
for _, wf := range workflows {
|
|
if wf.TriggerEvent != 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(baseCommit, input.Event, input.Payload)
|
|
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 == actions_module.GithubEventPullRequestTarget {
|
|
detectedWorkflows = append(detectedWorkflows, wf)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(detectedWorkflows) == 0 {
|
|
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,
|
|
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
|
|
}
|
|
if err := actions_model.InsertRun(ctx, run, jobs); err != nil {
|
|
log.Error("InsertRun: %v", err)
|
|
continue
|
|
}
|
|
if jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID}); err != nil {
|
|
log.Error("FindRunJobs: %v", err)
|
|
} else {
|
|
CreateCommitStatus(ctx, jobs...)
|
|
}
|
|
|
|
}
|
|
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.ToRelease(ctx, 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
|
|
}
|