Skip to content

Commit af33a5d

Browse files
michaelmcneesclaude
andcommitted
fix(config): inline git_sync validators to break test import cycle
The config → repo → auth chain caused `go test` to fail in both ./internal/auth and ./internal/repo with "import cycle not allowed in test". Fix by inlining validateGitSyncName, validateGitSyncPollInterval, and validateGitSyncURL as unexported helpers in config.go, removing the import edge from config → repo. The canonical exported symbols in internal/repo/types.go are unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent be683a3 commit af33a5d

1 file changed

Lines changed: 54 additions & 7 deletions

File tree

packages/engine/internal/config/config.go

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import (
99
"log/slog"
1010
"net/url"
1111
"os"
12+
"regexp"
1213
"strings"
1314
"time"
1415

1516
"github.com/dvflw/mantle/internal/budget"
1617
"github.com/dvflw/mantle/internal/dbdefaults"
1718
"github.com/dvflw/mantle/internal/netutil"
18-
"github.com/dvflw/mantle/internal/repo"
1919
"github.com/spf13/cobra"
2020
"github.com/spf13/viper"
2121
)
@@ -363,19 +363,17 @@ func Load(cmd *cobra.Command) (*Config, error) {
363363
// it only needs to catch the "user@host" form that people may put in a
364364
// config file. Full store-level validation runs again at registration time.
365365
for i, r := range cfg.GitSync.Repos {
366-
if err := repo.ValidateName(r.Name); err != nil {
366+
if err := validateGitSyncName(r.Name); err != nil {
367367
return nil, fmt.Errorf("git_sync.repos[%d]: %w", i, err)
368368
}
369369
if r.URL == "" {
370370
return nil, fmt.Errorf("git_sync.repos[%d] (%q): url is required", i, r.Name)
371371
}
372-
if parsed, err := url.Parse(r.URL); err != nil {
373-
return nil, fmt.Errorf("git_sync.repos[%d] (%q): invalid url: %w", i, r.Name, err)
374-
} else if parsed.User != nil {
375-
return nil, fmt.Errorf("git_sync.repos[%d] (%q): url must not embed credentials", i, r.Name)
372+
if err := validateGitSyncURL(r.URL); err != nil {
373+
return nil, fmt.Errorf("git_sync.repos[%d] (%q): %w", i, r.Name, err)
376374
}
377375
if r.PollInterval != "" {
378-
if err := repo.ValidatePollInterval(r.PollInterval); err != nil {
376+
if err := validateGitSyncPollInterval(r.PollInterval); err != nil {
379377
return nil, fmt.Errorf("git_sync.repos[%d] (%q): %w", i, r.Name, err)
380378
}
381379
}
@@ -415,3 +413,52 @@ func Load(cmd *cobra.Command) (*Config, error) {
415413

416414
return &cfg, nil
417415
}
416+
417+
// gitSyncNamePattern enforces DNS-label-like names: lowercase alphanumerics,
418+
// underscores, and hyphens, starting and ending with an alphanumeric.
419+
// This is a local copy of the pattern in packages/engine/internal/repo/types.go
420+
// kept here to avoid a config → repo → auth test-time import cycle.
421+
var gitSyncNamePattern = regexp.MustCompile(`^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$`)
422+
423+
// validateGitSyncName is a local copy of repo.ValidateName to avoid a
424+
// config → repo → auth test-time import cycle. Keep in sync with the
425+
// canonical version in packages/engine/internal/repo/types.go.
426+
func validateGitSyncName(name string) error {
427+
if name == "" {
428+
return fmt.Errorf("repo name is required")
429+
}
430+
if len(name) > 63 {
431+
return fmt.Errorf("invalid repo name %q: length %d exceeds 63-char cap", name, len(name))
432+
}
433+
if !gitSyncNamePattern.MatchString(name) {
434+
return fmt.Errorf("invalid repo name %q: must match %s", name, gitSyncNamePattern.String())
435+
}
436+
return nil
437+
}
438+
439+
// validateGitSyncPollInterval is a local copy of repo.ValidatePollInterval.
440+
// See validateGitSyncName for the rationale.
441+
func validateGitSyncPollInterval(interval string) error {
442+
d, err := time.ParseDuration(interval)
443+
if err != nil {
444+
return fmt.Errorf("invalid poll_interval %q: %w", interval, err)
445+
}
446+
if d < 10*time.Second {
447+
return fmt.Errorf("poll_interval %q below 10s minimum", interval)
448+
}
449+
return nil
450+
}
451+
452+
// validateGitSyncURL rejects any URL that embeds credentials in the
453+
// userinfo component (https://user@host or https://user:pass@host). All
454+
// auth material must flow through the Credential reference.
455+
func validateGitSyncURL(raw string) error {
456+
u, err := url.Parse(raw)
457+
if err != nil {
458+
return fmt.Errorf("invalid url %q: %w", raw, err)
459+
}
460+
if u.User != nil {
461+
return fmt.Errorf("repo url must not embed credentials — use the Credential field instead")
462+
}
463+
return nil
464+
}

0 commit comments

Comments
 (0)