Skip to content

Commit 0d55f64

Browse files
lunnywolfogretechknowlogickwxiaoguang
authored
chore(actions): support cron schedule task (#26655)
Replace #22751 1. only support the default branch in the repository setting. 2. autoload schedule data from the schedule table after starting the service. 3. support specific syntax like `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly` ## How to use See the [GitHub Actions document](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule) for getting more detailed information. ```yaml on: schedule: - cron: '30 5 * * 1,3' - cron: '30 5 * * 2,4' jobs: test_schedule: runs-on: ubuntu-latest steps: - name: Not on Monday or Wednesday if: github.event.schedule != '30 5 * * 1,3' run: echo "This step will be skipped on Monday and Wednesday" - name: Every time run: echo "This step will always run" ``` Signed-off-by: Bo-Yi.Wu <[email protected]> --------- Co-authored-by: Jason Song <[email protected]> Co-authored-by: techknowlogick <[email protected]> Co-authored-by: wxiaoguang <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent b62c8e7 commit 0d55f64

File tree

13 files changed

+693
-9
lines changed

13 files changed

+693
-9
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ require (
9090
github.com/prometheus/client_golang v1.16.0
9191
github.com/quasoft/websspi v1.1.2
9292
github.com/redis/go-redis/v9 v9.0.5
93+
github.com/robfig/cron/v3 v3.0.1
9394
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
9495
github.com/sassoftware/go-rpmutils v0.2.0
9596
github.com/sergi/go-diff v1.3.1
@@ -254,7 +255,6 @@ require (
254255
github.com/rhysd/actionlint v1.6.25 // indirect
255256
github.com/rivo/uniseg v0.4.4 // indirect
256257
github.com/robfig/cron v1.2.0 // indirect
257-
github.com/robfig/cron/v3 v3.0.1 // indirect
258258
github.com/rogpeppe/go-internal v1.11.0 // indirect
259259
github.com/rs/xid v1.5.0 // indirect
260260
github.com/russross/blackfriday/v2 v2.1.0 // indirect

models/actions/schedule.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"context"
8+
"time"
9+
10+
"code.gitea.io/gitea/models/db"
11+
repo_model "code.gitea.io/gitea/models/repo"
12+
user_model "code.gitea.io/gitea/models/user"
13+
"code.gitea.io/gitea/modules/timeutil"
14+
webhook_module "code.gitea.io/gitea/modules/webhook"
15+
16+
"github.com/robfig/cron/v3"
17+
)
18+
19+
// ActionSchedule represents a schedule of a workflow file
20+
type ActionSchedule struct {
21+
ID int64
22+
Title string
23+
Specs []string
24+
RepoID int64 `xorm:"index"`
25+
Repo *repo_model.Repository `xorm:"-"`
26+
OwnerID int64 `xorm:"index"`
27+
WorkflowID string
28+
TriggerUserID int64
29+
TriggerUser *user_model.User `xorm:"-"`
30+
Ref string
31+
CommitSHA string
32+
Event webhook_module.HookEventType
33+
EventPayload string `xorm:"LONGTEXT"`
34+
Content []byte
35+
Created timeutil.TimeStamp `xorm:"created"`
36+
Updated timeutil.TimeStamp `xorm:"updated"`
37+
}
38+
39+
func init() {
40+
db.RegisterModel(new(ActionSchedule))
41+
}
42+
43+
// GetSchedulesMapByIDs returns the schedules by given id slice.
44+
func GetSchedulesMapByIDs(ids []int64) (map[int64]*ActionSchedule, error) {
45+
schedules := make(map[int64]*ActionSchedule, len(ids))
46+
return schedules, db.GetEngine(db.DefaultContext).In("id", ids).Find(&schedules)
47+
}
48+
49+
// GetReposMapByIDs returns the repos by given id slice.
50+
func GetReposMapByIDs(ids []int64) (map[int64]*repo_model.Repository, error) {
51+
repos := make(map[int64]*repo_model.Repository, len(ids))
52+
return repos, db.GetEngine(db.DefaultContext).In("id", ids).Find(&repos)
53+
}
54+
55+
var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
56+
57+
// CreateScheduleTask creates new schedule task.
58+
func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
59+
// Return early if there are no rows to insert
60+
if len(rows) == 0 {
61+
return nil
62+
}
63+
64+
// Begin transaction
65+
ctx, committer, err := db.TxContext(ctx)
66+
if err != nil {
67+
return err
68+
}
69+
defer committer.Close()
70+
71+
// Loop through each schedule row
72+
for _, row := range rows {
73+
// Create new schedule row
74+
if err = db.Insert(ctx, row); err != nil {
75+
return err
76+
}
77+
78+
// Loop through each schedule spec and create a new spec row
79+
now := time.Now()
80+
81+
for _, spec := range row.Specs {
82+
// Parse the spec and check for errors
83+
schedule, err := cronParser.Parse(spec)
84+
if err != nil {
85+
continue // skip to the next spec if there's an error
86+
}
87+
88+
// Insert the new schedule spec row
89+
if err = db.Insert(ctx, &ActionScheduleSpec{
90+
RepoID: row.RepoID,
91+
ScheduleID: row.ID,
92+
Spec: spec,
93+
Next: timeutil.TimeStamp(schedule.Next(now).Unix()),
94+
}); err != nil {
95+
return err
96+
}
97+
}
98+
}
99+
100+
// Commit transaction
101+
return committer.Commit()
102+
}
103+
104+
func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
105+
ctx, committer, err := db.TxContext(ctx)
106+
if err != nil {
107+
return err
108+
}
109+
defer committer.Close()
110+
111+
if _, err := db.GetEngine(ctx).Delete(&ActionSchedule{RepoID: id}); err != nil {
112+
return err
113+
}
114+
115+
if _, err := db.GetEngine(ctx).Delete(&ActionScheduleSpec{RepoID: id}); err != nil {
116+
return err
117+
}
118+
119+
return committer.Commit()
120+
}

models/actions/schedule_list.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"context"
8+
9+
"code.gitea.io/gitea/models/db"
10+
repo_model "code.gitea.io/gitea/models/repo"
11+
user_model "code.gitea.io/gitea/models/user"
12+
"code.gitea.io/gitea/modules/container"
13+
14+
"xorm.io/builder"
15+
)
16+
17+
type ScheduleList []*ActionSchedule
18+
19+
// GetUserIDs returns a slice of user's id
20+
func (schedules ScheduleList) GetUserIDs() []int64 {
21+
ids := make(container.Set[int64], len(schedules))
22+
for _, schedule := range schedules {
23+
ids.Add(schedule.TriggerUserID)
24+
}
25+
return ids.Values()
26+
}
27+
28+
func (schedules ScheduleList) GetRepoIDs() []int64 {
29+
ids := make(container.Set[int64], len(schedules))
30+
for _, schedule := range schedules {
31+
ids.Add(schedule.RepoID)
32+
}
33+
return ids.Values()
34+
}
35+
36+
func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error {
37+
userIDs := schedules.GetUserIDs()
38+
users := make(map[int64]*user_model.User, len(userIDs))
39+
if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil {
40+
return err
41+
}
42+
for _, schedule := range schedules {
43+
if schedule.TriggerUserID == user_model.ActionsUserID {
44+
schedule.TriggerUser = user_model.NewActionsUser()
45+
} else {
46+
schedule.TriggerUser = users[schedule.TriggerUserID]
47+
}
48+
}
49+
return nil
50+
}
51+
52+
func (schedules ScheduleList) LoadRepos() error {
53+
repoIDs := schedules.GetRepoIDs()
54+
repos, err := repo_model.GetRepositoriesMapByIDs(repoIDs)
55+
if err != nil {
56+
return err
57+
}
58+
for _, schedule := range schedules {
59+
schedule.Repo = repos[schedule.RepoID]
60+
}
61+
return nil
62+
}
63+
64+
type FindScheduleOptions struct {
65+
db.ListOptions
66+
RepoID int64
67+
OwnerID int64
68+
}
69+
70+
func (opts FindScheduleOptions) toConds() builder.Cond {
71+
cond := builder.NewCond()
72+
if opts.RepoID > 0 {
73+
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
74+
}
75+
if opts.OwnerID > 0 {
76+
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
77+
}
78+
79+
return cond
80+
}
81+
82+
func FindSchedules(ctx context.Context, opts FindScheduleOptions) (ScheduleList, int64, error) {
83+
e := db.GetEngine(ctx).Where(opts.toConds())
84+
if !opts.ListAll && opts.PageSize > 0 && opts.Page >= 1 {
85+
e.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
86+
}
87+
var schedules ScheduleList
88+
total, err := e.Desc("id").FindAndCount(&schedules)
89+
return schedules, total, err
90+
}
91+
92+
func CountSchedules(ctx context.Context, opts FindScheduleOptions) (int64, error) {
93+
return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionSchedule))
94+
}

models/actions/schedule_spec.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"context"
8+
9+
"code.gitea.io/gitea/models/db"
10+
repo_model "code.gitea.io/gitea/models/repo"
11+
"code.gitea.io/gitea/modules/timeutil"
12+
13+
"github.com/robfig/cron/v3"
14+
)
15+
16+
// ActionScheduleSpec represents a schedule spec of a workflow file
17+
type ActionScheduleSpec struct {
18+
ID int64
19+
RepoID int64 `xorm:"index"`
20+
Repo *repo_model.Repository `xorm:"-"`
21+
ScheduleID int64 `xorm:"index"`
22+
Schedule *ActionSchedule `xorm:"-"`
23+
24+
// Next time the job will run, or the zero time if Cron has not been
25+
// started or this entry's schedule is unsatisfiable
26+
Next timeutil.TimeStamp `xorm:"index"`
27+
// Prev is the last time this job was run, or the zero time if never.
28+
Prev timeutil.TimeStamp
29+
Spec string
30+
31+
Created timeutil.TimeStamp `xorm:"created"`
32+
Updated timeutil.TimeStamp `xorm:"updated"`
33+
}
34+
35+
func (s *ActionScheduleSpec) Parse() (cron.Schedule, error) {
36+
return cronParser.Parse(s.Spec)
37+
}
38+
39+
func init() {
40+
db.RegisterModel(new(ActionScheduleSpec))
41+
}
42+
43+
func UpdateScheduleSpec(ctx context.Context, spec *ActionScheduleSpec, cols ...string) error {
44+
sess := db.GetEngine(ctx).ID(spec.ID)
45+
if len(cols) > 0 {
46+
sess.Cols(cols...)
47+
}
48+
_, err := sess.Update(spec)
49+
return err
50+
}

models/actions/schedule_spec_list.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"context"
8+
9+
"code.gitea.io/gitea/models/db"
10+
repo_model "code.gitea.io/gitea/models/repo"
11+
"code.gitea.io/gitea/modules/container"
12+
13+
"xorm.io/builder"
14+
)
15+
16+
type SpecList []*ActionScheduleSpec
17+
18+
func (specs SpecList) GetScheduleIDs() []int64 {
19+
ids := make(container.Set[int64], len(specs))
20+
for _, spec := range specs {
21+
ids.Add(spec.ScheduleID)
22+
}
23+
return ids.Values()
24+
}
25+
26+
func (specs SpecList) LoadSchedules() error {
27+
scheduleIDs := specs.GetScheduleIDs()
28+
schedules, err := GetSchedulesMapByIDs(scheduleIDs)
29+
if err != nil {
30+
return err
31+
}
32+
for _, spec := range specs {
33+
spec.Schedule = schedules[spec.ScheduleID]
34+
}
35+
36+
repoIDs := specs.GetRepoIDs()
37+
repos, err := GetReposMapByIDs(repoIDs)
38+
if err != nil {
39+
return err
40+
}
41+
for _, spec := range specs {
42+
spec.Repo = repos[spec.RepoID]
43+
}
44+
45+
return nil
46+
}
47+
48+
func (specs SpecList) GetRepoIDs() []int64 {
49+
ids := make(container.Set[int64], len(specs))
50+
for _, spec := range specs {
51+
ids.Add(spec.RepoID)
52+
}
53+
return ids.Values()
54+
}
55+
56+
func (specs SpecList) LoadRepos() error {
57+
repoIDs := specs.GetRepoIDs()
58+
repos, err := repo_model.GetRepositoriesMapByIDs(repoIDs)
59+
if err != nil {
60+
return err
61+
}
62+
for _, spec := range specs {
63+
spec.Repo = repos[spec.RepoID]
64+
}
65+
return nil
66+
}
67+
68+
type FindSpecOptions struct {
69+
db.ListOptions
70+
RepoID int64
71+
Next int64
72+
}
73+
74+
func (opts FindSpecOptions) toConds() builder.Cond {
75+
cond := builder.NewCond()
76+
if opts.RepoID > 0 {
77+
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
78+
}
79+
80+
if opts.Next > 0 {
81+
cond = cond.And(builder.Lte{"next": opts.Next})
82+
}
83+
84+
return cond
85+
}
86+
87+
func FindSpecs(ctx context.Context, opts FindSpecOptions) (SpecList, int64, error) {
88+
e := db.GetEngine(ctx).Where(opts.toConds())
89+
if opts.PageSize > 0 && opts.Page >= 1 {
90+
e.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
91+
}
92+
var specs SpecList
93+
total, err := e.Desc("id").FindAndCount(&specs)
94+
if err != nil {
95+
return nil, 0, err
96+
}
97+
98+
if err := specs.LoadSchedules(); err != nil {
99+
return nil, 0, err
100+
}
101+
return specs, total, nil
102+
}
103+
104+
func CountSpecs(ctx context.Context, opts FindSpecOptions) (int64, error) {
105+
return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionScheduleSpec))
106+
}

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,8 @@ var migrations = []Migration{
526526
NewMigration("Allow archiving labels", v1_21.AddArchivedUnixColumInLabelTable),
527527
// v272 -> v273
528528
NewMigration("Add Version to ActionRun table", v1_21.AddVersionToActionRunTable),
529+
// v273 -> v274
530+
NewMigration("Add Action Schedule Table", v1_21.AddActionScheduleTable),
529531
}
530532

531533
// GetCurrentDBVersion returns the current db version

0 commit comments

Comments
 (0)