Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions services/actions/notifier_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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))
}
294 changes: 294 additions & 0 deletions tests/integration/actions_schedule_test.go
Original file line number Diff line number Diff line change
@@ -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)
}