Merge pull request '[ACTIONS] port scheduled actions from Gitea' (#2826) from earl-warren/forgejo:wip-gitea-schedule into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2826
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-03-27 06:23:56 +00:00
commit ac672fc3ff
21 changed files with 294 additions and 135 deletions

View file

@ -329,7 +329,6 @@ package "code.gitea.io/gitea/services/pull"
package "code.gitea.io/gitea/services/repository" package "code.gitea.io/gitea/services/repository"
func IsErrForkAlreadyExist func IsErrForkAlreadyExist
func UpdateRepositoryUnits
package "code.gitea.io/gitea/services/repository/archiver" package "code.gitea.io/gitea/services/repository/archiver"
func ArchiveRepository func ArchiveRepository

View file

@ -170,14 +170,16 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
return err return err
} }
// CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow. // CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string) error { // It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
// Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'. func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
// Find all runs in the specified repository, reference, and workflow with non-final status
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{ runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
RepoID: repoID, RepoID: repoID,
Ref: ref, Ref: ref,
WorkflowID: workflowID, WorkflowID: workflowID,
Status: []Status{StatusRunning, StatusWaiting}, TriggerEvent: event,
Status: []Status{StatusRunning, StatusWaiting, StatusBlocked},
}) })
if err != nil { if err != nil {
return err return err

View file

@ -10,6 +10,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
webhook_module "code.gitea.io/gitea/modules/webhook"
"xorm.io/builder" "xorm.io/builder"
) )
@ -71,6 +72,7 @@ type FindRunOptions struct {
WorkflowID string WorkflowID string
Ref string // the commit/tag/… that caused this workflow Ref string // the commit/tag/… that caused this workflow
TriggerUserID int64 TriggerUserID int64
TriggerEvent webhook_module.HookEventType
Approved bool // not util.OptionalBool, it works only when it's true Approved bool // not util.OptionalBool, it works only when it's true
Status []Status Status []Status
} }
@ -98,6 +100,9 @@ func (opts FindRunOptions) ToConds() builder.Cond {
if opts.Ref != "" { if opts.Ref != "" {
cond = cond.And(builder.Eq{"ref": opts.Ref}) cond = cond.And(builder.Eq{"ref": opts.Ref})
} }
if opts.TriggerEvent != "" {
cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent})
}
return cond return cond
} }

View file

@ -5,6 +5,7 @@ package actions
import ( import (
"context" "context"
"fmt"
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -118,3 +119,22 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
return committer.Commit() return committer.Commit()
} }
func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error {
// If actions disabled when there is schedule task, this will remove the outdated schedule tasks
// There is no other place we can do this because the app.ini will be changed manually
if err := DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
return fmt.Errorf("DeleteCronTaskByRepo: %v", err)
}
// cancel running cron jobs of this repository and delete old schedules
if err := CancelPreviousJobs(
ctx,
repo.ID,
repo.DefaultBranch,
"",
webhook_module.HookEventSchedule,
); err != nil {
return fmt.Errorf("CancelPreviousJobs: %v", err)
}
return nil
}

View file

@ -292,7 +292,7 @@ func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *
} }
// RenameBranch rename a branch // RenameBranch rename a branch
func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(isDefault bool) error) (err error) { func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(ctx context.Context, isDefault bool) error) (err error) {
ctx, committer, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return err return err
@ -367,7 +367,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
} }
// 5. do git action // 5. do git action
if err = gitAction(isDefault); err != nil { if err = gitAction(ctx, isDefault); err != nil {
return err return err
} }

View file

@ -4,6 +4,7 @@
package git_test package git_test
import ( import (
"context"
"testing" "testing"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -132,7 +133,7 @@ func TestRenameBranch(t *testing.T) {
}, git_model.WhitelistOptions{})) }, git_model.WhitelistOptions{}))
assert.NoError(t, committer.Commit()) assert.NoError(t, committer.Commit())
assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(isDefault bool) error { assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(ctx context.Context, isDefault bool) error {
_isDefault = isDefault _isDefault = isDefault
return nil return nil
})) }))

View file

@ -316,29 +316,3 @@ func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error {
_, err := db.GetEngine(ctx).ID(unit.ID).Update(unit) _, err := db.GetEngine(ctx).ID(unit.ID).Update(unit)
return err return err
} }
// UpdateRepositoryUnits updates a repository's units
func UpdateRepositoryUnits(ctx context.Context, repo *Repository, units []RepoUnit, deleteUnitTypes []unit.Type) (err error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
// Delete existing settings of units before adding again
for _, u := range units {
deleteUnitTypes = append(deleteUnitTypes, u.Type)
}
if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(RepoUnit)); err != nil {
return err
}
if len(units) > 0 {
if err = db.Insert(ctx, units); err != nil {
return err
}
}
return committer.Commit()
}

View file

@ -74,9 +74,6 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
case GithubEventGollum: case GithubEventGollum:
return triggedEvent == webhook_module.HookEventWiki return triggedEvent == webhook_module.HookEventWiki
case GithubEventSchedule:
return triggedEvent == webhook_module.HookEventSchedule
case GithubEventIssues: case GithubEventIssues:
switch triggedEvent { switch triggedEvent {
case webhook_module.HookEventIssues, case webhook_module.HookEventIssues,
@ -119,6 +116,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
return triggedEvent == webhook_module.HookEventIssueComment || return triggedEvent == webhook_module.HookEventIssueComment ||
triggedEvent == webhook_module.HookEventPullRequestComment triggedEvent == webhook_module.HookEventPullRequestComment
case GithubEventSchedule:
return triggedEvent == webhook_module.HookEventSchedule
default: default:
return eventName == string(triggedEvent) return eventName == string(triggedEvent)
} }

View file

@ -22,7 +22,7 @@ import (
type DetectedWorkflow struct { type DetectedWorkflow struct {
EntryName string EntryName string
TriggerEvent string TriggerEvent *jobparser.Event
Content []byte Content []byte
} }
@ -103,6 +103,7 @@ func DetectWorkflows(
commit *git.Commit, commit *git.Commit,
triggedEvent webhook_module.HookEventType, triggedEvent webhook_module.HookEventType,
payload api.Payloader, payload api.Payloader,
detectSchedule bool,
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { ) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
entries, err := ListWorkflows(commit) entries, err := ListWorkflows(commit)
if err != nil { if err != nil {
@ -117,6 +118,7 @@ func DetectWorkflows(
return nil, nil, err return nil, nil, err
} }
// one workflow may have multiple events
events, err := GetEventsFromContent(content) events, err := GetEventsFromContent(content)
if err != nil { if err != nil {
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
@ -125,17 +127,18 @@ func DetectWorkflows(
for _, evt := range events { for _, evt := range events {
log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent) log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent)
if evt.IsSchedule() { if evt.IsSchedule() {
dwf := &DetectedWorkflow{ if detectSchedule {
EntryName: entry.Name(), dwf := &DetectedWorkflow{
TriggerEvent: evt.Name, EntryName: entry.Name(),
Content: content, TriggerEvent: evt,
Content: content,
}
schedules = append(schedules, dwf)
} }
schedules = append(schedules, dwf) } else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
}
if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
dwf := &DetectedWorkflow{ dwf := &DetectedWorkflow{
EntryName: entry.Name(), EntryName: entry.Name(),
TriggerEvent: evt.Name, TriggerEvent: evt,
Content: content, Content: content,
} }
workflows = append(workflows, dwf) workflows = append(workflows, dwf)
@ -146,6 +149,41 @@ func DetectWorkflows(
return workflows, schedules, nil return workflows, schedules, nil
} }
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
entries, err := ListWorkflows(commit)
if err != nil {
return nil, err
}
wfs := make([]*DetectedWorkflow, 0, len(entries))
for _, entry := range entries {
content, err := GetContentFromEntry(entry)
if err != nil {
return nil, err
}
// one workflow may have multiple events
events, err := GetEventsFromContent(content)
if err != nil {
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
continue
}
for _, evt := range events {
if evt.IsSchedule() {
log.Trace("detect scheduled workflow: %q", entry.Name())
dwf := &DetectedWorkflow{
EntryName: entry.Name(),
TriggerEvent: evt,
Content: content,
}
wfs = append(wfs, dwf)
}
}
}
return wfs, nil
}
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
if !canGithubEventMatch(evt.Name, triggedEvent) { if !canGithubEventMatch(evt.Name, triggedEvent) {
return false return false
@ -153,11 +191,11 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
switch triggedEvent { switch triggedEvent {
case // events with no activity types case // events with no activity types
webhook_module.HookEventSchedule,
webhook_module.HookEventCreate, webhook_module.HookEventCreate,
webhook_module.HookEventDelete, webhook_module.HookEventDelete,
webhook_module.HookEventFork, webhook_module.HookEventFork,
webhook_module.HookEventWiki: webhook_module.HookEventWiki,
webhook_module.HookEventSchedule:
if len(evt.Acts()) != 0 { if len(evt.Acts()) != 0 {
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts()) log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
} }

View file

@ -1001,7 +1001,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
} }
if len(units)+len(deleteUnitTypes) > 0 { if len(units)+len(deleteUnitTypes) > 0 {
if err := repo_model.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err) ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err)
return err return err
} }

View file

@ -203,7 +203,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi
} }
return &api.WikiPage{ return &api.WikiPage{
WikiPageMetaData: convert.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository), WikiPageMetaData: wiki_service.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository),
ContentBase64: content, ContentBase64: content,
CommitCount: commitsCount, CommitCount: commitsCount,
Sidebar: sidebarContent, Sidebar: sidebarContent,
@ -333,7 +333,7 @@ func ListWikiPages(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "WikiFilenameToName", err) ctx.Error(http.StatusInternalServerError, "WikiFilenameToName", err)
return return
} }
pages = append(pages, convert.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository)) pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository))
} }
ctx.SetTotalCountHeader(int64(len(entries))) ctx.SetTotalCountHeader(int64(len(entries)))

View file

@ -6,14 +6,12 @@ package setting
import ( import (
"net/http" "net/http"
repo_model "code.gitea.io/gitea/models/repo" git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify" repo_service "code.gitea.io/gitea/services/repository"
) )
// SetDefaultBranchPost set default branch // SetDefaultBranchPost set default branch
@ -36,23 +34,14 @@ func SetDefaultBranchPost(ctx *context.Context) {
} }
branch := ctx.FormString("branch") branch := ctx.FormString("branch")
if !ctx.Repo.GitRepo.IsBranchExist(branch) { if err := repo_service.SetRepoDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, branch); err != nil {
ctx.Status(http.StatusNotFound) switch {
return case git_model.IsErrBranchNotExist(err):
} else if repo.DefaultBranch != branch { ctx.Status(http.StatusNotFound)
repo.DefaultBranch = branch default:
if err := gitrepo.SetDefaultBranch(ctx, repo, branch); err != nil {
if !git.IsErrUnsupportedVersion(err) {
ctx.ServerError("SetDefaultBranch", err)
return
}
}
if err := repo_model.UpdateDefaultBranch(ctx, repo); err != nil {
ctx.ServerError("SetDefaultBranch", err) ctx.ServerError("SetDefaultBranch", err)
return
} }
return
notify_service.ChangeDefaultBranch(ctx, repo)
} }
log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)

View file

@ -269,7 +269,7 @@ func UnitsPost(ctx *context.Context) {
return return
} }
if err := repo_model.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil {
ctx.ServerError("UpdateRepositoryUnits", err) ctx.ServerError("UpdateRepositoryUnits", err)
return return
} }

View file

@ -123,6 +123,9 @@ func notify(ctx context.Context, input *notifyInput) error {
return nil return nil
} }
if unit_model.TypeActions.UnitGlobalDisabled() { if unit_model.TypeActions.UnitGlobalDisabled() {
if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
log.Error("CleanRepoScheduleTasks: %v", err)
}
return nil return nil
} }
if err := input.Repo.LoadUnits(ctx); err != nil { if err := input.Repo.LoadUnits(ctx); err != nil {
@ -167,23 +170,32 @@ func notify(ctx context.Context, input *notifyInput) error {
var detectedWorkflows []*actions_module.DetectedWorkflow var detectedWorkflows []*actions_module.DetectedWorkflow
actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig() actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, input.Event, input.Payload) shouldDetectSchedules := input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch
workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit,
input.Event,
input.Payload,
shouldDetectSchedules,
)
if err != nil { if err != nil {
return fmt.Errorf("DetectWorkflows: %w", err) return fmt.Errorf("DetectWorkflows: %w", err)
} }
if len(workflows) == 0 { log.Trace("repo %s with commit %s event %s find %d workflows and %d schedules",
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID) input.Repo.RepoPath(),
} else { commit.ID,
for _, wf := range workflows { input.Event,
if actionsConfig.IsWorkflowDisabled(wf.EntryName) { len(workflows),
log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName) len(schedules),
continue )
}
if wf.TriggerEvent != actions_module.GithubEventPullRequestTarget { for _, wf := range workflows {
detectedWorkflows = append(detectedWorkflows, wf) 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)
} }
} }
@ -200,7 +212,7 @@ func notify(ctx context.Context, input *notifyInput) error {
} }
return fmt.Errorf("gitRepo.GetCommit: %w", err) return fmt.Errorf("gitRepo.GetCommit: %w", err)
} }
baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload) baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload, false)
if err != nil { if err != nil {
return fmt.Errorf("DetectWorkflows: %w", err) return fmt.Errorf("DetectWorkflows: %w", err)
} }
@ -208,15 +220,17 @@ func notify(ctx context.Context, input *notifyInput) error {
log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.RepoPath(), baseCommit.ID) log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.RepoPath(), baseCommit.ID)
} else { } else {
for _, wf := range baseWorkflows { for _, wf := range baseWorkflows {
if wf.TriggerEvent == actions_module.GithubEventPullRequestTarget { if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget {
detectedWorkflows = append(detectedWorkflows, wf) detectedWorkflows = append(detectedWorkflows, wf)
} }
} }
} }
} }
if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil { if shouldDetectSchedules {
return err if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
return err
}
} }
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref) return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
@ -307,7 +321,7 @@ func handleWorkflows(
IsForkPullRequest: isForkPullRequest, IsForkPullRequest: isForkPullRequest,
Event: input.Event, Event: input.Event,
EventPayload: string(p), EventPayload: string(p),
TriggerEvent: dwf.TriggerEvent, TriggerEvent: dwf.TriggerEvent.Name,
Status: actions_model.StatusWaiting, Status: actions_model.StatusWaiting,
} }
if need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer); err != nil { if need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer); err != nil {
@ -334,16 +348,17 @@ func handleWorkflows(
continue continue
} }
// cancel running jobs if the event is push // cancel running jobs if the event is push or pull_request_sync
if run.Event == webhook_module.HookEventPush { if run.Event == webhook_module.HookEventPush ||
// cancel running jobs of the same workflow run.Event == webhook_module.HookEventPullRequestSync {
if err := actions_model.CancelRunningJobs( if err := actions_model.CancelPreviousJobs(
ctx, ctx,
run.RepoID, run.RepoID,
run.Ref, run.Ref,
run.WorkflowID, run.WorkflowID,
run.Event,
); err != nil { ); err != nil {
log.Error("CancelRunningJobs: %v", err) log.Error("CancelPreviousJobs: %v", err)
} }
} }
@ -467,8 +482,8 @@ func handleSchedules(
log.Error("CountSchedules: %v", err) log.Error("CountSchedules: %v", err)
return err return err
} else if count > 0 { } else if count > 0 {
if err := actions_model.DeleteScheduleTaskByRepo(ctx, input.Repo.ID); err != nil { if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
log.Error("DeleteCronTaskByRepo: %v", err) log.Error("CleanRepoScheduleTasks: %v", err)
} }
} }
@ -508,26 +523,49 @@ func handleSchedules(
TriggerUserID: input.Doer.ID, TriggerUserID: input.Doer.ID,
Ref: input.Repo.DefaultBranch, Ref: input.Repo.DefaultBranch,
CommitSHA: commit.ID.String(), CommitSHA: commit.ID.String(),
Event: webhook_module.HookEventType(api.HookScheduleCreated), Event: input.Event,
EventPayload: string(p), EventPayload: string(p),
Specs: schedules, Specs: schedules,
Content: dwf.Content, Content: dwf.Content,
} }
// 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,
); err != nil {
log.Error("CancelRunningJobs: %v", err)
}
}
crons = append(crons, run) crons = append(crons, run)
} }
return actions_model.CreateScheduleTask(ctx, crons) 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 {
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
// 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)
}

View file

@ -10,6 +10,7 @@ import (
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
@ -54,18 +55,26 @@ func startTasks(ctx context.Context) error {
// cancel running jobs if the event is push // cancel running jobs if the event is push
if row.Schedule.Event == webhook_module.HookEventPush { if row.Schedule.Event == webhook_module.HookEventPush {
// cancel running jobs of the same workflow // cancel running jobs of the same workflow
if err := actions_model.CancelRunningJobs( if err := actions_model.CancelPreviousJobs(
ctx, ctx,
row.RepoID, row.RepoID,
row.Schedule.Ref, row.Schedule.Ref,
row.Schedule.WorkflowID, row.Schedule.WorkflowID,
webhook_module.HookEventSchedule,
); err != nil { ); err != nil {
log.Error("CancelRunningJobs: %v", err) log.Error("CancelPreviousJobs: %v", err)
} }
} }
cfg := row.Repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() cfg, err := row.Repo.GetUnit(ctx, unit.TypeActions)
if cfg.IsWorkflowDisabled(row.Schedule.WorkflowID) { if err != nil {
if repo_model.IsErrUnitTypeNotExist(err) {
// Skip the actions unit of this repo is disabled.
continue
}
return fmt.Errorf("GetUnit: %w", err)
}
if cfg.ActionsConfig().IsWorkflowDisabled(row.Schedule.WorkflowID) {
continue continue
} }
@ -112,8 +121,8 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
Ref: cron.Ref, Ref: cron.Ref,
CommitSHA: cron.CommitSHA, CommitSHA: cron.CommitSHA,
Event: cron.Event, Event: cron.Event,
TriggerEvent: string(webhook_module.HookEventSchedule),
EventPayload: cron.EventPayload, EventPayload: cron.EventPayload,
TriggerEvent: string(webhook_module.HookEventSchedule),
ScheduleID: cron.ID, ScheduleID: cron.ID,
Status: actions_model.StatusWaiting, Status: actions_model.StatusWaiting,
} }

View file

@ -6,11 +6,8 @@ package convert
import ( import (
"time" "time"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
wiki_service "code.gitea.io/gitea/services/wiki"
) )
// ToWikiCommit convert a git commit into a WikiCommit // ToWikiCommit convert a git commit into a WikiCommit
@ -46,15 +43,3 @@ func ToWikiCommitList(commits []*git.Commit, total int64) *api.WikiCommitList {
Count: total, Count: total,
} }
} }
// ToWikiPageMetaData converts meta information to a WikiPageMetaData
func ToWikiPageMetaData(wikiName wiki_service.WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData {
subURL := string(wikiName)
_, title := wiki_service.WebPathToUserTitle(wikiName)
return &api.WikiPageMetaData{
Title: title,
HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", subURL),
SubURL: subURL,
LastCommit: ToWikiCommit(lastCommit),
}
}

View file

@ -10,6 +10,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
@ -23,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/queue"
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
webhook_module "code.gitea.io/gitea/modules/webhook"
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
files_service "code.gitea.io/gitea/services/repository/files" files_service "code.gitea.io/gitea/services/repository/files"
@ -359,13 +361,28 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
return "from_not_exist", nil return "from_not_exist", nil
} }
if err := git_model.RenameBranch(ctx, repo, from, to, func(isDefault bool) error { if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error {
err2 := gitRepo.RenameBranch(from, to) err2 := gitRepo.RenameBranch(from, to)
if err2 != nil { if err2 != nil {
return err2 return err2
} }
if isDefault { if isDefault {
// if default branch changed, we need to delete all schedules and cron jobs
if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
log.Error("DeleteCronTaskByRepo: %v", err)
}
// cancel running cron jobs of this repository and delete old schedules
if err := actions_model.CancelPreviousJobs(
ctx,
repo.ID,
from,
"",
webhook_module.HookEventSchedule,
); err != nil {
log.Error("CancelPreviousJobs: %v", err)
}
err2 = gitrepo.SetDefaultBranch(ctx, repo, to) err2 = gitrepo.SetDefaultBranch(ctx, repo, to)
if err2 != nil { if err2 != nil {
return err2 return err2
@ -498,3 +515,50 @@ func AddAllRepoBranchesToSyncQueue(ctx context.Context, doerID int64) error {
} }
return nil return nil
} }
func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, newBranchName string) error {
if repo.DefaultBranch == newBranchName {
return nil
}
if !gitRepo.IsBranchExist(newBranchName) {
return git_model.ErrBranchNotExist{
BranchName: newBranchName,
}
}
oldDefaultBranchName := repo.DefaultBranch
repo.DefaultBranch = newBranchName
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := repo_model.UpdateDefaultBranch(ctx, repo); err != nil {
return err
}
if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
log.Error("DeleteCronTaskByRepo: %v", err)
}
// cancel running cron jobs of this repository and delete old schedules
if err := actions_model.CancelPreviousJobs(
ctx,
repo.ID,
oldDefaultBranchName,
"",
webhook_module.HookEventSchedule,
); err != nil {
log.Error("CancelPreviousJobs: %v", err)
}
if err := gitrepo.SetDefaultBranch(ctx, repo, newBranchName); err != nil {
if !git.IsErrUnsupportedVersion(err) {
return err
}
}
return nil
}); err != nil {
return err
}
notify_service.ChangeDefaultBranch(ctx, repo)
return nil
}

View file

@ -5,10 +5,14 @@ package repository
import ( import (
"context" "context"
"slices"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log"
actions_service "code.gitea.io/gitea/services/actions"
) )
// UpdateRepositoryUnits updates a repository's units // UpdateRepositoryUnits updates a repository's units
@ -24,6 +28,21 @@ func UpdateRepositoryUnits(ctx context.Context, repo *repo_model.Repository, uni
deleteUnitTypes = append(deleteUnitTypes, u.Type) deleteUnitTypes = append(deleteUnitTypes, u.Type)
} }
if slices.Contains(deleteUnitTypes, unit.TypeActions) {
if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
log.Error("CleanRepoScheduleTasks: %v", err)
}
}
for _, u := range units {
if u.Type == unit.TypeActions {
if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
log.Error("DetectAndHandleSchedules: %v", err)
}
break
}
}
if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(repo_model.RepoUnit)); err != nil { if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(repo_model.RepoUnit)); err != nil {
return err return err
} }

View file

@ -21,6 +21,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/sync" "code.gitea.io/gitea/modules/sync"
asymkey_service "code.gitea.io/gitea/services/asymkey" asymkey_service "code.gitea.io/gitea/services/asymkey"
repo_service "code.gitea.io/gitea/services/repository"
) )
// TODO: use clustered lock (unique queue? or *abuse* cache) // TODO: use clustered lock (unique queue? or *abuse* cache)
@ -399,7 +400,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
// DeleteWiki removes the actual and local copy of repository wiki. // DeleteWiki removes the actual and local copy of repository wiki.
func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error { func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error {
if err := repo_model.UpdateRepositoryUnits(ctx, repo, nil, []unit.Type{unit.TypeWiki}); err != nil { if err := repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit.Type{unit.TypeWiki}); err != nil {
return err return err
} }

View file

@ -9,7 +9,10 @@ import (
"strings" "strings"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/convert"
) )
// To define the wiki related concepts: // To define the wiki related concepts:
@ -155,3 +158,15 @@ func UserTitleToWebPath(base, title string) WebPath {
} }
return WebPath(title) return WebPath(title)
} }
// ToWikiPageMetaData converts meta information to a WikiPageMetaData
func ToWikiPageMetaData(wikiName WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData {
subURL := string(wikiName)
_, title := WebPathToUserTitle(wikiName)
return &api.WikiPageMetaData{
Title: title,
HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", subURL),
SubURL: subURL,
LastCommit: convert.ToWikiCommit(lastCommit),
}
}

View file

@ -635,7 +635,7 @@ func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, en
} }
} }
err := repo_model.UpdateRepositoryUnits(db.DefaultContext, repo, units, disabledUnits) err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, units, disabledUnits)
assert.NoError(t, err) assert.NoError(t, err)
} }