From a572dd865920ffa92c77643a748139c3c9305231 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Wed, 13 May 2026 15:14:24 -0500 Subject: [PATCH 1/7] feat: allow admins to set default pipeline config paths Signed-off-by: jolheiser --- cmd/server/flags.go | 9 +++++++++ cmd/server/setup.go | 9 +++++++++ server/services/config/forge.go | 6 ++++-- shared/constant/constant.go | 17 ++++++++++------- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/cmd/server/flags.go b/cmd/server/flags.go index 2638bf5f712..b34ab03e591 100644 --- a/cmd/server/flags.go +++ b/cmd/server/flags.go @@ -289,6 +289,15 @@ var flags = append([]cli.Flag{ Name: "config-extension-netrc", Usage: "whether global configuration extension should receive netrc data", }, + &cli.StringSliceFlag{ + Sources: cli.EnvVars("WOODPECKER_DEFAULT_PIPELINE_CONFIGS"), + Name: "default-pipeline-configs", + Usage: "default pipeline config paths to check", + Value: constant.DefaultConfigOrder, + Config: cli.StringConfig{ + TrimSpace: true, + }, + }, &cli.StringFlag{ Sources: cli.EnvVars("WOODPECKER_REGISTRY_EXTENSION_ENDPOINT"), Name: "registry-extension-endpoint", diff --git a/cmd/server/setup.go b/cmd/server/setup.go index 6e8bb194c1b..8e7b7082770 100644 --- a/cmd/server/setup.go +++ b/cmd/server/setup.go @@ -22,6 +22,7 @@ import ( "fmt" "net/url" "os" + "path/filepath" "strings" "time" @@ -45,6 +46,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/store" "go.woodpecker-ci.org/woodpecker/v3/server/store/datastore" "go.woodpecker-ci.org/woodpecker/v3/server/store/types" + "go.woodpecker-ci.org/woodpecker/v3/shared/constant" ) const ( @@ -158,6 +160,13 @@ func setupJWTSecret(_store store.Store) (string, error) { } func setupEvilGlobals(ctx context.Context, c *cli.Command, s store.Store) (err error) { + // default config paths + constant.DefaultConfigOrder = c.StringSlice("default-pipeline-configs") + for _, dc := range constant.DefaultConfigOrder { + ext := filepath.Ext(dc) + constant.ConfigExtensions[ext] = struct{}{} + } + // services server.Config.Services.Logs = logging.New() server.Config.Services.Membership = setupMembershipService(ctx, s) diff --git a/server/services/config/forge.go b/server/services/config/forge.go index 57bf040e8bf..269663b56bc 100644 --- a/server/services/config/forge.go +++ b/server/services/config/forge.go @@ -18,6 +18,7 @@ import ( "context" "errors" "fmt" + "path/filepath" "strings" "time" @@ -97,7 +98,7 @@ func (f *forgeFetcherContext) fetch(c context.Context, config string) ([]*types. log.Trace().Msgf("configFetcher[%s]: user did not define own config, following default procedure", f.repo.FullName) // for the order see shared/constants/constants.go - fileMetas, err := f.getFirstAvailableConfig(ctx, constant.DefaultConfigOrder[:]) + fileMetas, err := f.getFirstAvailableConfig(ctx, constant.DefaultConfigOrder) if err == nil { return fileMetas, nil } @@ -114,7 +115,8 @@ func filterPipelineFiles(files []*types.FileMeta) []*types.FileMeta { var res []*types.FileMeta for _, file := range files { - if strings.HasSuffix(file.Name, ".yml") || strings.HasSuffix(file.Name, ".yaml") { + ext := filepath.Ext(file.Name) + if _, ok := constant.ConfigExtensions[ext]; ok { res = append(res, file) } } diff --git a/shared/constant/constant.go b/shared/constant/constant.go index e56957b821d..21bf50c05f1 100644 --- a/shared/constant/constant.go +++ b/shared/constant/constant.go @@ -16,13 +16,16 @@ package constant import "time" -// DefaultConfigOrder represent the priority in witch woodpecker search for a pipeline config by default -// folders are indicated by supplying a trailing slash. -var DefaultConfigOrder = [...]string{ - ".woodpecker/", - ".woodpecker.yaml", - ".woodpecker.yml", -} +var ( + // DefaultConfigOrder represent the priority in which woodpecker searches for a pipeline config by default + // folders are indicated by supplying a trailing slash. + DefaultConfigOrder = []string{ + ".woodpecker/", + ".woodpecker.yaml", + ".woodpecker.yml", + } + ConfigExtensions = make(map[string]struct{}) +) const ( // DefaultClonePlugin can be changed by 'WOODPECKER_DEFAULT_CLONE_PLUGIN' at runtime. From 225cd68411d3b1d8dbce8aa813c88abad2d9950a Mon Sep 17 00:00:00 2001 From: jolheiser Date: Thu, 14 May 2026 10:24:20 -0500 Subject: [PATCH 2/7] refactor: move from constant to server/config Signed-off-by: jolheiser --- cli/common/pipeline.go | 4 ++-- cmd/server/setup.go | 11 +++++------ server/config.go | 2 ++ server/services/config/forge.go | 7 +++---- shared/constant/constant.go | 17 +++++++---------- 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/cli/common/pipeline.go b/cli/common/pipeline.go index ba560ba1e1e..9e697ee61b4 100644 --- a/cli/common/pipeline.go +++ b/cli/common/pipeline.go @@ -22,11 +22,11 @@ import ( "github.com/urfave/cli/v3" - "go.woodpecker-ci.org/woodpecker/v3/shared/constant" + "go.woodpecker-ci.org/woodpecker/v3/server" ) func DetectPipelineConfig() (isDir bool, config string, _ error) { - for _, config := range constant.DefaultConfigOrder { + for _, config := range server.Config.Pipeline.ConfigPaths { shouldBeDir := strings.HasSuffix(config, "/") config = strings.TrimSuffix(config, "/") diff --git a/cmd/server/setup.go b/cmd/server/setup.go index 8e7b7082770..73049129ccc 100644 --- a/cmd/server/setup.go +++ b/cmd/server/setup.go @@ -46,7 +46,6 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/store" "go.woodpecker-ci.org/woodpecker/v3/server/store/datastore" "go.woodpecker-ci.org/woodpecker/v3/server/store/types" - "go.woodpecker-ci.org/woodpecker/v3/shared/constant" ) const ( @@ -160,11 +159,11 @@ func setupJWTSecret(_store store.Store) (string, error) { } func setupEvilGlobals(ctx context.Context, c *cli.Command, s store.Store) (err error) { - // default config paths - constant.DefaultConfigOrder = c.StringSlice("default-pipeline-configs") - for _, dc := range constant.DefaultConfigOrder { - ext := filepath.Ext(dc) - constant.ConfigExtensions[ext] = struct{}{} + // pipeline config paths + server.Config.Pipeline.ConfigPaths = c.StringSlice("default-pipeline-configs") + server.Config.Pipeline.ConfigExtensions = make(map[string]struct{}) + for _, dc := range server.Config.Pipeline.ConfigPaths { + server.Config.Pipeline.ConfigExtensions[filepath.Ext(dc)] = struct{}{} } // services diff --git a/server/config.go b/server/config.go index e6e05099a43..025ea8ed6dd 100644 --- a/server/config.go +++ b/server/config.go @@ -81,6 +81,8 @@ var Config = struct { HTTP string HTTPS string } + ConfigPaths []string + ConfigExtensions map[string]struct{} // TODO: remove with version 4.x ForceIgnoreServiceFailure bool } diff --git a/server/services/config/forge.go b/server/services/config/forge.go index 269663b56bc..0ebf68fdd1d 100644 --- a/server/services/config/forge.go +++ b/server/services/config/forge.go @@ -24,10 +24,10 @@ import ( "github.com/rs/zerolog/log" + "go.woodpecker-ci.org/woodpecker/v3/server" "go.woodpecker-ci.org/woodpecker/v3/server/forge" "go.woodpecker-ci.org/woodpecker/v3/server/forge/types" "go.woodpecker-ci.org/woodpecker/v3/server/model" - "go.woodpecker-ci.org/woodpecker/v3/shared/constant" ) type forgeFetcher struct { @@ -98,7 +98,7 @@ func (f *forgeFetcherContext) fetch(c context.Context, config string) ([]*types. log.Trace().Msgf("configFetcher[%s]: user did not define own config, following default procedure", f.repo.FullName) // for the order see shared/constants/constants.go - fileMetas, err := f.getFirstAvailableConfig(ctx, constant.DefaultConfigOrder) + fileMetas, err := f.getFirstAvailableConfig(ctx, server.Config.Pipeline.ConfigPaths) if err == nil { return fileMetas, nil } @@ -115,8 +115,7 @@ func filterPipelineFiles(files []*types.FileMeta) []*types.FileMeta { var res []*types.FileMeta for _, file := range files { - ext := filepath.Ext(file.Name) - if _, ok := constant.ConfigExtensions[ext]; ok { + if _, ok := server.Config.Pipeline.ConfigExtensions[filepath.Ext(file.Name)]; ok { res = append(res, file) } } diff --git a/shared/constant/constant.go b/shared/constant/constant.go index 21bf50c05f1..9de20ca53e0 100644 --- a/shared/constant/constant.go +++ b/shared/constant/constant.go @@ -16,16 +16,13 @@ package constant import "time" -var ( - // DefaultConfigOrder represent the priority in which woodpecker searches for a pipeline config by default - // folders are indicated by supplying a trailing slash. - DefaultConfigOrder = []string{ - ".woodpecker/", - ".woodpecker.yaml", - ".woodpecker.yml", - } - ConfigExtensions = make(map[string]struct{}) -) +// DefaultConfigOrder represent the priority in which woodpecker searches for a pipeline config by default +// folders are indicated by supplying a trailing slash. +var DefaultConfigOrder = []string{ + ".woodpecker/", + ".woodpecker.yaml", + ".woodpecker.yml", +} const ( // DefaultClonePlugin can be changed by 'WOODPECKER_DEFAULT_CLONE_PLUGIN' at runtime. From 2ee67bdc04416a1abb2a8abe51cf589ba1a79306 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Thu, 14 May 2026 10:53:11 -0500 Subject: [PATCH 3/7] revert: cli will still use constant, only server will use config Signed-off-by: jolheiser --- cli/common/pipeline.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/common/pipeline.go b/cli/common/pipeline.go index 9e697ee61b4..ba560ba1e1e 100644 --- a/cli/common/pipeline.go +++ b/cli/common/pipeline.go @@ -22,11 +22,11 @@ import ( "github.com/urfave/cli/v3" - "go.woodpecker-ci.org/woodpecker/v3/server" + "go.woodpecker-ci.org/woodpecker/v3/shared/constant" ) func DetectPipelineConfig() (isDir bool, config string, _ error) { - for _, config := range server.Config.Pipeline.ConfigPaths { + for _, config := range constant.DefaultConfigOrder { shouldBeDir := strings.HasSuffix(config, "/") config = strings.TrimSuffix(config, "/") From b0249821823cfa7b8dd74465c375779a61d88aec Mon Sep 17 00:00:00 2001 From: jolheiser Date: Thu, 14 May 2026 10:58:40 -0500 Subject: [PATCH 4/7] docs: add documentation for the new env var Signed-off-by: jolheiser --- .../docs/30-administration/10-configuration/10-server.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/docs/30-administration/10-configuration/10-server.md b/docs/docs/30-administration/10-configuration/10-server.md index 6b6fcb695e7..8ab528eb0ca 100644 --- a/docs/docs/30-administration/10-configuration/10-server.md +++ b/docs/docs/30-administration/10-configuration/10-server.md @@ -980,6 +980,15 @@ Specify a configuration extension endpoint, see [Configuration Extension](../../ --- +### DEFAULT_PIPELINE_CONFIGS + +- Name: `WOODPECKER_DEFAULT_PIPELINE_CONFIGS` +- Default: `.woodpecker/`, `.woodpecker.yaml`, `.woodpecker.yml` + +Specify the default pipeline config paths. + +--- + ### CONFIG_EXTENSION_EXCLUSIVE - Name: `CONFIG_EXTENSION_EXCLUSIVE` From 0a301110f2e6363116c1e7723ca3a2e8133ca38c Mon Sep 17 00:00:00 2001 From: jolheiser Date: Tue, 26 May 2026 10:17:52 -0500 Subject: [PATCH 5/7] fix: move to forgeFetcher Signed-off-by: jolheiser --- cmd/server/flags.go | 9 ++++ cmd/server/setup.go | 8 ---- .../10-configuration/10-server.md | 9 ++++ server/config.go | 2 - server/services/config/combined_test.go | 3 +- server/services/config/forge.go | 48 +++++++++++-------- server/services/setup.go | 2 +- 7 files changed, 49 insertions(+), 32 deletions(-) diff --git a/cmd/server/flags.go b/cmd/server/flags.go index b34ab03e591..d42900b4a77 100644 --- a/cmd/server/flags.go +++ b/cmd/server/flags.go @@ -298,6 +298,15 @@ var flags = append([]cli.Flag{ TrimSpace: true, }, }, + &cli.StringSliceFlag{ + Sources: cli.EnvVars("WOODPECKER_DEFAULT_PIPELINE_CONFIG_EXTENSIONS"), + Name: "default-pipeline-config-extensions", + Usage: "default pipeline config extensions when scanning a pipeline config directory", + Value: []string{".yaml", ".yml"}, + Config: cli.StringConfig{ + TrimSpace: true, + }, + }, &cli.StringFlag{ Sources: cli.EnvVars("WOODPECKER_REGISTRY_EXTENSION_ENDPOINT"), Name: "registry-extension-endpoint", diff --git a/cmd/server/setup.go b/cmd/server/setup.go index 73049129ccc..6e8bb194c1b 100644 --- a/cmd/server/setup.go +++ b/cmd/server/setup.go @@ -22,7 +22,6 @@ import ( "fmt" "net/url" "os" - "path/filepath" "strings" "time" @@ -159,13 +158,6 @@ func setupJWTSecret(_store store.Store) (string, error) { } func setupEvilGlobals(ctx context.Context, c *cli.Command, s store.Store) (err error) { - // pipeline config paths - server.Config.Pipeline.ConfigPaths = c.StringSlice("default-pipeline-configs") - server.Config.Pipeline.ConfigExtensions = make(map[string]struct{}) - for _, dc := range server.Config.Pipeline.ConfigPaths { - server.Config.Pipeline.ConfigExtensions[filepath.Ext(dc)] = struct{}{} - } - // services server.Config.Services.Logs = logging.New() server.Config.Services.Membership = setupMembershipService(ctx, s) diff --git a/docs/docs/30-administration/10-configuration/10-server.md b/docs/docs/30-administration/10-configuration/10-server.md index 8ab528eb0ca..bcf4a0c785f 100644 --- a/docs/docs/30-administration/10-configuration/10-server.md +++ b/docs/docs/30-administration/10-configuration/10-server.md @@ -989,6 +989,15 @@ Specify the default pipeline config paths. --- +### DEFAULT_PIPELINE_CONFIG_EXTENSIONS + +- Name: `WOODPECKER_DEFAULT_PIPELINE_CONFIG_EXTENSIONS` +- Default: `.yaml`, `.yml` + +Specify the default pipeline config extensions when scanning a pipeline config directory. + +--- + ### CONFIG_EXTENSION_EXCLUSIVE - Name: `CONFIG_EXTENSION_EXCLUSIVE` diff --git a/server/config.go b/server/config.go index 025ea8ed6dd..e6e05099a43 100644 --- a/server/config.go +++ b/server/config.go @@ -81,8 +81,6 @@ var Config = struct { HTTP string HTTPS string } - ConfigPaths []string - ConfigExtensions map[string]struct{} // TODO: remove with version 4.x ForceIgnoreServiceFailure bool } diff --git a/server/services/config/combined_test.go b/server/services/config/combined_test.go index 3c274bf92d5..fb8e2914cc2 100644 --- a/server/services/config/combined_test.go +++ b/server/services/config/combined_test.go @@ -36,6 +36,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/utils" + "go.woodpecker-ci.org/woodpecker/v3/shared/constant" ) func TestFetchFromConfigService(t *testing.T) { @@ -220,7 +221,7 @@ func TestFetchFromConfigService(t *testing.T) { f.On("Netrc", mock.Anything, mock.Anything).Return(&model.Netrc{Machine: "mock", Login: "mock", Password: "mock"}, nil) - forgeFetcher := config.NewForge(time.Second*3, 3) + forgeFetcher := config.NewForge(time.Second*3, 3, constant.DefaultConfigOrder, []string{".yaml", ".yml"}) configFetcher := config.NewCombined(forgeFetcher, httpFetcher) files, err := configFetcher.Fetch( t.Context(), diff --git a/server/services/config/forge.go b/server/services/config/forge.go index 0ebf68fdd1d..710f972276d 100644 --- a/server/services/config/forge.go +++ b/server/services/config/forge.go @@ -19,26 +19,30 @@ import ( "errors" "fmt" "path/filepath" + "slices" "strings" "time" "github.com/rs/zerolog/log" - "go.woodpecker-ci.org/woodpecker/v3/server" "go.woodpecker-ci.org/woodpecker/v3/server/forge" "go.woodpecker-ci.org/woodpecker/v3/server/forge/types" "go.woodpecker-ci.org/woodpecker/v3/server/model" ) type forgeFetcher struct { - timeout time.Duration - retryCount uint + timeout time.Duration + retryCount uint + configPaths []string + configExtensions []string } -func NewForge(timeout time.Duration, retries uint) Service { +func NewForge(timeout time.Duration, retries uint, configPaths, configExtensions []string) Service { return &forgeFetcher{ - timeout: timeout, - retryCount: retries, + timeout: timeout, + retryCount: retries, + configPaths: configPaths, + configExtensions: configExtensions, } } @@ -49,11 +53,13 @@ func (f *forgeFetcher) Fetch(ctx context.Context, forge forge.Forge, user *model } ffc := &forgeFetcherContext{ - forge: forge, - user: user, - repo: repo, - pipeline: pipeline, - timeout: f.timeout, + forge: forge, + user: user, + repo: repo, + pipeline: pipeline, + timeout: f.timeout, + configPaths: f.configPaths, + configExtensions: f.configExtensions, } // try to fetch multiple times @@ -70,11 +76,13 @@ func (f *forgeFetcher) Fetch(ctx context.Context, forge forge.Forge, user *model } type forgeFetcherContext struct { - forge forge.Forge - user *model.User - repo *model.Repo - pipeline *model.Pipeline - timeout time.Duration + forge forge.Forge + user *model.User + repo *model.Repo + pipeline *model.Pipeline + timeout time.Duration + configPaths []string + configExtensions []string } // fetch attempts to fetch the configuration file(s) for the given config string. @@ -98,7 +106,7 @@ func (f *forgeFetcherContext) fetch(c context.Context, config string) ([]*types. log.Trace().Msgf("configFetcher[%s]: user did not define own config, following default procedure", f.repo.FullName) // for the order see shared/constants/constants.go - fileMetas, err := f.getFirstAvailableConfig(ctx, server.Config.Pipeline.ConfigPaths) + fileMetas, err := f.getFirstAvailableConfig(ctx, f.configPaths) if err == nil { return fileMetas, nil } @@ -111,11 +119,11 @@ func (f *forgeFetcherContext) fetch(c context.Context, config string) ([]*types. } } -func filterPipelineFiles(files []*types.FileMeta) []*types.FileMeta { +func (f *forgeFetcherContext) filterPipelineFiles(files []*types.FileMeta) []*types.FileMeta { var res []*types.FileMeta for _, file := range files { - if _, ok := server.Config.Pipeline.ConfigExtensions[filepath.Ext(file.Name)]; ok { + if slices.Contains(f.configExtensions, filepath.Ext(file.Name)) { res = append(res, file) } } @@ -155,7 +163,7 @@ func (f *forgeFetcherContext) getFirstAvailableConfig(c context.Context, configs } continue } - files = filterPipelineFiles(files) + files = f.filterPipelineFiles(files) if len(files) != 0 { log.Trace().Msgf("configFetcher[%s]: found %d files in '%s'", f.repo.FullName, len(files), fileOrFolder) return files, nil diff --git a/server/services/setup.go b/server/services/setup.go index d11395f2ceb..f2fc455a986 100644 --- a/server/services/setup.go +++ b/server/services/setup.go @@ -76,7 +76,7 @@ func setupConfigService(c *cli.Command, client *utils.Client) (config.Service, e if retries == 0 { return nil, fmt.Errorf("WOODPECKER_FORGE_RETRY can not be 0") } - configFetcher := config.NewForge(timeout, retries) + configFetcher := config.NewForge(timeout, retries, c.StringSlice("default-pipeline-configs"), c.StringSlice("default-pipeline-config-extensions")) if endpoint := c.String("config-extension-endpoint"); endpoint != "" { httpFetcher := config.NewHTTP(endpoint, client, c.Bool("config-extension-netrc")) From 8e60bb346adc576276e7cb1d82cd4d31e5f73d0d Mon Sep 17 00:00:00 2001 From: jolheiser Date: Tue, 26 May 2026 12:04:44 -0500 Subject: [PATCH 6/7] fix: update test Signed-off-by: jolheiser --- server/services/config/forge_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/services/config/forge_test.go b/server/services/config/forge_test.go index 6902942fcbe..d4dc7f21843 100644 --- a/server/services/config/forge_test.go +++ b/server/services/config/forge_test.go @@ -27,6 +27,7 @@ import ( forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types" "go.woodpecker-ci.org/woodpecker/v3/server/model" "go.woodpecker-ci.org/woodpecker/v3/server/services/config" + "go.woodpecker-ci.org/woodpecker/v3/shared/constant" ) func TestFetch(t *testing.T) { @@ -307,6 +308,8 @@ func TestFetch(t *testing.T) { configFetcher := config.NewForge( time.Second*3, 3, + constant.DefaultConfigOrder, + []string{".yaml", ".yml"}, ) files, err := configFetcher.Fetch( t.Context(), From 62e7804688168fc12bfc1a664c1e7a716268500e Mon Sep 17 00:00:00 2001 From: jolheiser Date: Tue, 26 May 2026 12:45:50 -0500 Subject: [PATCH 7/7] fix: update e2e Signed-off-by: jolheiser --- e2e/setup/server.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/e2e/setup/server.go b/e2e/setup/server.go index 2088254d4db..1e1feeca5cd 100644 --- a/e2e/setup/server.go +++ b/e2e/setup/server.go @@ -43,6 +43,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/services" "go.woodpecker-ci.org/woodpecker/v3/server/services/permissions" "go.woodpecker-ci.org/woodpecker/v3/server/store" + "go.woodpecker-ci.org/woodpecker/v3/shared/constant" ) const ( @@ -151,6 +152,8 @@ func newTestManager(s store.Store, mockForge *forge_mocks.MockForge) (services.M // Forge flags — gitea=true satisfies setupForgeService's type switch. &cli.BoolFlag{Name: string(TestForgeType), Value: true}, &cli.StringFlag{Name: "forge-url", Value: "https://forge.example.test"}, + &cli.StringSliceFlag{Name: "default-pipeline-configs", Value: constant.DefaultConfigOrder}, + &cli.StringSliceFlag{Name: "default-pipeline-config-extensions", Value: []string{".yaml", ".yml"}}, }, }