Skip to content

Conversation

@sawka
Copy link
Member

@sawka sawka commented Nov 11, 2025

No description provided.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 11, 2025

Walkthrough

The pull request adds line-oriented streaming utilities and integrates them into build output capture. A new LineWriter (io.Writer) is added in both pkg/util/utilfn and tsunami/util to buffer bytes and invoke a per-line callback, with Flush support and a 128 KiB max-line behavior. tsunami/util also receives StreamToLines, StreamToLinesChan, LineOutput, and ReadLineWithTimeout. tsunami/build/build.go is updated to embed and use LineWriter inside OutputCapture so build/go mod subprocess stdout/stderr can be captured per-line. A golangci.yml lint exclusion change is included.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Verify line buffering, partial-line handling, and max-line (128 KiB) enforcement across both LineWriter implementations.
  • Check Flush semantics and that no trailing newlines are emitted.
  • Inspect OutputCapture changes: Write, Flush, Printf routing, nil-receiver behavior, and mutex-protected line append callback.
  • Review StreamToLines/StreamToLinesChan error propagation (non-EOF vs EOF) and ReadLineWithTimeout timeout semantics.
  • Confirm why LineWriter is implemented in two packages (consistency or intended duplication) and reconcile API/behavior differences.
  • Review .golangci.yml changes for correct linter exclusions and any unintended rule removals.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive No pull request description was provided by the author, so it cannot be evaluated against the changeset. Add a description explaining the purpose and context of these changes, such as why capturing build and mod tidy output to OutputCapture is beneficial.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: capturing go build and go mod tidy output to OutputCapture, which is the primary focus of the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sawka/capture-more-builder-output

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f36b1c and f7196ef.

📒 Files selected for processing (3)
  • pkg/util/utilfn/streamtolines.go (1 hunks)
  • tsunami/build/build.go (3 hunks)
  • tsunami/util/streamtolines.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tsunami/build/build.go (1)
tsunami/util/streamtolines.go (2)
  • LineWriter (89-92)
  • NewLineWriter (95-99)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)

Comment on lines +102 to +113
func (lw *LineWriter) Write(p []byte) (n int, err error) {
streamToLines_processBuf(&lw.lineBuf, p, lw.lineFn)
return len(p), nil
}

// Flush outputs any remaining buffered data as a final line.
// Should be called when the input stream is complete (e.g., at EOF).
func (lw *LineWriter) Flush() {
if len(lw.lineBuf.buf) > 0 && !lw.lineBuf.inLongLine {
lw.lineFn(lw.lineBuf.buf)
lw.lineBuf.buf = nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard utilfn.LineWriter with a mutex.

This copy of LineWriter is hit by the same concurrent Stdout/Stderr writes. Without locking, shared lineBuf is racy and corrupts captured lines. Please add the same synchronization as suggested in the util package version so both stay in sync.

Apply the identical mutex changes shown on the util version (import sync, add a mutex field, and lock in Write/Flush).

🤖 Prompt for AI Agents
In pkg/util/utilfn/streamtolines.go around lines 102 to 113, the LineWriter is
not concurrency-safe: add synchronization matching the util package variant by
importing "sync", adding a mutex field (e.g., mu sync.Mutex) to the LineWriter
struct, and surrounding the body of Write and Flush with mu.Lock()/defer
mu.Unlock() so accesses to lw.lineBuf are serialized; ensure the Mutex is used
in both methods and no other behavior changes are introduced.

Comment on lines 50 to 62
func (oc *OutputCapture) Write(p []byte) (n int, err error) {
if oc == nil {
return os.Stdout.Write(p)
}
return oc.lineWriter.Write(p)
}

func (oc *OutputCapture) Flush() {
if oc == nil || oc.lineWriter == nil {
return
}
oc.lineWriter.Flush()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Preserve zero-value usability of OutputCapture.

OutputCapture is exported and previously worked when callers used a zero value (&build.OutputCapture{} or an embedded struct). After this change, lineWriter stays nil and Write now panics when the build code routes stdout/stderr through it. Please keep zero-value usage safe—e.g., lazily initialize lineWriter inside Write/Flush (with the same callback) or move the initialization into a private helper invoked whenever needed.

One option:

 func (oc *OutputCapture) Write(p []byte) (n int, err error) {
 	if oc == nil {
 		return os.Stdout.Write(p)
 	}
+	oc.ensureLineWriter()
 	return oc.lineWriter.Write(p)
 }

 func (oc *OutputCapture) Flush() {
-	if oc == nil || oc.lineWriter == nil {
+	if oc == nil {
 		return
 	}
+	oc.ensureLineWriter()
 	oc.lineWriter.Flush()
 }
+
+func (oc *OutputCapture) ensureLineWriter() {
+	if oc.lineWriter != nil {
+		return
+	}
+	if oc.lines == nil {
+		oc.lines = make([]string, 0)
+	}
+	oc.lineWriter = util.NewLineWriter(func(line []byte) {
+		oc.lock.Lock()
+		defer oc.lock.Unlock()
+		oc.lines = append(oc.lines, string(line))
+	})
+}

Make sure to set up the callback exactly once so existing constructors remain compatible.

Comment on lines +102 to +113
func (lw *LineWriter) Write(p []byte) (n int, err error) {
streamToLines_processBuf(&lw.lineBuf, p, lw.lineFn)
return len(p), nil
}

// Flush outputs any remaining buffered data as a final line.
// Should be called when the input stream is complete (e.g., at EOF).
func (lw *LineWriter) Flush() {
if len(lw.lineBuf.buf) > 0 && !lw.lineBuf.inLongLine {
lw.lineFn(lw.lineBuf.buf)
lw.lineBuf.buf = nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

LineWriter must be concurrency-safe.

OutputCapture wires both Stdout and Stderr of exec.Cmd to the same LineWriter. Those pipes are copied in parallel goroutines, so Write and Flush can run concurrently. Because lineBuf is mutated without synchronization, we’ll hit races and corrupt buffered data (lines interleaving or panics). Protect the state with a mutex (and mirror that in Flush) before wiring this through build output.

Apply this diff as a starting point:

@@
-import (
-	"bytes"
-	"context"
-	"io"
-	"time"
-)
+import (
+	"bytes"
+	"context"
+	"io"
+	"sync"
+	"time"
+)
@@
 type LineWriter struct {
+	mu      sync.Mutex
 	lineBuf lineBuf
 	lineFn  func([]byte)
 }
@@
 func (lw *LineWriter) Write(p []byte) (n int, err error) {
+	lw.mu.Lock()
+	defer lw.mu.Unlock()
 	streamToLines_processBuf(&lw.lineBuf, p, lw.lineFn)
 	return len(p), nil
 }
@@
 func (lw *LineWriter) Flush() {
+	lw.mu.Lock()
+	defer lw.mu.Unlock()
 	if len(lw.lineBuf.buf) > 0 && !lw.lineBuf.inLongLine {
 		lw.lineFn(lw.lineBuf.buf)
 		lw.lineBuf.buf = nil
 	}
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In tsunami/util/streamtolines.go around lines 102 to 113, the LineWriter is not
concurrency-safe: its Write and Flush mutate lw.lineBuf concurrently when stdout
and stderr pipes are copied in parallel. Add a sync.Mutex field to LineWriter
and use it to guard all mutations/read of lw.lineBuf in Write and Flush (lock at
start, defer unlock) so buffered data and inLongLine are accessed atomically;
ensure any early returns still unlock via defer and that Flush mirrors the same
locking to prevent races and corruption.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
tsunami/build/build.go (3)

49-56: Critical: Zero-value OutputCapture will panic in Write method.

The previous review comment about preserving zero-value usability has not been addressed. If a caller creates &OutputCapture{} (without using MakeOutputCapture()), lineWriter will be nil, and line 55 will panic when dereferencing it.

Apply lazy initialization as suggested in the previous review:

 func (oc *OutputCapture) Write(p []byte) (n int, err error) {
 	if oc == nil {
 		return os.Stdout.Write(p)
 	}
+	oc.ensureLineWriter()
 	oc.lock.Lock()
 	defer oc.lock.Unlock()
 	return oc.lineWriter.Write(p)
 }

Add the helper method:

func (oc *OutputCapture) ensureLineWriter() {
	if oc.lineWriter != nil {
		return
	}
	if oc.lines == nil {
		oc.lines = make([]string, 0)
	}
	oc.lineWriter = util.NewLineWriter(func(line []byte) {
		oc.lines = append(oc.lines, string(line))
	})
}

58-65: Update Flush to use lazy initialization for consistency.

Once lazy initialization is added to Write, update this method similarly to ensure consistent behavior.

 func (oc *OutputCapture) Flush() {
-	if oc == nil || oc.lineWriter == nil {
+	if oc == nil {
 		return
 	}
+	oc.ensureLineWriter()
 	oc.lock.Lock()
 	defer oc.lock.Unlock()
 	oc.lineWriter.Flush()
 }

346-355: Critical: Incorrect logic causes panic when verbose is true but oc is nil.

The condition at line 346 uses OR (||) instead of AND (&&). When verbose is true but oc is nil:

  • Line 347 panics on oc.Printf
  • Lines 348-349 set stdout/stderr to nil (likely causing silent output loss)
  • Line 355 panics on oc.Flush()

Apply this fix:

-	if oc != nil || verbose {
+	if oc != nil {
 		oc.Printf("Running go mod tidy")
 		tidyCmd.Stdout = oc
 		tidyCmd.Stderr = oc
+	} else if verbose {
+		tidyCmd.Stdout = os.Stdout
+		tidyCmd.Stderr = os.Stderr
 	}
 
 	if err := tidyCmd.Run(); err != nil {
 		return fmt.Errorf("failed to run go mod tidy: %w", err)
 	}
-	oc.Flush()
+	if oc != nil {
+		oc.Flush()
+	}
🧹 Nitpick comments (1)
tsunami/build/build.go (1)

67-76: Simplify nil check in Printf.

The check for lineWriter == nil is unnecessary since Printf doesn't use lineWriter—it appends directly to oc.lines.

-func (oc *OutputCapture) Printf(format string, args ...interface{}) {
-	if oc == nil || oc.lineWriter == nil {
-		log.Printf(format, args...)
-		return
-	}
+func (oc *OutputCapture) Printf(format string, args ...interface{}) {
+	if oc == nil {
+		log.Printf(format, args...)
+		return
+	}
 	line := fmt.Sprintf(format, args...)
 	oc.lock.Lock()
 	defer oc.lock.Unlock()
 	oc.lines = append(oc.lines, line)
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65c661b and 16e0c42.

📒 Files selected for processing (1)
  • tsunami/build/build.go (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tsunami/build/build.go (1)
pkg/util/utilfn/streamtolines.go (2)
  • LineWriter (89-92)
  • NewLineWriter (95-99)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)
  • GitHub Check: Build for TestDriver.ai
🔇 Additional comments (1)
tsunami/build/build.go (1)

38-47: LGTM!

The initialization of lineWriter with the callback is well-designed. The callback appends to oc.lines without explicit locking because it's invoked within the critical section of Write/Flush methods that already hold the lock.

Comment on lines +679 to +688
if oc != nil || opts.Verbose {
oc.Printf("Running: %s", strings.Join(buildCmd.Args, " "))
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
buildCmd.Stdout = oc
buildCmd.Stderr = oc
}

if err := buildCmd.Run(); err != nil {
return fmt.Errorf("failed to build application: %w", err)
}
oc.Flush()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Incorrect logic causes panic when verbose is true but oc is nil.

Same issue as in createGoMod: the OR condition at line 679 causes panics at lines 680 and 688 when verbose is true but oc is nil.

Apply this fix:

-	if oc != nil || opts.Verbose {
+	if oc != nil {
 		oc.Printf("Running: %s", strings.Join(buildCmd.Args, " "))
 		buildCmd.Stdout = oc
 		buildCmd.Stderr = oc
+	} else if opts.Verbose {
+		buildCmd.Stdout = os.Stdout
+		buildCmd.Stderr = os.Stderr
 	}
 
 	if err := buildCmd.Run(); err != nil {
 		return fmt.Errorf("failed to build application: %w", err)
 	}
-	oc.Flush()
+	if oc != nil {
+		oc.Flush()
+	}
🤖 Prompt for AI Agents
In tsunami/build/build.go around lines 679 to 688, the condition uses "oc != nil
|| opts.Verbose" which leads to dereferencing a nil oc when verbose is true;
change the guard so all uses of oc (oc.Printf, buildCmd.Stdout, buildCmd.Stderr,
and oc.Flush) only execute when oc != nil (e.g., use oc != nil && opts.Verbose
for the print if you need verbosity tied to opts, or simply check oc != nil
before assigning Stdout/Stderr and calling Printf), and ensure oc.Flush() is
called only when oc != nil to avoid the panic.

@sawka sawka merged commit 34062ad into main Nov 11, 2025
6 of 7 checks passed
@sawka sawka deleted the sawka/capture-more-builder-output branch November 11, 2025 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants