mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-04-15 01:41:56 +00:00
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.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div v-if="innerParameter" class="space-y-4">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<InputField v-slot="{ id }" :label="$t('parameters.name')">
|
||||
<TextField
|
||||
:id="id"
|
||||
v-model="innerParameter.name"
|
||||
:placeholder="$t('parameters.name')"
|
||||
required
|
||||
/>
|
||||
</InputField>
|
||||
|
||||
<InputField v-slot="{ id }" :label="$t('parameters.branch')">
|
||||
<select
|
||||
:id="id"
|
||||
v-model="innerParameter.branch"
|
||||
class="block w-full rounded-md border border-wp-control-neutral-200 bg-wp-control-neutral-100 px-3 py-2 text-sm text-wp-text-100"
|
||||
>
|
||||
<option value="*">{{ $t('parameters.any_branch') }}</option>
|
||||
<option
|
||||
v-for="branch in branches"
|
||||
:key="branch"
|
||||
:value="branch"
|
||||
>
|
||||
{{ branch }}
|
||||
</option>
|
||||
</select>
|
||||
</InputField>
|
||||
|
||||
<InputField v-slot="{ id }" :label="$t('parameters.type')">
|
||||
<select
|
||||
:id="id"
|
||||
v-model="innerParameter.type"
|
||||
class="block w-full rounded-md border border-wp-control-neutral-200 bg-wp-control-neutral-100 px-3 py-2 text-sm text-wp-text-100"
|
||||
required
|
||||
>
|
||||
<option v-for="type in parameterTypes" :key="type" :value="type">
|
||||
{{ $t(`parameters.types.${type}`) }}
|
||||
</option>
|
||||
</select>
|
||||
</InputField>
|
||||
|
||||
<template v-if="innerParameter.type">
|
||||
<InputField v-if="showDefaultValue" v-slot="{ id }" :label="$t('parameters.default_value')">
|
||||
<template v-if="isBoolean">
|
||||
<Checkbox
|
||||
:id="id"
|
||||
v-model="innerParameter.default_value"
|
||||
:label="$t('parameters.set_by_default')"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="isPassword">
|
||||
<TextField
|
||||
:id="id"
|
||||
v-model="innerParameter.default_value"
|
||||
:type="passwordVisibility ? 'text' : 'password'"
|
||||
@dblclick="passwordVisibility = true"
|
||||
@blur="passwordVisibility = false"
|
||||
class="text-sm"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="isChoice">
|
||||
<TextField
|
||||
:id="id"
|
||||
v-model="innerParameter.default_value"
|
||||
:lines="5"
|
||||
:placeholder="$t('parameters.choices_placeholder')"
|
||||
/>
|
||||
<span class="mt-1 text-sm text-wp-text-alt-100">{{ $t('parameters.choices_help') }}</span>
|
||||
</template>
|
||||
<template v-else-if="isText">
|
||||
<TextField
|
||||
:id="id"
|
||||
v-model="innerParameter.default_value"
|
||||
:lines="3"
|
||||
class="text-sm"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<TextField
|
||||
:id="id"
|
||||
v-model="innerParameter.default_value"
|
||||
class="text-sm"
|
||||
/>
|
||||
</template>
|
||||
</InputField>
|
||||
|
||||
<InputField v-if="showTrimString" v-slot="{ id }" :label="$t('parameters.trim_string')">
|
||||
<Checkbox
|
||||
:id="id"
|
||||
v-model="innerParameter.trim_string"
|
||||
:label="innerParameter.trim_string ? 'true' : 'false'"
|
||||
/>
|
||||
</InputField>
|
||||
</template>
|
||||
|
||||
<InputField v-slot="{ id }" :label="$t('parameters.description')">
|
||||
<TextField
|
||||
:id="id"
|
||||
v-model="innerParameter.description"
|
||||
:lines="3"
|
||||
class="text-sm"
|
||||
/>
|
||||
</InputField>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button type="button" color="gray" :text="$t('cancel')" @click="$emit('cancel')" />
|
||||
<Button
|
||||
type="submit"
|
||||
color="green"
|
||||
:is-loading="isSaving"
|
||||
:text="existingParameter ? $t('parameters.save') : $t('parameters.add')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref, watch } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import Button from '~/components/atomic/Button.vue';
|
||||
import Checkbox from '~/components/form/Checkbox.vue';
|
||||
import InputField from '~/components/form/InputField.vue';
|
||||
import TextField from '~/components/form/TextField.vue';
|
||||
import useApiClient from '~/compositions/useApiClient';
|
||||
import type { Parameter, Repo } from '~/lib/api/types';
|
||||
import { ParameterType } from '~/lib/api/types';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Partial<Parameter>;
|
||||
existingParameter?: boolean;
|
||||
isSaving?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: Partial<Parameter>): void;
|
||||
(e: 'save', value: Partial<Parameter>): void;
|
||||
(e: 'cancel'): void;
|
||||
}>();
|
||||
|
||||
const apiClient = useApiClient();
|
||||
const repo = inject('repo') as Ref<Repo>;
|
||||
const branches = ref<string[]>([]);
|
||||
|
||||
const parameterTypes = Object.values(ParameterType);
|
||||
|
||||
// Create a local copy of the parameter to avoid mutating props directly
|
||||
const innerParameter = ref<Partial<Parameter>>({ ...props.modelValue });
|
||||
const passwordVisibility = ref(false);
|
||||
|
||||
// Update local copy when prop changes
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
innerParameter.value = { ...newVal };
|
||||
}, { deep: true });
|
||||
|
||||
// Update parent when local copy changes
|
||||
watch(innerParameter, (newVal) => {
|
||||
emit('update:modelValue', newVal);
|
||||
}, { deep: true });
|
||||
|
||||
// Computed properties to control field visibility
|
||||
const isBoolean = computed(() => innerParameter.value.type === ParameterType.Boolean);
|
||||
const isPassword = computed(() => innerParameter.value.type === ParameterType.Password);
|
||||
const isText = computed(() => innerParameter.value.type === ParameterType.Text);
|
||||
const isChoice = computed(() =>
|
||||
innerParameter.value.type === ParameterType.SingleChoice ||
|
||||
innerParameter.value.type === ParameterType.MultipleChoice
|
||||
);
|
||||
const showDefaultValue = computed(() => innerParameter.value.type !== undefined);
|
||||
const showTrimString = computed(() => !isBoolean.value && !isPassword.value);
|
||||
|
||||
// Initialize default values based on type
|
||||
watch(() => innerParameter.value.type, (newType) => {
|
||||
// Don't override existing values when editing
|
||||
if (props.existingParameter) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newType === ParameterType.Boolean) {
|
||||
innerParameter.value.default_value = 'false';
|
||||
innerParameter.value.trim_string = false;
|
||||
} else if (newType === ParameterType.SingleChoice || newType === ParameterType.MultipleChoice) {
|
||||
innerParameter.value.default_value = '';
|
||||
innerParameter.value.trim_string = true;
|
||||
} else {
|
||||
innerParameter.value.default_value = '';
|
||||
innerParameter.value.trim_string = true;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Special handling for boolean values
|
||||
watch(() => innerParameter.value.default_value, (newVal) => {
|
||||
if (isBoolean.value && typeof newVal === 'string') {
|
||||
// Convert string 'true'/'false' to boolean for checkbox
|
||||
innerParameter.value.default_value = newVal === 'true';
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Load branches when component is mounted
|
||||
async function loadBranches() {
|
||||
if (!repo.value) return;
|
||||
|
||||
try {
|
||||
const branchList = await apiClient.getRepoBranches(repo.value.id);
|
||||
branches.value = branchList.map(b => b);
|
||||
} catch (error) {
|
||||
console.error('Failed to load branches:', error);
|
||||
}
|
||||
}
|
||||
|
||||
loadBranches();
|
||||
|
||||
function handleSubmit() {
|
||||
// Ensure all required fields are present
|
||||
if (!innerParameter.value.name || !innerParameter.value.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure branch has a value
|
||||
if (!innerParameter.value.branch) {
|
||||
innerParameter.value.branch = '*';
|
||||
}
|
||||
|
||||
// Convert boolean default_value to string
|
||||
if (isBoolean.value) {
|
||||
innerParameter.value.default_value = innerParameter.value.default_value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
// Emit the save event with the parameter data
|
||||
emit('save', innerParameter.value);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<ListItem v-for="parameter in parameters" :key="parameter.name" class="flex flex-col gap-2 !bg-wp-background-200 dark:!bg-wp-background-100">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-bold">{{ parameter.name }}</h3>
|
||||
<div class="flex items-center gap-1">
|
||||
<Icon name="push" class="h-4 w-4" />
|
||||
<span>{{ parameter.branch }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="md:display-unset ml-auto hidden space-x-2">
|
||||
<Badge :label="$t(`parameters.types.${parameter.type}`)" />
|
||||
</div>
|
||||
<IconButton
|
||||
:title="$t('parameters.edit')"
|
||||
icon="edit"
|
||||
@click="$emit('edit', parameter)"
|
||||
/>
|
||||
<IconButton
|
||||
:title="$t('parameters.delete')"
|
||||
icon="trash"
|
||||
@click="$emit('delete', parameter)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="parameter.description" class="text-sm text-wp-text-alt-100">
|
||||
{{ parameter.description }}
|
||||
</p>
|
||||
</ListItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Badge from "~/components/atomic/Badge.vue";
|
||||
import Icon from '~/components/atomic/Icon.vue';
|
||||
import IconButton from '~/components/atomic/IconButton.vue';
|
||||
import ListItem from '~/components/atomic/ListItem.vue';
|
||||
import type { Parameter } from '~/lib/api/types/parameter';
|
||||
|
||||
defineProps<{
|
||||
parameters: Parameter[];
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'edit', parameter: Parameter): void;
|
||||
(e: 'delete', parameter: Parameter): void;
|
||||
}>();
|
||||
</script>
|
||||
@@ -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<Parameter[] | null> {
|
||||
return this._get(`/api/repos/${repo.id}/parameters`) as Promise<Parameter[] | null>;
|
||||
}
|
||||
|
||||
async getParameter(repo: Repo, id: number): Promise<Parameter | null> {
|
||||
return this._get(`/api/repos/${repo.id}/parameters/${id}`) as Promise<Parameter | null>;
|
||||
}
|
||||
|
||||
async createParameter(repo: Repo, parameter: Partial<Parameter>): Promise<Parameter | null> {
|
||||
return this._post(`/api/repos/${repo.id}/parameters`, parameter) as Promise<Parameter | null>;
|
||||
}
|
||||
|
||||
async updateParameter(repo: Repo, parameter: Partial<Parameter>): Promise<Parameter | null> {
|
||||
return this._patch(`/api/repos/${repo.id}/parameters/${parameter.id}`, parameter) as Promise<Parameter | null>;
|
||||
}
|
||||
|
||||
async deleteParameter(repo: Repo, id: number): Promise<unknown> {
|
||||
return this._delete(`/api/repos/${repo.id}/parameters/${id}`);
|
||||
}
|
||||
|
||||
async getSecretList(repoId: number, opts?: PaginationOptions): Promise<Secret[] | null> {
|
||||
const query = encodeQueryString(opts);
|
||||
return this._get(`/api/repos/${repoId}/secrets?${query}`) as Promise<Secret[] | null>;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -3,8 +3,104 @@
|
||||
<form @submit.prevent="triggerManualPipeline">
|
||||
<span class="text-wp-text-100 text-xl">{{ $t('repo.manual_pipeline.title') }}</span>
|
||||
<InputField v-slot="{ id }" :label="$t('repo.manual_pipeline.select_branch')">
|
||||
<SelectField :id="id" v-model="payload.branch" :options="branches" required />
|
||||
<SelectField
|
||||
:id="id"
|
||||
v-model="payload.branch"
|
||||
:options="branches"
|
||||
required
|
||||
@update:model-value="loadParameters"
|
||||
/>
|
||||
</InputField>
|
||||
|
||||
<!-- Parameters section -->
|
||||
<template v-if="parameters.length > 0">
|
||||
<InputField :label="$t('repo.manual_pipeline.parameters.title')">
|
||||
<span class="text-wp-text-alt-100 mb-2 text-sm">{{ $t('repo.manual_pipeline.parameters.desc') }}</span>
|
||||
</InputField>
|
||||
|
||||
<div
|
||||
v-for="param in parameters"
|
||||
:key="param.id"
|
||||
class="border-wp-background-300 !bg-wp-background-200 dark:!bg-wp-background-100 mb-4 items-center rounded-md border p-4"
|
||||
>
|
||||
<InputField v-slot="{ id }" :label="param.name">
|
||||
<!-- Boolean parameter -->
|
||||
<Checkbox
|
||||
v-if="param.type === 'boolean'"
|
||||
:id="id"
|
||||
v-model="paramValues[param.name]"
|
||||
:label="`${paramValues[param.name]}`"
|
||||
class="text-sm"
|
||||
/>
|
||||
|
||||
<!-- Single choice parameter -->
|
||||
<SelectField
|
||||
v-else-if="param.type === 'single_choice'"
|
||||
:id="id"
|
||||
v-model="paramValues[param.name]"
|
||||
:options="getOptionsFromDefaultValue(param.default_value, true)"
|
||||
class="dark:bg-wp-background-200 text-sm"
|
||||
/>
|
||||
|
||||
<!-- Multiple choice parameter -->
|
||||
<SelectField
|
||||
v-else-if="param.type === 'multiple_choice'"
|
||||
:id="id"
|
||||
v-model="paramValues[param.name]"
|
||||
:options="getOptionsFromDefaultValue(param.default_value)"
|
||||
multiple
|
||||
class="dark:bg-wp-background-200 text-sm"
|
||||
:style="{ height: `${getOptionsFromDefaultValue(param.default_value).length * 19 + 2}px` }"
|
||||
/>
|
||||
|
||||
<!-- String parameter -->
|
||||
<TextField
|
||||
v-else-if="param.type === 'string'"
|
||||
:id="id"
|
||||
v-model="paramValues[param.name]"
|
||||
:placeholder="param.name"
|
||||
class="dark:bg-wp-background-200 text-sm"
|
||||
/>
|
||||
|
||||
<!-- Text parameter -->
|
||||
<textarea
|
||||
v-else-if="param.type === 'text'"
|
||||
:id="id"
|
||||
v-model="paramValues[param.name]"
|
||||
class="border-wp-control-neutral-200 text-wp-text-100 dark:bg-wp-background-200 block w-full rounded-md border bg-white px-3 py-2 text-sm"
|
||||
rows="4"
|
||||
/>
|
||||
|
||||
<!-- Password parameter -->
|
||||
<TextField
|
||||
v-else-if="param.type === 'password'"
|
||||
:id="id"
|
||||
v-model="paramValues[param.name]"
|
||||
:placeholder="param.name"
|
||||
:type="passwordVisibility[param.name] ? 'text' : 'password'"
|
||||
class="dark:bg-wp-background-200 text-sm"
|
||||
@dblclick="passwordVisibility[param.name] = true"
|
||||
@blur="passwordVisibility[param.name] = false"
|
||||
/>
|
||||
|
||||
<!-- Trim string checkbox if applicable -->
|
||||
<div v-if="param.trim_string && ['string', 'text'].includes(param.type)" class="mt-2">
|
||||
<Checkbox
|
||||
:id="`${id}-trim`"
|
||||
v-model="paramTrimEnabled[param.name]"
|
||||
:label="$t('repo.manual_pipeline.parameters.trim')"
|
||||
class="text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Parameter description -->
|
||||
<div v-if="param.description" class="text-wp-text-alt-100 mt-2 text-sm">
|
||||
{{ param.description }}
|
||||
</div>
|
||||
</InputField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<InputField v-slot="{ id }" :label="$t('repo.manual_pipeline.variables.title')">
|
||||
<span class="text-wp-text-alt-100 mb-2 text-sm">{{ $t('repo.manual_pipeline.variables.desc') }}</span>
|
||||
<KeyValueEditor
|
||||
@@ -32,14 +128,17 @@ import { useRouter } from 'vue-router';
|
||||
|
||||
import Button from '~/components/atomic/Button.vue';
|
||||
import Icon from '~/components/atomic/Icon.vue';
|
||||
import Checkbox from '~/components/form/Checkbox.vue';
|
||||
import InputField from '~/components/form/InputField.vue';
|
||||
import KeyValueEditor from '~/components/form/KeyValueEditor.vue';
|
||||
import SelectField from '~/components/form/SelectField.vue';
|
||||
import TextField from '~/components/form/TextField.vue';
|
||||
import Panel from '~/components/layout/Panel.vue';
|
||||
import useApiClient from '~/compositions/useApiClient';
|
||||
import { requiredInject } from '~/compositions/useInjectProvide';
|
||||
import { usePaginate } from '~/compositions/usePaginate';
|
||||
import { useWPTitle } from '~/compositions/useWPTitle';
|
||||
import type { Parameter } from '~/lib/api/types';
|
||||
|
||||
defineProps<{
|
||||
open: boolean;
|
||||
@@ -69,12 +168,115 @@ const isFormValid = computed(() => {
|
||||
return payload.value.branch !== '' && isVariablesValid.value;
|
||||
});
|
||||
|
||||
const pipelineOptions = computed(() => ({
|
||||
...payload.value,
|
||||
variables: payload.value.variables,
|
||||
}));
|
||||
const pipelineOptions = computed(() => {
|
||||
// Start with the base payload
|
||||
const options = {
|
||||
...payload.value,
|
||||
variables: { ...payload.value.variables },
|
||||
};
|
||||
|
||||
// Add parameter values
|
||||
parameters.value.forEach((param) => {
|
||||
let value = paramValues.value[param.name];
|
||||
|
||||
// Convert boolean to string
|
||||
if (param.type === 'boolean') {
|
||||
value = value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
// Join multiple choice values
|
||||
if (param.type === 'multiple_choice' && Array.isArray(value)) {
|
||||
value = value.join(',');
|
||||
}
|
||||
|
||||
// Apply trim if enabled
|
||||
if (paramTrimEnabled.value[param.name] && typeof value === 'string') {
|
||||
value = value.trim();
|
||||
}
|
||||
|
||||
//options.variables[param.name.toUpperCase()] = value;
|
||||
options.variables[param.name] = value;
|
||||
});
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
const loading = ref(true);
|
||||
const parameters = ref<Parameter[]>([]);
|
||||
const paramValues = ref<Record<string, any>>({});
|
||||
const paramTrimEnabled = ref<Record<string, boolean>>({});
|
||||
const passwordVisibility = ref<Record<string, boolean>>({});
|
||||
|
||||
function getOptionsFromDefaultValue(defaultValue: string, addEmptyOption = false) {
|
||||
const options = defaultValue
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((value) => ({
|
||||
text: value,
|
||||
value,
|
||||
}));
|
||||
|
||||
// For single choice, add empty option at the start
|
||||
if (addEmptyOption && options.length > 0) {
|
||||
options.unshift({ text: '-- empty --', value: '' });
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async function loadParameters() {
|
||||
if (!payload.value.branch) return;
|
||||
|
||||
try {
|
||||
const allParams = await apiClient.getParameters(repo.value);
|
||||
const selectedBranch = payload.value.branch;
|
||||
|
||||
// Create a map to store parameters by name
|
||||
const paramMap = new Map<string, Parameter>();
|
||||
|
||||
// First pass: add wildcard (*) parameters
|
||||
allParams.forEach((param) => {
|
||||
if (param.branch === '*') {
|
||||
paramMap.set(param.name, param);
|
||||
}
|
||||
});
|
||||
|
||||
// Second pass: override with branch-specific parameters
|
||||
allParams.forEach((param) => {
|
||||
if (param.branch === selectedBranch) {
|
||||
paramMap.set(param.name, param);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert map back to array
|
||||
parameters.value = Array.from(paramMap.values());
|
||||
|
||||
// Initialize parameter values with defaults
|
||||
parameters.value.forEach((param) => {
|
||||
if (param.type === 'boolean') {
|
||||
paramValues.value[param.name] = param.default_value === 'true';
|
||||
} else if (param.type === 'single_choice') {
|
||||
paramValues.value[param.name] = ''; // Empty string for default empty selection
|
||||
} else if (param.type === 'multiple_choice') {
|
||||
paramValues.value[param.name] = []; // Empty array for no selections
|
||||
} else {
|
||||
paramValues.value[param.name] = param.default_value;
|
||||
}
|
||||
|
||||
if (param.type === 'password') {
|
||||
passwordVisibility.value[param.name] = false;
|
||||
}
|
||||
|
||||
if (param.trim_string) {
|
||||
paramTrimEnabled.value[param.name] = true;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load parameters:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load parameters when component mounts
|
||||
onMounted(async () => {
|
||||
if (!repoPermissions.value.push) {
|
||||
notifications.notify({ type: 'error', title: i18n.t('repo.settings.not_allowed') });
|
||||
@@ -87,6 +289,9 @@ onMounted(async () => {
|
||||
value: e,
|
||||
}));
|
||||
loading.value = false;
|
||||
if (payload.value.branch) {
|
||||
loadParameters();
|
||||
}
|
||||
});
|
||||
|
||||
async function triggerManualPipeline() {
|
||||
@@ -107,3 +312,9 @@ async function triggerManualPipeline() {
|
||||
|
||||
useWPTitle(computed(() => [i18n.t('repo.manual_pipeline.trigger'), repo.value.full_name]));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dark input[type='checkbox'] {
|
||||
background-color: var(--wp-background-200) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<Panel
|
||||
v-if="pipelineVariables && Object.keys(pipelineVariables).length > 0"
|
||||
collapsable
|
||||
collapsed-by-default
|
||||
:title="$t('repo.pipeline.variables')"
|
||||
>
|
||||
<div class="overflow-auto font-mono whitespace-pre">
|
||||
<div
|
||||
v-for="(value, key) in pipelineVariables"
|
||||
:key="key"
|
||||
class="border-wp-background-300 flex border-b py-2 last:border-b-0"
|
||||
>
|
||||
<span class="text-wp-text-100 min-w-[100px]">{{ key }}:</span>
|
||||
<span class="text-wp-text-100 flex-1">{{ value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
v-for="pipelineConfig in pipelineConfigsDecoded"
|
||||
:key="pipelineConfig.hash"
|
||||
@@ -29,6 +47,7 @@ import { useWPTitle } from '~/compositions/useWPTitle';
|
||||
const repo = requiredInject('repo');
|
||||
const pipeline = requiredInject('pipeline');
|
||||
const pipelineConfigs = requiredInject('pipeline-configs');
|
||||
const pipelineVariables = requiredInject('pipeline-variables');
|
||||
|
||||
const pipelineConfigsDecoded = computed(
|
||||
() =>
|
||||
|
||||
@@ -156,6 +156,9 @@ provide('pipeline', pipeline as Ref<Pipeline>); // can't be undefined because of
|
||||
const pipelineConfigs = ref<PipelineConfig[]>();
|
||||
provide('pipeline-configs', pipelineConfigs);
|
||||
|
||||
const pipelineVariables = ref<Record<string, string>>();
|
||||
provide('pipeline-variables', pipelineVariables);
|
||||
|
||||
watch(
|
||||
pipeline,
|
||||
() => {
|
||||
@@ -174,6 +177,7 @@ async function loadPipeline(): Promise<void> {
|
||||
}
|
||||
|
||||
pipelineConfigs.value = await apiClient.getPipelineConfig(repo.value.id, pipeline.value.number);
|
||||
pipelineVariables.value = pipeline.value.variables;
|
||||
}
|
||||
|
||||
const { doSubmit: cancelPipeline, isLoading: isCancelingPipeline } = useAsyncAction(async () => {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<Settings :title="$t('parameters.parameters')" :description="$t('parameters.desc')" docs-url="docs/usage/parameters">
|
||||
<template #headerActions>
|
||||
<Button
|
||||
v-if="selectedParameter"
|
||||
:text="$t('parameters.show')"
|
||||
start-icon="back"
|
||||
@click="selectedParameter = undefined"
|
||||
/>
|
||||
<Button
|
||||
v-else
|
||||
:text="$t('parameters.add')"
|
||||
start-icon="plus"
|
||||
@click="showAddParameter()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<ParameterList
|
||||
v-if="!selectedParameter"
|
||||
:parameters="parameters"
|
||||
:is-deleting="isDeleting"
|
||||
@edit="editParameter"
|
||||
@delete="deleteParameter"
|
||||
/>
|
||||
|
||||
<ParameterEdit
|
||||
v-else
|
||||
v-model="selectedParameter"
|
||||
:existing-parameter="isEditingParameter"
|
||||
:is-saving="isSaving"
|
||||
@save="createParameter"
|
||||
@cancel="selectedParameter = undefined"
|
||||
/>
|
||||
</Settings>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Button from '~/components/atomic/Button.vue';
|
||||
import Settings from '~/components/layout/Settings.vue';
|
||||
import ParameterEdit from '~/components/parameters/ParameterEdit.vue';
|
||||
import ParameterList from '~/components/parameters/ParameterList.vue';
|
||||
import { useAsyncAction } from '~/compositions/useAsyncAction';
|
||||
import useNotifications from '~/compositions/useNotifications';
|
||||
import type { Parameter, Repo } from '~/lib/api/types';
|
||||
import { ParameterType } from '~/lib/api/types';
|
||||
import useApiClient from '~/compositions/useApiClient';
|
||||
|
||||
const apiClient = useApiClient();
|
||||
const notifications = useNotifications();
|
||||
const i18n = useI18n();
|
||||
const repo = inject('repo') as Ref<Repo>;
|
||||
|
||||
const parameters = ref<Parameter[]>([]);
|
||||
const selectedParameter = ref<Partial<Parameter>>();
|
||||
const emptyParameter: Partial<Parameter> = {
|
||||
repo_id: repo.value?.id,
|
||||
name: '',
|
||||
branch: '*',
|
||||
type: ParameterType.String,
|
||||
description: '',
|
||||
default_value: '',
|
||||
trim_string: true,
|
||||
};
|
||||
|
||||
const isEditingParameter = computed(() => selectedParameter.value?.id !== undefined);
|
||||
|
||||
async function resetPage() {
|
||||
parameters.value = (await apiClient.getParameters(repo.value)) || [];
|
||||
}
|
||||
|
||||
function showAddParameter() {
|
||||
selectedParameter.value = cloneDeep(emptyParameter);
|
||||
}
|
||||
|
||||
const { doSubmit: createParameter, isLoading: isSaving } = useAsyncAction(async () => {
|
||||
if (!repo?.value || !selectedParameter.value) {
|
||||
throw new Error("Unexpected: Can't load repo");
|
||||
}
|
||||
|
||||
if (isEditingParameter.value) {
|
||||
await apiClient.updateParameter(repo.value, selectedParameter.value);
|
||||
} else {
|
||||
await apiClient.createParameter(repo.value, selectedParameter.value);
|
||||
}
|
||||
notifications.notify({
|
||||
title: isEditingParameter.value ? i18n.t('parameters.saved') : i18n.t('parameters.created'),
|
||||
type: 'success',
|
||||
});
|
||||
selectedParameter.value = undefined;
|
||||
await resetPage();
|
||||
});
|
||||
|
||||
const { doSubmit: deleteParameter, isLoading: isDeleting } = useAsyncAction(async (_parameter: Parameter) => {
|
||||
if (!repo?.value) {
|
||||
throw new Error("Unexpected: Can't load repo");
|
||||
}
|
||||
|
||||
await apiClient.deleteParameter(repo.value, _parameter.id);
|
||||
notifications.notify({ title: i18n.t('parameters.deleted'), type: 'success' });
|
||||
await resetPage();
|
||||
});
|
||||
|
||||
function editParameter(parameter: Parameter) {
|
||||
selectedParameter.value = cloneDeep(parameter);
|
||||
}
|
||||
|
||||
resetPage();
|
||||
</script>
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<Tab icon="settings-outline" :to="{ name: 'repo-settings' }" :title="$t('repo.settings.general.general')" />
|
||||
<Tab icon="secret" :to="{ name: 'repo-settings-secrets' }" :title="$t('secrets.secrets')" />
|
||||
<Tab icon="secret" :to="{ name: 'repo-settings-parameters' }" :title="$t('parameters.parameters')" />
|
||||
<Tab icon="docker" :to="{ name: 'repo-settings-registries' }" :title="$t('registries.registries')" />
|
||||
<Tab icon="cron" :to="{ name: 'repo-settings-crons' }" :title="$t('repo.settings.crons.crons')" />
|
||||
<Tab icon="tag" :to="{ name: 'repo-settings-badge' }" :title="$t('repo.settings.badge.badge')" />
|
||||
|
||||
Reference in New Issue
Block a user