Skip to content

Commit c89412a

Browse files
authored
feat(config): discover .github/poutine.yml as a config path (#424)
--------- Signed-off-by: graelo <graelo@graelo.cc>
1 parent 7f0cfee commit c89412a

3 files changed

Lines changed: 106 additions & 14 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ poutine analyze_org my-org/project --token "$GL_TOKEN" --scm gitlab --scm-base-u
111111
--scm SCM platform (default: github, gitlab)
112112
--scm-base-url Base URI of the self-hosted SCM instance
113113
--threads Number of threads to use (default: 2)
114-
--config Path to the configuration file (default: .poutine.yml)
114+
--config Path to the configuration file (default: .poutine.yml in the working directory, or .github/poutine.yml)
115115
--skip Add rules to the skip list for the current run (can be specified multiple times)
116116
--verbose Enable debug logging
117117
--fail-on-violation Exit with a non-zero code (10) when violations are found
@@ -125,7 +125,7 @@ See [.poutine.sample.yml](.poutine.sample.yml) for an example configuration file
125125

126126
#### Configuration
127127

128-
Create a `.poutine.yml` configuration file in your current working directory, or use a custom path with the `--config` flag:
128+
Create a `.poutine.yml` configuration file in your current working directory, or keep it alongside your other GitHub metadata at `.github/poutine.yml` — both are auto-discovered. When both exist, `.poutine.yml` at the repo root wins. To use a custom path, pass the `--config` flag (which takes precedence over both):
129129

130130
```bash
131131
poutine analyze_local . --config my-config.yml

cmd/config_discovery_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestFindDefaultConfigFile_NoConfig(t *testing.T) {
13+
dir := t.TempDir()
14+
assert.Empty(t, findDefaultConfigFile(dir))
15+
}
16+
17+
func TestFindDefaultConfigFile_RootYml(t *testing.T) {
18+
dir := t.TempDir()
19+
path := filepath.Join(dir, ".poutine.yml")
20+
require.NoError(t, os.WriteFile(path, []byte("ignoreForks: true\n"), 0o644))
21+
22+
assert.Equal(t, path, findDefaultConfigFile(dir))
23+
}
24+
25+
func TestFindDefaultConfigFile_RootYaml(t *testing.T) {
26+
dir := t.TempDir()
27+
path := filepath.Join(dir, ".poutine.yaml")
28+
require.NoError(t, os.WriteFile(path, []byte("ignoreForks: true\n"), 0o644))
29+
30+
assert.Equal(t, path, findDefaultConfigFile(dir))
31+
}
32+
33+
func TestFindDefaultConfigFile_GithubDir(t *testing.T) {
34+
dir := t.TempDir()
35+
require.NoError(t, os.Mkdir(filepath.Join(dir, ".github"), 0o755))
36+
path := filepath.Join(dir, ".github", "poutine.yml")
37+
require.NoError(t, os.WriteFile(path, []byte("ignoreForks: true\n"), 0o644))
38+
39+
assert.Equal(t, path, findDefaultConfigFile(dir))
40+
}
41+
42+
func TestFindDefaultConfigFile_RootTakesPrecedence(t *testing.T) {
43+
dir := t.TempDir()
44+
rootPath := filepath.Join(dir, ".poutine.yml")
45+
require.NoError(t, os.WriteFile(rootPath, []byte("ignoreForks: true\n"), 0o644))
46+
47+
require.NoError(t, os.Mkdir(filepath.Join(dir, ".github"), 0o755))
48+
ghPath := filepath.Join(dir, ".github", "poutine.yml")
49+
require.NoError(t, os.WriteFile(ghPath, []byte("ignoreForks: false\n"), 0o644))
50+
51+
assert.Equal(t, rootPath, findDefaultConfigFile(dir),
52+
"`.poutine.yml` at repo root must take precedence over `.github/poutine.yml`")
53+
}
54+
55+
// TestFindDefaultConfigFile_IgnoresDirectoryNamedLikeConfig ensures that a
56+
// directory (rather than a file) at a candidate path is skipped — otherwise
57+
// a stray `.poutine.yml/` dir would be wrongly reported as the config file.
58+
func TestFindDefaultConfigFile_IgnoresDirectoryNamedLikeConfig(t *testing.T) {
59+
dir := t.TempDir()
60+
require.NoError(t, os.Mkdir(filepath.Join(dir, ".poutine.yml"), 0o755))
61+
62+
require.NoError(t, os.Mkdir(filepath.Join(dir, ".github"), 0o755))
63+
ghPath := filepath.Join(dir, ".github", "poutine.yml")
64+
require.NoError(t, os.WriteFile(ghPath, []byte("ignoreForks: true\n"), 0o644))
65+
66+
assert.Equal(t, ghPath, findDefaultConfigFile(dir))
67+
}

cmd/root.go

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func init() {
138138
}
139139
}
140140

141-
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is .poutine.yml in the current directory)")
141+
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is .poutine.yml in the current directory or .github/poutine.yml)")
142142
RootCmd.PersistentFlags().StringVarP(&Format, "format", "f", "pretty", "Output format (pretty, json, sarif)")
143143
RootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "Enable verbose logging")
144144
RootCmd.PersistentFlags().StringVarP(&ScmProvider, "scm", "s", "github", "SCM platform (github, gitlab)")
@@ -153,20 +153,20 @@ func init() {
153153

154154
func initConfig() {
155155
viper.AutomaticEnv()
156-
if cfgFile != "" {
157-
viper.SetConfigFile(cfgFile)
158-
} else {
159-
viper.AddConfigPath(".")
160-
viper.SetConfigName(".poutine")
156+
157+
configPath := cfgFile
158+
if configPath == "" {
159+
configPath = findDefaultConfigFile(".")
160+
}
161+
162+
if configPath == "" {
163+
return
161164
}
162165

166+
viper.SetConfigFile(configPath)
163167
if err := viper.ReadInConfig(); err != nil {
164-
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
165-
return
166-
} else {
167-
log.Error().Err(err).Msg("Can't read config")
168-
os.Exit(1)
169-
}
168+
log.Error().Err(err).Msg("Can't read config")
169+
os.Exit(1)
170170
}
171171

172172
if err := viper.Unmarshal(&config); err != nil {
@@ -175,6 +175,31 @@ func initConfig() {
175175
}
176176
}
177177

178+
// findDefaultConfigFile returns the path of the first default config file
179+
// found under baseDir, in order of precedence:
180+
// 1. <baseDir>/.poutine.<ext> (working directory)
181+
// 2. <baseDir>/.github/poutine.<ext> (GitHub convention — no leading dot)
182+
//
183+
// Extensions are those supported by viper (yml, yaml, json, toml, ...).
184+
// Returns "" if no default config file is found.
185+
func findDefaultConfigFile(baseDir string) string {
186+
candidates := []struct {
187+
dir, name string
188+
}{
189+
{baseDir, ".poutine"},
190+
{filepath.Join(baseDir, ".github"), "poutine"},
191+
}
192+
for _, c := range candidates {
193+
for _, ext := range viper.SupportedExts {
194+
path := filepath.Join(c.dir, c.name+"."+ext)
195+
if info, err := os.Stat(path); err == nil && !info.IsDir() {
196+
return path
197+
}
198+
}
199+
}
200+
return ""
201+
}
202+
178203
func cleanup() {
179204
log.Debug().Msg("Cleaning up temp directories")
180205
globPattern := filepath.Join(os.TempDir(), analyze.TEMP_DIR_PREFIX)

0 commit comments

Comments
 (0)