-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <appleboy.tw@gmail.com> --------- Co-authored-by: Jason Song <i@wolfogre.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
- Loading branch information
1 parent
b62c8e7
commit 0d55f64
Showing
13 changed files
with
693 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
// Copyright 2023 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package actions | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"code.gitea.io/gitea/models/db" | ||
repo_model "code.gitea.io/gitea/models/repo" | ||
user_model "code.gitea.io/gitea/models/user" | ||
"code.gitea.io/gitea/modules/timeutil" | ||
webhook_module "code.gitea.io/gitea/modules/webhook" | ||
|
||
"github.com/robfig/cron/v3" | ||
) | ||
|
||
// ActionSchedule represents a schedule of a workflow file | ||
type ActionSchedule struct { | ||
ID int64 | ||
Title string | ||
Specs []string | ||
RepoID int64 `xorm:"index"` | ||
Repo *repo_model.Repository `xorm:"-"` | ||
OwnerID int64 `xorm:"index"` | ||
WorkflowID string | ||
TriggerUserID int64 | ||
TriggerUser *user_model.User `xorm:"-"` | ||
Ref string | ||
CommitSHA string | ||
Event webhook_module.HookEventType | ||
EventPayload string `xorm:"LONGTEXT"` | ||
Content []byte | ||
Created timeutil.TimeStamp `xorm:"created"` | ||
Updated timeutil.TimeStamp `xorm:"updated"` | ||
} | ||
|
||
func init() { | ||
db.RegisterModel(new(ActionSchedule)) | ||
} | ||
|
||
// GetSchedulesMapByIDs returns the schedules by given id slice. | ||
func GetSchedulesMapByIDs(ids []int64) (map[int64]*ActionSchedule, error) { | ||
schedules := make(map[int64]*ActionSchedule, len(ids)) | ||
return schedules, db.GetEngine(db.DefaultContext).In("id", ids).Find(&schedules) | ||
} | ||
|
||
// GetReposMapByIDs returns the repos by given id slice. | ||
func GetReposMapByIDs(ids []int64) (map[int64]*repo_model.Repository, error) { | ||
repos := make(map[int64]*repo_model.Repository, len(ids)) | ||
return repos, db.GetEngine(db.DefaultContext).In("id", ids).Find(&repos) | ||
} | ||
|
||
var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) | ||
|
||
// CreateScheduleTask creates new schedule task. | ||
func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error { | ||
// Return early if there are no rows to insert | ||
if len(rows) == 0 { | ||
return nil | ||
} | ||
|
||
// Begin transaction | ||
ctx, committer, err := db.TxContext(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
defer committer.Close() | ||
|
||
// Loop through each schedule row | ||
for _, row := range rows { | ||
// Create new schedule row | ||
if err = db.Insert(ctx, row); err != nil { | ||
return err | ||
} | ||
|
||
// Loop through each schedule spec and create a new spec row | ||
now := time.Now() | ||
|
||
for _, spec := range row.Specs { | ||
// Parse the spec and check for errors | ||
schedule, err := cronParser.Parse(spec) | ||
if err != nil { | ||
continue // skip to the next spec if there's an error | ||
} | ||
|
||
// Insert the new schedule spec row | ||
if err = db.Insert(ctx, &ActionScheduleSpec{ | ||
RepoID: row.RepoID, | ||
ScheduleID: row.ID, | ||
Spec: spec, | ||
Next: timeutil.TimeStamp(schedule.Next(now).Unix()), | ||
}); err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
|
||
// Commit transaction | ||
return committer.Commit() | ||
} | ||
|
||
func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error { | ||
ctx, committer, err := db.TxContext(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
defer committer.Close() | ||
|
||
if _, err := db.GetEngine(ctx).Delete(&ActionSchedule{RepoID: id}); err != nil { | ||
return err | ||
} | ||
|
||
if _, err := db.GetEngine(ctx).Delete(&ActionScheduleSpec{RepoID: id}); err != nil { | ||
return err | ||
} | ||
|
||
return committer.Commit() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// Copyright 2023 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package actions | ||
|
||
import ( | ||
"context" | ||
|
||
"code.gitea.io/gitea/models/db" | ||
repo_model "code.gitea.io/gitea/models/repo" | ||
user_model "code.gitea.io/gitea/models/user" | ||
"code.gitea.io/gitea/modules/container" | ||
|
||
"xorm.io/builder" | ||
) | ||
|
||
type ScheduleList []*ActionSchedule | ||
|
||
// GetUserIDs returns a slice of user's id | ||
func (schedules ScheduleList) GetUserIDs() []int64 { | ||
ids := make(container.Set[int64], len(schedules)) | ||
for _, schedule := range schedules { | ||
ids.Add(schedule.TriggerUserID) | ||
} | ||
return ids.Values() | ||
} | ||
|
||
func (schedules ScheduleList) GetRepoIDs() []int64 { | ||
ids := make(container.Set[int64], len(schedules)) | ||
for _, schedule := range schedules { | ||
ids.Add(schedule.RepoID) | ||
} | ||
return ids.Values() | ||
} | ||
|
||
func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error { | ||
userIDs := schedules.GetUserIDs() | ||
users := make(map[int64]*user_model.User, len(userIDs)) | ||
if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil { | ||
return err | ||
} | ||
for _, schedule := range schedules { | ||
if schedule.TriggerUserID == user_model.ActionsUserID { | ||
schedule.TriggerUser = user_model.NewActionsUser() | ||
} else { | ||
schedule.TriggerUser = users[schedule.TriggerUserID] | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (schedules ScheduleList) LoadRepos() error { | ||
repoIDs := schedules.GetRepoIDs() | ||
repos, err := repo_model.GetRepositoriesMapByIDs(repoIDs) | ||
if err != nil { | ||
return err | ||
} | ||
for _, schedule := range schedules { | ||
schedule.Repo = repos[schedule.RepoID] | ||
} | ||
return nil | ||
} | ||
|
||
type FindScheduleOptions struct { | ||
db.ListOptions | ||
RepoID int64 | ||
OwnerID int64 | ||
} | ||
|
||
func (opts FindScheduleOptions) toConds() builder.Cond { | ||
cond := builder.NewCond() | ||
if opts.RepoID > 0 { | ||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) | ||
} | ||
if opts.OwnerID > 0 { | ||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) | ||
} | ||
|
||
return cond | ||
} | ||
|
||
func FindSchedules(ctx context.Context, opts FindScheduleOptions) (ScheduleList, int64, error) { | ||
e := db.GetEngine(ctx).Where(opts.toConds()) | ||
if !opts.ListAll && opts.PageSize > 0 && opts.Page >= 1 { | ||
e.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) | ||
} | ||
var schedules ScheduleList | ||
total, err := e.Desc("id").FindAndCount(&schedules) | ||
return schedules, total, err | ||
} | ||
|
||
func CountSchedules(ctx context.Context, opts FindScheduleOptions) (int64, error) { | ||
return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionSchedule)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// Copyright 2023 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package actions | ||
|
||
import ( | ||
"context" | ||
|
||
"code.gitea.io/gitea/models/db" | ||
repo_model "code.gitea.io/gitea/models/repo" | ||
"code.gitea.io/gitea/modules/timeutil" | ||
|
||
"github.com/robfig/cron/v3" | ||
) | ||
|
||
// ActionScheduleSpec represents a schedule spec of a workflow file | ||
type ActionScheduleSpec struct { | ||
ID int64 | ||
RepoID int64 `xorm:"index"` | ||
Repo *repo_model.Repository `xorm:"-"` | ||
ScheduleID int64 `xorm:"index"` | ||
Schedule *ActionSchedule `xorm:"-"` | ||
|
||
// Next time the job will run, or the zero time if Cron has not been | ||
// started or this entry's schedule is unsatisfiable | ||
Next timeutil.TimeStamp `xorm:"index"` | ||
// Prev is the last time this job was run, or the zero time if never. | ||
Prev timeutil.TimeStamp | ||
Spec string | ||
|
||
Created timeutil.TimeStamp `xorm:"created"` | ||
Updated timeutil.TimeStamp `xorm:"updated"` | ||
} | ||
|
||
func (s *ActionScheduleSpec) Parse() (cron.Schedule, error) { | ||
return cronParser.Parse(s.Spec) | ||
} | ||
|
||
func init() { | ||
db.RegisterModel(new(ActionScheduleSpec)) | ||
} | ||
|
||
func UpdateScheduleSpec(ctx context.Context, spec *ActionScheduleSpec, cols ...string) error { | ||
sess := db.GetEngine(ctx).ID(spec.ID) | ||
if len(cols) > 0 { | ||
sess.Cols(cols...) | ||
} | ||
_, err := sess.Update(spec) | ||
return err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// Copyright 2023 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package actions | ||
|
||
import ( | ||
"context" | ||
|
||
"code.gitea.io/gitea/models/db" | ||
repo_model "code.gitea.io/gitea/models/repo" | ||
"code.gitea.io/gitea/modules/container" | ||
|
||
"xorm.io/builder" | ||
) | ||
|
||
type SpecList []*ActionScheduleSpec | ||
|
||
func (specs SpecList) GetScheduleIDs() []int64 { | ||
ids := make(container.Set[int64], len(specs)) | ||
for _, spec := range specs { | ||
ids.Add(spec.ScheduleID) | ||
} | ||
return ids.Values() | ||
} | ||
|
||
func (specs SpecList) LoadSchedules() error { | ||
scheduleIDs := specs.GetScheduleIDs() | ||
schedules, err := GetSchedulesMapByIDs(scheduleIDs) | ||
if err != nil { | ||
return err | ||
} | ||
for _, spec := range specs { | ||
spec.Schedule = schedules[spec.ScheduleID] | ||
} | ||
|
||
repoIDs := specs.GetRepoIDs() | ||
repos, err := GetReposMapByIDs(repoIDs) | ||
if err != nil { | ||
return err | ||
} | ||
for _, spec := range specs { | ||
spec.Repo = repos[spec.RepoID] | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (specs SpecList) GetRepoIDs() []int64 { | ||
ids := make(container.Set[int64], len(specs)) | ||
for _, spec := range specs { | ||
ids.Add(spec.RepoID) | ||
} | ||
return ids.Values() | ||
} | ||
|
||
func (specs SpecList) LoadRepos() error { | ||
repoIDs := specs.GetRepoIDs() | ||
repos, err := repo_model.GetRepositoriesMapByIDs(repoIDs) | ||
if err != nil { | ||
return err | ||
} | ||
for _, spec := range specs { | ||
spec.Repo = repos[spec.RepoID] | ||
} | ||
return nil | ||
} | ||
|
||
type FindSpecOptions struct { | ||
db.ListOptions | ||
RepoID int64 | ||
Next int64 | ||
} | ||
|
||
func (opts FindSpecOptions) toConds() builder.Cond { | ||
cond := builder.NewCond() | ||
if opts.RepoID > 0 { | ||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) | ||
} | ||
|
||
if opts.Next > 0 { | ||
cond = cond.And(builder.Lte{"next": opts.Next}) | ||
} | ||
|
||
return cond | ||
} | ||
|
||
func FindSpecs(ctx context.Context, opts FindSpecOptions) (SpecList, int64, error) { | ||
e := db.GetEngine(ctx).Where(opts.toConds()) | ||
if opts.PageSize > 0 && opts.Page >= 1 { | ||
e.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) | ||
} | ||
var specs SpecList | ||
total, err := e.Desc("id").FindAndCount(&specs) | ||
if err != nil { | ||
return nil, 0, err | ||
} | ||
|
||
if err := specs.LoadSchedules(); err != nil { | ||
return nil, 0, err | ||
} | ||
return specs, total, nil | ||
} | ||
|
||
func CountSpecs(ctx context.Context, opts FindSpecOptions) (int64, error) { | ||
return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionScheduleSpec)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.