Skip to content

Commit f85cd7a

Browse files
GiteaBotZettat123wxiaoguang
authored
Fix actions schedule update issue (#35767) (#35774)
Backport #35767 by @Zettat123 Fix #34472 Add integration tests for actions schedule update. --------- Co-authored-by: Zettat123 <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent cf644d5 commit f85cd7a

File tree

2 files changed

+302
-12
lines changed

2 files changed

+302
-12
lines changed

services/actions/notifier_helper.go

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,12 @@ func notify(ctx context.Context, input *notifyInput) error {
236236
}
237237

238238
if shouldDetectSchedules {
239-
if err := handleSchedules(ctx, schedules, commit, input, ref.String()); err != nil {
239+
if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
240240
return err
241241
}
242242
}
243243

244-
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref.String())
244+
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
245245
}
246246

247247
func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) bool {
@@ -293,7 +293,7 @@ func handleWorkflows(
293293
detectedWorkflows []*actions_module.DetectedWorkflow,
294294
commit *git.Commit,
295295
input *notifyInput,
296-
ref string,
296+
ref git.RefName,
297297
) error {
298298
if len(detectedWorkflows) == 0 {
299299
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
@@ -329,7 +329,7 @@ func handleWorkflows(
329329
WorkflowID: dwf.EntryName,
330330
TriggerUserID: input.Doer.ID,
331331
TriggerUser: input.Doer,
332-
Ref: ref,
332+
Ref: ref.String(),
333333
CommitSHA: commit.ID.String(),
334334
IsForkPullRequest: isForkPullRequest,
335335
Event: input.Event,
@@ -500,13 +500,9 @@ func handleSchedules(
500500
detectedWorkflows []*actions_module.DetectedWorkflow,
501501
commit *git.Commit,
502502
input *notifyInput,
503-
ref string,
503+
ref git.RefName,
504504
) error {
505-
branch, err := commit.GetBranchName()
506-
if err != nil {
507-
return err
508-
}
509-
if branch != input.Repo.DefaultBranch {
505+
if ref.BranchName() != input.Repo.DefaultBranch {
510506
log.Trace("commit branch is not default branch in repo")
511507
return nil
512508
}
@@ -552,7 +548,7 @@ func handleSchedules(
552548
WorkflowID: dwf.EntryName,
553549
TriggerUserID: user_model.ActionsUserID,
554550
TriggerUser: user_model.NewActionsUser(),
555-
Ref: ref,
551+
Ref: ref.String(),
556552
CommitSHA: commit.ID.String(),
557553
Event: input.Event,
558554
EventPayload: string(p),
@@ -614,5 +610,5 @@ func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository)
614610
// so we use action user as the Doer of the notifyInput
615611
notifyInput := newNotifyInputForSchedules(repo)
616612

617-
return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch)
613+
return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, git.RefNameFromBranch(repo.DefaultBranch))
618614
}
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package integration
5+
6+
import (
7+
"net/url"
8+
"strconv"
9+
"strings"
10+
"testing"
11+
12+
actions_model "code.gitea.io/gitea/models/actions"
13+
auth_model "code.gitea.io/gitea/models/auth"
14+
git_model "code.gitea.io/gitea/models/git"
15+
issues_model "code.gitea.io/gitea/models/issues"
16+
repo_model "code.gitea.io/gitea/models/repo"
17+
unit_model "code.gitea.io/gitea/models/unit"
18+
"code.gitea.io/gitea/models/unittest"
19+
user_model "code.gitea.io/gitea/models/user"
20+
"code.gitea.io/gitea/modules/migration"
21+
api "code.gitea.io/gitea/modules/structs"
22+
"code.gitea.io/gitea/modules/util"
23+
mirror_service "code.gitea.io/gitea/services/mirror"
24+
repo_service "code.gitea.io/gitea/services/repository"
25+
files_service "code.gitea.io/gitea/services/repository/files"
26+
27+
"github.com/stretchr/testify/assert"
28+
)
29+
30+
func TestScheduleUpdate(t *testing.T) {
31+
t.Run("Push", testScheduleUpdatePush)
32+
t.Run("PullMerge", testScheduleUpdatePullMerge)
33+
t.Run("DisableAndEnableActionsUnit", testScheduleUpdateDisableAndEnableActionsUnit)
34+
t.Run("ArchiveAndUnarchive", testScheduleUpdateArchiveAndUnarchive)
35+
t.Run("MirrorSync", testScheduleUpdateMirrorSync)
36+
}
37+
38+
func testScheduleUpdatePush(t *testing.T) {
39+
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
40+
newCron := "30 5 * * 1,3"
41+
pushScheduleChange(t, u, repo, newCron)
42+
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
43+
assert.NoError(t, err)
44+
return branch.CommitID, newCron
45+
})
46+
}
47+
48+
func testScheduleUpdatePullMerge(t *testing.T) {
49+
newBranchName := "feat1"
50+
workflowTreePath := ".gitea/workflows/actions-schedule.yml"
51+
workflowContent := `name: actions-schedule
52+
on:
53+
schedule:
54+
- cron: '@every 2m' # update to 2m
55+
jobs:
56+
job:
57+
runs-on: ubuntu-latest
58+
steps:
59+
- run: echo 'schedule workflow'
60+
`
61+
62+
mergeStyles := []repo_model.MergeStyle{
63+
repo_model.MergeStyleMerge,
64+
repo_model.MergeStyleRebase,
65+
repo_model.MergeStyleRebaseMerge,
66+
repo_model.MergeStyleSquash,
67+
repo_model.MergeStyleFastForwardOnly,
68+
}
69+
70+
for _, mergeStyle := range mergeStyles {
71+
t.Run(string(mergeStyle), func(t *testing.T) {
72+
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
73+
// update workflow file
74+
_, err := files_service.ChangeRepoFiles(t.Context(), repo, user, &files_service.ChangeRepoFilesOptions{
75+
NewBranch: newBranchName,
76+
Files: []*files_service.ChangeRepoFile{
77+
{
78+
Operation: "update",
79+
TreePath: workflowTreePath,
80+
ContentReader: strings.NewReader(workflowContent),
81+
},
82+
},
83+
Message: "update workflow schedule",
84+
})
85+
assert.NoError(t, err)
86+
87+
// create pull request
88+
apiPull, err := doAPICreatePullRequest(testContext, repo.OwnerName, repo.Name, repo.DefaultBranch, newBranchName)(t)
89+
assert.NoError(t, err)
90+
91+
// merge pull request
92+
testPullMerge(t, testContext.Session, repo.OwnerName, repo.Name, strconv.FormatInt(apiPull.Index, 10), mergeStyle, false)
93+
94+
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
95+
return pull.MergedCommitID, "@every 2m"
96+
})
97+
})
98+
}
99+
100+
t.Run(string(repo_model.MergeStyleManuallyMerged), func(t *testing.T) {
101+
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
102+
// enable manual-merge
103+
doAPIEditRepository(testContext, &api.EditRepoOption{
104+
HasPullRequests: util.ToPointer(true),
105+
AllowManualMerge: util.ToPointer(true),
106+
})(t)
107+
108+
// update workflow file
109+
fileResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user, &files_service.ChangeRepoFilesOptions{
110+
NewBranch: newBranchName,
111+
Files: []*files_service.ChangeRepoFile{
112+
{
113+
Operation: "update",
114+
TreePath: workflowTreePath,
115+
ContentReader: strings.NewReader(workflowContent),
116+
},
117+
},
118+
Message: "update workflow schedule",
119+
})
120+
assert.NoError(t, err)
121+
122+
// merge and push
123+
dstPath := t.TempDir()
124+
u.Path = repo.FullName() + ".git"
125+
u.User = url.UserPassword(repo.OwnerName, userPassword)
126+
doGitClone(dstPath, u)(t)
127+
doGitMerge(dstPath, "origin/"+newBranchName)(t)
128+
doGitPushTestRepository(dstPath, "origin", repo.DefaultBranch)(t)
129+
130+
// create pull request
131+
apiPull, err := doAPICreatePullRequest(testContext, repo.OwnerName, repo.Name, repo.DefaultBranch, newBranchName)(t)
132+
assert.NoError(t, err)
133+
134+
// merge pull request manually
135+
doAPIManuallyMergePullRequest(testContext, repo.OwnerName, repo.Name, fileResp.Commit.SHA, apiPull.Index)(t)
136+
137+
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
138+
assert.Equal(t, issues_model.PullRequestStatusManuallyMerged, pull.Status)
139+
return pull.MergedCommitID, "@every 2m"
140+
})
141+
})
142+
}
143+
144+
func testScheduleUpdateMirrorSync(t *testing.T) {
145+
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
146+
// create mirror repo
147+
opts := migration.MigrateOptions{
148+
RepoName: "actions-schedule-mirror",
149+
Description: "Test mirror for actions-schedule",
150+
Private: false,
151+
Mirror: true,
152+
CloneAddr: repo.CloneLinkGeneral(t.Context()).HTTPS,
153+
}
154+
mirrorRepo, err := repo_service.CreateRepositoryDirectly(t.Context(), user, user, repo_service.CreateRepoOptions{
155+
Name: opts.RepoName,
156+
Description: opts.Description,
157+
IsPrivate: opts.Private,
158+
IsMirror: opts.Mirror,
159+
DefaultBranch: repo.DefaultBranch,
160+
Status: repo_model.RepositoryBeingMigrated,
161+
}, false)
162+
assert.NoError(t, err)
163+
assert.True(t, mirrorRepo.IsMirror)
164+
mirrorRepo, err = repo_service.MigrateRepositoryGitData(t.Context(), user, mirrorRepo, opts, nil)
165+
assert.NoError(t, err)
166+
mirrorContext := NewAPITestContext(t, user.Name, mirrorRepo.Name, auth_model.AccessTokenScopeWriteRepository)
167+
168+
// enable actions unit for mirror repo
169+
assert.False(t, mirrorRepo.UnitEnabled(t.Context(), unit_model.TypeActions))
170+
doAPIEditRepository(mirrorContext, &api.EditRepoOption{
171+
HasActions: util.ToPointer(true),
172+
})(t)
173+
actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID})
174+
scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID})
175+
assert.Equal(t, "@every 1m", scheduleSpec.Spec)
176+
177+
// update remote repo
178+
newCron := "30 5,17 * * 2,4"
179+
pushScheduleChange(t, u, repo, newCron)
180+
repoDefaultBranch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
181+
assert.NoError(t, err)
182+
183+
// sync
184+
ok := mirror_service.SyncPullMirror(t.Context(), mirrorRepo.ID)
185+
assert.True(t, ok)
186+
mirrorRepoDefaultBranch, err := git_model.GetBranch(t.Context(), mirrorRepo.ID, mirrorRepo.DefaultBranch)
187+
assert.NoError(t, err)
188+
assert.Equal(t, repoDefaultBranch.CommitID, mirrorRepoDefaultBranch.CommitID)
189+
190+
// check updated schedule
191+
actionSchedule = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID})
192+
scheduleSpec = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID})
193+
assert.Equal(t, newCron, scheduleSpec.Spec)
194+
195+
return repoDefaultBranch.CommitID, newCron
196+
})
197+
}
198+
199+
func testScheduleUpdateArchiveAndUnarchive(t *testing.T) {
200+
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
201+
doAPIEditRepository(testContext, &api.EditRepoOption{
202+
Archived: util.ToPointer(true),
203+
})(t)
204+
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
205+
doAPIEditRepository(testContext, &api.EditRepoOption{
206+
Archived: util.ToPointer(false),
207+
})(t)
208+
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
209+
assert.NoError(t, err)
210+
return branch.CommitID, "@every 1m"
211+
})
212+
}
213+
214+
func testScheduleUpdateDisableAndEnableActionsUnit(t *testing.T) {
215+
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
216+
doAPIEditRepository(testContext, &api.EditRepoOption{
217+
HasActions: util.ToPointer(false),
218+
})(t)
219+
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
220+
doAPIEditRepository(testContext, &api.EditRepoOption{
221+
HasActions: util.ToPointer(true),
222+
})(t)
223+
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
224+
assert.NoError(t, err)
225+
return branch.CommitID, "@every 1m"
226+
})
227+
}
228+
229+
type scheduleUpdateTrigger func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string)
230+
231+
func doTestScheduleUpdate(t *testing.T, updateTrigger scheduleUpdateTrigger) {
232+
onGiteaRun(t, func(t *testing.T, u *url.URL) {
233+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
234+
session := loginUser(t, user2.Name)
235+
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
236+
237+
apiRepo := createActionsTestRepo(t, token, "actions-schedule", false)
238+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
239+
assert.NoError(t, repo.LoadAttributes(t.Context()))
240+
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
241+
defer doAPIDeleteRepository(httpContext)(t)
242+
243+
wfTreePath := ".gitea/workflows/actions-schedule.yml"
244+
wfFileContent := `name: actions-schedule
245+
on:
246+
schedule:
247+
- cron: '@every 1m'
248+
jobs:
249+
job:
250+
runs-on: ubuntu-latest
251+
steps:
252+
- run: echo 'schedule workflow'
253+
`
254+
255+
opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent)
256+
apiFileResp := createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts1)
257+
258+
actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: repo.ID, CommitSHA: apiFileResp.Commit.SHA})
259+
scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID, ScheduleID: actionSchedule.ID})
260+
assert.Equal(t, "@every 1m", scheduleSpec.Spec)
261+
262+
commitID, expectedSpec := updateTrigger(t, u, httpContext, user2, repo)
263+
264+
actionSchedule = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: repo.ID, CommitSHA: commitID})
265+
scheduleSpec = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID, ScheduleID: actionSchedule.ID})
266+
assert.Equal(t, expectedSpec, scheduleSpec.Spec)
267+
})
268+
}
269+
270+
func pushScheduleChange(t *testing.T, u *url.URL, repo *repo_model.Repository, newCron string) {
271+
workflowTreePath := ".gitea/workflows/actions-schedule.yml"
272+
workflowContent := `name: actions-schedule
273+
on:
274+
schedule:
275+
- cron: '` + newCron + `'
276+
jobs:
277+
job:
278+
runs-on: ubuntu-latest
279+
steps:
280+
- run: echo 'schedule workflow'
281+
`
282+
283+
dstPath := t.TempDir()
284+
u.Path = repo.FullName() + ".git"
285+
u.User = url.UserPassword(repo.OwnerName, userPassword)
286+
doGitClone(dstPath, u)(t)
287+
doGitCheckoutWriteFileCommit(localGitAddCommitOptions{
288+
LocalRepoPath: dstPath,
289+
CheckoutBranch: repo.DefaultBranch,
290+
TreeFilePath: workflowTreePath,
291+
TreeFileContent: workflowContent,
292+
})(t)
293+
doGitPushTestRepository(dstPath, "origin", repo.DefaultBranch)(t)
294+
}

0 commit comments

Comments
 (0)