diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 66916e9301742..0f21310c984b2 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -236,12 +236,12 @@ func notify(ctx context.Context, input *notifyInput) error { } if shouldDetectSchedules { - if err := handleSchedules(ctx, schedules, commit, input, ref.String()); err != nil { + if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil { return err } } - return handleWorkflows(ctx, detectedWorkflows, commit, input, ref.String()) + return handleWorkflows(ctx, detectedWorkflows, commit, input, ref) } func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) bool { @@ -293,7 +293,7 @@ func handleWorkflows( detectedWorkflows []*actions_module.DetectedWorkflow, commit *git.Commit, input *notifyInput, - ref string, + ref git.RefName, ) error { if len(detectedWorkflows) == 0 { log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID) @@ -329,7 +329,7 @@ func handleWorkflows( WorkflowID: dwf.EntryName, TriggerUserID: input.Doer.ID, TriggerUser: input.Doer, - Ref: ref, + Ref: ref.String(), CommitSHA: commit.ID.String(), IsForkPullRequest: isForkPullRequest, Event: input.Event, @@ -500,13 +500,9 @@ func handleSchedules( detectedWorkflows []*actions_module.DetectedWorkflow, commit *git.Commit, input *notifyInput, - ref string, + ref git.RefName, ) error { - branch, err := commit.GetBranchName() - if err != nil { - return err - } - if branch != input.Repo.DefaultBranch { + if ref.BranchName() != input.Repo.DefaultBranch { log.Trace("commit branch is not default branch in repo") return nil } @@ -552,7 +548,7 @@ func handleSchedules( WorkflowID: dwf.EntryName, TriggerUserID: user_model.ActionsUserID, TriggerUser: user_model.NewActionsUser(), - Ref: ref, + Ref: ref.String(), CommitSHA: commit.ID.String(), Event: input.Event, EventPayload: string(p), @@ -614,5 +610,5 @@ func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) // so we use action user as the Doer of the notifyInput notifyInput := newNotifyInputForSchedules(repo) - return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch) + return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, git.RefNameFromBranch(repo.DefaultBranch)) } diff --git a/tests/integration/actions_schedule_test.go b/tests/integration/actions_schedule_test.go new file mode 100644 index 0000000000000..84da13fb20ae6 --- /dev/null +++ b/tests/integration/actions_schedule_test.go @@ -0,0 +1,294 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/url" + "strconv" + "strings" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/migration" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + mirror_service "code.gitea.io/gitea/services/mirror" + repo_service "code.gitea.io/gitea/services/repository" + files_service "code.gitea.io/gitea/services/repository/files" + + "github.com/stretchr/testify/assert" +) + +func TestScheduleUpdate(t *testing.T) { + t.Run("Push", testScheduleUpdatePush) + t.Run("PullMerge", testScheduleUpdatePullMerge) + t.Run("DisableAndEnableActionsUnit", testScheduleUpdateDisableAndEnableActionsUnit) + t.Run("ArchiveAndUnarchive", testScheduleUpdateArchiveAndUnarchive) + t.Run("MirrorSync", testScheduleUpdateMirrorSync) +} + +func testScheduleUpdatePush(t *testing.T) { + doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) { + newCron := "30 5 * * 1,3" + pushScheduleChange(t, u, repo, newCron) + branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch) + assert.NoError(t, err) + return branch.CommitID, newCron + }) +} + +func testScheduleUpdatePullMerge(t *testing.T) { + newBranchName := "feat1" + workflowTreePath := ".gitea/workflows/actions-schedule.yml" + workflowContent := `name: actions-schedule +on: + schedule: + - cron: '@every 2m' # update to 2m +jobs: + job: + runs-on: ubuntu-latest + steps: + - run: echo 'schedule workflow' +` + + mergeStyles := []repo_model.MergeStyle{ + repo_model.MergeStyleMerge, + repo_model.MergeStyleRebase, + repo_model.MergeStyleRebaseMerge, + repo_model.MergeStyleSquash, + repo_model.MergeStyleFastForwardOnly, + } + + for _, mergeStyle := range mergeStyles { + t.Run(string(mergeStyle), func(t *testing.T) { + doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) { + // update workflow file + _, err := files_service.ChangeRepoFiles(t.Context(), repo, user, &files_service.ChangeRepoFilesOptions{ + NewBranch: newBranchName, + Files: []*files_service.ChangeRepoFile{ + { + Operation: "update", + TreePath: workflowTreePath, + ContentReader: strings.NewReader(workflowContent), + }, + }, + Message: "update workflow schedule", + }) + assert.NoError(t, err) + + // create pull request + apiPull, err := doAPICreatePullRequest(testContext, repo.OwnerName, repo.Name, repo.DefaultBranch, newBranchName)(t) + assert.NoError(t, err) + + // merge pull request + testPullMerge(t, testContext.Session, repo.OwnerName, repo.Name, strconv.FormatInt(apiPull.Index, 10), mergeStyle, false) + + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID}) + return pull.MergedCommitID, "@every 2m" + }) + }) + } + + t.Run(string(repo_model.MergeStyleManuallyMerged), func(t *testing.T) { + doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) { + // enable manual-merge + doAPIEditRepository(testContext, &api.EditRepoOption{ + HasPullRequests: util.ToPointer(true), + AllowManualMerge: util.ToPointer(true), + })(t) + + // update workflow file + fileResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user, &files_service.ChangeRepoFilesOptions{ + NewBranch: newBranchName, + Files: []*files_service.ChangeRepoFile{ + { + Operation: "update", + TreePath: workflowTreePath, + ContentReader: strings.NewReader(workflowContent), + }, + }, + Message: "update workflow schedule", + }) + assert.NoError(t, err) + + // merge and push + dstPath := t.TempDir() + u.Path = repo.FullName() + ".git" + u.User = url.UserPassword(repo.OwnerName, userPassword) + doGitClone(dstPath, u)(t) + doGitMerge(dstPath, "origin/"+newBranchName)(t) + doGitPushTestRepository(dstPath, "origin", repo.DefaultBranch)(t) + + // create pull request + apiPull, err := doAPICreatePullRequest(testContext, repo.OwnerName, repo.Name, repo.DefaultBranch, newBranchName)(t) + assert.NoError(t, err) + + // merge pull request manually + doAPIManuallyMergePullRequest(testContext, repo.OwnerName, repo.Name, fileResp.Commit.SHA, apiPull.Index)(t) + + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID}) + assert.Equal(t, issues_model.PullRequestStatusManuallyMerged, pull.Status) + return pull.MergedCommitID, "@every 2m" + }) + }) +} + +func testScheduleUpdateMirrorSync(t *testing.T) { + doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) { + // create mirror repo + opts := migration.MigrateOptions{ + RepoName: "actions-schedule-mirror", + Description: "Test mirror for actions-schedule", + Private: false, + Mirror: true, + CloneAddr: repo.CloneLinkGeneral(t.Context()).HTTPS, + } + mirrorRepo, err := repo_service.CreateRepositoryDirectly(t.Context(), user, user, repo_service.CreateRepoOptions{ + Name: opts.RepoName, + Description: opts.Description, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + DefaultBranch: repo.DefaultBranch, + Status: repo_model.RepositoryBeingMigrated, + }, false) + assert.NoError(t, err) + assert.True(t, mirrorRepo.IsMirror) + mirrorRepo, err = repo_service.MigrateRepositoryGitData(t.Context(), user, mirrorRepo, opts, nil) + assert.NoError(t, err) + mirrorContext := NewAPITestContext(t, user.Name, mirrorRepo.Name, auth_model.AccessTokenScopeWriteRepository) + + // enable actions unit for mirror repo + assert.False(t, mirrorRepo.UnitEnabled(t.Context(), unit_model.TypeActions)) + doAPIEditRepository(mirrorContext, &api.EditRepoOption{ + HasActions: util.ToPointer(true), + })(t) + actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID}) + scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID}) + assert.Equal(t, "@every 1m", scheduleSpec.Spec) + + // update remote repo + newCron := "30 5,17 * * 2,4" + pushScheduleChange(t, u, repo, newCron) + repoDefaultBranch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch) + assert.NoError(t, err) + + // sync + ok := mirror_service.SyncPullMirror(t.Context(), mirrorRepo.ID) + assert.True(t, ok) + mirrorRepoDefaultBranch, err := git_model.GetBranch(t.Context(), mirrorRepo.ID, mirrorRepo.DefaultBranch) + assert.NoError(t, err) + assert.Equal(t, repoDefaultBranch.CommitID, mirrorRepoDefaultBranch.CommitID) + + // check updated schedule + actionSchedule = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID}) + scheduleSpec = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID}) + assert.Equal(t, newCron, scheduleSpec.Spec) + + return repoDefaultBranch.CommitID, newCron + }) +} + +func testScheduleUpdateArchiveAndUnarchive(t *testing.T) { + doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) { + doAPIEditRepository(testContext, &api.EditRepoOption{ + Archived: util.ToPointer(true), + })(t) + assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID})) + doAPIEditRepository(testContext, &api.EditRepoOption{ + Archived: util.ToPointer(false), + })(t) + branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch) + assert.NoError(t, err) + return branch.CommitID, "@every 1m" + }) +} + +func testScheduleUpdateDisableAndEnableActionsUnit(t *testing.T) { + doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) { + doAPIEditRepository(testContext, &api.EditRepoOption{ + HasActions: util.ToPointer(false), + })(t) + assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID})) + doAPIEditRepository(testContext, &api.EditRepoOption{ + HasActions: util.ToPointer(true), + })(t) + branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch) + assert.NoError(t, err) + return branch.CommitID, "@every 1m" + }) +} + +type scheduleUpdateTrigger func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) + +func doTestScheduleUpdate(t *testing.T, updateTrigger scheduleUpdateTrigger) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-schedule", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + assert.NoError(t, repo.LoadAttributes(t.Context())) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + wfTreePath := ".gitea/workflows/actions-schedule.yml" + wfFileContent := `name: actions-schedule +on: + schedule: + - cron: '@every 1m' +jobs: + job: + runs-on: ubuntu-latest + steps: + - run: echo 'schedule workflow' +` + + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent) + apiFileResp := createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts1) + + actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: repo.ID, CommitSHA: apiFileResp.Commit.SHA}) + scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID, ScheduleID: actionSchedule.ID}) + assert.Equal(t, "@every 1m", scheduleSpec.Spec) + + commitID, expectedSpec := updateTrigger(t, u, httpContext, user2, repo) + + actionSchedule = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: repo.ID, CommitSHA: commitID}) + scheduleSpec = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID, ScheduleID: actionSchedule.ID}) + assert.Equal(t, expectedSpec, scheduleSpec.Spec) + }) +} + +func pushScheduleChange(t *testing.T, u *url.URL, repo *repo_model.Repository, newCron string) { + workflowTreePath := ".gitea/workflows/actions-schedule.yml" + workflowContent := `name: actions-schedule +on: + schedule: + - cron: '` + newCron + `' +jobs: + job: + runs-on: ubuntu-latest + steps: + - run: echo 'schedule workflow' +` + + dstPath := t.TempDir() + u.Path = repo.FullName() + ".git" + u.User = url.UserPassword(repo.OwnerName, userPassword) + doGitClone(dstPath, u)(t) + doGitCheckoutWriteFileCommit(localGitAddCommitOptions{ + LocalRepoPath: dstPath, + CheckoutBranch: repo.DefaultBranch, + TreeFilePath: workflowTreePath, + TreeFileContent: workflowContent, + })(t) + doGitPushTestRepository(dstPath, "origin", repo.DefaultBranch)(t) +}