From 7e960b4a2b5942cda49842a8b4f11eeee5ed2af3 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 13 Feb 2025 15:47:30 +0000 Subject: [PATCH] Added support for managing manual pipeline parameters Introduced backend API endpoints, database operations, and web UI components for creating, updating, listing, and deleting manual pipeline parameters. This update includes tests for datastore functions, integration with the Vue.js web interface, and mock implementation for parameter-related methods. --- server/api/repo_parameter.go | 152 +++++++++ server/model/parameter.go | 63 ++++ server/router/api.go | 7 + server/services/manager.go | 12 + server/services/mocks/manager.go | 295 ++++++++++++++++++ server/services/parameter/db.go | 61 ++++ server/services/parameter/db_test.go | 45 +++ server/services/parameter/mocks/service.go | 219 +++++++++++++ server/services/parameter/service.go | 32 ++ server/services/setup.go | 5 + server/store/datastore/migration/migration.go | 1 + server/store/datastore/parameter.go | 72 +++++ server/store/datastore/parameter_test.go | 118 +++++++ server/store/mocks/store.go | 192 ++++++++++++ server/store/store.go | 10 + web/src/assets/locales/en.json | 38 ++- .../components/parameters/ParameterEdit.vue | 234 ++++++++++++++ .../components/parameters/ParameterList.vue | 50 +++ web/src/lib/api/index.ts | 21 ++ web/src/lib/api/types/index.ts | 1 + web/src/lib/api/types/parameter.ts | 19 ++ web/src/router.ts | 6 + web/src/views/repo/RepoManualPipeline.vue | 221 ++++++++++++- .../views/repo/pipeline/PipelineConfig.vue | 19 ++ .../views/repo/pipeline/PipelineWrapper.vue | 4 + web/src/views/repo/settings/Parameters.vue | 113 +++++++ web/src/views/repo/settings/RepoSettings.vue | 1 + 27 files changed, 2005 insertions(+), 6 deletions(-) create mode 100644 server/api/repo_parameter.go create mode 100644 server/model/parameter.go create mode 100644 server/services/mocks/manager.go create mode 100644 server/services/parameter/db.go create mode 100644 server/services/parameter/db_test.go create mode 100644 server/services/parameter/mocks/service.go create mode 100644 server/services/parameter/service.go create mode 100644 server/store/datastore/parameter.go create mode 100644 server/store/datastore/parameter_test.go create mode 100644 web/src/components/parameters/ParameterEdit.vue create mode 100644 web/src/components/parameters/ParameterList.vue create mode 100644 web/src/lib/api/types/parameter.ts create mode 100644 web/src/views/repo/settings/Parameters.vue diff --git a/server/api/repo_parameter.go b/server/api/repo_parameter.go new file mode 100644 index 000000000..c728528bc --- /dev/null +++ b/server/api/repo_parameter.go @@ -0,0 +1,152 @@ +package api + +import ( + "errors" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "go.woodpecker-ci.org/woodpecker/v3/server" + "go.woodpecker-ci.org/woodpecker/v3/server/model" + "go.woodpecker-ci.org/woodpecker/v3/server/router/middleware/session" + "go.woodpecker-ci.org/woodpecker/v3/server/store/types" +) + +// GetParameter returns a repository parameter by ID. +func GetParameter(c *gin.Context) { + repo := session.Repo(c) + paramID, err := strconv.ParseInt(c.Param("parameter"), 10, 64) + if err != nil { + c.String(http.StatusBadRequest, "Invalid parameter ID") + return + } + + parameterService := server.Config.Services.Manager.ParameterServiceFromRepo(repo) + parameter, err := parameterService.ParameterFindByID(repo, paramID) + if err != nil { + handleDBError(c, err) + return + } + c.JSON(http.StatusOK, parameter) +} + +// PostParameter persists a parameter. +func PostParameter(c *gin.Context) { + repo := session.Repo(c) + + in := new(model.Parameter) + if err := c.Bind(in); err != nil { + c.String(http.StatusBadRequest, "Error parsing parameter. %s", err) + return + } + in.RepoID = repo.ID + + if err := in.Validate(); err != nil { + c.String(http.StatusBadRequest, "Error validating parameter. %s", err) + return + } + + parameterService := server.Config.Services.Manager.ParameterServiceFromRepo(repo) + + // Check if parameter with same name and branch already exists + existing, err := parameterService.ParameterFindByNameAndBranch(repo, in.Name, in.Branch) + if err != nil && !errors.Is(err, types.RecordNotExist) { + handleDBError(c, err) + return + } + if existing != nil && existing.ID != 0 { + c.String(http.StatusConflict, "Parameter with name '%s' already exists for branch '%s': existing: %d, new: %d", in.Name, in.Branch, existing.ID, in.ID) + return + } + + err = parameterService.ParameterCreate(repo, in) + if err != nil { + handleDBError(c, err) + return + } + parameter, err := parameterService.ParameterFind(repo, in.Name) + c.JSON(http.StatusOK, parameter) +} + +// PatchParameter updates an existing parameter by ID +func PatchParameter(c *gin.Context) { + repo := session.Repo(c) + paramID, err := strconv.ParseInt(c.Param("parameter"), 10, 64) + if err != nil { + c.String(http.StatusBadRequest, "Invalid parameter ID") + return + } + + in := new(model.Parameter) + if err := c.Bind(in); err != nil { + c.String(http.StatusBadRequest, "Error parsing parameter. %s", err) + return + } + in.RepoID = repo.ID + in.ID = paramID + + if err := in.Validate(); err != nil { + c.String(http.StatusBadRequest, "Error validating parameter. %s", err) + return + } + + parameterService := server.Config.Services.Manager.ParameterServiceFromRepo(repo) + + // Get existing parameter to check if name/branch changed + existing, err := parameterService.ParameterFindByID(repo, paramID) + if err != nil { + handleDBError(c, err) + return + } + + // If name or branch changed, check for conflicts + if existing.Name != in.Name || existing.Branch != in.Branch { + conflict, err := parameterService.ParameterFindByNameAndBranch(repo, in.Name, in.Branch) + if err != nil && !errors.Is(err, types.RecordNotExist) { + handleDBError(c, err) + return + } + if conflict != nil && conflict.ID != 0 && conflict.ID != paramID { + c.String(http.StatusConflict, "Parameter with name '%s' already exists for branch '%s'", in.Name, in.Branch) + return + } + } + + err = parameterService.ParameterUpdate(repo, in) + if err != nil { + handleDBError(c, err) + return + } + parameter, err := parameterService.ParameterFindByID(repo, paramID) + c.JSON(http.StatusOK, parameter) +} + +// GetParameterList returns all repository parameters. +func GetParameterList(c *gin.Context) { + repo := session.Repo(c) + parameterService := server.Config.Services.Manager.ParameterServiceFromRepo(repo) + list, err := parameterService.ParameterList(repo) + if err != nil { + handleDBError(c, err) + return + } + c.JSON(http.StatusOK, list) +} + +// DeleteParameter deletes a parameter by ID. +func DeleteParameter(c *gin.Context) { + repo := session.Repo(c) + paramID, err := strconv.ParseInt(c.Param("parameter"), 10, 64) + if err != nil { + c.String(http.StatusBadRequest, "Invalid parameter ID") + return + } + + parameterService := server.Config.Services.Manager.ParameterServiceFromRepo(repo) + if err := parameterService.ParameterDeleteByID(repo, paramID); err != nil { + handleDBError(c, err) + return + } + c.Status(http.StatusNoContent) +} diff --git a/server/model/parameter.go b/server/model/parameter.go new file mode 100644 index 000000000..7befbe9b7 --- /dev/null +++ b/server/model/parameter.go @@ -0,0 +1,63 @@ +package model + +import ( + "errors" + "fmt" +) + +var ( + ErrParameterNameInvalid = errors.New("invalid parameter name") + ErrParameterTypeInvalid = errors.New("invalid parameter type") +) + +type ParameterType string + +const ( + ParameterTypeBoolean ParameterType = "boolean" + ParameterTypeSingleChoice ParameterType = "single_choice" + ParameterTypeMultipleChoice ParameterType = "multiple_choice" + ParameterTypeString ParameterType = "string" + ParameterTypeText ParameterType = "text" + ParameterTypePassword ParameterType = "password" +) + +// Parameter represents a configurable parameter for a repository. +type Parameter struct { + ID int64 `json:"id" xorm:"pk autoincr 'parameter_id'"` + RepoID int64 `json:"repo_id" xorm:"UNIQUE(s) 'parameter_repo_id'"` + Name string `json:"name" xorm:"UNIQUE(s) 'parameter_name'"` + Branch string `json:"branch" xorm:"UNIQUE(s) 'parameter_branch'"` + Type ParameterType `json:"type" xorm:"'parameter_type'"` + Description string `json:"description" xorm:"TEXT 'parameter_description'"` + DefaultValue string `json:"default_value" xorm:"TEXT 'parameter_default_value'"` + TrimString bool `json:"trim_string" xorm:"'parameter_trim_string'"` +} + +// TableName return database table name for xorm. +func (Parameter) TableName() string { + return "parameters" +} + +// Validate validates the required fields and formats. +func (p *Parameter) Validate() error { + switch { + case len(p.Name) == 0: + return fmt.Errorf("%w: empty name", ErrParameterNameInvalid) + case len(p.Branch) == 0: + return fmt.Errorf("%w: empty branch", ErrParameterNameInvalid) + case !validParameterType(p.Type): + return fmt.Errorf("%w: %s", ErrParameterTypeInvalid, p.Type) + default: + return nil + } +} + +func validParameterType(t ParameterType) bool { + switch t { + case ParameterTypeBoolean, ParameterTypeSingleChoice, ParameterTypeMultipleChoice, + ParameterTypeString, ParameterTypeText, ParameterTypePassword: + return true + default: + return false + } +} diff --git a/server/router/api.go b/server/router/api.go index 23f306aa3..d7e06bbd6 100644 --- a/server/router/api.go +++ b/server/router/api.go @@ -126,6 +126,13 @@ func apiRoutes(e *gin.RouterGroup) { // requires push permissions repo.DELETE("/logs/:number", session.MustPush, api.DeletePipelineLogs) + // requires push permissions + repo.GET("/parameters", api.GetParameterList) + repo.POST("/parameters", session.MustPush, api.PostParameter) + repo.GET("/parameters/:parameter", api.GetParameter) + repo.PATCH("/parameters/:parameter", session.MustPush, api.PatchParameter) + repo.DELETE("/parameters/:parameter", session.MustPush, api.DeleteParameter) + // requires push permissions repo.GET("/secrets", session.MustPush, api.GetSecretList) repo.POST("/secrets", session.MustPush, api.PostSecret) diff --git a/server/services/manager.go b/server/services/manager.go index 43e370cd5..d115beb52 100644 --- a/server/services/manager.go +++ b/server/services/manager.go @@ -26,6 +26,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/model" "go.woodpecker-ci.org/woodpecker/v3/server/services/config" "go.woodpecker-ci.org/woodpecker/v3/server/services/environment" + "go.woodpecker-ci.org/woodpecker/v3/server/services/parameter" "go.woodpecker-ci.org/woodpecker/v3/server/services/registry" "go.woodpecker-ci.org/woodpecker/v3/server/services/secret" "go.woodpecker-ci.org/woodpecker/v3/server/services/utils" @@ -38,6 +39,7 @@ type SetupForge func(forge *model.Forge) (forge.Forge, error) type Manager interface { SignaturePublicKey() crypto.PublicKey + ParameterServiceFromRepo(repo *model.Repo) parameter.Service SecretServiceFromRepo(repo *model.Repo) secret.Service SecretService() secret.Service RegistryServiceFromRepo(repo *model.Repo) registry.Service @@ -53,6 +55,7 @@ type manager struct { signaturePrivateKey crypto.PrivateKey signaturePublicKey crypto.PublicKey store store.Store + parameter parameter.Service secret secret.Service registry registry.Service config config.Service @@ -87,6 +90,7 @@ func NewManager(c *cli.Command, store store.Store, setupForge SetupForge) (Manag signaturePrivateKey: signaturePrivateKey, signaturePublicKey: signaturePublicKey, store: store, + parameter: setupParameterService(store), secret: setupSecretService(store), registry: setupRegistryService(store, c.String("docker-config")), config: configService, @@ -101,10 +105,18 @@ func (m *manager) SignaturePublicKey() crypto.PublicKey { return m.signaturePublicKey } +func (m *manager) ParameterServiceFromRepo(_ *model.Repo) parameter.Service { + return m.ParameterService() +} + func (m *manager) SecretServiceFromRepo(_ *model.Repo) secret.Service { return m.SecretService() } +func (m *manager) ParameterService() parameter.Service { + return m.parameter +} + func (m *manager) SecretService() secret.Service { return m.secret } diff --git a/server/services/mocks/manager.go b/server/services/mocks/manager.go new file mode 100644 index 000000000..12330c516 --- /dev/null +++ b/server/services/mocks/manager.go @@ -0,0 +1,295 @@ +// Code generated by mockery. DO NOT EDIT. + +//go:build test +// +build test + +package mocks + +import ( + crypto "crypto" + + config "go.woodpecker-ci.org/woodpecker/v3/server/services/config" + + environment "go.woodpecker-ci.org/woodpecker/v3/server/services/environment" + + forge "go.woodpecker-ci.org/woodpecker/v3/server/forge" + + mock "github.com/stretchr/testify/mock" + + model "go.woodpecker-ci.org/woodpecker/v3/server/model" + + parameter "go.woodpecker-ci.org/woodpecker/v3/server/services/parameter" + + registry "go.woodpecker-ci.org/woodpecker/v3/server/services/registry" + + secret "go.woodpecker-ci.org/woodpecker/v3/server/services/secret" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// ConfigServiceFromRepo provides a mock function with given fields: repo +func (_m *Manager) ConfigServiceFromRepo(repo *model.Repo) config.Service { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for ConfigServiceFromRepo") + } + + var r0 config.Service + if rf, ok := ret.Get(0).(func(*model.Repo) config.Service); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(config.Service) + } + } + + return r0 +} + +// EnvironmentService provides a mock function with no fields +func (_m *Manager) EnvironmentService() environment.Service { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for EnvironmentService") + } + + var r0 environment.Service + if rf, ok := ret.Get(0).(func() environment.Service); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(environment.Service) + } + } + + return r0 +} + +// ForgeByID provides a mock function with given fields: forgeID +func (_m *Manager) ForgeByID(forgeID int64) (forge.Forge, error) { + ret := _m.Called(forgeID) + + if len(ret) == 0 { + panic("no return value specified for ForgeByID") + } + + var r0 forge.Forge + var r1 error + if rf, ok := ret.Get(0).(func(int64) (forge.Forge, error)); ok { + return rf(forgeID) + } + if rf, ok := ret.Get(0).(func(int64) forge.Forge); ok { + r0 = rf(forgeID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(forge.Forge) + } + } + + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(forgeID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeFromRepo provides a mock function with given fields: repo +func (_m *Manager) ForgeFromRepo(repo *model.Repo) (forge.Forge, error) { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for ForgeFromRepo") + } + + var r0 forge.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo) (forge.Forge, error)); ok { + return rf(repo) + } + if rf, ok := ret.Get(0).(func(*model.Repo) forge.Forge); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(forge.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo) error); ok { + r1 = rf(repo) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeFromUser provides a mock function with given fields: user +func (_m *Manager) ForgeFromUser(user *model.User) (forge.Forge, error) { + ret := _m.Called(user) + + if len(ret) == 0 { + panic("no return value specified for ForgeFromUser") + } + + var r0 forge.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.User) (forge.Forge, error)); ok { + return rf(user) + } + if rf, ok := ret.Get(0).(func(*model.User) forge.Forge); ok { + r0 = rf(user) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(forge.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.User) error); ok { + r1 = rf(user) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ParameterServiceFromRepo provides a mock function with given fields: repo +func (_m *Manager) ParameterServiceFromRepo(repo *model.Repo) parameter.Service { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for ParameterServiceFromRepo") + } + + var r0 parameter.Service + if rf, ok := ret.Get(0).(func(*model.Repo) parameter.Service); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(parameter.Service) + } + } + + return r0 +} + +// RegistryService provides a mock function with no fields +func (_m *Manager) RegistryService() registry.Service { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RegistryService") + } + + var r0 registry.Service + if rf, ok := ret.Get(0).(func() registry.Service); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(registry.Service) + } + } + + return r0 +} + +// RegistryServiceFromRepo provides a mock function with given fields: repo +func (_m *Manager) RegistryServiceFromRepo(repo *model.Repo) registry.Service { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for RegistryServiceFromRepo") + } + + var r0 registry.Service + if rf, ok := ret.Get(0).(func(*model.Repo) registry.Service); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(registry.Service) + } + } + + return r0 +} + +// SecretService provides a mock function with no fields +func (_m *Manager) SecretService() secret.Service { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for SecretService") + } + + var r0 secret.Service + if rf, ok := ret.Get(0).(func() secret.Service); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(secret.Service) + } + } + + return r0 +} + +// SecretServiceFromRepo provides a mock function with given fields: repo +func (_m *Manager) SecretServiceFromRepo(repo *model.Repo) secret.Service { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for SecretServiceFromRepo") + } + + var r0 secret.Service + if rf, ok := ret.Get(0).(func(*model.Repo) secret.Service); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(secret.Service) + } + } + + return r0 +} + +// SignaturePublicKey provides a mock function with no fields +func (_m *Manager) SignaturePublicKey() crypto.PublicKey { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for SignaturePublicKey") + } + + var r0 crypto.PublicKey + if rf, ok := ret.Get(0).(func() crypto.PublicKey); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(crypto.PublicKey) + } + } + + return r0 +} + +// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewManager(t interface { + mock.TestingT + Cleanup(func()) +}) *Manager { + mock := &Manager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/server/services/parameter/db.go b/server/services/parameter/db.go new file mode 100644 index 000000000..8f5c4cb25 --- /dev/null +++ b/server/services/parameter/db.go @@ -0,0 +1,61 @@ +// Copyright 2024 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parameter + +import ( + "go.woodpecker-ci.org/woodpecker/v3/server/model" + "go.woodpecker-ci.org/woodpecker/v3/server/store" +) + +type db struct { + store store.Store +} + +// NewDB returns a new parameter service. +func NewDB(store store.Store) Service { + return &db{store: store} +} + +func (d *db) ParameterFind(repo *model.Repo, name string) (*model.Parameter, error) { + return d.store.ParameterFind(repo, name) +} + +func (d *db) ParameterFindByID(repo *model.Repo, id int64) (*model.Parameter, error) { + return d.store.ParameterFindByID(repo, id) +} + +func (d *db) ParameterFindByNameAndBranch(repo *model.Repo, name string, branch string) (*model.Parameter, error) { + return d.store.ParameterFindByNameAndBranch(repo, name, branch) +} + +func (d *db) ParameterList(repo *model.Repo) ([]*model.Parameter, error) { + return d.store.ParameterList(repo) +} + +func (d *db) ParameterCreate(repo *model.Repo, parameter *model.Parameter) error { + return d.store.ParameterCreate(repo, parameter) +} + +func (d *db) ParameterUpdate(repo *model.Repo, parameter *model.Parameter) error { + return d.store.ParameterUpdate(repo, parameter) +} + +func (d *db) ParameterDelete(repo *model.Repo, name string) error { + return d.store.ParameterDelete(repo, name) +} + +func (d *db) ParameterDeleteByID(repo *model.Repo, id int64) error { + return d.store.ParameterDeleteByID(repo, id) +} diff --git a/server/services/parameter/db_test.go b/server/services/parameter/db_test.go new file mode 100644 index 000000000..1c3595513 --- /dev/null +++ b/server/services/parameter/db_test.go @@ -0,0 +1,45 @@ +// Copyright 2024 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parameter_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "go.woodpecker-ci.org/woodpecker/v3/server/model" + "go.woodpecker-ci.org/woodpecker/v3/server/services/parameter" + mocks_store "go.woodpecker-ci.org/woodpecker/v3/server/store/mocks" +) + +func TestParameterList(t *testing.T) { + mockStore := mocks_store.NewStore(t) + + testParam := &model.Parameter{ + ID: 1, + RepoID: 1, + Name: "test", + Type: "string", + Description: "test parameter", + } + + mockStore.On("ParameterList", mock.Anything).Return([]*model.Parameter{testParam}, nil) + + s, err := parameter.NewDB(mockStore).ParameterList(&model.Repo{}) + assert.NoError(t, err) + assert.Len(t, s, 1) + assert.Equal(t, "test", s[0].Name) +} diff --git a/server/services/parameter/mocks/service.go b/server/services/parameter/mocks/service.go new file mode 100644 index 000000000..9e6359884 --- /dev/null +++ b/server/services/parameter/mocks/service.go @@ -0,0 +1,219 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + model "go.woodpecker-ci.org/woodpecker/v3/server/model" +) + +// Service is an autogenerated mock type for the Service type +type Service struct { + mock.Mock +} + +// ParameterCreate provides a mock function with given fields: _a0, _a1 +func (_m *Service) ParameterCreate(_a0 *model.Repo, _a1 *model.Parameter) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterCreate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Repo, *model.Parameter) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ParameterDelete provides a mock function with given fields: _a0, _a1 +func (_m *Service) ParameterDelete(_a0 *model.Repo, _a1 string) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterDelete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Repo, string) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ParameterDeleteByID provides a mock function with given fields: _a0, _a1 +func (_m *Service) ParameterDeleteByID(_a0 *model.Repo, _a1 int64) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterDeleteByID") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Repo, int64) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ParameterFind provides a mock function with given fields: _a0, _a1 +func (_m *Service) ParameterFind(_a0 *model.Repo, _a1 string) (*model.Parameter, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterFind") + } + + var r0 *model.Parameter + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo, string) (*model.Parameter, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(*model.Repo, string) *model.Parameter); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Parameter) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo, string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ParameterFindByID provides a mock function with given fields: _a0, _a1 +func (_m *Service) ParameterFindByID(_a0 *model.Repo, _a1 int64) (*model.Parameter, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterFindByID") + } + + var r0 *model.Parameter + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo, int64) (*model.Parameter, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(*model.Repo, int64) *model.Parameter); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Parameter) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo, int64) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ParameterFindByNameAndBranch provides a mock function with given fields: repo, name, branch +func (_m *Service) ParameterFindByNameAndBranch(repo *model.Repo, name string, branch string) (*model.Parameter, error) { + ret := _m.Called(repo, name, branch) + + if len(ret) == 0 { + panic("no return value specified for ParameterFindByNameAndBranch") + } + + var r0 *model.Parameter + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo, string, string) (*model.Parameter, error)); ok { + return rf(repo, name, branch) + } + if rf, ok := ret.Get(0).(func(*model.Repo, string, string) *model.Parameter); ok { + r0 = rf(repo, name, branch) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Parameter) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo, string, string) error); ok { + r1 = rf(repo, name, branch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ParameterList provides a mock function with given fields: _a0 +func (_m *Service) ParameterList(_a0 *model.Repo) ([]*model.Parameter, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for ParameterList") + } + + var r0 []*model.Parameter + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo) ([]*model.Parameter, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(*model.Repo) []*model.Parameter); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Parameter) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ParameterUpdate provides a mock function with given fields: _a0, _a1 +func (_m *Service) ParameterUpdate(_a0 *model.Repo, _a1 *model.Parameter) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterUpdate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Repo, *model.Parameter) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewService(t interface { + mock.TestingT + Cleanup(func()) +}) *Service { + mock := &Service{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/server/services/parameter/service.go b/server/services/parameter/service.go new file mode 100644 index 000000000..1c41ed742 --- /dev/null +++ b/server/services/parameter/service.go @@ -0,0 +1,32 @@ +// Copyright 2024 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parameter + +import "go.woodpecker-ci.org/woodpecker/v3/server/model" + +//go:generate mockery --name Service --output mocks --case underscore + +// Service defines a service for managing parameters. +type Service interface { + // Repository parameters + ParameterFind(*model.Repo, string) (*model.Parameter, error) + ParameterFindByID(*model.Repo, int64) (*model.Parameter, error) + ParameterFindByNameAndBranch(repo *model.Repo, name string, branch string) (*model.Parameter, error) + ParameterList(*model.Repo) ([]*model.Parameter, error) + ParameterCreate(*model.Repo, *model.Parameter) error + ParameterUpdate(*model.Repo, *model.Parameter) error + ParameterDelete(*model.Repo, string) error + ParameterDeleteByID(*model.Repo, int64) error +} diff --git a/server/services/setup.go b/server/services/setup.go index 1079326f6..4660dab1b 100644 --- a/server/services/setup.go +++ b/server/services/setup.go @@ -28,6 +28,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/model" "go.woodpecker-ci.org/woodpecker/v3/server/services/config" + "go.woodpecker-ci.org/woodpecker/v3/server/services/parameter" "go.woodpecker-ci.org/woodpecker/v3/server/services/registry" "go.woodpecker-ci.org/woodpecker/v3/server/services/secret" "go.woodpecker-ci.org/woodpecker/v3/server/services/utils" @@ -46,6 +47,10 @@ func setupRegistryService(store store.Store, dockerConfig string) registry.Servi return registry.NewDB(store) } +func setupParameterService(store store.Store) parameter.Service { + return parameter.NewDB(store) +} + func setupSecretService(store store.Store) secret.Service { // TODO(1544): fix encrypted store // // encryption diff --git a/server/store/datastore/migration/migration.go b/server/store/datastore/migration/migration.go index e5e72ea78..3971807e1 100644 --- a/server/store/datastore/migration/migration.go +++ b/server/store/datastore/migration/migration.go @@ -67,6 +67,7 @@ var allBeans = []any{ new(model.Step), new(model.Registry), new(model.Repo), + new(model.Parameter), new(model.Secret), new(model.Task), new(model.User), diff --git a/server/store/datastore/parameter.go b/server/store/datastore/parameter.go new file mode 100644 index 000000000..23106ae9a --- /dev/null +++ b/server/store/datastore/parameter.go @@ -0,0 +1,72 @@ +// Copyright 2024 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datastore + +import ( + "go.woodpecker-ci.org/woodpecker/v3/server/model" +) + +func (s storage) ParameterFind(repo *model.Repo, name string) (*model.Parameter, error) { + parameter := new(model.Parameter) + return parameter, wrapGet(s.engine.Where("parameter_repo_id = ? AND parameter_name = ?", repo.ID, name).Get(parameter)) +} + +func (s storage) ParameterFindByID(repo *model.Repo, id int64) (*model.Parameter, error) { + parameter := new(model.Parameter) + return parameter, wrapGet(s.engine.Where("parameter_repo_id = ? AND parameter_id = ?", repo.ID, id).Get(parameter)) +} + +func (s storage) ParameterFindByNameAndBranch(repo *model.Repo, name string, branch string) (*model.Parameter, error) { + parameter := new(model.Parameter) + return parameter, wrapGet(s.engine.Where("parameter_repo_id = ? AND parameter_name = ? AND parameter_branch = ?", repo.ID, name, branch).Get(parameter)) +} + +func (s storage) ParameterList(repo *model.Repo) ([]*model.Parameter, error) { + var parameters []*model.Parameter + return parameters, s.engine.Where("parameter_repo_id = ?", repo.ID).OrderBy("parameter_name").Find(¶meters) +} + +func (s storage) ParameterCreate(repo *model.Repo, parameter *model.Parameter) error { + if err := parameter.Validate(); err != nil { + return err + } + parameter.RepoID = repo.ID + // only Insert set auto created ID back to object + _, err := s.engine.Insert(parameter) + return err +} + +func (s storage) ParameterUpdate(repo *model.Repo, parameter *model.Parameter) error { + if err := parameter.Validate(); err != nil { + return err + } + parameter.RepoID = repo.ID + _, err := s.engine.Where("parameter_repo_id = ? AND parameter_id = ?", repo.ID, parameter.ID). + AllCols(). + Update(parameter) + return err +} + +func (s storage) ParameterDelete(repo *model.Repo, name string) error { + _, err := s.engine.Where("parameter_repo_id = ? AND parameter_id = ?", repo.ID, name). + Delete(&model.Parameter{}) + return err +} + +func (s storage) ParameterDeleteByID(repo *model.Repo, id int64) error { + _, err := s.engine.Where("parameter_repo_id = ? AND parameter_id = ?", repo.ID, id). + Delete(&model.Parameter{}) + return err +} diff --git a/server/store/datastore/parameter_test.go b/server/store/datastore/parameter_test.go new file mode 100644 index 000000000..b0afe62f4 --- /dev/null +++ b/server/store/datastore/parameter_test.go @@ -0,0 +1,118 @@ +// Copyright 2024 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datastore + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "go.woodpecker-ci.org/woodpecker/v3/server/model" +) + +func TestParameterFind(t *testing.T) { + store, closer := newTestStore(t, new(model.Parameter)) + defer closer() + + repo := &model.Repo{ + ID: 1, + } + parameter := &model.Parameter{ + RepoID: repo.ID, + Name: "foo", + Type: model.ParameterTypeString, + Description: "test parameter", + } + + assert.NoError(t, store.ParameterCreate(repo, parameter)) + parameter, err := store.ParameterFind(repo, parameter.Name) + assert.NoError(t, err) + assert.Equal(t, "foo", parameter.Name) +} + +func TestParameterList(t *testing.T) { + store, closer := newTestStore(t, new(model.Parameter)) + defer closer() + + repo := &model.Repo{ + ID: 1, + } + parameters := []*model.Parameter{ + { + RepoID: repo.ID, + Name: "foo", + Type: model.ParameterTypeString, + Description: "test parameter 1", + }, + { + RepoID: repo.ID, + Name: "bar", + Type: model.ParameterTypeBoolean, + Description: "test parameter 2", + }, + } + + for _, parameter := range parameters { + assert.NoError(t, store.ParameterCreate(repo, parameter)) + } + + list, err := store.ParameterList(repo) + assert.NoError(t, err) + assert.Len(t, list, len(parameters)) +} + +func TestParameterUpdate(t *testing.T) { + store, closer := newTestStore(t, new(model.Parameter)) + defer closer() + + repo := &model.Repo{ + ID: 1, + } + parameter := &model.Parameter{ + RepoID: repo.ID, + Name: "foo", + Type: model.ParameterTypeString, + Description: "test parameter", + } + + assert.NoError(t, store.ParameterCreate(repo, parameter)) + parameter.Description = "updated description" + assert.NoError(t, store.ParameterUpdate(repo, parameter)) + + updated, err := store.ParameterFind(repo, parameter.Name) + assert.NoError(t, err) + assert.Equal(t, "updated description", updated.Description) +} + +func TestParameterDelete(t *testing.T) { + store, closer := newTestStore(t, new(model.Parameter)) + defer closer() + + repo := &model.Repo{ + ID: 1, + } + parameter := &model.Parameter{ + RepoID: repo.ID, + Name: "foo", + Type: model.ParameterTypeString, + Description: "test parameter", + } + + assert.NoError(t, store.ParameterCreate(repo, parameter)) + assert.NoError(t, store.ParameterDelete(repo, parameter.Name)) + + _, err := store.ParameterFind(repo, parameter.Name) + assert.Error(t, err) +} diff --git a/server/store/mocks/store.go b/server/store/mocks/store.go index 1a9fead5a..0bca107c7 100644 --- a/server/store/mocks/store.go +++ b/server/store/mocks/store.go @@ -1808,6 +1808,198 @@ func (_m *Store) OrgUpdate(_a0 *model.Org) error { return r0 } +// ParameterCreate provides a mock function with given fields: _a0, _a1 +func (_m *Store) ParameterCreate(_a0 *model.Repo, _a1 *model.Parameter) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterCreate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Repo, *model.Parameter) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ParameterDelete provides a mock function with given fields: _a0, _a1 +func (_m *Store) ParameterDelete(_a0 *model.Repo, _a1 string) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterDelete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Repo, string) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ParameterDeleteByID provides a mock function with given fields: _a0, _a1 +func (_m *Store) ParameterDeleteByID(_a0 *model.Repo, _a1 int64) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterDeleteByID") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Repo, int64) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ParameterFind provides a mock function with given fields: _a0, _a1 +func (_m *Store) ParameterFind(_a0 *model.Repo, _a1 string) (*model.Parameter, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterFind") + } + + var r0 *model.Parameter + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo, string) (*model.Parameter, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(*model.Repo, string) *model.Parameter); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Parameter) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo, string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ParameterFindByID provides a mock function with given fields: _a0, _a1 +func (_m *Store) ParameterFindByID(_a0 *model.Repo, _a1 int64) (*model.Parameter, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterFindByID") + } + + var r0 *model.Parameter + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo, int64) (*model.Parameter, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(*model.Repo, int64) *model.Parameter); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Parameter) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo, int64) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ParameterFindByNameAndBranch provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Store) ParameterFindByNameAndBranch(_a0 *model.Repo, _a1 string, _a2 string) (*model.Parameter, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for ParameterFindByNameAndBranch") + } + + var r0 *model.Parameter + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo, string, string) (*model.Parameter, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(*model.Repo, string, string) *model.Parameter); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Parameter) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo, string, string) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ParameterList provides a mock function with given fields: _a0 +func (_m *Store) ParameterList(_a0 *model.Repo) ([]*model.Parameter, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for ParameterList") + } + + var r0 []*model.Parameter + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo) ([]*model.Parameter, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(*model.Repo) []*model.Parameter); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Parameter) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ParameterUpdate provides a mock function with given fields: _a0, _a1 +func (_m *Store) ParameterUpdate(_a0 *model.Repo, _a1 *model.Parameter) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ParameterUpdate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Repo, *model.Parameter) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // PermFind provides a mock function with given fields: user, repo func (_m *Store) PermFind(user *model.User, repo *model.Repo) (*model.Perm, error) { ret := _m.Called(user, repo) diff --git a/server/store/store.go b/server/store/store.go index e12b85a05..81180354e 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -110,6 +110,16 @@ type Store interface { ConfigPersist(*model.Config) (*model.Config, error) PipelineConfigCreate(*model.PipelineConfig) error + // Parameters + ParameterFind(*model.Repo, string) (*model.Parameter, error) + ParameterFindByID(*model.Repo, int64) (*model.Parameter, error) + ParameterFindByNameAndBranch(*model.Repo, string, string) (*model.Parameter, error) + ParameterList(*model.Repo) ([]*model.Parameter, error) + ParameterCreate(*model.Repo, *model.Parameter) error + ParameterUpdate(*model.Repo, *model.Parameter) error + ParameterDelete(*model.Repo, string) error + ParameterDeleteByID(*model.Repo, int64) error + // Secrets SecretFind(*model.Repo, string) (*model.Secret, error) SecretList(*model.Repo, bool, *model.ListOptions) ([]*model.Secret, error) diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 358b72741..1384f3beb 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -48,7 +48,12 @@ "name": "Variable name", "value": "Variable value" }, - "show_pipelines": "Show pipelines" + "show_pipelines": "Show pipelines", + "parameters": { + "title": "Parameters", + "trim": "Trim whitespace", + "desc": "Parameters are predefined variables passed to your manual pipeline." + } }, "deploy_pipeline": { "title": "Trigger a deployment for current pipeline #{pipelineId}", @@ -210,6 +215,7 @@ "loading": "Loading…", "no_logs": "No logs", "pipeline": "Pipeline #{pipelineId}", + "variables": "Pipeline Variables", "log_title": "Step Logs", "log_download_error": "An error occurred while downloading the log file", "log_delete_confirm": "Do you really want to delete the step logs?", @@ -453,6 +459,36 @@ } } }, + "parameters": { + "parameters": "Parameters", + "desc": "Parameters are exposed as environment variables in manual pipelines.", + "description": "Description", + "add": "Add Parameter", + "show": "Show Parameters", + "edit": "Edit Parameter", + "delete": "Delete Parameter", + "created": "Parameter created", + "save": "Save parameter", + "saved": "Parameter saved", + "deleted": "Parameter deleted", + "name": "Name", + "branch": "Branch", + "type": "Type", + "default_value": "Default Value", + "trim_string": "Allow Whitespace Trim", + "set_by_default": "Set by Default", + "types": { + "boolean": "Boolean", + "single_choice": "Single Choice", + "multiple_choice": "Multiple Choice", + "string": "String", + "text": "Text", + "password": "Password" + }, + "choices_placeholder": "Enter one choice per line", + "choices_help": "Enter each choice on a new line.", + "any_branch": "Any (*)" + }, "secrets": { "secrets": "Secrets", "desc": "Secrets can be used in all pipelines of this repository.", diff --git a/web/src/components/parameters/ParameterEdit.vue b/web/src/components/parameters/ParameterEdit.vue new file mode 100644 index 000000000..152c0f177 --- /dev/null +++ b/web/src/components/parameters/ParameterEdit.vue @@ -0,0 +1,234 @@ + + + diff --git a/web/src/components/parameters/ParameterList.vue b/web/src/components/parameters/ParameterList.vue new file mode 100644 index 000000000..e086ebd92 --- /dev/null +++ b/web/src/components/parameters/ParameterList.vue @@ -0,0 +1,50 @@ + + + diff --git a/web/src/lib/api/index.ts b/web/src/lib/api/index.ts index 588b054b7..cea03fc80 100644 --- a/web/src/lib/api/index.ts +++ b/web/src/lib/api/index.ts @@ -6,6 +6,7 @@ import type { Forge, Org, OrgPermissions, + Parameter, Pipeline, PipelineConfig, PipelineFeed, @@ -157,6 +158,26 @@ export default class WoodpeckerClient extends ApiClient { return this._delete(`/api/repos/${repoId}/logs/${pipeline}/${step}`); } + async getParameters(repo: Repo): Promise { + return this._get(`/api/repos/${repo.id}/parameters`) as Promise; + } + + async getParameter(repo: Repo, id: number): Promise { + return this._get(`/api/repos/${repo.id}/parameters/${id}`) as Promise; + } + + async createParameter(repo: Repo, parameter: Partial): Promise { + return this._post(`/api/repos/${repo.id}/parameters`, parameter) as Promise; + } + + async updateParameter(repo: Repo, parameter: Partial): Promise { + return this._patch(`/api/repos/${repo.id}/parameters/${parameter.id}`, parameter) as Promise; + } + + async deleteParameter(repo: Repo, id: number): Promise { + return this._delete(`/api/repos/${repo.id}/parameters/${id}`); + } + async getSecretList(repoId: number, opts?: PaginationOptions): Promise { const query = encodeQueryString(opts); return this._get(`/api/repos/${repoId}/secrets?${query}`) as Promise; diff --git a/web/src/lib/api/types/index.ts b/web/src/lib/api/types/index.ts index 1015ebf94..2ddf12094 100644 --- a/web/src/lib/api/types/index.ts +++ b/web/src/lib/api/types/index.ts @@ -8,6 +8,7 @@ export * from './pull_request'; export * from './queue'; export * from './registry'; export * from './repo'; +export * from './parameter'; export * from './secret'; export * from './user'; export * from './webhook'; diff --git a/web/src/lib/api/types/parameter.ts b/web/src/lib/api/types/parameter.ts new file mode 100644 index 000000000..18a68c027 --- /dev/null +++ b/web/src/lib/api/types/parameter.ts @@ -0,0 +1,19 @@ +export enum ParameterType { + Boolean = 'boolean', + SingleChoice = 'single_choice', + MultipleChoice = 'multiple_choice', + String = 'string', + Text = 'text', + Password = 'password' +} + +export interface Parameter { + id: string; + repo_id: number; + name: string; + branch: string; + type: ParameterType; + description: string; + default_value: string; + trim_string: boolean; +} diff --git a/web/src/router.ts b/web/src/router.ts index a0cabdabb..3bc7b6ebe 100644 --- a/web/src/router.ts +++ b/web/src/router.ts @@ -130,6 +130,12 @@ const routes: RouteRecordRaw[] = [ component: (): Component => import('~/views/repo/settings/Secrets.vue'), props: true, }, + { + path: 'parameters', + name: 'repo-settings-parameters', + component: (): Component => import('~/views/repo/settings/Parameters.vue'), + props: true, + }, { path: 'registries', name: 'repo-settings-registries', diff --git a/web/src/views/repo/RepoManualPipeline.vue b/web/src/views/repo/RepoManualPipeline.vue index d698a0b4d..a5aa5e117 100644 --- a/web/src/views/repo/RepoManualPipeline.vue +++ b/web/src/views/repo/RepoManualPipeline.vue @@ -3,8 +3,104 @@
{{ $t('repo.manual_pipeline.title') }} - + + + +