Skip to content

Commit 5f0a75e

Browse files
committed
fix actions schedule
1 parent cddff73 commit 5f0a75e

File tree

2 files changed

+305
-12
lines changed

2 files changed

+305
-12
lines changed

services/actions/notifier_helper.go

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

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

242-
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref.String())
242+
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
243243
}
244244

245245
func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) bool {
@@ -291,7 +291,7 @@ func handleWorkflows(
291291
detectedWorkflows []*actions_module.DetectedWorkflow,
292292
commit *git.Commit,
293293
input *notifyInput,
294-
ref string,
294+
ref git.RefName,
295295
) error {
296296
if len(detectedWorkflows) == 0 {
297297
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RelativePath(), commit.ID)
@@ -327,7 +327,7 @@ func handleWorkflows(
327327
WorkflowID: dwf.EntryName,
328328
TriggerUserID: input.Doer.ID,
329329
TriggerUser: input.Doer,
330-
Ref: ref,
330+
Ref: ref.String(),
331331
CommitSHA: commit.ID.String(),
332332
IsForkPullRequest: isForkPullRequest,
333333
Event: input.Event,
@@ -442,13 +442,9 @@ func handleSchedules(
442442
detectedWorkflows []*actions_module.DetectedWorkflow,
443443
commit *git.Commit,
444444
input *notifyInput,
445-
ref string,
445+
ref git.RefName,
446446
) error {
447-
branch, err := commit.GetBranchName()
448-
if err != nil {
449-
return err
450-
}
451-
if branch != input.Repo.DefaultBranch {
447+
if ref.BranchName() != input.Repo.DefaultBranch {
452448
log.Trace("commit branch is not default branch in repo")
453449
return nil
454450
}
@@ -494,7 +490,7 @@ func handleSchedules(
494490
WorkflowID: dwf.EntryName,
495491
TriggerUserID: user_model.ActionsUserID,
496492
TriggerUser: user_model.NewActionsUser(),
497-
Ref: ref,
493+
Ref: ref.String(),
498494
CommitSHA: commit.ID.String(),
499495
Event: input.Event,
500496
EventPayload: string(p),
@@ -538,5 +534,5 @@ func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository)
538534
// so we use action user as the Doer of the notifyInput
539535
notifyInput := newNotifyInputForSchedules(repo)
540536

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

0 commit comments

Comments
 (0)