From 8c231aa99d986d6d51d587facfbd69e2348a4d82 Mon Sep 17 00:00:00 2001 From: qwerty287 <80460567+qwerty287@users.noreply.github.com> Date: Wed, 24 Dec 2025 10:02:03 +0100 Subject: [PATCH] Allow to disable a cron (#5896) Co-authored-by: 6543 <6543@obermui.de> --- cmd/server/openapi/docs.go | 22 ++++++++++++- pipeline/rpc/proto/woodpecker.pb.go | 4 +-- pipeline/rpc/proto/woodpecker_grpc.pb.go | 32 +++++++++---------- server/api/cron.go | 39 ++++++++++++++++-------- server/model/cron.go | 8 +++++ server/store/datastore/cron.go | 2 +- server/store/datastore/cron_test.go | 13 ++++---- web/src/assets/locales/en.json | 3 +- web/src/lib/api/types/cron.ts | 1 + web/src/views/repo/settings/Crons.vue | 12 ++++++++ 10 files changed, 96 insertions(+), 40 deletions(-) diff --git a/cmd/server/openapi/docs.go b/cmd/server/openapi/docs.go index c232b012a2..4d45d973dd 100644 --- a/cmd/server/openapi/docs.go +++ b/cmd/server/openapi/docs.go @@ -2681,7 +2681,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/Cron" + "$ref": "#/definitions/CronPatch" } } ], @@ -4707,6 +4707,9 @@ const docTemplate = `{ "creator_id": { "type": "integer" }, + "enabled": { + "type": "boolean" + }, "id": { "type": "integer" }, @@ -4725,6 +4728,23 @@ const docTemplate = `{ } } }, + "CronPatch": { + "type": "object", + "properties": { + "branch": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "schedule": { + "type": "string" + } + } + }, "Feed": { "type": "object", "properties": { diff --git a/pipeline/rpc/proto/woodpecker.pb.go b/pipeline/rpc/proto/woodpecker.pb.go index 562836b0d3..a08a42f089 100644 --- a/pipeline/rpc/proto/woodpecker.pb.go +++ b/pipeline/rpc/proto/woodpecker.pb.go @@ -15,8 +15,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.32.1 +// protoc-gen-go v1.36.11 +// protoc v6.33.1 // source: woodpecker.proto package proto diff --git a/pipeline/rpc/proto/woodpecker_grpc.pb.go b/pipeline/rpc/proto/woodpecker_grpc.pb.go index 80dbfb3a15..033af68779 100644 --- a/pipeline/rpc/proto/woodpecker_grpc.pb.go +++ b/pipeline/rpc/proto/woodpecker_grpc.pb.go @@ -15,8 +15,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v6.32.1 +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.33.1 // source: woodpecker.proto package proto @@ -212,37 +212,37 @@ type WoodpeckerServer interface { type UnimplementedWoodpeckerServer struct{} func (UnimplementedWoodpeckerServer) Version(context.Context, *Empty) (*VersionResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Version not implemented") + return nil, status.Error(codes.Unimplemented, "method Version not implemented") } func (UnimplementedWoodpeckerServer) Next(context.Context, *NextRequest) (*NextResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Next not implemented") + return nil, status.Error(codes.Unimplemented, "method Next not implemented") } func (UnimplementedWoodpeckerServer) Init(context.Context, *InitRequest) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method Init not implemented") + return nil, status.Error(codes.Unimplemented, "method Init not implemented") } func (UnimplementedWoodpeckerServer) Wait(context.Context, *WaitRequest) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method Wait not implemented") + return nil, status.Error(codes.Unimplemented, "method Wait not implemented") } func (UnimplementedWoodpeckerServer) Done(context.Context, *DoneRequest) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method Done not implemented") + return nil, status.Error(codes.Unimplemented, "method Done not implemented") } func (UnimplementedWoodpeckerServer) Extend(context.Context, *ExtendRequest) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method Extend not implemented") + return nil, status.Error(codes.Unimplemented, "method Extend not implemented") } func (UnimplementedWoodpeckerServer) Update(context.Context, *UpdateRequest) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") + return nil, status.Error(codes.Unimplemented, "method Update not implemented") } func (UnimplementedWoodpeckerServer) Log(context.Context, *LogRequest) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method Log not implemented") + return nil, status.Error(codes.Unimplemented, "method Log not implemented") } func (UnimplementedWoodpeckerServer) RegisterAgent(context.Context, *RegisterAgentRequest) (*RegisterAgentResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RegisterAgent not implemented") + return nil, status.Error(codes.Unimplemented, "method RegisterAgent not implemented") } func (UnimplementedWoodpeckerServer) UnregisterAgent(context.Context, *Empty) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method UnregisterAgent not implemented") + return nil, status.Error(codes.Unimplemented, "method UnregisterAgent not implemented") } func (UnimplementedWoodpeckerServer) ReportHealth(context.Context, *ReportHealthRequest) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method ReportHealth not implemented") + return nil, status.Error(codes.Unimplemented, "method ReportHealth not implemented") } func (UnimplementedWoodpeckerServer) mustEmbedUnimplementedWoodpeckerServer() {} func (UnimplementedWoodpeckerServer) testEmbeddedByValue() {} @@ -255,7 +255,7 @@ type UnsafeWoodpeckerServer interface { } func RegisterWoodpeckerServer(s grpc.ServiceRegistrar, srv WoodpeckerServer) { - // If the following call pancis, it indicates UnimplementedWoodpeckerServer was + // If the following call panics, it indicates UnimplementedWoodpeckerServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. @@ -564,7 +564,7 @@ type WoodpeckerAuthServer interface { type UnimplementedWoodpeckerAuthServer struct{} func (UnimplementedWoodpeckerAuthServer) Auth(context.Context, *AuthRequest) (*AuthResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Auth not implemented") + return nil, status.Error(codes.Unimplemented, "method Auth not implemented") } func (UnimplementedWoodpeckerAuthServer) mustEmbedUnimplementedWoodpeckerAuthServer() {} func (UnimplementedWoodpeckerAuthServer) testEmbeddedByValue() {} @@ -577,7 +577,7 @@ type UnsafeWoodpeckerAuthServer interface { } func RegisterWoodpeckerAuthServer(s grpc.ServiceRegistrar, srv WoodpeckerAuthServer) { - // If the following call pancis, it indicates UnimplementedWoodpeckerAuthServer was + // If the following call panics, it indicates UnimplementedWoodpeckerAuthServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/server/api/cron.go b/server/api/cron.go index 78b05b6fe5..b8d9ecfa56 100644 --- a/server/api/cron.go +++ b/server/api/cron.go @@ -128,6 +128,7 @@ func PostCron(c *gin.Context) { CreatorID: user.ID, Schedule: in.Schedule, Branch: in.Branch, + Enabled: in.Enabled, } if err := cron.Validate(); err != nil { c.String(http.StatusUnprocessableEntity, "Error inserting cron. validate failed: %s", err) @@ -164,10 +165,10 @@ func PostCron(c *gin.Context) { // @Produce json // @Success 200 {object} Cron // @Tags Repository cron jobs -// @Param Authorization header string true "Insert your personal access token" default(Bearer ) -// @Param repo_id path int true "the repository id" -// @Param cron path string true "the cron job id" -// @Param cronJob body Cron true "the cron job data" +// @Param Authorization header string true "Insert your personal access token" default(Bearer ) +// @Param repo_id path int true "the repository id" +// @Param cron path string true "the cron job id" +// @Param cronJob body CronPatch true "the cron job data" func PatchCron(c *gin.Context) { repo := session.Repo(c) user := session.User(c) @@ -185,7 +186,7 @@ func PatchCron(c *gin.Context) { return } - in := new(model.Cron) + in := new(model.CronPatch) err = c.Bind(in) if err != nil { c.String(http.StatusBadRequest, "Error parsing request. %s", err) @@ -197,26 +198,38 @@ func PatchCron(c *gin.Context) { handleDBError(c, err) return } - if in.Branch != "" { + if in.Branch != nil && *in.Branch != "" { // check if branch exists on forge - _, err := _forge.BranchHead(c, user, repo, in.Branch) + _, err := _forge.BranchHead(c, user, repo, *in.Branch) if err != nil { c.String(http.StatusBadRequest, "Error inserting cron. branch not resolved: %s", err) return } - cron.Branch = in.Branch + cron.Branch = *in.Branch } - if in.Schedule != "" { - cron.Schedule = in.Schedule - nextExec, err := cronScheduler.CalcNewNext(in.Schedule, time.Now()) + if in.Schedule != nil && *in.Schedule != "" { + cron.Schedule = *in.Schedule + nextExec, err := cronScheduler.CalcNewNext(*in.Schedule, time.Now()) if err != nil { c.String(http.StatusBadRequest, "Error inserting cron. schedule could not parsed: %s", err) return } cron.NextExec = nextExec.Unix() } - if in.Name != "" { - cron.Name = in.Name + if in.Name != nil && *in.Name != "" { + cron.Name = *in.Name + } + if in.Enabled != nil { + cron.Enabled = *in.Enabled + // if we re-enable a cron we have to calc NextExec because it was not while disabled + if cron.Enabled { + nextExec, err := cronScheduler.CalcNewNext(*in.Schedule, time.Now()) + if err != nil { + c.String(http.StatusInternalServerError, "Cron schedule could not parsed: %s", err) + return + } + cron.NextExec = nextExec.Unix() + } } cron.CreatorID = user.ID diff --git a/server/model/cron.go b/server/model/cron.go index 7af8890856..f865adf068 100644 --- a/server/model/cron.go +++ b/server/model/cron.go @@ -29,6 +29,7 @@ type Cron struct { Schedule string `json:"schedule" xorm:"schedule NOT NULL"` // @weekly, 3min, ... Created int64 `json:"created" xorm:"created NOT NULL DEFAULT 0"` Branch string `json:"branch" xorm:"branch"` + Enabled bool `json:"enabled" xorm:"enabled NOT NULL DEFAULT TRUE"` } // @name Cron // TableName returns the database table name for xorm. @@ -53,3 +54,10 @@ func (c *Cron) Validate() error { return nil } + +type CronPatch struct { + Name *string `json:"name"` + Schedule *string `json:"schedule"` + Branch *string `json:"branch"` + Enabled *bool `json:"enabled"` +} // @name CronPatch diff --git a/server/store/datastore/cron.go b/server/store/datastore/cron.go index 36d97dbb7b..85530036f1 100644 --- a/server/store/datastore/cron.go +++ b/server/store/datastore/cron.go @@ -50,7 +50,7 @@ func (s storage) CronDelete(repo *model.Repo, id int64) error { // CronListNextExecute returns limited number of jobs with NextExec being less or equal to the provided unix timestamp. func (s storage) CronListNextExecute(nextExec, limit int64) ([]*model.Cron, error) { crons := make([]*model.Cron, 0, limit) - return crons, s.engine.Join("INNER", "repos", "repos.id = crons.repo_id").Where(builder.Lte{"next_exec": nextExec}).And(builder.Eq{"repos.active": true}).Limit(int(limit)).Find(&crons) + return crons, s.engine.Join("INNER", "repos", "repos.id = crons.repo_id").Where(builder.Lte{"next_exec": nextExec}).And(builder.Eq{"repos.active": true, "enabled": true}).Limit(int(limit)).Find(&crons) } // CronGetLock try to get a lock by updating NextExec. diff --git a/server/store/datastore/cron_test.go b/server/store/datastore/cron_test.go index 8ee385aad0..289c57a27b 100644 --- a/server/store/datastore/cron_test.go +++ b/server/store/datastore/cron_test.go @@ -57,12 +57,13 @@ func TestCronListNextExecute(t *testing.T) { now := time.Now().Unix() - assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "some", RepoID: repo1.ID, NextExec: now})) - assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "aaaa", RepoID: repo1.ID, NextExec: now})) - assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "bbbb", RepoID: repo1.ID, NextExec: now})) - assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "none", RepoID: repo1.ID, NextExec: now + 1000})) - assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "test", RepoID: repo1.ID, NextExec: now + 2000})) - assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "disabled-repo", RepoID: repo2.ID, NextExec: now})) + assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "some", RepoID: repo1.ID, NextExec: now, Enabled: true})) + assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "aaaa", RepoID: repo1.ID, NextExec: now, Enabled: true})) + assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "bbbb", RepoID: repo1.ID, NextExec: now, Enabled: true})) + assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "none", RepoID: repo1.ID, NextExec: now + 1000, Enabled: true})) + assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "test", RepoID: repo1.ID, NextExec: now + 2000, Enabled: true})) + assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "disabled-repo", RepoID: repo2.ID, NextExec: now, Enabled: true})) + assert.NoError(t, store.CronCreate(&model.Cron{Schedule: "@every 1h", Name: "disabled-cron", RepoID: repo1.ID, NextExec: now, Enabled: false})) jobs, err = store.CronListNextExecute(now, 10) assert.NoError(t, err) diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index f53265d99a..2426835d21 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -167,7 +167,8 @@ "placeholder": "Schedule" }, "edit": "Edit cron", - "delete": "Delete cron" + "delete": "Delete cron", + "enabled": "Enabled" }, "badge": { "badge": "Badge", diff --git a/web/src/lib/api/types/cron.ts b/web/src/lib/api/types/cron.ts index d7b70f3e0b..d586985632 100644 --- a/web/src/lib/api/types/cron.ts +++ b/web/src/lib/api/types/cron.ts @@ -3,5 +3,6 @@ export interface Cron { name: string; branch: string; schedule: string; + enabled: boolean; next_exec: number; } diff --git a/web/src/views/repo/settings/Crons.vue b/web/src/views/repo/settings/Crons.vue index 86b0e36dbe..f5b9a85783 100644 --- a/web/src/views/repo/settings/Crons.vue +++ b/web/src/views/repo/settings/Crons.vue @@ -70,6 +70,8 @@ /> + + >(); const isEditingCron = computed(() => !!selectedCron.value?.id); const date = useDate(); +const selectedCronEnabled = computed({ + async set(_enabled) { + selectedCron.value!.enabled = _enabled; + }, + get() { + return selectedCron.value!.enabled !== undefined ? selectedCron.value!.enabled : true; + }, +}); + async function loadCrons(page: number): Promise { return apiClient.getCronList(repo.value.id, { page }); }