-
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
Open
deviantony
wants to merge
6
commits into
main
Choose a base branch
from
claude/interactive-remote-builds-01Qm6kRQQ5xC2kP1P5x67PoM
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f7efbd3
Add interactive build capabilities with progress tracking and TUI das…
claude 1dada8e
Refactor dashboard to be fully passive with auto-scrolling logs
claude eedf0dc
Integrate passive TUI dashboard directly into deploy and redeploy com…
claude 5577597
Address code review feedback for interactive build dashboard
claude 389e441
Optimize dashboard performance and reduce code duplication
claude f202920
Fix remaining race conditions and improve error handling
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| # Interactive Remote Builds - Implementation Guide | ||
|
|
||
| ## Current State ✅ | ||
|
|
||
| Your codebase **already has** the foundation for interactive builds: | ||
|
|
||
| - ✅ **Real-time streaming**: `internal/portainer/client.go:360-366` streams JSON lines from Docker API | ||
| - ✅ **Log parsing**: `internal/build/logger.go` extracts build steps, errors, and progress | ||
| - ✅ **Parallel execution**: `internal/build/orchestrator.go` builds multiple services concurrently | ||
| - ✅ **TUI libraries**: Already using `charmbracelet/bubbletea`, `bubbles`, and `lipgloss` | ||
|
|
||
| ## What's New 🎯 | ||
|
|
||
| - ✅ **Passive TUI Dashboard** - Real-time build monitoring with progress bars and live logs | ||
| - ✅ **Auto-updating display** - No interaction required, just watch builds progress | ||
| - ✅ **Multi-service tracking** - See all builds at once with individual progress | ||
| - ✅ **Build timing metrics** - Duration tracking per service | ||
|
|
||
| ## Recommended: Passive TUI Dashboard ⭐ | ||
|
|
||
| **What you get:** | ||
| ``` | ||
| ╭─────────────────────────────────────────────────────────────────╮ | ||
| │ Building Services │ | ||
| │ Complete: 2 | Building: 1 | Failed: 0 | Total: 5 │ | ||
| │ │ | ||
| │ ✓ frontend ████████████████████ 100% (45s) │ | ||
| │ │ | ||
| │ ● backend ████████░░░░░░░░░░░░ 60% (12s) │ | ||
| │ Step 6/10 : RUN npm install │ | ||
| │ ---> Running in a1b2c3d4e5f6 │ | ||
| │ npm WARN deprecated [email protected] │ | ||
| │ │ | ||
| │ ⏳ database ░░░░░░░░░░░░░░░░░░░░ 0% (queued) │ | ||
| │ │ | ||
| │ ✓ nginx ████████████████████ 100% (8s) │ | ||
| │ │ | ||
| │ ● worker ██████░░░░░░░░░░░░░░ 40% (22s) │ | ||
| │ Step 4/10 : COPY . . │ | ||
| │ ---> c2d3e4f5a6b7 │ | ||
| │ │ | ||
| │ Press q or Ctrl+C to quit │ | ||
| ╰─────────────────────────────────────────────────────────────────╯ | ||
| ``` | ||
|
|
||
| **Features:** | ||
| - 🎯 **Fully passive** - Just displays information, no interaction needed | ||
| - 📊 **Progress bars** - Visual progress for each service | ||
| - 📜 **Auto-scrolling logs** - Last 3 lines shown per building service | ||
| - ⏱️ **Build timers** - Real-time duration tracking | ||
| - 🎨 **Color-coded status** - Queued (gray), Building (blue), Complete (green), Failed (red) | ||
| - 🔄 **Real-time updates** - Dashboard refreshes automatically as builds progress | ||
|
|
||
| **Already implemented in:** | ||
| - `internal/build/dashboard.go` - Complete TUI dashboard | ||
| - `internal/build/dashboard_integration_example.go` - Integration guide | ||
|
|
||
| --- | ||
|
|
||
| ### Alternative: Simple Progress Bars | ||
|
|
||
| **What you get:** | ||
| ``` | ||
| BUILD frontend ████████████░░░░░░░░ [6/10] (23s) | ||
| BUILD backend ██████░░░░░░░░░░░░░░ [2/8] (5s) | ||
| ``` | ||
|
|
||
| **Changes needed:** | ||
| 1. Add `UpdateProgress(serviceName, current, total)` to `BuildLogger` interface | ||
| 2. Implement progress tracking in `StyledBuildLogger` using `bubbles/progress` | ||
| 3. Extract "Step X/Y" from Docker output in `buildRemote()` callback | ||
|
|
||
| **See:** `example_simple_progress.go` for exact code | ||
|
|
||
| --- | ||
|
|
||
| ### Option C: Enhanced Features (⏱️ Ongoing) | ||
|
|
||
| Additional capabilities to add later: | ||
|
|
||
| 1. **Cancellation Support** | ||
| - Gracefully stop builds with Ctrl+C | ||
| - Add context cancellation to `BuildImage()` | ||
|
|
||
| 2. **Log Export** | ||
| - Save per-service build logs to files | ||
| - Format: JSON or plain text | ||
|
|
||
| 3. **Build Analytics** | ||
| - Cache hit/miss statistics | ||
| - Build speed tracking over time | ||
| - Slowest build step identification | ||
|
|
||
| 4. **Resource Monitoring** | ||
| - Real-time CPU/memory usage during builds | ||
| - Network I/O for image pulls | ||
| - Requires Docker stats API integration | ||
|
|
||
| ## Implementation Guide | ||
|
|
||
| ### Using the Passive TUI Dashboard | ||
|
|
||
| **The dashboard is already fully implemented!** You just need to integrate it into your build commands. | ||
|
|
||
| **Step 1: Add terminal detection dependency** | ||
| ```bash | ||
| go get golang.org/x/term | ||
| ``` | ||
|
|
||
| **Step 2: Integrate into `cmd/deploy/deploy.go`** (or `cmd/redeploy/redeploy.go`) | ||
|
|
||
| See complete example in `internal/build/dashboard_integration_example.go`. | ||
|
|
||
| **Minimal integration:** | ||
| ```go | ||
| import ( | ||
| "os" | ||
| "time" | ||
| "golang.org/x/term" | ||
| "github.com/deviantony/pctl/internal/build" | ||
| ) | ||
|
|
||
| // Before calling BuildServices: | ||
| var logger build.BuildLogger = build.NewStyledBuildLogger("BUILD") | ||
| var dashboard *build.BuildDashboard | ||
|
|
||
| // Check if terminal supports TUI and multiple services | ||
| 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 = build.NewBuildDashboard(serviceNames) | ||
| logger = build.NewDashboardBuildLogger(dashboard) | ||
| dashboard.Start() | ||
| } | ||
|
|
||
| // Create orchestrator with the logger | ||
| orchestrator := build.NewBuildOrchestrator(client, buildConfig, envID, stackName, logger) | ||
|
|
||
| // Build services | ||
| imageTags, err := orchestrator.BuildServices(servicesWithBuild) | ||
|
|
||
| // Stop dashboard after builds complete | ||
| if dashboard != nil { | ||
| time.Sleep(2 * time.Second) // Keep visible for a moment | ||
| dashboard.Stop() | ||
| } | ||
| ``` | ||
|
|
||
| **Step 3: Test both modes** | ||
| ```bash | ||
| # Interactive mode (shows TUI dashboard) | ||
| pctl deploy --stack my-stack --environment 1 | ||
|
|
||
| # Non-interactive mode (regular logs) | ||
| pctl deploy --stack my-stack --environment 1 > log.txt | ||
| ``` | ||
|
|
||
| That's it! The dashboard will: | ||
| - ✅ Auto-detect if running in a terminal | ||
| - ✅ Fall back to regular logs if not interactive | ||
| - ✅ Update in real-time as builds progress | ||
| - ✅ Show progress bars, logs, and timings | ||
| - ✅ Work with parallel builds out of the box | ||
|
|
||
| ### Optional Enhancements | ||
|
|
||
| Add features based on user feedback: | ||
| - Cancellation support (high priority) | ||
| - Log export to files (useful for debugging) | ||
| - Build analytics and cache statistics (nice to have) | ||
|
|
||
| ## Docker JSON Stream Format | ||
|
|
||
| Understanding what Docker sends helps with parsing: | ||
|
|
||
| ```json | ||
| {"stream":"Step 1/8 : FROM node:18-alpine\n"} | ||
| {"stream":" ---> 7d5b57e3d3e5\n"} | ||
| {"stream":"Step 2/8 : WORKDIR /app\n"} | ||
| {"stream":" ---> Running in a1b2c3d4e5f6\n"} | ||
| {"stream":"Removing intermediate container a1b2c3d4e5f6\n"} | ||
| {"stream":" ---> c2d3e4f5a6b7\n"} | ||
| ... | ||
| {"aux":{"ID":"sha256:abc123..."}} | ||
| {"stream":"Successfully built abc123...\n"} | ||
| {"stream":"Successfully tagged myapp:latest\n"} | ||
| ``` | ||
|
|
||
| **Error format:** | ||
| ```json | ||
| {"error":"build failed","errorDetail":{"message":"RUN failed with exit code 1"}} | ||
| ``` | ||
|
|
||
| **Current parsing:** `internal/build/logger.go:99-133` | ||
|
|
||
| ## Testing Checklist | ||
|
|
||
| - [ ] Single service build shows progress | ||
| - [ ] Multiple services build in parallel with separate progress bars | ||
| - [ ] Progress resets correctly between builds | ||
| - [ ] Works in non-interactive mode (e.g., CI/CD pipelines) | ||
| - [ ] Handles build failures gracefully | ||
| - [ ] Ctrl+C cancels cleanly | ||
| - [ ] Terminal resize handled (for dashboard mode) | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| ### Progress bar not updating | ||
| - Check that regex matches Docker output: `Step (\d+)/(\d+)` | ||
| - Verify callback is being called: add debug print | ||
|
|
||
| ### Dashboard crashes on resize | ||
| - Ensure `tea.WindowSizeMsg` handler updates viewport dimensions | ||
|
|
||
| ### Works locally but not in CI | ||
| - Add terminal detection: `isatty.IsTerminal(os.Stdout.Fd())` | ||
| - Fall back to simple logger when not a TTY | ||
|
|
||
| ## Additional Resources | ||
|
|
||
| - **Charmbracelet Bubbles**: https://github.com/charmbracelet/bubbles | ||
| - **Bubbletea Tutorial**: https://github.com/charmbracelet/bubbletea/tree/master/tutorials | ||
| - **Docker Build API**: https://docs.docker.com/engine/api/v1.43/#tag/Image/operation/ImageBuild | ||
| - **Docker JSON Stream**: Newline-delimited JSON (NDJSON) | ||
|
|
||
| ## Quick Reference | ||
|
|
||
| | Feature | Location | Lines | Purpose | | ||
| |---------|----------|-------|---------| | ||
| | Streaming | `internal/portainer/client.go` | 360-366 | Reads Docker JSON lines | | ||
| | Log parsing | `internal/build/logger.go` | 89-134 | Cleans and styles output | | ||
| | Build callback | `internal/build/orchestrator.go` | 214-216 | Receives each log line | | ||
| | Parallel builds | `internal/build/orchestrator.go` | 66-88 | Semaphore + goroutines | | ||
| | Dashboard UI | `internal/build/dashboard.go` | - | Interactive TUI (created) | | ||
|
|
||
| ## Next Steps | ||
|
|
||
| 1. **Try the simple progress bar implementation first** (`example_simple_progress.go`) | ||
| 2. Test with a multi-service stack | ||
| 3. Gather feedback from users | ||
| 4. Add dashboard if needed for complex builds | ||
| 5. Iterate based on usage patterns | ||
|
|
||
| Good luck! 🚀 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,93 @@ | ||||||||||||
| package main | ||||||||||||
|
|
||||||||||||
| // EXAMPLE: Minimal code to add progress bars to builds | ||||||||||||
| // This shows the 3 simple changes needed | ||||||||||||
|
|
||||||||||||
| // STEP 1: Add to internal/build/logger.go (line 23) | ||||||||||||
| /* | ||||||||||||
| type StyledBuildLogger struct { | ||||||||||||
| prefix string | ||||||||||||
| mu sync.Mutex | ||||||||||||
| // ... existing styles ... | ||||||||||||
|
|
||||||||||||
| // NEW: Add progress tracking | ||||||||||||
| progressBars map[string]progress.Model | ||||||||||||
| progressMu sync.Mutex | ||||||||||||
| } | ||||||||||||
| */ | ||||||||||||
|
|
||||||||||||
| // STEP 2: Add method to internal/build/logger.go (after line 86) | ||||||||||||
| /* | ||||||||||||
| import "github.com/charmbracelet/bubbles/progress" | ||||||||||||
|
|
||||||||||||
| func (l *StyledBuildLogger) UpdateProgress(serviceName string, current, total int) { | ||||||||||||
| l.progressMu.Lock() | ||||||||||||
| defer l.progressMu.Unlock() | ||||||||||||
|
|
||||||||||||
| if l.progressBars == nil { | ||||||||||||
| l.progressBars = make(map[string]progress.Model) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| if _, exists := l.progressBars[serviceName]; !exists { | ||||||||||||
| l.progressBars[serviceName] = progress.New(progress.WithDefaultGradient()) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
|
||||||||||||
| if total == 0 { | |
| return | |
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 isgolang.org/x/termwithterm.IsTerminal(int(os.Stdout.Fd())). Update this to match the actual implementation used in the dashboard integration example.