From 64f66f9113766c7be07c674132181d829fafeb92 Mon Sep 17 00:00:00 2001 From: qwerty287 Date: Tue, 10 Mar 2026 16:49:10 +0100 Subject: [PATCH 1/8] Enable crons if created via CLI --- cli/repo/cron/cron_add.go | 7 ++++ cli/repo/cron/cron_update.go | 7 ++++ server/api/cron.go | 10 +++++ server/store/datastore/cron.go | 5 +++ server/store/mocks/mock_Store.go | 66 +++++++++++++++++++++++++++++++ server/store/store.go | 1 + woodpecker-go/woodpecker/types.go | 1 + 7 files changed, 97 insertions(+) diff --git a/cli/repo/cron/cron_add.go b/cli/repo/cron/cron_add.go index df86ef483c6..2884181f27d 100644 --- a/cli/repo/cron/cron_add.go +++ b/cli/repo/cron/cron_add.go @@ -47,6 +47,11 @@ var cronCreateCmd = &cli.Command{ Usage: "cron schedule", Required: true, }, + &cli.BoolFlag{ + Name: "enabled", + Usage: "whether cron is enabled", + Value: true, + }, common.FormatFlag(tmplCronList, true), }, } @@ -58,6 +63,7 @@ func cronCreate(ctx context.Context, c *cli.Command) error { schedule = c.String("schedule") repoIDOrFullName = c.String("repository") format = c.String("format") + "\n" + enabled = c.Bool("enabled") ) if repoIDOrFullName == "" { repoIDOrFullName = c.Args().First() @@ -77,6 +83,7 @@ func cronCreate(ctx context.Context, c *cli.Command) error { Name: cronName, Branch: branch, Schedule: schedule, + Enabled: enabled, } cron, err = client.CronCreate(repoID, cron) if err != nil { diff --git a/cli/repo/cron/cron_update.go b/cli/repo/cron/cron_update.go index 323893027fa..bd1678f2e6c 100644 --- a/cli/repo/cron/cron_update.go +++ b/cli/repo/cron/cron_update.go @@ -50,6 +50,11 @@ var cronUpdateCmd = &cli.Command{ Name: "schedule", Usage: "cron schedule", }, + &cli.BoolFlag{ + Name: "enabled", + Usage: "whether cron is enabled", + Value: true, + }, common.FormatFlag(tmplCronList, true), }, } @@ -62,6 +67,7 @@ func cronUpdate(ctx context.Context, c *cli.Command) error { branch = c.String("branch") schedule = c.String("schedule") format = c.String("format") + "\n" + enabled = c.Bool("enabled") ) if repoIDOrFullName == "" { repoIDOrFullName = c.Args().First() @@ -79,6 +85,7 @@ func cronUpdate(ctx context.Context, c *cli.Command) error { Name: jobName, Branch: branch, Schedule: schedule, + Enabled: enabled, } cron, err = client.CronUpdate(repoID, cron) if err != nil { diff --git a/server/api/cron.go b/server/api/cron.go index 119b22d368b..69ce1331e89 100644 --- a/server/api/cron.go +++ b/server/api/cron.go @@ -152,6 +152,16 @@ func PostCron(c *gin.Context) { } } + nameExists, err := _store.CronExists(repo, in.Name) + if err != nil { + handleDBError(c, err) + return + } + if nameExists { + c.String(http.StatusConflict, "cron with this exists for this repo already") + return + } + if err := _store.CronCreate(cron); err != nil { c.String(http.StatusInternalServerError, "Error inserting cron %q. %s", in.Name, err) return diff --git a/server/store/datastore/cron.go b/server/store/datastore/cron.go index 85530036f1c..5111ec8822a 100644 --- a/server/store/datastore/cron.go +++ b/server/store/datastore/cron.go @@ -28,6 +28,11 @@ func (s storage) CronCreate(cron *model.Cron) error { return err } +func (s storage) CronExists(repo *model.Repo, name string) (bool, error) { + cron := new(model.Cron) + return s.engine.Where("name = ?", name).And("repo_id = ?", repo.ID).Exist(cron) +} + func (s storage) CronFind(repo *model.Repo, id int64) (*model.Cron, error) { cron := new(model.Cron) return cron, wrapGet(s.engine.ID(id).Where("repo_id = ?", repo.ID).Get(cron)) diff --git a/server/store/mocks/mock_Store.go b/server/store/mocks/mock_Store.go index 7a0a6ba7a54..629bdb2e0dc 100644 --- a/server/store/mocks/mock_Store.go +++ b/server/store/mocks/mock_Store.go @@ -940,6 +940,72 @@ func (_c *MockStore_CronDelete_Call) RunAndReturn(run func(repo *model.Repo, n i return _c } +// CronExists provides a mock function for the type MockStore +func (_mock *MockStore) CronExists(repo *model.Repo, s string) (bool, error) { + ret := _mock.Called(repo, s) + + if len(ret) == 0 { + panic("no return value specified for CronExists") + } + + var r0 bool + var r1 error + if returnFunc, ok := ret.Get(0).(func(*model.Repo, string) (bool, error)); ok { + return returnFunc(repo, s) + } + if returnFunc, ok := ret.Get(0).(func(*model.Repo, string) bool); ok { + r0 = returnFunc(repo, s) + } else { + r0 = ret.Get(0).(bool) + } + if returnFunc, ok := ret.Get(1).(func(*model.Repo, string) error); ok { + r1 = returnFunc(repo, s) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockStore_CronExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CronExists' +type MockStore_CronExists_Call struct { + *mock.Call +} + +// CronExists is a helper method to define mock.On call +// - repo *model.Repo +// - s string +func (_e *MockStore_Expecter) CronExists(repo interface{}, s interface{}) *MockStore_CronExists_Call { + return &MockStore_CronExists_Call{Call: _e.mock.On("CronExists", repo, s)} +} + +func (_c *MockStore_CronExists_Call) Run(run func(repo *model.Repo, s string)) *MockStore_CronExists_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 *model.Repo + if args[0] != nil { + arg0 = args[0].(*model.Repo) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockStore_CronExists_Call) Return(b bool, err error) *MockStore_CronExists_Call { + _c.Call.Return(b, err) + return _c +} + +func (_c *MockStore_CronExists_Call) RunAndReturn(run func(repo *model.Repo, s string) (bool, error)) *MockStore_CronExists_Call { + _c.Call.Return(run) + return _c +} + // CronFind provides a mock function for the type MockStore func (_mock *MockStore) CronFind(repo *model.Repo, n int64) (*model.Cron, error) { ret := _mock.Called(repo, n) diff --git a/server/store/store.go b/server/store/store.go index 5041749081b..c9fb6367e8e 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -163,6 +163,7 @@ type Store interface { // Cron CronCreate(*model.Cron) error + CronExists(*model.Repo, string) (bool, error) CronFind(*model.Repo, int64) (*model.Cron, error) CronList(*model.Repo, *model.ListOptions) ([]*model.Cron, error) CronUpdate(*model.Repo, *model.Cron) error diff --git a/woodpecker-go/woodpecker/types.go b/woodpecker-go/woodpecker/types.go index d9a2a8dd4f0..7fa65c1de4a 100644 --- a/woodpecker-go/woodpecker/types.go +++ b/woodpecker-go/woodpecker/types.go @@ -257,6 +257,7 @@ type ( Schedule string `json:"schedule"` Created int64 `json:"created"` Branch string `json:"branch"` + Enabled bool `json:"enabled"` } // PipelineOptions is the JSON data for creating a new pipeline. From b2d1952a297bbe9fac75a6a9f1b033e6a11263b5 Mon Sep 17 00:00:00 2001 From: qwerty287 Date: Wed, 18 Mar 2026 10:30:15 +0100 Subject: [PATCH 2/8] use error instead of separate check --- server/api/cron.go | 18 +++++++----------- server/store/datastore/cron.go | 11 ++++++----- server/store/datastore/cron_test.go | 2 +- server/store/store.go | 1 - 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/server/api/cron.go b/server/api/cron.go index 69ce1331e89..7c1af5ac598 100644 --- a/server/api/cron.go +++ b/server/api/cron.go @@ -15,8 +15,10 @@ package api import ( + "fmt" "net/http" "strconv" + "strings" "time" "github.com/gin-gonic/gin" @@ -152,18 +154,12 @@ func PostCron(c *gin.Context) { } } - nameExists, err := _store.CronExists(repo, in.Name) - if err != nil { - handleDBError(c, err) - return - } - if nameExists { - c.String(http.StatusConflict, "cron with this exists for this repo already") - return - } - if err := _store.CronCreate(cron); err != nil { - c.String(http.StatusInternalServerError, "Error inserting cron %q. %s", in.Name, err) + if err.Error() == "cron with this name exists already for this repo" { + c.String(http.StatusConflict, "cron with this exists for this repo already") + } else { + c.String(http.StatusInternalServerError, "Error inserting cron %q. %s", in.Name, err) + } return } c.JSON(http.StatusOK, cron) diff --git a/server/store/datastore/cron.go b/server/store/datastore/cron.go index 5111ec8822a..8e5bd45709e 100644 --- a/server/store/datastore/cron.go +++ b/server/store/datastore/cron.go @@ -15,6 +15,9 @@ package datastore import ( + "errors" + "strings" + "xorm.io/builder" "go.woodpecker-ci.org/woodpecker/v3/server/model" @@ -25,14 +28,12 @@ func (s storage) CronCreate(cron *model.Cron) error { return err } _, err := s.engine.Insert(cron) + if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") { + return errors.New("cron with this name exists already for this repo") + } return err } -func (s storage) CronExists(repo *model.Repo, name string) (bool, error) { - cron := new(model.Cron) - return s.engine.Where("name = ?", name).And("repo_id = ?", repo.ID).Exist(cron) -} - func (s storage) CronFind(repo *model.Repo, id int64) (*model.Cron, error) { cron := new(model.Cron) return cron, wrapGet(s.engine.ID(id).Where("repo_id = ?", repo.ID).Get(cron)) diff --git a/server/store/datastore/cron_test.go b/server/store/datastore/cron_test.go index 289c57a27be..357a7de2572 100644 --- a/server/store/datastore/cron_test.go +++ b/server/store/datastore/cron_test.go @@ -33,7 +33,7 @@ func TestCronCreate(t *testing.T) { assert.NotEqualValues(t, 0, cron1.ID) // cannot insert cron job with same repoID and title - assert.Error(t, store.CronCreate(cron1)) + assert.EqualError(t, store.CronCreate(cron1), "cron with this name exists already for this repo") oldID := cron1.ID assert.NoError(t, store.CronDelete(repo, oldID)) diff --git a/server/store/store.go b/server/store/store.go index c9fb6367e8e..5041749081b 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -163,7 +163,6 @@ type Store interface { // Cron CronCreate(*model.Cron) error - CronExists(*model.Repo, string) (bool, error) CronFind(*model.Repo, int64) (*model.Cron, error) CronList(*model.Repo, *model.ListOptions) ([]*model.Cron, error) CronUpdate(*model.Repo, *model.Cron) error From bd2a347b53f3e7d246d453b21b4a24b0b175dd78 Mon Sep 17 00:00:00 2001 From: qwerty287 Date: Wed, 18 Mar 2026 11:23:45 +0100 Subject: [PATCH 3/8] fix --- server/store/datastore/cron.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/store/datastore/cron.go b/server/store/datastore/cron.go index 8e5bd45709e..4b1f49f9dc8 100644 --- a/server/store/datastore/cron.go +++ b/server/store/datastore/cron.go @@ -28,7 +28,7 @@ func (s storage) CronCreate(cron *model.Cron) error { return err } _, err := s.engine.Insert(cron) - if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") { + if err != nil && strings.HasPrefix(err.Error(), "UNIQUE constraint failed") { return errors.New("cron with this name exists already for this repo") } return err From bd2e1ae160ce52b8c885ed6b35e83412a9563412 Mon Sep 17 00:00:00 2001 From: qwerty287 Date: Wed, 18 Mar 2026 11:31:46 +0100 Subject: [PATCH 4/8] pq and mysql --- server/store/datastore/cron.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/store/datastore/cron.go b/server/store/datastore/cron.go index 4b1f49f9dc8..7e24994bcc6 100644 --- a/server/store/datastore/cron.go +++ b/server/store/datastore/cron.go @@ -28,7 +28,7 @@ func (s storage) CronCreate(cron *model.Cron) error { return err } _, err := s.engine.Insert(cron) - if err != nil && strings.HasPrefix(err.Error(), "UNIQUE constraint failed") { + if err != nil && (strings.HasPrefix(err.Error(), "UNIQUE constraint failed") || strings.HasPrefix(err.Error(), "pq: duplicate key value violates unique constraint") || strings.Contains(err.Error(), "Duplicate entry")) { return errors.New("cron with this name exists already for this repo") } return err From d03ad3dcd731b801dc0ecbfdd8f769ab3cc753b6 Mon Sep 17 00:00:00 2001 From: qwerty287 Date: Wed, 18 Mar 2026 12:01:54 +0100 Subject: [PATCH 5/8] generate --- server/api/cron.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/api/cron.go b/server/api/cron.go index 7c1af5ac598..6a6ef45b1f7 100644 --- a/server/api/cron.go +++ b/server/api/cron.go @@ -15,10 +15,8 @@ package api import ( - "fmt" "net/http" "strconv" - "strings" "time" "github.com/gin-gonic/gin" From 3a7ee593e6bf69d6761ba315a88b36a3ff89e122 Mon Sep 17 00:00:00 2001 From: qwerty287 Date: Wed, 18 Mar 2026 16:07:53 +0100 Subject: [PATCH 6/8] custom error --- server/api/cron.go | 4 +++- server/store/datastore/cron.go | 4 ++-- server/store/datastore/cron_test.go | 3 ++- server/store/types/errors.go | 7 ++++++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/server/api/cron.go b/server/api/cron.go index 6a6ef45b1f7..879c84b40a1 100644 --- a/server/api/cron.go +++ b/server/api/cron.go @@ -15,6 +15,7 @@ package api import ( + "errors" "net/http" "strconv" "time" @@ -28,6 +29,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/pipeline" "go.woodpecker-ci.org/woodpecker/v3/server/router/middleware/session" "go.woodpecker-ci.org/woodpecker/v3/server/store" + "go.woodpecker-ci.org/woodpecker/v3/server/store/types" ) // GetCron @@ -153,7 +155,7 @@ func PostCron(c *gin.Context) { } if err := _store.CronCreate(cron); err != nil { - if err.Error() == "cron with this name exists already for this repo" { + if errors.Is(types.UniqueExists, err) { c.String(http.StatusConflict, "cron with this exists for this repo already") } else { c.String(http.StatusInternalServerError, "Error inserting cron %q. %s", in.Name, err) diff --git a/server/store/datastore/cron.go b/server/store/datastore/cron.go index 7e24994bcc6..ada7bd3cd83 100644 --- a/server/store/datastore/cron.go +++ b/server/store/datastore/cron.go @@ -15,12 +15,12 @@ package datastore import ( - "errors" "strings" "xorm.io/builder" "go.woodpecker-ci.org/woodpecker/v3/server/model" + "go.woodpecker-ci.org/woodpecker/v3/server/store/types" ) func (s storage) CronCreate(cron *model.Cron) error { @@ -29,7 +29,7 @@ func (s storage) CronCreate(cron *model.Cron) error { } _, err := s.engine.Insert(cron) if err != nil && (strings.HasPrefix(err.Error(), "UNIQUE constraint failed") || strings.HasPrefix(err.Error(), "pq: duplicate key value violates unique constraint") || strings.Contains(err.Error(), "Duplicate entry")) { - return errors.New("cron with this name exists already for this repo") + return types.UniqueExists } return err } diff --git a/server/store/datastore/cron_test.go b/server/store/datastore/cron_test.go index 357a7de2572..7c91d11437c 100644 --- a/server/store/datastore/cron_test.go +++ b/server/store/datastore/cron_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" "go.woodpecker-ci.org/woodpecker/v3/server/model" + "go.woodpecker-ci.org/woodpecker/v3/server/store/types" ) func TestCronCreate(t *testing.T) { @@ -33,7 +34,7 @@ func TestCronCreate(t *testing.T) { assert.NotEqualValues(t, 0, cron1.ID) // cannot insert cron job with same repoID and title - assert.EqualError(t, store.CronCreate(cron1), "cron with this name exists already for this repo") + assert.ErrorIs(t, types.UniqueExists, store.CronCreate(cron1)) oldID := cron1.ID assert.NoError(t, store.CronDelete(repo, oldID)) diff --git a/server/store/types/errors.go b/server/store/types/errors.go index c7901a1c6ed..d0c2eca36c0 100644 --- a/server/store/types/errors.go +++ b/server/store/types/errors.go @@ -14,6 +14,11 @@ package types -import "database/sql" +import ( + "database/sql" + "errors" +) var RecordNotExist = sql.ErrNoRows + +var UniqueExists = errors.New("unique constraint failed") From 301b364369351e18723ad14903deec25309097a7 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Wed, 18 Mar 2026 17:21:22 +0100 Subject: [PATCH 7/8] Apply suggestions from code review Co-authored-by: 6543 <6543@obermui.de> --- server/api/cron.go | 2 +- server/store/datastore/cron.go | 2 +- server/store/datastore/cron_test.go | 2 +- server/store/types/errors.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/api/cron.go b/server/api/cron.go index 879c84b40a1..8545eecf705 100644 --- a/server/api/cron.go +++ b/server/api/cron.go @@ -155,7 +155,7 @@ func PostCron(c *gin.Context) { } if err := _store.CronCreate(cron); err != nil { - if errors.Is(types.UniqueExists, err) { + if errors.Is(types.ErrUniqueExists, err) { c.String(http.StatusConflict, "cron with this exists for this repo already") } else { c.String(http.StatusInternalServerError, "Error inserting cron %q. %s", in.Name, err) diff --git a/server/store/datastore/cron.go b/server/store/datastore/cron.go index ada7bd3cd83..a27a295af1a 100644 --- a/server/store/datastore/cron.go +++ b/server/store/datastore/cron.go @@ -29,7 +29,7 @@ func (s storage) CronCreate(cron *model.Cron) error { } _, err := s.engine.Insert(cron) if err != nil && (strings.HasPrefix(err.Error(), "UNIQUE constraint failed") || strings.HasPrefix(err.Error(), "pq: duplicate key value violates unique constraint") || strings.Contains(err.Error(), "Duplicate entry")) { - return types.UniqueExists + return types.ErrUniqueExists } return err } diff --git a/server/store/datastore/cron_test.go b/server/store/datastore/cron_test.go index 7c91d11437c..bfe66219bb2 100644 --- a/server/store/datastore/cron_test.go +++ b/server/store/datastore/cron_test.go @@ -34,7 +34,7 @@ func TestCronCreate(t *testing.T) { assert.NotEqualValues(t, 0, cron1.ID) // cannot insert cron job with same repoID and title - assert.ErrorIs(t, types.UniqueExists, store.CronCreate(cron1)) + assert.ErrorIs(t, types.ErrUniqueExists, store.CronCreate(cron1)) oldID := cron1.ID assert.NoError(t, store.CronDelete(repo, oldID)) diff --git a/server/store/types/errors.go b/server/store/types/errors.go index d0c2eca36c0..21efb406a75 100644 --- a/server/store/types/errors.go +++ b/server/store/types/errors.go @@ -21,4 +21,4 @@ import ( var RecordNotExist = sql.ErrNoRows -var UniqueExists = errors.New("unique constraint failed") +var ErrUniqueExists = errors.New("unique constraint failed") From 1148f63644cb1f39041864fde58f3beeac17efad Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Wed, 18 Mar 2026 18:53:18 +0100 Subject: [PATCH 8/8] Apply suggestion from @6543 --- server/api/cron.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api/cron.go b/server/api/cron.go index 8545eecf705..f982832a3d4 100644 --- a/server/api/cron.go +++ b/server/api/cron.go @@ -155,7 +155,7 @@ func PostCron(c *gin.Context) { } if err := _store.CronCreate(cron); err != nil { - if errors.Is(types.ErrUniqueExists, err) { + if errors.Is(err, types.ErrUniqueExists) { c.String(http.StatusConflict, "cron with this exists for this repo already") } else { c.String(http.StatusInternalServerError, "Error inserting cron %q. %s", in.Name, err)