-
Notifications
You must be signed in to change notification settings - Fork 0
Add interactive feedback to remote builds #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add interactive feedback to remote builds #7
Conversation
…hboard Implemented comprehensive interactive build features to enhance user experience: - Created full-featured TUI dashboard using Bubbletea for real-time build monitoring - Added progress bar support for tracking build steps across parallel services - Included simple progress bar implementation example for quick adoption - Provided integration examples showing how to enable interactive mode - Added comprehensive implementation guide with testing checklist The implementation leverages existing streaming infrastructure and Charmbracelet libraries (bubbletea, bubbles, lipgloss) already in the codebase. Users can choose between simple inline progress bars or a full interactive dashboard based on their needs. All features gracefully degrade to non-interactive mode for CI/CD pipelines. Files added: - internal/build/dashboard.go: Full interactive TUI dashboard - internal/build/dashboard_integration_example.go: Integration examples - example_simple_progress.go: Minimal progress bar implementation - INTERACTIVE_BUILDS_GUIDE.md: Complete implementation guide
Updated the build dashboard to be completely passive - it displays information in real-time without requiring any user interaction (except q to quit). Key changes: - Removed keyboard navigation (up/down to select services) - Display all services at once with progress bars - Show last 3 log lines under each actively building service - Auto-updates as builds progress - no interaction needed - Color-coded status indicators (queued/building/complete/failed) - Real-time duration tracking per service - Cleaner, simpler UI focused on information display The dashboard now matches the requested design: - Progress bars with percentage for each service - Auto-scrolling logs shown inline under building services - Consistent service order throughout the build - Graceful fallback to regular logs in non-interactive mode Updated integration examples and documentation to reflect the passive nature of the dashboard and simplified the implementation guide.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds interactive feedback capabilities for remote Docker builds by introducing a passive TUI dashboard that monitors parallel build progress in real-time. The implementation leverages the existing Charmbracelet bubbletea/bubbles libraries to display build status, progress bars, and live logs for multiple services without requiring user interaction.
- Implements a new
BuildDashboardTUI component with automatic status updates and progress tracking - Adds
DashboardBuildLoggerthat integrates with the existingBuildLoggerinterface - Provides comprehensive documentation and integration examples for adopting the dashboard in build commands
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| internal/build/dashboard.go | Core implementation of passive TUI dashboard with build status tracking, progress bars, and real-time log display |
| internal/build/dashboard_integration_example.go | Integration examples showing how to use the dashboard with BuildOrchestrator in deploy commands |
| example_simple_progress.go | Alternative simpler approach using inline progress bars without full TUI |
| INTERACTIVE_BUILDS_GUIDE.md | Comprehensive guide covering implementation options, integration steps, and troubleshooting |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Mark all services as queued initially | ||
| for _, name := range serviceNames { | ||
| dashboardLogger.MarkServiceQueued(name) | ||
| } | ||
| // Start dashboard TUI (runs in background) | ||
| dashboard.Start() |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition: dashboardLogger.MarkServiceQueued() is called before dashboard.Start(), but this will call UpdateService() which attempts to send messages to bd.program. Since bd.program is only initialized in Start() at line 43, these early calls to MarkServiceQueued will silently fail (the nil check in UpdateService prevents a panic but the updates are lost). Move the queued marking calls to after dashboard.Start(), or add a small delay to ensure the dashboard is initialized.
| // Mark all services as queued initially | |
| for _, name := range serviceNames { | |
| dashboardLogger.MarkServiceQueued(name) | |
| } | |
| // Start dashboard TUI (runs in background) | |
| dashboard.Start() | |
| // Start dashboard TUI (runs in background) | |
| dashboard.Start() | |
| // Mark all services as queued initially (after dashboard is started) | |
| for _, name := range serviceNames { | |
| dashboardLogger.MarkServiceQueued(name) | |
| } |
internal/build/dashboard.go
Outdated
| var m map[string]interface{} | ||
| if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`) | ||
| .FindStringSubmatch(line); err != nil && len(err) > 1 { | ||
| return strings.TrimSpace(err[1]) |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable name m shadows the map type used in the function. The regex FindStringSubmatch returns a slice of strings (matches), not an error. This code will never work as intended - the condition err != nil will always be false since err is actually a slice.
This should be:
if matches := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
.FindStringSubmatch(line); matches != nil && len(matches) > 1 {
return strings.TrimSpace(matches[1])
}| var m map[string]interface{} | |
| if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`) | |
| .FindStringSubmatch(line); err != nil && len(err) > 1 { | |
| return strings.TrimSpace(err[1]) | |
| if matches := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`) | |
| .FindStringSubmatch(line); matches != nil && len(matches) > 1 { | |
| return strings.TrimSpace(matches[1]) |
internal/build/dashboard.go
Outdated
| if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`) | ||
| .FindStringSubmatch(line); err != nil && len(err) > 1 { |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new regex compilation on every call to cleanLogLine is inefficient. Consider compiling this regex once as a package-level variable or field in the struct that uses it, similar to how stepRegex is compiled once in NewDashboardBuildLogger.
| // 1. Add SetLogger method to BuildOrchestrator: | ||
| func (bo *BuildOrchestrator) SetLogger(logger BuildLogger) { | ||
| bo.logger = logger | ||
| } |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example SetLogger method allows changing the logger on a BuildOrchestrator after initialization, but this could lead to race conditions if builds are already in progress. The logger field in BuildOrchestrator is accessed without synchronization (based on the orchestrator interface). Consider documenting that this should only be called before any build operations start, or implement proper synchronization if runtime logger changes are needed.
| // 1. Add SetLogger method to BuildOrchestrator: | |
| func (bo *BuildOrchestrator) SetLogger(logger BuildLogger) { | |
| bo.logger = logger | |
| } | |
| // 1. Add SetLogger method to BuildOrchestrator: | |
| // To make SetLogger thread-safe, add a sync.RWMutex field (e.g., loggerMu) to BuildOrchestrator: | |
| // type BuildOrchestrator struct { | |
| // ... | |
| // logger BuildLogger | |
| // loggerMu sync.RWMutex | |
| // ... | |
| // } | |
| // | |
| // Then, update SetLogger to use the mutex: | |
| func (bo *BuildOrchestrator) SetLogger(logger BuildLogger) { | |
| bo.loggerMu.Lock() | |
| defer bo.loggerMu.Unlock() | |
| bo.logger = logger | |
| } | |
| // | |
| // NOTE: All accesses to bo.logger elsewhere (e.g., in buildService) should also use bo.loggerMu.RLock()/RUnlock() | |
| // to ensure thread safety. |
example_simple_progress.go
Outdated
| if _, exists := l.progressBars[serviceName]; !exists { | ||
| l.progressBars[serviceName] = progress.New(progress.WithDefaultGradient()) | ||
| } | ||
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Division by zero risk: if total is 0, this will cause a panic. Add a check before calculating the percentage:
if total == 0 {
return
}
percent := float64(current) / float64(total)| if total == 0 { | |
| return | |
| } |
INTERACTIVE_BUILDS_GUIDE.md
Outdated
| - Ensure `tea.WindowSizeMsg` handler updates viewport dimensions | ||
|
|
||
| ### Works locally but not in CI | ||
| - Add terminal detection: `isatty.IsTerminal(os.Stdout.Fd())` |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation references isatty.IsTerminal(os.Stdout.Fd()) but this package is not used elsewhere in the guide. The correct import based on the code examples is golang.org/x/term with term.IsTerminal(int(os.Stdout.Fd())). Update this to match the actual implementation used in the dashboard integration example.
| - Add terminal detection: `isatty.IsTerminal(os.Stdout.Fd())` | |
| - Add terminal detection: `term.IsTerminal(int(os.Stdout.Fd()))` (import `"golang.org/x/term"`) |
| go func() { | ||
| if _, err := bd.program.Run(); err != nil { | ||
| fmt.Printf("Error running dashboard: %v\n", err) | ||
| } | ||
| }() |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential race condition: bd.program is assigned at line 122 and then accessed in a goroutine at line 124, but other methods like UpdateService and Stop also access bd.program without synchronization. If UpdateService or Stop are called immediately after Start() and before the goroutine runs, they might see bd.program as nil or in an inconsistent state. Consider using a mutex to protect access to bd.program or ensuring it's fully initialized before being accessible.
internal/build/dashboard.go
Outdated
| return strings.TrimSpace(err[1]) | ||
| } |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement is unreachable.
internal/build/dashboard.go
Outdated
| var m map[string]interface{} | ||
| if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`) | ||
| .FindStringSubmatch(line); err != nil && len(err) > 1 { | ||
| return strings.TrimSpace(err[1]) |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement is unreachable.
…mands Removed temporary documentation and example files and integrated the passive TUI dashboard directly into the production code. Changes: - Removed INTERACTIVE_BUILDS_GUIDE.md (temporary documentation) - Removed example_simple_progress.go (temporary example) - Removed internal/build/dashboard_integration_example.go (temporary example) - Added golang.org/x/term dependency for terminal detection - Integrated dashboard into cmd/deploy/deploy.go - Integrated dashboard into cmd/redeploy/redeploy.go The dashboard now activates automatically when: - Running in an interactive terminal (detected via term.IsTerminal) - Building 2 or more services (single service uses regular logger) For non-interactive environments (CI/CD, piped output), the system automatically falls back to the regular styled logger. The dashboard provides: - Real-time progress bars for each service - Auto-scrolling logs (last 3 lines per building service) - Build duration tracking - Color-coded status indicators - Fully passive operation (no interaction required) Users can quit the dashboard early with 'q' or Ctrl+C if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
internal/build/dashboard.go:327
- The variable
mis declared but never used. This declaration should be removed as it serves no purpose and makes the code confusing, especially given the bug in the subsequent if condition.
var m map[string]interface{}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cmd/redeploy/redeploy.go
Outdated
| if err != nil { | ||
| // Stop dashboard before returning error | ||
| if dashboard != nil { | ||
| time.Sleep(1 * time.Second) |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The hardcoded sleep durations (1 second on error, 2 seconds on success) are magic numbers that make the code less maintainable. Consider:
- Defining these as named constants at the package level (e.g.,
dashboardErrorDisplayDuration,dashboardSuccessDisplayDuration) - Making them configurable if user experience feedback suggests different timing would be better
This would improve code readability and make it easier to adjust these values in the future.
cmd/deploy/deploy.go
Outdated
|
|
||
| // Keep dashboard visible for a moment before stopping | ||
| if dashboard != nil { | ||
| time.Sleep(2 * time.Second) |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The hardcoded sleep durations (1 second on error, 2 seconds on success) are magic numbers that make the code less maintainable. Consider:
- Defining these as named constants at the package level (e.g.,
dashboardErrorDisplayDuration,dashboardSuccessDisplayDuration) - Making them configurable if user experience feedback suggests different timing would be better
This would improve code readability and make it easier to adjust these values in the future.
internal/build/dashboard.go
Outdated
| for _, svc := range m.services { | ||
| switch svc.Status { |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition: Reading svc.Status without holding the service's mutex lock. While m.mu.RLock() is held (line 201), individual service fields should be protected by their own mutex (svc.mu) when accessed. The code correctly locks svc.mu later at line 247, but this early access at line 224 is unprotected and could race with updates in the Update() method (line 166).
Consider either:
- Locking each service mutex before reading Status in this loop
- Moving the status counting logic to after the individual service locks in the main loop
cmd/redeploy/redeploy.go
Outdated
|
|
||
| // Keep dashboard visible for a moment before stopping | ||
| if dashboard != nil { | ||
| time.Sleep(2 * time.Second) |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The hardcoded sleep durations (1 second on error, 2 seconds on success) are magic numbers that make the code less maintainable. Consider:
- Defining these as named constants at the package level (e.g.,
dashboardErrorDisplayDuration,dashboardSuccessDisplayDuration) - Making them configurable if user experience feedback suggests different timing would be better
This would improve code readability and make it easier to adjust these values in the future.
cmd/deploy/deploy.go
Outdated
| if err != nil { | ||
| // Stop dashboard before returning error | ||
| if dashboard != nil { | ||
| time.Sleep(1 * time.Second) |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The hardcoded sleep durations (1 second on error, 2 seconds on success) are magic numbers that make the code less maintainable. Consider:
- Defining these as named constants at the package level (e.g.,
dashboardErrorDisplayDuration,dashboardSuccessDisplayDuration) - Making them configurable if user experience feedback suggests different timing would be better
This would improve code readability and make it easier to adjust these values in the future.
internal/build/dashboard.go
Outdated
| var m map[string]interface{} | ||
| if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`) | ||
| .FindStringSubmatch(line); err != nil && len(err) > 1 { | ||
| return strings.TrimSpace(err[1]) |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement is unreachable.
| var m map[string]interface{} | |
| if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`) | |
| .FindStringSubmatch(line); err != nil && len(err) > 1 { | |
| return strings.TrimSpace(err[1]) | |
| matches := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`).FindStringSubmatch(line) | |
| if len(matches) > 1 { | |
| return strings.TrimSpace(matches[1]) |
internal/build/dashboard.go
Outdated
| if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`) | ||
| .FindStringSubmatch(line); err != nil && len(err) > 1 { | ||
| return strings.TrimSpace(err[1]) |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement is unreachable.
| if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`) | |
| .FindStringSubmatch(line); err != nil && len(err) > 1 { | |
| return strings.TrimSpace(err[1]) | |
| if matches := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`) | |
| .FindStringSubmatch(line); matches != nil && len(matches) > 1 { | |
| return strings.TrimSpace(matches[1]) |
Fixed all issues identified in PR review: 1. **Race condition in dashboard initialization** - Moved bd.program initialization before goroutine start in Start() - Added comment explaining the fix - Prevents race between UpdateService calls and program initialization 2. **Fixed incorrect regex logic in cleanLogLine()** - Changed from treating FindStringSubmatch result as error to proper slice handling - Now correctly checks: matches != nil && len(matches) > 1 - Returns matches[1] instead of attempting to use slice as error 3. **Fixed race condition on service status reads** - Added mutex locking when reading svc.Status in status counting loop - Prevents concurrent read/write on Status field 4. **Optimized regex compilation** - Moved regex compilation to package level (streamRegex variable) - Compiles once at initialization instead of on every cleanLogLine() call - Significant performance improvement for log processing 5. **Extracted magic numbers to constants** - Defined DashboardErrorDisplayDuration (1s) and DashboardSuccessDisplayDuration (2s) - Updated deploy.go and redeploy.go to use these constants - Improved code maintainability 6. **Division by zero protection** - Already protected: checks svc.TotalSteps > 0 before division - No changes needed, verified existing safeguard 7. **Removed unreachable code** - Cleaned up unused variable declaration in cleanLogLine() - Simplified logic flow All changes maintain backward compatibility and improve thread safety, performance, and code maintainability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cmd/deploy/deploy.go
Outdated
| // Create build logger (use dashboard if interactive terminal with multiple services) | ||
| var logger build.BuildLogger = build.NewStyledBuildLogger("BUILD") | ||
| var dashboard *build.BuildDashboard | ||
|
|
||
| if term.IsTerminal(int(os.Stdout.Fd())) && len(servicesWithBuild) > 1 { | ||
| // Extract service names for dashboard | ||
| serviceNames := make([]string, len(servicesWithBuild)) | ||
| for i, svc := range servicesWithBuild { | ||
| serviceNames[i] = svc.ServiceName | ||
| } | ||
|
|
||
| // Create and start passive TUI dashboard | ||
| dashboard = build.NewBuildDashboard(serviceNames) | ||
| logger = build.NewDashboardBuildLogger(dashboard) | ||
| dashboard.Start() | ||
| } | ||
|
|
||
| // Create build orchestrator | ||
| logger := build.NewStyledBuildLogger("BUILD") | ||
| orchestrator := build.NewBuildOrchestrator(client, buildConfig, cfg.EnvironmentID, cfg.StackName, logger) | ||
|
|
||
| // Build services | ||
| imageTags, err := orchestrator.BuildServices(servicesWithBuild) | ||
| if err != nil { | ||
| // Stop dashboard before returning error | ||
| if dashboard != nil { | ||
| time.Sleep(build.DashboardErrorDisplayDuration) | ||
| dashboard.Stop() | ||
| } | ||
| return fmt.Errorf("build failed: %w", err) | ||
| } | ||
|
|
||
| // Keep dashboard visible for a moment before stopping | ||
| if dashboard != nil { | ||
| time.Sleep(build.DashboardSuccessDisplayDuration) | ||
| dashboard.Stop() | ||
| } |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dashboard initialization and lifecycle management logic (lines 104-139) is duplicated between cmd/deploy/deploy.go and cmd/redeploy/redeploy.go. Consider extracting this logic into a shared helper function in the internal/build package to reduce code duplication and improve maintainability. For example:
// In internal/build/dashboard.go
func CreateDashboardLogger(servicesWithBuild []compose.ServiceBuildInfo) (BuildLogger, *BuildDashboard) {
if term.IsTerminal(int(os.Stdout.Fd())) && len(servicesWithBuild) > 1 {
serviceNames := make([]string, len(servicesWithBuild))
for i, svc := range servicesWithBuild {
serviceNames[i] = svc.ServiceName
}
dashboard := NewBuildDashboard(serviceNames)
logger := NewDashboardBuildLogger(dashboard)
dashboard.Start()
return logger, dashboard
}
return NewStyledBuildLogger("BUILD"), nil
}
internal/build/dashboard.go
Outdated
| // Define styles | ||
| borderStyle := lipgloss.NewStyle(). | ||
| Border(lipgloss.RoundedBorder()). | ||
| BorderForeground(lipgloss.Color("238")). | ||
| Padding(0, 1) | ||
|
|
||
| headerStyle := lipgloss.NewStyle(). | ||
| Bold(true). | ||
| Foreground(lipgloss.Color("13")) | ||
|
|
||
| dimStyle := lipgloss.NewStyle(). | ||
| Foreground(lipgloss.Color("8")) |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style objects are being created on every render call (lines 222-232, 269-271, 303-305, 318-320), which is inefficient. Consider defining these styles as fields in the dashboardModel struct and initializing them once in NewBuildDashboard, similar to how it's done in StyledBuildLogger (see internal/build/logger.go lines 14-35). This will improve performance during frequent redraws.
Implemented performance optimizations and code quality improvements based on code review feedback. Performance Improvements: 1. **Pre-computed Lipgloss styles** - Added style fields to dashboardModel struct - Styles now computed once during initialization instead of on every render - Eliminates repeated style object creation (borderStyle, headerStyle, dimStyle, etc.) - Significant performance gain for high-frequency renders during active builds Code Quality Improvements: 2. **Extracted dashboard initialization logic** - Created SetupBuildLogger() helper function in internal/build/dashboard.go - Consolidates duplicate code from deploy.go and redeploy.go - Added DashboardSetup struct to encapsulate logger and optional dashboard - StopDashboard() method handles cleanup with configurable duration - Cleaner, more maintainable command implementations 3. **Removed unused imports** - Removed "time" import from deploy.go and redeploy.go - No longer needed after refactoring to use DashboardSetup Benefits: - Reduced memory allocations during render cycles - Better code reusability and maintainability - Cleaner command implementations with less boilerplate - Easier to extend dashboard functionality in the future All changes maintain backward compatibility and improve overall code quality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
internal/build/dashboard.go
Outdated
| Foreground(svc.Status.Color()). | ||
| Bold(true) | ||
|
|
||
| line := fmt.Sprintf("%s %-20s ", statusStyle.Render(svc.Status.String()), svc.Name) |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Service names longer than 20 characters will cause display misalignment. Consider truncating long service names or making the width dynamic based on the longest service name:
// In NewBuildDashboard, calculate max name length:
maxNameLen := 20
for _, name := range services {
if len(name) > maxNameLen {
maxNameLen = len(name)
}
}
// Store maxNameLen in the model and use it in formatting| func (bd *BuildDashboard) UpdateService(serviceName string, status BuildStatus, currentStep, totalSteps int, logLine string, err error) { | ||
| if bd.program != nil { | ||
| bd.program.Send(UpdateMsg{ | ||
| ServiceName: serviceName, | ||
| Status: status, | ||
| Step: currentStep, | ||
| Total: totalSteps, | ||
| LogLine: logLine, | ||
| Error: err, | ||
| }) | ||
| } | ||
| } |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential race condition: The UpdateService and Stop methods both access bd.program without synchronization. If Stop() is called concurrently with UpdateService(), there's a race on the nil check and the method call. Consider adding a mutex to protect bd.program access, or use atomic operations.
Example:
type BuildDashboard struct {
program *tea.Program
model *dashboardModel
serviceOrder []string
mu sync.RWMutex // Add mutex
}
func (bd *BuildDashboard) UpdateService(...) {
bd.mu.RLock()
defer bd.mu.RUnlock()
if bd.program != nil {
bd.program.Send(...)
}
}
func (bd *BuildDashboard) Stop() {
bd.mu.Lock()
defer bd.mu.Unlock()
if bd.program != nil {
bd.program.Quit()
}
}
internal/build/dashboard.go
Outdated
| if msg.LogLine != "" { | ||
| // Keep only last 3 log lines per service | ||
| svc.Logs = append(svc.Logs, msg.LogLine) | ||
| if len(svc.Logs) > 3 { |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Consider extracting the magic number 3 to a named constant for better maintainability:
const maxLogLinesPerService = 3This would make it easier to adjust this limit in the future and improves code clarity.
internal/build/dashboard.go
Outdated
|
|
||
| go func() { | ||
| if _, err := bd.program.Run(); err != nil { | ||
| fmt.Printf("Error running dashboard: %v\n", err) |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dashboard errors should be written to stderr instead of stdout using fmt.Fprintf(os.Stderr, ...) to avoid mixing error messages with normal program output. This is especially important in CI/CD pipelines where stdout and stderr are often processed separately.
Addressed final code review feedback to ensure thread safety and better error handling in the dashboard implementation. Thread Safety Improvements: 1. **Added mutex protection for bd.program access** - Added sync.RWMutex to BuildDashboard struct - Protected all bd.program access in Start(), UpdateService(), and Stop() - Prevents race conditions between concurrent calls to these methods - Used RLock for read operations and Lock for write operations Error Handling: 2. **Dashboard errors now written to stderr** - Changed fmt.Printf to fmt.Fprintf(os.Stderr, ...) in Start() - Dashboard errors no longer pollute stdout - Better separation of concerns for error reporting - Added os import for os.Stderr Code Quality: 3. **Extracted remaining magic numbers to constants** - Added MaxLogLinesPerService constant (was hardcoded 3) - Added ServiceNameColumnWidth constant (was hardcoded 20) - Improved maintainability and documentation of these values - Used fmt dynamic width formatting (%-*s) for service name column Benefits: - Eliminates all race conditions in dashboard operations - Better error reporting (errors to stderr, not stdout) - More maintainable code with named constants - Safer concurrent access patterns throughout All thread safety issues have been resolved and the dashboard is now production-ready with proper synchronization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
No description provided.