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>
This commit is contained in:
parent
b62c8e7765
commit
0d55f64e6c
2
go.mod
2
go.mod
|
@ -90,6 +90,7 @@ require (
|
||||||
github.com/prometheus/client_golang v1.16.0
|
github.com/prometheus/client_golang v1.16.0
|
||||||
github.com/quasoft/websspi v1.1.2
|
github.com/quasoft/websspi v1.1.2
|
||||||
github.com/redis/go-redis/v9 v9.0.5
|
github.com/redis/go-redis/v9 v9.0.5
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||||
github.com/sassoftware/go-rpmutils v0.2.0
|
github.com/sassoftware/go-rpmutils v0.2.0
|
||||||
github.com/sergi/go-diff v1.3.1
|
github.com/sergi/go-diff v1.3.1
|
||||||
|
@ -254,7 +255,6 @@ require (
|
||||||
github.com/rhysd/actionlint v1.6.25 // indirect
|
github.com/rhysd/actionlint v1.6.25 // indirect
|
||||||
github.com/rivo/uniseg v0.4.4 // indirect
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
github.com/robfig/cron v1.2.0 // indirect
|
github.com/robfig/cron v1.2.0 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
github.com/rs/xid v1.5.0 // indirect
|
github.com/rs/xid v1.5.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
|
120
models/actions/schedule.go
Normal file
120
models/actions/schedule.go
Normal file
|
@ -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()
|
||||||
|
}
|
94
models/actions/schedule_list.go
Normal file
94
models/actions/schedule_list.go
Normal file
|
@ -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))
|
||||||
|
}
|
50
models/actions/schedule_spec.go
Normal file
50
models/actions/schedule_spec.go
Normal file
|
@ -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
|
||||||
|
}
|
106
models/actions/schedule_spec_list.go
Normal file
106
models/actions/schedule_spec_list.go
Normal file
|
@ -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))
|
||||||
|
}
|
|
@ -526,6 +526,8 @@ var migrations = []Migration{
|
||||||
NewMigration("Allow archiving labels", v1_21.AddArchivedUnixColumInLabelTable),
|
NewMigration("Allow archiving labels", v1_21.AddArchivedUnixColumInLabelTable),
|
||||||
// v272 -> v273
|
// v272 -> v273
|
||||||
NewMigration("Add Version to ActionRun table", v1_21.AddVersionToActionRunTable),
|
NewMigration("Add Version to ActionRun table", v1_21.AddVersionToActionRunTable),
|
||||||
|
// v273 -> v274
|
||||||
|
NewMigration("Add Action Schedule Table", v1_21.AddActionScheduleTable),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current db version
|
// GetCurrentDBVersion returns the current db version
|
||||||
|
|
45
models/migrations/v1_21/v273.go
Normal file
45
models/migrations/v1_21/v273.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_21 //nolint
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddActionScheduleTable(x *xorm.Engine) error {
|
||||||
|
type ActionSchedule struct {
|
||||||
|
ID int64
|
||||||
|
Title string
|
||||||
|
Specs []string
|
||||||
|
RepoID int64 `xorm:"index"`
|
||||||
|
OwnerID int64 `xorm:"index"`
|
||||||
|
WorkflowID string
|
||||||
|
TriggerUserID int64
|
||||||
|
Ref string
|
||||||
|
CommitSHA string
|
||||||
|
Event string
|
||||||
|
EventPayload string `xorm:"LONGTEXT"`
|
||||||
|
Content []byte
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionScheduleSpec struct {
|
||||||
|
ID int64
|
||||||
|
RepoID int64 `xorm:"index"`
|
||||||
|
ScheduleID int64 `xorm:"index"`
|
||||||
|
Spec string
|
||||||
|
Next timeutil.TimeStamp `xorm:"index"`
|
||||||
|
Prev timeutil.TimeStamp
|
||||||
|
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync(
|
||||||
|
new(ActionSchedule),
|
||||||
|
new(ActionScheduleSpec),
|
||||||
|
)
|
||||||
|
}
|
|
@ -170,6 +170,8 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
|
||||||
&actions_model.ActionRunJob{RepoID: repoID},
|
&actions_model.ActionRunJob{RepoID: repoID},
|
||||||
&actions_model.ActionRun{RepoID: repoID},
|
&actions_model.ActionRun{RepoID: repoID},
|
||||||
&actions_model.ActionRunner{RepoID: repoID},
|
&actions_model.ActionRunner{RepoID: repoID},
|
||||||
|
&actions_model.ActionScheduleSpec{RepoID: repoID},
|
||||||
|
&actions_model.ActionSchedule{RepoID: repoID},
|
||||||
&actions_model.ActionArtifact{RepoID: repoID},
|
&actions_model.ActionArtifact{RepoID: repoID},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("deleteBeans: %w", err)
|
return fmt.Errorf("deleteBeans: %w", err)
|
||||||
|
|
|
@ -95,18 +95,25 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) {
|
||||||
return events, nil
|
return events, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DetectWorkflows(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) ([]*DetectedWorkflow, error) {
|
func DetectWorkflows(
|
||||||
|
gitRepo *git.Repository,
|
||||||
|
commit *git.Commit,
|
||||||
|
triggedEvent webhook_module.HookEventType,
|
||||||
|
payload api.Payloader,
|
||||||
|
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
|
||||||
entries, err := ListWorkflows(commit)
|
entries, err := ListWorkflows(commit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
workflows := make([]*DetectedWorkflow, 0, len(entries))
|
workflows := make([]*DetectedWorkflow, 0, len(entries))
|
||||||
|
schedules := make([]*DetectedWorkflow, 0, len(entries))
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
content, err := GetContentFromEntry(entry)
|
content, err := GetContentFromEntry(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
events, err := GetEventsFromContent(content)
|
events, err := GetEventsFromContent(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
|
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
|
||||||
|
@ -114,6 +121,14 @@ func DetectWorkflows(gitRepo *git.Repository, commit *git.Commit, triggedEvent w
|
||||||
}
|
}
|
||||||
for _, evt := range events {
|
for _, evt := range events {
|
||||||
log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent)
|
log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent)
|
||||||
|
if evt.IsSchedule() {
|
||||||
|
dwf := &DetectedWorkflow{
|
||||||
|
EntryName: entry.Name(),
|
||||||
|
TriggerEvent: evt.Name,
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
schedules = append(schedules, dwf)
|
||||||
|
}
|
||||||
if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
|
if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
|
||||||
dwf := &DetectedWorkflow{
|
dwf := &DetectedWorkflow{
|
||||||
EntryName: entry.Name(),
|
EntryName: entry.Name(),
|
||||||
|
@ -125,7 +140,7 @@ func DetectWorkflows(gitRepo *git.Repository, commit *git.Commit, triggedEvent w
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return workflows, nil
|
return workflows, schedules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
|
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
|
||||||
|
|
|
@ -2756,6 +2756,7 @@ dashboard.gc_lfs = Garbage collect LFS meta objects
|
||||||
dashboard.stop_zombie_tasks = Stop zombie tasks
|
dashboard.stop_zombie_tasks = Stop zombie tasks
|
||||||
dashboard.stop_endless_tasks = Stop endless tasks
|
dashboard.stop_endless_tasks = Stop endless tasks
|
||||||
dashboard.cancel_abandoned_jobs = Cancel abandoned jobs
|
dashboard.cancel_abandoned_jobs = Cancel abandoned jobs
|
||||||
|
dashboard.start_schedule_tasks = Start schedule tasks
|
||||||
dashboard.sync_branch.started = Branches Sync started
|
dashboard.sync_branch.started = Branches Sync started
|
||||||
dashboard.rebuild_issue_indexer = Rebuild issue indexer
|
dashboard.rebuild_issue_indexer = Rebuild issue indexer
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -24,6 +25,7 @@ import (
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
"github.com/nektos/act/pkg/jobparser"
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
var methodCtxKey struct{}
|
var methodCtxKey struct{}
|
||||||
|
@ -143,15 +145,15 @@ func notify(ctx context.Context, input *notifyInput) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var detectedWorkflows []*actions_module.DetectedWorkflow
|
var detectedWorkflows []*actions_module.DetectedWorkflow
|
||||||
workflows, err := actions_module.DetectWorkflows(gitRepo, commit, input.Event, input.Payload)
|
actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
|
||||||
|
workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, input.Event, input.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("DetectWorkflows: %w", err)
|
return fmt.Errorf("DetectWorkflows: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(workflows) == 0 {
|
if len(workflows) == 0 {
|
||||||
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
|
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
|
||||||
} else {
|
} else {
|
||||||
actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
|
|
||||||
|
|
||||||
for _, wf := range workflows {
|
for _, wf := range workflows {
|
||||||
if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
|
if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
|
||||||
log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName)
|
log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName)
|
||||||
|
@ -171,7 +173,7 @@ func notify(ctx context.Context, input *notifyInput) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("gitRepo.GetCommit: %w", err)
|
return fmt.Errorf("gitRepo.GetCommit: %w", err)
|
||||||
}
|
}
|
||||||
baseWorkflows, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload)
|
baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("DetectWorkflows: %w", err)
|
return fmt.Errorf("DetectWorkflows: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -186,7 +188,22 @@ func notify(ctx context.Context, input *notifyInput) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := handleSchedules(ctx, schedules, commit, input); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWorkflows(
|
||||||
|
ctx context.Context,
|
||||||
|
detectedWorkflows []*actions_module.DetectedWorkflow,
|
||||||
|
commit *git.Commit,
|
||||||
|
input *notifyInput,
|
||||||
|
ref string,
|
||||||
|
) error {
|
||||||
if len(detectedWorkflows) == 0 {
|
if len(detectedWorkflows) == 0 {
|
||||||
|
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,3 +367,86 @@ func ifNeedApproval(ctx context.Context, run *actions_model.ActionRun, repo *rep
|
||||||
log.Trace("need approval because it's the first time user %d triggered actions", user.ID)
|
log.Trace("need approval because it's the first time user %d triggered actions", user.ID)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleSchedules(
|
||||||
|
ctx context.Context,
|
||||||
|
detectedWorkflows []*actions_module.DetectedWorkflow,
|
||||||
|
commit *git.Commit,
|
||||||
|
input *notifyInput,
|
||||||
|
) error {
|
||||||
|
if len(detectedWorkflows) == 0 {
|
||||||
|
log.Trace("repo %s with commit %s couldn't find schedules", input.Repo.RepoPath(), commit.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
branch, err := commit.GetBranchName()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if branch != input.Repo.DefaultBranch {
|
||||||
|
log.Trace("commit branch is not default branch in repo")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, _, err := actions_model.FindSchedules(ctx, actions_model.FindScheduleOptions{RepoID: input.Repo.ID})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("FindCrons: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows) > 0 {
|
||||||
|
if err := actions_model.DeleteScheduleTaskByRepo(ctx, input.Repo.ID); err != nil {
|
||||||
|
log.Error("DeleteCronTaskByRepo: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := json.Marshal(input.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("json.Marshal: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
crons := make([]*actions_model.ActionSchedule, 0, len(detectedWorkflows))
|
||||||
|
for _, dwf := range detectedWorkflows {
|
||||||
|
// Check cron job condition. Only working in default branch
|
||||||
|
workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("ReadWorkflow: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
schedules := workflow.OnSchedule()
|
||||||
|
if len(schedules) == 0 {
|
||||||
|
log.Warn("no schedule event")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
run := &actions_model.ActionSchedule{
|
||||||
|
Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
|
||||||
|
RepoID: input.Repo.ID,
|
||||||
|
OwnerID: input.Repo.OwnerID,
|
||||||
|
WorkflowID: dwf.EntryName,
|
||||||
|
TriggerUserID: input.Doer.ID,
|
||||||
|
Ref: input.Ref,
|
||||||
|
CommitSHA: commit.ID.String(),
|
||||||
|
Event: input.Event,
|
||||||
|
EventPayload: string(p),
|
||||||
|
Specs: schedules,
|
||||||
|
Content: dwf.Content,
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel running jobs if the event is push
|
||||||
|
if run.Event == webhook_module.HookEventPush {
|
||||||
|
// cancel running jobs of the same workflow
|
||||||
|
if err := actions_model.CancelRunningJobs(
|
||||||
|
ctx,
|
||||||
|
run.RepoID,
|
||||||
|
run.Ref,
|
||||||
|
run.WorkflowID,
|
||||||
|
); err != nil {
|
||||||
|
log.Error("CancelRunningJobs: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crons = append(crons, run)
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions_model.CreateScheduleTask(ctx, crons)
|
||||||
|
}
|
||||||
|
|
135
services/actions/schedule_tasks.go
Normal file
135
services/actions/schedule_tasks.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/jobparser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StartScheduleTasks start the task
|
||||||
|
func StartScheduleTasks(ctx context.Context) error {
|
||||||
|
return startTasks(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// startTasks retrieves specifications in pages, creates a schedule task for each specification,
|
||||||
|
// and updates the specification's next run time and previous run time.
|
||||||
|
// The function returns an error if there's an issue with finding or updating the specifications.
|
||||||
|
func startTasks(ctx context.Context) error {
|
||||||
|
// Set the page size
|
||||||
|
pageSize := 50
|
||||||
|
|
||||||
|
// Retrieve specs in pages until all specs have been retrieved
|
||||||
|
now := time.Now()
|
||||||
|
for page := 1; ; page++ {
|
||||||
|
// Retrieve the specs for the current page
|
||||||
|
specs, _, err := actions_model.FindSpecs(ctx, actions_model.FindSpecOptions{
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
},
|
||||||
|
Next: now.Unix(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find specs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through each spec and create a schedule task for it
|
||||||
|
for _, row := range specs {
|
||||||
|
// cancel running jobs if the event is push
|
||||||
|
if row.Schedule.Event == webhook_module.HookEventPush {
|
||||||
|
// cancel running jobs of the same workflow
|
||||||
|
if err := actions_model.CancelRunningJobs(
|
||||||
|
ctx,
|
||||||
|
row.RepoID,
|
||||||
|
row.Schedule.Ref,
|
||||||
|
row.Schedule.WorkflowID,
|
||||||
|
); err != nil {
|
||||||
|
log.Error("CancelRunningJobs: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CreateScheduleTask(ctx, row.Schedule); err != nil {
|
||||||
|
log.Error("CreateScheduleTask: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the spec
|
||||||
|
schedule, err := row.Parse()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Parse: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the spec's next run time and previous run time
|
||||||
|
row.Prev = row.Next
|
||||||
|
row.Next = timeutil.TimeStamp(schedule.Next(now.Add(1 * time.Minute)).Unix())
|
||||||
|
if err := actions_model.UpdateScheduleSpec(ctx, row, "prev", "next"); err != nil {
|
||||||
|
log.Error("UpdateScheduleSpec: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop if all specs have been retrieved
|
||||||
|
if len(specs) < pageSize {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateScheduleTask creates a scheduled task from a cron action schedule.
|
||||||
|
// It creates an action run based on the schedule, inserts it into the database, and creates commit statuses for each job.
|
||||||
|
func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) error {
|
||||||
|
// Create a new action run based on the schedule
|
||||||
|
run := &actions_model.ActionRun{
|
||||||
|
Title: cron.Title,
|
||||||
|
RepoID: cron.RepoID,
|
||||||
|
OwnerID: cron.OwnerID,
|
||||||
|
WorkflowID: cron.WorkflowID,
|
||||||
|
TriggerUserID: cron.TriggerUserID,
|
||||||
|
Ref: cron.Ref,
|
||||||
|
CommitSHA: cron.CommitSHA,
|
||||||
|
Event: cron.Event,
|
||||||
|
EventPayload: cron.EventPayload,
|
||||||
|
Status: actions_model.StatusWaiting,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the workflow specification from the cron schedule
|
||||||
|
workflows, err := jobparser.Parse(cron.Content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the action run and its associated jobs into the database
|
||||||
|
if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the jobs for the newly created action run
|
||||||
|
jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create commit statuses for each job
|
||||||
|
for _, job := range jobs {
|
||||||
|
if err := createCommitStatus(ctx, job); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return nil if no errors occurred
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ func initActionsTasks() {
|
||||||
registerStopZombieTasks()
|
registerStopZombieTasks()
|
||||||
registerStopEndlessTasks()
|
registerStopEndlessTasks()
|
||||||
registerCancelAbandonedJobs()
|
registerCancelAbandonedJobs()
|
||||||
|
registerScheduleTasks()
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerStopZombieTasks() {
|
func registerStopZombieTasks() {
|
||||||
|
@ -49,3 +50,16 @@ func registerCancelAbandonedJobs() {
|
||||||
return actions_service.CancelAbandonedJobs(ctx)
|
return actions_service.CancelAbandonedJobs(ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// registerScheduleTasks registers a scheduled task that runs every minute to start any due schedule tasks.
|
||||||
|
func registerScheduleTasks() {
|
||||||
|
// Register the task with a unique name, enabled status, and schedule for every minute.
|
||||||
|
RegisterTaskFatal("start_schedule_tasks", &BaseConfig{
|
||||||
|
Enabled: true,
|
||||||
|
RunAtStart: false,
|
||||||
|
Schedule: "@every 1m",
|
||||||
|
}, func(ctx context.Context, _ *user_model.User, cfg Config) error {
|
||||||
|
// Call the function to start schedule tasks and pass the context.
|
||||||
|
return actions_service.StartScheduleTasks(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue