From 7ed1e1b9f920136497ec94ecc46820d10d62d812 Mon Sep 17 00:00:00 2001 From: alex <53851759+alxxjohn@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:18:55 -0400 Subject: [PATCH] fix(cli): fail closed on missing policy files --- internal/cli/helpers.go | 3 +-- internal/codeguard/config/io.go | 6 ++++++ pkg/codeguard/sdk_config.go | 3 +++ tests/cli/scan_test.go | 28 ++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/internal/cli/helpers.go b/internal/cli/helpers.go index 6cd6323d..0c7bbb0d 100644 --- a/internal/cli/helpers.go +++ b/internal/cli/helpers.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "os" "strings" service "github.com/devr-tools/codeguard/pkg/codeguard" @@ -54,7 +53,7 @@ func loadScanConfigWithFallback(path string, profile string, defaultConfigReques if err == nil { return applyProfileOverride(cfg, profile) } - if !defaultConfigRequested || !errors.Is(err, os.ErrNotExist) { + if !defaultConfigRequested || !errors.Is(err, service.ErrConfigNotFound) { return service.Config{}, err } diff --git a/internal/codeguard/config/io.go b/internal/codeguard/config/io.go index 210a868f..55965fb2 100644 --- a/internal/codeguard/config/io.go +++ b/internal/codeguard/config/io.go @@ -17,6 +17,9 @@ import ( const maxConfigFileBytes = 32 << 20 var ( + // ErrConfigNotFound identifies a missing top-level configuration file. It is + // distinct from missing artifacts referenced by an existing configuration. + ErrConfigNotFound = errors.New("config file not found") defaultConfigNames = []string{"codeguard.yaml", "codeguard.yml", "codeguard.json"} directoryConfigNames = []string{"codeguard.yaml", "codeguard.yml", "codeguard.json", "config.yaml", "config.yml", "config.json"} defaultConfigDirs = []string{".", ".codeguard"} @@ -30,6 +33,9 @@ func LoadFile(path string) (core.Config, error) { f, err := os.Open(resolvedPath) //nolint:gosec // operator-supplied config path; read is size-capped by LimitReader below if err != nil { + if errors.Is(err, os.ErrNotExist) { + return core.Config{}, fmt.Errorf("%w: %w", ErrConfigNotFound, err) + } return core.Config{}, err } defer func() { _ = f.Close() }() diff --git a/pkg/codeguard/sdk_config.go b/pkg/codeguard/sdk_config.go index 8bc0c339..e925f05b 100644 --- a/pkg/codeguard/sdk_config.go +++ b/pkg/codeguard/sdk_config.go @@ -2,6 +2,9 @@ package codeguard import "github.com/devr-tools/codeguard/internal/codeguard/config" +// ErrConfigNotFound identifies a missing top-level configuration file. +var ErrConfigNotFound = config.ErrConfigNotFound + // ExampleConfig returns CodeGuard's complete, ready-to-edit starter // configuration. It is intended for callers creating a new configuration, // rather than as a way to normalize a partial Config. diff --git a/tests/cli/scan_test.go b/tests/cli/scan_test.go index ebe94a10..a797111d 100644 --- a/tests/cli/scan_test.go +++ b/tests/cli/scan_test.go @@ -235,6 +235,34 @@ func TestRunScanFolderWithExplicitMissingConfigStillFails(t *testing.T) { } } +func TestRunScanFolderWithMissingDesignRulesDoesNotFallBack(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("getwd: %v", err) + } + dir := t.TempDir() + if err := os.Chdir(dir); err != nil { + t.Fatalf("chdir tempdir: %v", err) + } + t.Cleanup(func() { _ = os.Chdir(cwd) }) + + writeScanTestFile(t, filepath.Join(dir, "codeguard.yml"), ` +name: repository-policy +checks: + design_rules_file: .codeguard/missing-design-rules.yml +`) + writeScanTestFile(t, filepath.Join(dir, "sub", "main.go"), "package main\n") + + var stdout, stderr bytes.Buffer + code := cli.Run([]string{"scan", "-folder", "sub"}, strings.NewReader(""), &stdout, &stderr) + if code != 1 { + t.Fatalf("expected exit 1, got %d; stdout = %s", code, stdout.String()) + } + if !strings.Contains(stderr.String(), "checks.design_rules_file") { + t.Fatalf("expected missing design rules error, got %s", stderr.String()) + } +} + func writeScanTestFile(t *testing.T, path string, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {