From 12c1ed6d06fc7a812bd7e1683aeb9a656da3a585 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Tue, 14 Apr 2026 11:26:21 +0200 Subject: [PATCH] fix: provider output handling and watch rebuild re-invocation Provider info and error messages containing newlines broke the TTY progress display (timer drifting to a new line, broken cursor movement). Extract only the first line for progress events via firstLine(). Full messages remain available through the provider's own debug message type. Skip provider services during watch rebuild convergence by adding a SkipProviders flag to CreateOptions, set only by the watch rebuild path. This prevents unnecessary re-invocation of providers on every file change while preserving normal provider execution for all other commands (up, create, run, scale). Signed-off-by: Guillaume Lours --- pkg/api/api.go | 2 ++ pkg/compose/convergence.go | 6 ++++++ pkg/compose/plugins.go | 13 +++++++++++-- pkg/compose/watch.go | 7 ++++--- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index deefc1e52e9..1e84cca2bf7 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -278,6 +278,8 @@ type CreateOptions struct { Timeout *time.Duration // QuietPull makes the pulling process quiet QuietPull bool + // SkipProviders skips provider services during convergence (e.g. watch rebuild) + SkipProviders bool } // StartOptions group options of the Start API diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index 973699e8945..609f8039490 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -100,6 +100,12 @@ func (c *convergence) apply(ctx context.Context, project *types.Project, options return err } + // Skip provider services when the caller opted out (e.g. watch rebuild), + // since providers were already set up during initial "up". + if service.Provider != nil && options.SkipProviders { + return nil + } + return tracing.SpanWrapFunc("service/apply", tracing.ServiceOptions(service), func(ctx context.Context) error { strategy := options.RecreateDependencies if slices.Contains(options.Services, name) { diff --git a/pkg/compose/plugins.go b/pkg/compose/plugins.go index 159394f6bf1..a0eee8a37bb 100644 --- a/pkg/compose/plugins.go +++ b/pkg/compose/plugins.go @@ -125,10 +125,10 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty } switch msg.Type { case ErrorType: - s.events.On(newEvent(service.Name, api.Error, msg.Message)) + s.events.On(newEvent(service.Name, api.Error, firstLine(msg.Message))) return nil, errors.New(msg.Message) case InfoType: - s.events.On(newEvent(service.Name, api.Working, msg.Message)) + s.events.On(newEvent(service.Name, api.Working, firstLine(msg.Message))) case SetEnvType: key, val, found := strings.Cut(msg.Message, "=") if !found { @@ -281,3 +281,12 @@ func (c CommandMetadata) CheckRequiredParameters(provider types.ServiceProviderC } return nil } + +// firstLine returns the first line of s, stripping any trailing newlines. +func firstLine(s string) string { + s = strings.TrimRight(s, "\n") + if i := strings.IndexByte(s, '\n'); i >= 0 { + return s[:i] + } + return s +} diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go index 39c94f79725..f63c51683ab 100644 --- a/pkg/compose/watch.go +++ b/pkg/compose/watch.go @@ -662,9 +662,10 @@ func (s *composeService) rebuild(ctx context.Context, project *types.Project, se options.LogTo.Log(api.WatchLogger, fmt.Sprintf("service(s) %q successfully built", services)) err = s.create(ctx, project, api.CreateOptions{ - Services: services, - Inherit: true, - Recreate: api.RecreateForce, + Services: services, + Inherit: true, + Recreate: api.RecreateForce, + SkipProviders: true, }) if err != nil { options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Failed to recreate services after update. Error: %v", err))