1
0
mirror of https://github.com/woodpecker-ci/woodpecker.git synced 2026-06-03 16:35:37 +02:00

Allow to disable a cron (#5896)

Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
qwerty287
2025-12-24 10:02:03 +01:00
committed by GitHub
parent 2e349b6a13
commit 8c231aa99d
10 changed files with 96 additions and 40 deletions
+21 -1
View File
@@ -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": {
+2 -2
View File
@@ -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
+16 -16
View File
@@ -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.
+26 -13
View File
@@ -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 <personal access token>)
// @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 <personal access token>)
// @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
+8
View File
@@ -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
+1 -1
View File
@@ -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.
+7 -6
View File
@@ -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)
+2 -1
View File
@@ -167,7 +167,8 @@
"placeholder": "Schedule"
},
"edit": "Edit cron",
"delete": "Delete cron"
"delete": "Delete cron",
"enabled": "Enabled"
},
"badge": {
"badge": "Badge",
+1
View File
@@ -3,5 +3,6 @@ export interface Cron {
name: string;
branch: string;
schedule: string;
enabled: boolean;
next_exec: number;
}
+12
View File
@@ -70,6 +70,8 @@
/>
</InputField>
<Checkbox v-model="selectedCronEnabled" :label="$t('repo.settings.crons.enabled')" />
<InputField v-slot="{ id }" :label="$t('repo.settings.crons.branch.title')">
<TextField
:id="id"
@@ -122,6 +124,7 @@ import Button from '~/components/atomic/Button.vue';
import Icon from '~/components/atomic/Icon.vue';
import IconButton from '~/components/atomic/IconButton.vue';
import ListItem from '~/components/atomic/ListItem.vue';
import Checkbox from '~/components/form/Checkbox.vue';
import InputField from '~/components/form/InputField.vue';
import TextField from '~/components/form/TextField.vue';
import Settings from '~/components/layout/Settings.vue';
@@ -144,6 +147,15 @@ const selectedCron = ref<Partial<Cron>>();
const isEditingCron = computed(() => !!selectedCron.value?.id);
const date = useDate();
const selectedCronEnabled = computed<boolean>({
async set(_enabled) {
selectedCron.value!.enabled = _enabled;
},
get() {
return selectedCron.value!.enabled !== undefined ? selectedCron.value!.enabled : true;
},
});
async function loadCrons(page: number): Promise<Cron[] | null> {
return apiClient.getCronList(repo.value.id, { page });
}