dd6fc4b69c
When multiple workflows are triggered by a single event (e.g. a pull_request with several matching workflow files), each InsertRun transaction acquires an X-lock on the repository row via UpdateRepoRunsNumbers and an index lock on action_run. Two concurrent transactions can deadlock when each holds one lock and waits for the other. InnoDB kills the lighter transaction, but handleWorkflows only logged the error and silently dropped the workflow run — making it appear as though pull_request events were never fired. This was the root cause of API-created PRs appearing to not trigger Actions workflows: the notification pipeline was correct, but the DB insert was lost to an unretried deadlock. The fix wraps PrepareRunAndInsert in a retry loop (up to 3 attempts with exponential backoff) that detects deadlock errors across MySQL, PostgreSQL, and SQLite. On deadlock, the rolled-back run fields are reset before the next attempt. Also adds db.IsErrDeadlock() for cross-engine deadlock detection and unit tests for the same. Closes #220 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
588 lines
19 KiB
Go
588 lines
19 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
actions_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/actions"
|
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
|
issues_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
|
packages_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/packages"
|
|
access_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access"
|
|
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
|
unit_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
|
user_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
|
actions_module "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/actions"
|
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/gitrepo"
|
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
|
api "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
|
webhook_module "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/webhook"
|
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/convert"
|
|
|
|
"github.com/nektos/act/pkg/model"
|
|
)
|
|
|
|
type methodCtxKeyType struct{}
|
|
|
|
var methodCtxKey methodCtxKeyType
|
|
|
|
// 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 git.RefName
|
|
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 newNotifyInputForSchedules(repo *repo_model.Repository) *notifyInput {
|
|
// the doer here will be ignored as we force using action user when handling schedules
|
|
return newNotifyInput(repo, user_model.NewActionsUser(), webhook_module.HookEventSchedule)
|
|
}
|
|
|
|
func (input *notifyInput) WithDoer(doer *user_model.User) *notifyInput {
|
|
input.Doer = doer
|
|
return input
|
|
}
|
|
|
|
func (input *notifyInput) WithRef(ref string) *notifyInput {
|
|
input.Ref = git.RefName(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 = git.RefName(pr.GetGitHeadRefName())
|
|
}
|
|
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 {
|
|
shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch
|
|
if input.Doer.IsGiteaActions() {
|
|
// 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)
|
|
|
|
// we should update schedule tasks in this case, because
|
|
// 1. schedule tasks cannot be triggered by other events, so cyclic triggering will not occur
|
|
// 2. some schedule tasks may update the repo periodically, so the refs of schedule tasks need to be updated
|
|
if shouldDetectSchedules {
|
|
return DetectAndHandleSchedules(ctx, input.Repo)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
if input.Repo.IsEmpty || input.Repo.IsArchived {
|
|
return nil
|
|
}
|
|
if unit_model.TypeActions.UnitGlobalDisabled() {
|
|
if err := 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 := gitrepo.OpenRepository(context.Background(), input.Repo)
|
|
if err != nil {
|
|
return fmt.Errorf("git.OpenRepository: %w", err)
|
|
}
|
|
defer gitRepo.Close()
|
|
|
|
ref := input.Ref
|
|
if ref.BranchName() != input.Repo.DefaultBranch && actions_module.IsDefaultBranchWorkflow(input.Event) {
|
|
if ref != "" {
|
|
log.Warn("Event %q should only trigger workflows on the default branch, but its ref is %q. Will fall back to the default branch",
|
|
input.Event, ref)
|
|
}
|
|
ref = git.RefNameFromBranch(input.Repo.DefaultBranch)
|
|
}
|
|
if ref == "" {
|
|
log.Warn("Ref of event %q is empty, will fall back to the default branch", input.Event)
|
|
ref = git.RefNameFromBranch(input.Repo.DefaultBranch)
|
|
}
|
|
|
|
commitID, err := gitRepo.GetRefCommitID(ref.String())
|
|
if err != nil {
|
|
return fmt.Errorf("gitRepo.GetRefCommitID: %w", err)
|
|
}
|
|
|
|
// Get the commit object for the ref
|
|
commit, err := gitRepo.GetCommit(commitID)
|
|
if err != nil {
|
|
return fmt.Errorf("gitRepo.GetCommit: %w", err)
|
|
}
|
|
|
|
if skipWorkflows(ctx, input, commit) {
|
|
return nil
|
|
}
|
|
|
|
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,
|
|
shouldDetectSchedules,
|
|
)
|
|
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.RelativePath(),
|
|
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.RelativePath(), 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.RelativePath(), baseCommit.ID)
|
|
} else {
|
|
for _, wf := range baseWorkflows {
|
|
if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget {
|
|
detectedWorkflows = append(detectedWorkflows, wf)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if shouldDetectSchedules {
|
|
if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
|
|
}
|
|
|
|
func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) bool {
|
|
// skip workflow runs with a configured skip-ci string in commit message or pr title if the event is push or pull_request(_sync)
|
|
// https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs
|
|
skipWorkflowEvents := []webhook_module.HookEventType{
|
|
webhook_module.HookEventPush,
|
|
webhook_module.HookEventPullRequest,
|
|
webhook_module.HookEventPullRequestSync,
|
|
}
|
|
if slices.Contains(skipWorkflowEvents, input.Event) {
|
|
for _, s := range setting.Actions.SkipWorkflowStrings {
|
|
if input.PullRequest != nil && strings.Contains(input.PullRequest.Issue.Title, s) {
|
|
log.Debug("repo %s: skipped run for pr %v because of %s string", input.Repo.RelativePath(), input.PullRequest.Issue.ID, s)
|
|
return true
|
|
}
|
|
if strings.Contains(commit.MessageRaw, s) {
|
|
log.Debug("repo %s with commit %s: skipped run because of %s string", input.Repo.RelativePath(), commit.ID, s)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
if input.Event == webhook_module.HookEventWorkflowRun {
|
|
wrun, ok := input.Payload.(*api.WorkflowRunPayload)
|
|
for i := 0; i < 5 && ok && wrun.WorkflowRun != nil; i++ {
|
|
if wrun.WorkflowRun.Event != "workflow_run" {
|
|
return false
|
|
}
|
|
r, err := actions_model.GetRunByRepoAndID(ctx, input.Repo.ID, wrun.WorkflowRun.ID)
|
|
if err != nil {
|
|
log.Error("GetRunByRepoAndID: %v", err)
|
|
return true
|
|
}
|
|
wrun, err = r.GetWorkflowRunEventPayload()
|
|
if err != nil {
|
|
log.Error("GetWorkflowRunEventPayload: %v", err)
|
|
return true
|
|
}
|
|
}
|
|
// skip workflow runs events exceeding the maximum of 5 recursive events
|
|
log.Debug("repo %s: skipped workflow_run because of recursive event of 5", input.Repo.RelativePath())
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func handleWorkflows(
|
|
ctx context.Context,
|
|
detectedWorkflows []*actions_module.DetectedWorkflow,
|
|
commit *git.Commit,
|
|
input *notifyInput,
|
|
ref git.RefName,
|
|
) error {
|
|
if len(detectedWorkflows) == 0 {
|
|
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RelativePath(), 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: commit.MessageTitle(),
|
|
RepoID: input.Repo.ID,
|
|
Repo: input.Repo,
|
|
OwnerID: input.Repo.OwnerID,
|
|
WorkflowID: dwf.EntryName,
|
|
TriggerUserID: input.Doer.ID,
|
|
TriggerUser: input.Doer,
|
|
Ref: ref.String(),
|
|
CommitSHA: commit.ID.String(),
|
|
IsForkPullRequest: isForkPullRequest,
|
|
Event: input.Event,
|
|
EventPayload: string(p),
|
|
TriggerEvent: dwf.TriggerEvent.Name,
|
|
Status: actions_model.StatusWaiting,
|
|
}
|
|
|
|
need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer)
|
|
if err != nil {
|
|
log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err)
|
|
continue
|
|
}
|
|
|
|
run.NeedApproval = need
|
|
|
|
if err := prepareRunAndInsertWithRetry(ctx, dwf.Content, run); err != nil {
|
|
log.Error("PrepareRunAndInsert: %v", err)
|
|
continue
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// prepareRunAndInsertWithRetry wraps PrepareRunAndInsert with retries on
|
|
// database deadlocks. When multiple workflow runs are inserted for the same
|
|
// event (e.g. several workflows triggered by a single pull_request), each
|
|
// InsertRun transaction acquires an X-lock on the repository row (via
|
|
// UpdateRepoRunsNumbers) and an index lock on action_run. Two concurrent
|
|
// transactions can deadlock when each holds one lock and waits for the other.
|
|
// InnoDB resolves this by killing the lighter transaction, but handleWorkflows
|
|
// only logged the error and moved on — silently dropping the workflow run.
|
|
// Retrying the insert is safe because the rolled-back transaction left no
|
|
// partial state.
|
|
func prepareRunAndInsertWithRetry(ctx context.Context, content []byte, run *actions_model.ActionRun) error {
|
|
const maxRetries = 3
|
|
backoff := 50 * time.Millisecond
|
|
|
|
// Save original values that InsertRun mutates inside its transaction.
|
|
// On deadlock rollback these become stale and must be reset before retry.
|
|
origTitle := run.Title
|
|
|
|
var err error
|
|
for attempt := range maxRetries {
|
|
if err = PrepareRunAndInsert(ctx, content, run, nil); err == nil {
|
|
return nil
|
|
}
|
|
if !db.IsErrDeadlock(err) {
|
|
return err
|
|
}
|
|
log.Warn("PrepareRunAndInsert deadlock (attempt %d/%d) for workflow %s in repo %d, retrying: %v",
|
|
attempt+1, maxRetries, run.WorkflowID, run.RepoID, err)
|
|
|
|
// Reset fields that InsertRun sets inside the (now rolled-back) transaction
|
|
// so the next attempt starts clean.
|
|
run.ID = 0
|
|
run.Index = 0
|
|
run.Status = actions_model.StatusWaiting
|
|
run.Title = origTitle
|
|
run.ConcurrencyGroup = ""
|
|
run.ConcurrencyCancel = false
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(backoff):
|
|
}
|
|
backoff *= 2
|
|
}
|
|
return fmt.Errorf("deadlock persisted after %d retries: %w", maxRetries, err)
|
|
}
|
|
|
|
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.GetDoerRepoPermission(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.GetDoerRepoPermission(ctx, repo, user); err != nil {
|
|
return false, fmt.Errorf("GetDoerRepoPermission: %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 := db.Count[actions_model.ActionRun](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 git.RefName,
|
|
) error {
|
|
if ref.BranchName() != input.Repo.DefaultBranch {
|
|
log.Trace("commit branch is not default branch in repo")
|
|
return nil
|
|
}
|
|
|
|
if count, err := db.Count[actions_model.ActionSchedule](ctx, actions_model.FindScheduleOptions{RepoID: input.Repo.ID}); err != nil {
|
|
log.Error("CountSchedules: %v", err)
|
|
return err
|
|
} else if count > 0 {
|
|
if err := 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.RelativePath(), 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: commit.MessageTitle(),
|
|
RepoID: input.Repo.ID,
|
|
Repo: input.Repo,
|
|
OwnerID: input.Repo.OwnerID,
|
|
WorkflowID: dwf.EntryName,
|
|
TriggerUserID: user_model.ActionsUserID,
|
|
TriggerUser: user_model.NewActionsUser(),
|
|
Ref: ref.String(),
|
|
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 {
|
|
if repo.IsEmpty || repo.IsArchived {
|
|
return nil
|
|
}
|
|
|
|
gitRepo, err := gitrepo.OpenRepository(context.Background(), repo)
|
|
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
|
|
// if repo is a mirror, commit author maybe an external user,
|
|
// so we use action user as the Doer of the notifyInput
|
|
notifyInput := newNotifyInputForSchedules(repo)
|
|
|
|
return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, git.RefNameFromBranch(repo.DefaultBranch))
|
|
}
|