From 23102152fbf272d995fbc95891e5a5c50b8fc127 Mon Sep 17 00:00:00 2001 From: John Olheiser Date: Tue, 26 May 2026 12:57:39 -0500 Subject: [PATCH] Add config to change default pipeline config paths and extensions (#6580) Signed-off-by: jolheiser --- cmd/server/flags.go | 18 +++++++ .../10-configuration/10-server.md | 18 +++++++ e2e/setup/server.go | 3 ++ server/services/config/combined_test.go | 3 +- server/services/config/forge.go | 49 +++++++++++-------- server/services/config/forge_test.go | 3 ++ server/services/setup.go | 2 +- shared/constant/constant.go | 4 +- 8 files changed, 76 insertions(+), 24 deletions(-) diff --git a/cmd/server/flags.go b/cmd/server/flags.go index c35dedff45..b98f9bf7f5 100644 --- a/cmd/server/flags.go +++ b/cmd/server/flags.go @@ -291,6 +291,24 @@ 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.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/docs/docs/30-administration/10-configuration/10-server.md b/docs/docs/30-administration/10-configuration/10-server.md index 6b6fcb695e..bcf4a0c785 100644 --- a/docs/docs/30-administration/10-configuration/10-server.md +++ b/docs/docs/30-administration/10-configuration/10-server.md @@ -980,6 +980,24 @@ 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. + +--- + +### 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/e2e/setup/server.go b/e2e/setup/server.go index 2088254d4d..1e1feeca5c 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"}}, }, } diff --git a/server/services/config/combined_test.go b/server/services/config/combined_test.go index 3c274bf92d..fb8e2914cc 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 57bf040e8b..710f972276 100644 --- a/server/services/config/forge.go +++ b/server/services/config/forge.go @@ -18,6 +18,8 @@ import ( "context" "errors" "fmt" + "path/filepath" + "slices" "strings" "time" @@ -26,18 +28,21 @@ import ( "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 { - 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, } } @@ -48,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 @@ -69,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. @@ -97,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, constant.DefaultConfigOrder[:]) + fileMetas, err := f.getFirstAvailableConfig(ctx, f.configPaths) if err == nil { return fileMetas, nil } @@ -110,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 strings.HasSuffix(file.Name, ".yml") || strings.HasSuffix(file.Name, ".yaml") { + if slices.Contains(f.configExtensions, filepath.Ext(file.Name)) { res = append(res, file) } } @@ -154,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/config/forge_test.go b/server/services/config/forge_test.go index 6902942fcb..d4dc7f2184 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(), diff --git a/server/services/setup.go b/server/services/setup.go index d11395f2ce..f2fc455a98 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")) diff --git a/shared/constant/constant.go b/shared/constant/constant.go index ac4510b5b7..a27c385c66 100644 --- a/shared/constant/constant.go +++ b/shared/constant/constant.go @@ -16,9 +16,9 @@ package constant import "time" -// DefaultConfigOrder represent the priority in witch woodpecker search for a pipeline config by default +// 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{ +var DefaultConfigOrder = []string{ ".woodpecker/", ".woodpecker.yaml", ".woodpecker.yml",