Skip to content

Commit 63eb64e

Browse files
authored
fix: suppress version mismatch warnings in independent versioning mode (#252)
* fix: suppress version mismatch warnings in independent versioning mode Add workspace.versioning field (independent|coordinated) to WorkspaceConfig. When set to independent, discover command shows mismatches as info summaries instead of warnings. Updates text, table, and JSON output formatters. JSON output gains a versioning_mode field for machine consumers. * test: discover versioning mode and mismatch warning tests Add TestWorkspaceConfig_IsIndependentVersioning, TestWorkspaceConfig_VersioningMode, TestLoadConfig_InvalidVersioning for config validation. Add versioning mode tests for JSON, text, table formatters and workflow mismatch handling. * style: fix gofmt formatting in output.go
1 parent 46faa06 commit 63eb64e

8 files changed

Lines changed: 473 additions & 13 deletions

File tree

internal/commands/discover/discovercmd.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func runDiscoverCmd(ctx context.Context, cmd *cli.Command, cfg *config.Config) e
7878
format := ParseOutputFormat(cmd.String("format"))
7979
quiet := cmd.Bool("quiet")
8080

81-
formatter := NewFormatter(format)
81+
formatter := NewFormatterWithConfig(format, cfg)
8282

8383
if quiet {
8484
// In quiet mode, only show summary
@@ -91,7 +91,7 @@ func runDiscoverCmd(ctx context.Context, cmd *cli.Command, cfg *config.Config) e
9191
noInteractive := cmd.Bool("no-interactive")
9292
if !noInteractive && format == FormatText {
9393
prompter := NewPrompter()
94-
workflow := NewWorkflow(prompter, result, rootDir)
94+
workflow := NewWorkflowWithConfig(prompter, result, rootDir, cfg)
9595
if _, err := workflow.Run(ctx); err != nil {
9696
return err
9797
}

internal/commands/discover/output.go

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,41 @@ import (
66
"os"
77
"strings"
88

9+
"github.com/indaco/sley/internal/config"
910
"github.com/indaco/sley/internal/discovery"
1011
"github.com/indaco/sley/internal/printer"
1112
)
1213

1314
// Formatter handles display of discovery results.
1415
type Formatter struct {
1516
format OutputFormat
17+
cfg *config.Config
1618
}
1719

1820
// NewFormatter creates a new Formatter with the specified output format.
1921
func NewFormatter(format OutputFormat) *Formatter {
2022
return &Formatter{format: format}
2123
}
2224

25+
// NewFormatterWithConfig creates a new Formatter with the specified output format and config.
26+
// The config is used to adjust output severity (e.g., independent versioning shows info instead of warnings).
27+
func NewFormatterWithConfig(format OutputFormat, cfg *config.Config) *Formatter {
28+
return &Formatter{format: format, cfg: cfg}
29+
}
30+
31+
// isIndependentVersioning returns true if the config indicates independent versioning mode.
32+
func (f *Formatter) isIndependentVersioning() bool {
33+
return f.cfg != nil && f.cfg.Workspace != nil && f.cfg.Workspace.IsIndependentVersioning()
34+
}
35+
36+
// versioningMode returns the effective versioning mode string from the config.
37+
func (f *Formatter) versioningMode() string {
38+
if f.cfg != nil && f.cfg.Workspace != nil {
39+
return f.cfg.Workspace.VersioningMode()
40+
}
41+
return "coordinated"
42+
}
43+
2344
// FormatResult formats the discovery result for display.
2445
func (f *Formatter) FormatResult(result *discovery.Result) string {
2546
switch f.format {
@@ -72,12 +93,22 @@ func (f *Formatter) formatText(result *discovery.Result) string {
7293

7394
// Mismatches section
7495
if len(result.Mismatches) > 0 {
75-
sb.WriteString(printer.Warning("Version Mismatches:"))
76-
sb.WriteString("\n")
77-
for _, m := range result.Mismatches {
78-
status := printer.Warning("⚠")
79-
fmt.Fprintf(&sb, " %s %s: expected %s, found %s\n",
80-
status, m.Source, m.ExpectedVersion, m.ActualVersion)
96+
if f.isIndependentVersioning() {
97+
sb.WriteString(printer.Info(fmt.Sprintf("Version Summary (independent versioning): %d module(s) at different versions", len(result.Mismatches))))
98+
sb.WriteString("\n")
99+
for _, m := range result.Mismatches {
100+
status := printer.Info("ℹ")
101+
fmt.Fprintf(&sb, " %s %s: %s (root: %s)\n",
102+
status, m.Source, m.ActualVersion, m.ExpectedVersion)
103+
}
104+
} else {
105+
sb.WriteString(printer.Warning("Version Mismatches:"))
106+
sb.WriteString("\n")
107+
for _, m := range result.Mismatches {
108+
status := printer.Warning("⚠")
109+
fmt.Fprintf(&sb, " %s %s: expected %s, found %s\n",
110+
status, m.Source, m.ExpectedVersion, m.ActualVersion)
111+
}
81112
}
82113
sb.WriteString("\n")
83114
}
@@ -133,7 +164,11 @@ func (f *Formatter) formatTable(result *discovery.Result) string {
133164

134165
// Mismatches table
135166
if len(result.Mismatches) > 0 {
136-
sb.WriteString("Version Mismatches:\n")
167+
if f.isIndependentVersioning() {
168+
fmt.Fprintf(&sb, "Version Summary (independent versioning): %d module(s) at different versions\n", len(result.Mismatches))
169+
} else {
170+
sb.WriteString("Version Mismatches:\n")
171+
}
137172
fmt.Fprintf(&sb, "%-30s %-15s %-15s\n", "SOURCE", "EXPECTED", "ACTUAL")
138173
sb.WriteString(strings.Repeat("-", 60) + "\n")
139174
for _, m := range result.Mismatches {
@@ -180,6 +215,7 @@ func (f *Formatter) formatJSON(result *discovery.Result) string {
180215

181216
output := struct {
182217
Mode string `json:"mode"`
218+
VersioningMode string `json:"versioning_mode"`
183219
Modules []jsonModule `json:"modules"`
184220
Manifests []jsonManifest `json:"manifests"`
185221
Mismatches []jsonMismatch `json:"mismatches"`
@@ -194,6 +230,7 @@ func (f *Formatter) formatJSON(result *discovery.Result) string {
194230
} `json:"summary"`
195231
}{
196232
Mode: result.Mode.String(),
233+
VersioningMode: f.versioningMode(),
197234
Modules: make([]jsonModule, len(result.Modules)),
198235
Manifests: make([]jsonManifest, len(result.Manifests)),
199236
Mismatches: make([]jsonMismatch, len(result.Mismatches)),
@@ -269,7 +306,11 @@ func (f *Formatter) formatSummary(result *discovery.Result) string {
269306
}
270307

271308
if mismatchCount > 0 {
272-
parts = append(parts, printer.Warning(fmt.Sprintf("%d mismatch(es)", mismatchCount)))
309+
if f.isIndependentVersioning() {
310+
parts = append(parts, printer.Info(fmt.Sprintf("%d version difference(s) (independent)", mismatchCount)))
311+
} else {
312+
parts = append(parts, printer.Warning(fmt.Sprintf("%d mismatch(es)", mismatchCount)))
313+
}
273314
}
274315

275316
if len(parts) == 0 {

internal/commands/discover/output_test.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"strings"
55
"testing"
66

7+
"github.com/indaco/sley/internal/config"
78
"github.com/indaco/sley/internal/discovery"
89
"github.com/indaco/sley/internal/parser"
910
"github.com/indaco/sley/internal/testutils"
@@ -700,3 +701,178 @@ func TestFormatter_FormatResult_DefaultCase(t *testing.T) {
700701
t.Error("expected text format output for invalid format")
701702
}
702703
}
704+
705+
func TestFormatJSON_VersioningMode(t *testing.T) {
706+
result := &discovery.Result{
707+
Mode: discovery.MultiModule,
708+
Modules: []discovery.Module{
709+
{Name: "root", RelPath: ".version", Version: "1.0.0"},
710+
{Name: "sub", RelPath: "sub/.version", Version: "2.0.0"},
711+
},
712+
Mismatches: []discovery.Mismatch{
713+
{Source: "sub/.version", ExpectedVersion: "1.0.0", ActualVersion: "2.0.0"},
714+
},
715+
}
716+
717+
tests := []struct {
718+
name string
719+
cfg *config.Config
720+
wantMode string
721+
}{
722+
{
723+
name: "independent config",
724+
cfg: &config.Config{
725+
Workspace: &config.WorkspaceConfig{Versioning: "independent"},
726+
},
727+
wantMode: `"versioning_mode": "independent"`,
728+
},
729+
{
730+
name: "coordinated config",
731+
cfg: &config.Config{
732+
Workspace: &config.WorkspaceConfig{Versioning: "coordinated"},
733+
},
734+
wantMode: `"versioning_mode": "coordinated"`,
735+
},
736+
{
737+
name: "nil config defaults to coordinated",
738+
cfg: nil,
739+
wantMode: `"versioning_mode": "coordinated"`,
740+
},
741+
{
742+
name: "empty workspace defaults to coordinated",
743+
cfg: &config.Config{},
744+
wantMode: `"versioning_mode": "coordinated"`,
745+
},
746+
}
747+
748+
for _, tt := range tests {
749+
t.Run(tt.name, func(t *testing.T) {
750+
var formatter *Formatter
751+
if tt.cfg != nil {
752+
formatter = NewFormatterWithConfig(FormatJSON, tt.cfg)
753+
} else {
754+
formatter = NewFormatter(FormatJSON)
755+
}
756+
output := formatter.FormatResult(result)
757+
758+
if !strings.Contains(output, tt.wantMode) {
759+
t.Errorf("JSON output missing expected versioning_mode.\nwant substring: %s\ngot: %s", tt.wantMode, output)
760+
}
761+
})
762+
}
763+
}
764+
765+
func TestFormatText_VersioningMode_IndependentMismatches(t *testing.T) {
766+
result := &discovery.Result{
767+
Mode: discovery.MultiModule,
768+
Modules: []discovery.Module{
769+
{Name: "root", RelPath: ".version", Version: "1.0.0"},
770+
{Name: "sub", RelPath: "sub/.version", Version: "2.0.0"},
771+
},
772+
Mismatches: []discovery.Mismatch{
773+
{Source: "sub/.version", ExpectedVersion: "1.0.0", ActualVersion: "2.0.0"},
774+
},
775+
}
776+
777+
t.Run("independent shows info language", func(t *testing.T) {
778+
cfg := &config.Config{
779+
Workspace: &config.WorkspaceConfig{Versioning: "independent"},
780+
}
781+
formatter := NewFormatterWithConfig(FormatText, cfg)
782+
output := formatter.FormatResult(result)
783+
784+
if !strings.Contains(output, "independent versioning") {
785+
t.Errorf("expected 'independent versioning' in output, got:\n%s", output)
786+
}
787+
if strings.Contains(output, "expected 1.0.0") {
788+
t.Errorf("independent mode should not use 'expected' warning language, got:\n%s", output)
789+
}
790+
})
791+
792+
t.Run("coordinated shows warning language", func(t *testing.T) {
793+
cfg := &config.Config{
794+
Workspace: &config.WorkspaceConfig{Versioning: "coordinated"},
795+
}
796+
formatter := NewFormatterWithConfig(FormatText, cfg)
797+
output := formatter.FormatResult(result)
798+
799+
if !strings.Contains(output, "Version Mismatch") {
800+
t.Errorf("expected 'Version Mismatch' in output, got:\n%s", output)
801+
}
802+
if !strings.Contains(output, "expected 1.0.0") {
803+
t.Errorf("expected warning language with 'expected', got:\n%s", output)
804+
}
805+
})
806+
}
807+
808+
func TestFormatTable_VersioningMode_IndependentMismatches(t *testing.T) {
809+
result := &discovery.Result{
810+
Mode: discovery.MultiModule,
811+
Modules: []discovery.Module{
812+
{Name: "root", RelPath: ".version", Version: "1.0.0"},
813+
},
814+
Mismatches: []discovery.Mismatch{
815+
{Source: "sub/.version", ExpectedVersion: "1.0.0", ActualVersion: "2.0.0"},
816+
},
817+
}
818+
819+
t.Run("independent shows version summary", func(t *testing.T) {
820+
cfg := &config.Config{
821+
Workspace: &config.WorkspaceConfig{Versioning: "independent"},
822+
}
823+
formatter := NewFormatterWithConfig(FormatTable, cfg)
824+
output := formatter.formatTable(result)
825+
826+
if !strings.Contains(output, "independent versioning") {
827+
t.Errorf("expected 'independent versioning' in table output, got:\n%s", output)
828+
}
829+
if strings.Contains(output, "Version Mismatches:") {
830+
t.Errorf("independent mode should not show 'Version Mismatches:' header, got:\n%s", output)
831+
}
832+
})
833+
834+
t.Run("coordinated shows mismatches header", func(t *testing.T) {
835+
formatter := NewFormatter(FormatTable)
836+
output := formatter.formatTable(result)
837+
838+
if !strings.Contains(output, "Version Mismatches:") {
839+
t.Errorf("expected 'Version Mismatches:' in table output, got:\n%s", output)
840+
}
841+
})
842+
}
843+
844+
func TestFormatter_formatSummary_VersioningMode(t *testing.T) {
845+
result := &discovery.Result{
846+
Modules: []discovery.Module{
847+
{Version: "1.0.0", RelPath: ".version"},
848+
},
849+
Mismatches: []discovery.Mismatch{
850+
{Source: "sub/.version"},
851+
{Source: "other/.version"},
852+
},
853+
}
854+
855+
t.Run("independent shows difference count", func(t *testing.T) {
856+
cfg := &config.Config{
857+
Workspace: &config.WorkspaceConfig{Versioning: "independent"},
858+
}
859+
formatter := NewFormatterWithConfig(FormatText, cfg)
860+
summary := formatter.formatSummary(result)
861+
862+
if !strings.Contains(summary, "version difference") {
863+
t.Errorf("expected 'version difference' in summary, got: %q", summary)
864+
}
865+
if !strings.Contains(summary, "independent") {
866+
t.Errorf("expected 'independent' in summary, got: %q", summary)
867+
}
868+
})
869+
870+
t.Run("coordinated shows mismatch count", func(t *testing.T) {
871+
formatter := NewFormatter(FormatText)
872+
summary := formatter.formatSummary(result)
873+
874+
if !strings.Contains(summary, "mismatch") {
875+
t.Errorf("expected 'mismatch' in summary, got: %q", summary)
876+
}
877+
})
878+
}

internal/commands/discover/workflow.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type Workflow struct {
2121
prompter Prompter
2222
result *discovery.Result
2323
rootDir string
24+
cfg *config.Config
2425
}
2526

2627
// NewWorkflow creates a new workflow handler.
@@ -32,6 +33,16 @@ func NewWorkflow(prompter Prompter, result *discovery.Result, rootDir string) *W
3233
}
3334
}
3435

36+
// NewWorkflowWithConfig creates a new workflow handler with config awareness.
37+
func NewWorkflowWithConfig(prompter Prompter, result *discovery.Result, rootDir string, cfg *config.Config) *Workflow {
38+
return &Workflow{
39+
prompter: prompter,
40+
result: result,
41+
rootDir: rootDir,
42+
cfg: cfg,
43+
}
44+
}
45+
3546
// Run executes the interactive workflow if appropriate.
3647
// Returns true if the workflow completed with actions taken.
3748
func (w *Workflow) Run(ctx context.Context) (bool, error) {
@@ -345,9 +356,14 @@ func (w *Workflow) runExistingConfigWorkflow(ctx context.Context) (bool, error)
345356
// runMismatchWorkflow offers to help resolve version mismatches.
346357
func (w *Workflow) runMismatchWorkflow(_ context.Context) (bool, error) {
347358
fmt.Println()
348-
printer.PrintWarning(fmt.Sprintf("Found %d version mismatch(es).", len(w.result.Mismatches)))
349-
printer.PrintFaint("Consider enabling the dependency-check plugin with auto-sync to keep versions in sync.")
350-
printer.PrintFaint("Run 'sley bump auto --sync' to sync versions during bumps.")
359+
if w.cfg != nil && w.cfg.Workspace != nil && w.cfg.Workspace.IsIndependentVersioning() {
360+
printer.PrintInfo(fmt.Sprintf("Version summary: %d module(s) at different versions (independent versioning).", len(w.result.Mismatches)))
361+
printer.PrintFaint("Each module manages its own version independently.")
362+
} else {
363+
printer.PrintWarning(fmt.Sprintf("Found %d version mismatch(es).", len(w.result.Mismatches)))
364+
printer.PrintFaint("Consider enabling the dependency-check plugin with auto-sync to keep versions in sync.")
365+
printer.PrintFaint("Run 'sley bump auto --sync' to sync versions during bumps.")
366+
}
351367
return false, nil
352368
}
353369

0 commit comments

Comments
 (0)