Skip to content

feat(cli): add progress renderer with phase tracking and animated vis…#2132

Merged
piotrjanik merged 5 commits into
open-component-model:mainfrom
piotrjanik:feat/929-log-prefetch-transfer
Apr 20, 2026
Merged

feat(cli): add progress renderer with phase tracking and animated vis…#2132
piotrjanik merged 5 commits into
open-component-model:mainfrom
piotrjanik:feat/929-log-prefetch-transfer

Conversation

@piotrjanik

@piotrjanik piotrjanik commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

What this PR does / why we need it

Adds a progress rendering system for the transfer component-version command, providing real-time feedback during long-running transfers.

Terminal mode (interactive TTY):

  • Animated spinner for simple phases (spec loading, version resolving)
  • Progress bar with scrolling item log during transfer execution
  • Color-coded status icons and error formatting with spec dumps for debugging

Non-terminal mode (piped output, CI):

  • Structured slog output for each operation phase and item-level events
  • Automatically selected when stderr is not a TTY

Architecture:

  • Tracker[T] orchestrates operation lifecycle and slog interception. slog is redirected to a shared buffer on the first terminal operation and restored on Stop(). Buffered logs are drained before each new operation and after the last one, keeping operation headers clean.
  • Visualizer[T] interface (Begin, HandleEvent, End) with two implementations: bar.NewVisualizer[T] for terminals and SlogVisualizer[T] for non-terminal fallback.
  • WithEvents and WithErrorFormatter options add event tracking and typed error formatting to any operation.
  • Transfer command's progress.go maps graph runtime events to typed Event[*Transformation] with domain-specific error rendering.

Caller API:

tracker := progress.NewTracker(ctx, cmd.ErrOrStderr(), bar.NewVisualizer[*graphPkg.Transformation])
defer tracker.Stop()

op := tracker.StartOperation("Loading transfer spec")
op.Finish(err)

op = tracker.StartOperation("Transferring component versions",
    progress.WithEvents(graph.Events(), mapEvent, graph.NodeCount()),
    progress.WithErrorFormatter(formatError))
graph.Process(ctx)
op.Finish(err)

Which issue(s) this PR fixes

Fixes: #929

@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces the prior event-channel progress model with a phase-based Tracker API, adding Simple and Tracked phase factories, new Basic and Bar visualizers with terminal animations, refactors progress types/interfaces (adds Event.Name, Begin/End), and updates the transfer command to run distinct named phases with conditional terminal wiring.

Changes

Cohort / File(s) Summary
Transfer command
cli/cmd/transfer/component-version/cmd.go, cli/cmd/transfer/component-version/progress.go, cli/cmd/transfer/component-version/progress_test.go
Swap graph-driven tracker for phase-based Tracker; split transfer into named phases (loading spec, resolving, transferring); only wire graph events in terminal mode; tests updated for phase lifecycle and tracker constructor.
Tracker core & phase model
cli/internal/render/progress/tracker.go, cli/internal/render/progress/tracker_test.go, cli/internal/render/progress/noop.go
Replace generic Tracker with non-generic phase-based API, add Phase/PhaseFactory/noopPhase, change Event shape (add Name), inject log buffer at phase start, add NewTracker(ctx,out) and StartPhase/Stop methods; tests adapted.
Phase factories & tests
cli/internal/render/progress/simple.go, cli/internal/render/progress/tracked.go, cli/internal/render/progress/phase_test.go
Add Simple and generic Tracked phase factories managing Begin/HandleEvent/End, implement event mapping and cancellation normalization, and add unit tests for lifecycle and mapping.
Bar visualizer & format/options
cli/internal/render/progress/bar/bar.go, cli/internal/render/progress/bar/options.go, cli/internal/render/progress/bar/format.go, cli/internal/render/progress/bar/bar_test.go
Rework bar visualizer to EventVisualizer with Begin/End, mutexed rendering, animation integration, change options/formatter model (remove generic options), add SidebarText helper, and update extensive tests.
Basic visualizer & simple visualizer removal
cli/internal/render/progress/bar/basic.go, cli/internal/render/progress/simple/simple.go, cli/internal/render/progress/simple/simple_test.go
Add Basic visualizer (animated header, log-buffer support) and Simple phase factory; remove older simple visualizer implementation and its tests.
Animation framework & tests
cli/internal/render/progress/bar/animation.go, cli/internal/render/progress/bar/animation_test.go
Introduce spinner/shimmer/dot animation utilities, ANSI helpers, RunAnimation loop, and unit tests for rendering and lifecycle.
Progress logging internals
cli/internal/render/progress/slog.go, cli/internal/render/progress/slog_test.go
Introduce SyncBuffer (thread-safe buffer), resolveHandlerLevel to preserve logging level, new buffered handler signature; tests added for level resolution and buffered logging.
Tracker visuals & helpers
cli/internal/render/progress/tracked.go, cli/internal/render/progress/phase_test.go, cli/internal/render/progress/phase_test.go
Add tracked-phase run-loop and tests verifying event forwarding, cancellation rewriting, and phase lifecycle behavior.
Misc tests & docs
cli/internal/render/progress/*_test.go, cli/docs/reference/ocm_transfer_component-version.md, kubernetes/.../*_test.go
Add/modify tests to reflect API changes; minor doc whitespace fix and small import reorder in Kubernetes tests.

Sequence Diagram(s)

sequenceDiagram
    participant Cmd as Transfer Command
    participant Tracker as Tracker
    participant Phase as PhaseFactory / Phase
    participant Visualizer as Visualizer / EventVisualizer
    participant Graph as Graph Builder/Processor

    Cmd->>Tracker: NewTracker(ctx, out)
    Cmd->>Tracker: StartPhase("Loading transfer spec", Simple/Tracked)
    Tracker->>Phase: instantiate(factory, tracker, name)
    Phase->>Visualizer: inject log buffer & Begin(name)
    Cmd->>Graph: build graph (dry-run or actual)
    Graph-->>Cmd: graph or error
    alt Graph built
        Cmd->>Phase: (if Tracked) feed graph.Events()
        loop events
            Phase->>Visualizer: HandleEvent(Event{Name, ID, State, Err, Data})
        end
        Cmd->>Phase: Complete()
        Phase->>Visualizer: End(nil)
    else Graph error
        Cmd->>Phase: Fail(err)
        Phase->>Visualizer: End(err)
    end
    Cmd->>Tracker: Stop()
    Tracker->>Tracker: restore slog/default logger
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

kind/chore

Suggested reviewers

  • matthiasbruns
  • fabianburth
  • frewilhelm

Poem

🐰 I hopped through phases, one to three,

Spinners shimmered bright for me.
Names found homes, logs cozy and neat,
The tracker danced with steady beat.
Hooray — progress finished, soft and sweet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(cli): add progress renderer with phase tracking and animated vis…' directly summarizes the main change: a progress rendering system with phase tracking and animation.
Linked Issues check ✅ Passed The PR implements the core requirements from issue #929: real-time visual feedback during transfer with prefetch step logging, animated spinner progress, and domain-aware error formatting for human readability.
Out of Scope Changes check ✅ Passed All changes are scoped to the progress rendering system and transfer command integration; minor import reordering in Kubernetes test files is trivial housekeeping.
Description check ✅ Passed The PR description clearly relates to the changeset, explaining a progress rendering system for the transfer command with architectural details and API usage examples.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions github-actions Bot added kind/feature new feature, enhancement, improvement, extension size/l Large labels Mar 31, 2026
@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch 3 times, most recently from 6e44051 to 000edc3 Compare March 31, 2026 21:57
@piotrjanik piotrjanik marked this pull request as ready for review March 31, 2026 22:26
@piotrjanik piotrjanik requested a review from a team as a code owner March 31, 2026 22:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cli/cmd/transfer/component-version/cmd.go (1)

211-214: ⚠️ Potential issue | 🔴 Critical

The tracked phase can hang forever here.

progress.Tracked drains events until the channel closes, and Phase.Complete() / Fail() wait for that drain in cli/internal/render/progress/phase.go:77-102. Here the builder gets an anonymous make(chan ...), so nothing closes it after graph.Process(ctx) returns; the command can block on both success and failure. Keep a reference to the channel passed into WithEvents(...) and close it when processing ends, or change the phase API to take an explicit done signal.

Also applies to: 251-258

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/cmd/transfer/component-version/cmd.go` around lines 211 - 214, The
anonymous event channel passed into
transfer.NewDefaultBuilder(...).WithEvents(make(chan graphRuntime.ProgressEvent,
eventBufferSize)) can never be closed, causing progress.Tracked to hang; create
a named channel (e.g. eventCh := make(chan graphRuntime.ProgressEvent,
eventBufferSize)), pass eventCh into WithEvents on the builder (used with
transfer.NewDefaultBuilder/...WithEvents(...).BuildAndCheck(tgd)), keep a
reference to that channel and close it when graph processing finishes (after
graph.Process(ctx) returns or in the error/finalization path) so
Phase.Complete()/Fail() and progress.Tracked can drain and exit; apply the same
change to the other occurrence mentioned (lines ~251-258).
🧹 Nitpick comments (5)
cli/cmd/transfer/component-version/progress_test.go (1)

23-33: This test no longer verifies the non-terminal visualizer path.

fmt.Sprintf("%T", tracker) will contain Tracker no matter which visualizer selectVisualizer installs, so a regression from simple to bar still passes here. Assert observable output or expose the configured visualizer through a test helper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/cmd/transfer/component-version/progress_test.go` around lines 23 - 33,
The test currently only checks the tracker type which doesn't verify which
visualizer selectVisualizer installed; update the test to assert the actual
visualizer used by either (a) asserting observable output written to the
captured writer by exercising tracker (e.g., start progress and emit updates,
then check out for expected simple vs bar output patterns) or (b) add a
test-only accessor in the progress package (e.g., Tracker.Visualizer() or a
helper that returns the installed visualizer) and assert its concrete type
returned by selectVisualizer; locate uses of selectVisualizer and
progress.NewTracker to implement the chosen approach and update the assertions
accordingly.
cli/internal/render/progress/bar/animation_test.go (1)

3-9: Fail fast if RunAnimation never ticks.

<-called can block the package until the global go test timeout if the callback is never invoked. Give this test its own timeout so the failure is local and fast.

⏱️ Suggested change
 import (
 	"bytes"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 )
@@
-	<-called // wait for at least one callback
+	select {
+	case <-called:
+	case <-time.After(time.Second):
+		t.Fatal("RunAnimation never invoked onSpin")
+	}
 	close(done)

Also applies to: 69-79

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/bar/animation_test.go` around lines 3 - 9, The
test currently waits indefinitely on the callback channel (called) from
RunAnimation and can hang the whole test suite if the callback never fires;
update the test(s) that read from the called channel (the RunAnimation callback
tests in this file) to use a bounded wait (e.g. select on <-called and
time.After with a short timeout) and call t.Fatalf/t.Error on timeout so the
failure is local and fast; apply the same pattern to the other similar test(s)
in this file that wait on called.
cli/internal/render/progress/tracker.go (2)

116-121: Consider logging buffered content before restoring logger.

When Stop() is called, any content remaining in t.logBuffer is silently discarded. If Stop() is called after a phase ends (normal flow), the buffer should be empty. However, if an unexpected early exit occurs, buffered logs are lost.

Consider draining the buffer to the restored logger before nil-ing previousLogger:

♻️ Optional: drain buffer before restoring
 func (t *Tracker) Stop() {
 	if t.previousLogger != nil {
+		// Drain any remaining buffered logs
+		if t.logBuffer != nil && t.logBuffer.Len() > 0 {
+			_, _ = io.Copy(t.out, t.logBuffer)
+		}
 		slog.SetDefault(t.previousLogger)
 		t.previousLogger = nil
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/tracker.go` around lines 116 - 121, When Stop()
is called on Tracker it currently restores t.previousLogger and discards any
entries left in t.logBuffer; modify Stop to drain/flush t.logBuffer to the
restored logger before clearing t.previousLogger so buffered messages are not
lost on unexpected exits. Specifically, after calling
slog.SetDefault(t.previousLogger) but before setting t.previousLogger = nil,
iterate over or otherwise flush t.logBuffer (using the same log formatting used
by Tracker) and emit each buffered entry to the new default logger; finally
clear the buffer and then nil out t.previousLogger. Ensure you reference
Tracker.Stop, t.logBuffer, t.previousLogger and slog.SetDefault in the change.

92-109: Global slog manipulation requires careful Stop() discipline.

NewTracker unconditionally redirects the global slog.Default() logger. If Stop() is never called (e.g., panic, early return), all subsequent logging in the process goes to the buffer. The context snippet shows production code uses defer tracker.Stop() correctly, but consider:

  1. Defensive nil check: If opts contains a nil function, the loop will panic.
  2. Documentation emphasis: The docstring mentions calling Stop, but a // IMPORTANT callout or returning a cleanup function could reduce misuse risk.

These are minor given the existing defer pattern in cmd.go, but worth noting for maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/tracker.go` around lines 92 - 109, NewTracker
currently unconditionally calls each TrackerOption and swaps the global
slog.Default, which can panic if an option is nil and risks leaving the global
logger redirected if Stop() is not called; update NewTracker to skip nil options
when iterating over opts (check each TrackerOption for nil before calling), and
to reduce misuse return a cleanup function alongside the *Tracker (e.g., return
(*Tracker, func()) that calls t.Stop()) or document an explicit "// IMPORTANT:
call Stop() or use the returned cleanup" comment near NewTracker; ensure the
implementation still sets t.previousLogger, t.logBuffer and calls
slog.SetDefault(slog.New(...)) and that any injected LogBufferAware visualizer
logic (SetLogBuffer) remains intact.
cli/internal/render/progress/bar/bar.go (1)

338-356: renderFinalHeader clears and redraws, but logs/bar rendered without checking total > 0.

In simple mode (total == 0), End() calls clearLines() and writes completion/failure directly (lines 141-154), never reaching renderFinalHeader. However, if someone were to call renderFinalHeader in simple mode (currently doesn't happen), it would call renderLogs() and renderBar() which assume tracked mode.

This is fine given current control flow, but adding a guard would make the code more defensive:

♻️ Optional defensive guard
 func (v *barVisualizer) renderFinalHeader(err error) {
+	if v.total == 0 {
+		return // Simple mode handled separately in End()
+	}
 	hasFailures := false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/bar/bar.go` around lines 338 - 356,
renderFinalHeader should avoid calling tracked-mode-only methods when in simple
mode: check the bar's total (v.total) before calling v.renderLogs() and
v.renderBar(). Inside renderFinalHeader (around the clearLines() call), add a
guard like "if v.total > 0 { v.renderLogs(); v.renderBar() }" so renderLogs and
renderBar are only invoked in tracked mode; keep the existing
WriteFailedLine/WriteCompletedLine behavior for simple mode. Ensure you
reference the v.total field and the methods renderLogs and renderBar when making
the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/cmd/transfer/component-version/cmd.go`:
- Around line 254-256: The current branch calls graph.Process(ctx) and passes
err into phase.Fail(err) but then returns a fixed error string from the
function, discarding the original error; change the return so the original error
is preserved by wrapping or returning it (e.g., replace the plain
fmt.Errorf("graph execution failed") with a wrapped error including err using %w
or return err directly) so callers receive the original failure (reference
symbols: graph.Process, phase.Fail).

In `@cli/cmd/transfer/component-version/progress.go`:
- Around line 67-70: The current formatTransformationName(t
*graphPkg.Transformation) always returns "ID [Type]" and ignores t.Spec; update
it to parse the unstructured spec map for keys "component", "version",
"resourceIdentity", and "resource" (in that priority) to build a human-readable
name like "component@version (resourceIdentity)" or "component (resource)" as
available, only falling back to t.ID and preserving the type suffix t.Type.Name
when spec fields are missing; implement this logic inside
formatTransformationName so tests like TestMapEvent_NameIsFormatted and
TestFormatTransformationName pass.

In `@cli/internal/render/progress/bar/bar.go`:
- Around line 199-210: The header rendering manually increments the spinner
frame (v.spinnerFrame++) inside barVisualizer.renderHeader causing the spinner
to advance twice when RunAnimation already updates frame counters via the
animation callback; remove the redundant v.spinnerFrame++ from renderHeader so
frame advancement is only handled by the animation callback path (keep the
WriteRunningLine call and rely on the spinFrame passed into
renderLocked/RunAnimation).
- Around line 128-169: The tracked-mode End path prints an "Errors:\n" header
when err != nil but doesn't emit the phase-level error details; update the End
method (in barVisualizer.End) after the events loop where it currently does if
err != nil { fmt.Fprint(v.out, "\nErrors:\n") } to also print the actual phase
error: if v.errorFormatter != nil use formatted := v.errorFormatter(err) and
write it (if non-empty), otherwise write the error value (e.g.,
fmt.Fprintln(v.out, err)), ensuring the output is emitted when no individual
event is Failed; keep the existing early-return behavior that calls
renderFailureSummary() when an event failed.

In `@cli/internal/render/progress/bar/format.go`:
- Around line 96-102: The doc comment for SidebarText shows the heavy vertical
bar '┃' but the implementation uses the light vertical bar '│' in SidebarText
rendering; update the comment example to use '│' (U+2502) so the example matches
the actual output produced by the SidebarText function (and any constants or
inline characters at the render sites that currently emit '│').

---

Outside diff comments:
In `@cli/cmd/transfer/component-version/cmd.go`:
- Around line 211-214: The anonymous event channel passed into
transfer.NewDefaultBuilder(...).WithEvents(make(chan graphRuntime.ProgressEvent,
eventBufferSize)) can never be closed, causing progress.Tracked to hang; create
a named channel (e.g. eventCh := make(chan graphRuntime.ProgressEvent,
eventBufferSize)), pass eventCh into WithEvents on the builder (used with
transfer.NewDefaultBuilder/...WithEvents(...).BuildAndCheck(tgd)), keep a
reference to that channel and close it when graph processing finishes (after
graph.Process(ctx) returns or in the error/finalization path) so
Phase.Complete()/Fail() and progress.Tracked can drain and exit; apply the same
change to the other occurrence mentioned (lines ~251-258).

---

Nitpick comments:
In `@cli/cmd/transfer/component-version/progress_test.go`:
- Around line 23-33: The test currently only checks the tracker type which
doesn't verify which visualizer selectVisualizer installed; update the test to
assert the actual visualizer used by either (a) asserting observable output
written to the captured writer by exercising tracker (e.g., start progress and
emit updates, then check out for expected simple vs bar output patterns) or (b)
add a test-only accessor in the progress package (e.g., Tracker.Visualizer() or
a helper that returns the installed visualizer) and assert its concrete type
returned by selectVisualizer; locate uses of selectVisualizer and
progress.NewTracker to implement the chosen approach and update the assertions
accordingly.

In `@cli/internal/render/progress/bar/animation_test.go`:
- Around line 3-9: The test currently waits indefinitely on the callback channel
(called) from RunAnimation and can hang the whole test suite if the callback
never fires; update the test(s) that read from the called channel (the
RunAnimation callback tests in this file) to use a bounded wait (e.g. select on
<-called and time.After with a short timeout) and call t.Fatalf/t.Error on
timeout so the failure is local and fast; apply the same pattern to the other
similar test(s) in this file that wait on called.

In `@cli/internal/render/progress/bar/bar.go`:
- Around line 338-356: renderFinalHeader should avoid calling tracked-mode-only
methods when in simple mode: check the bar's total (v.total) before calling
v.renderLogs() and v.renderBar(). Inside renderFinalHeader (around the
clearLines() call), add a guard like "if v.total > 0 { v.renderLogs();
v.renderBar() }" so renderLogs and renderBar are only invoked in tracked mode;
keep the existing WriteFailedLine/WriteCompletedLine behavior for simple mode.
Ensure you reference the v.total field and the methods renderLogs and renderBar
when making the change.

In `@cli/internal/render/progress/tracker.go`:
- Around line 116-121: When Stop() is called on Tracker it currently restores
t.previousLogger and discards any entries left in t.logBuffer; modify Stop to
drain/flush t.logBuffer to the restored logger before clearing t.previousLogger
so buffered messages are not lost on unexpected exits. Specifically, after
calling slog.SetDefault(t.previousLogger) but before setting t.previousLogger =
nil, iterate over or otherwise flush t.logBuffer (using the same log formatting
used by Tracker) and emit each buffered entry to the new default logger; finally
clear the buffer and then nil out t.previousLogger. Ensure you reference
Tracker.Stop, t.logBuffer, t.previousLogger and slog.SetDefault in the change.
- Around line 92-109: NewTracker currently unconditionally calls each
TrackerOption and swaps the global slog.Default, which can panic if an option is
nil and risks leaving the global logger redirected if Stop() is not called;
update NewTracker to skip nil options when iterating over opts (check each
TrackerOption for nil before calling), and to reduce misuse return a cleanup
function alongside the *Tracker (e.g., return (*Tracker, func()) that calls
t.Stop()) or document an explicit "// IMPORTANT: call Stop() or use the returned
cleanup" comment near NewTracker; ensure the implementation still sets
t.previousLogger, t.logBuffer and calls slog.SetDefault(slog.New(...)) and that
any injected LogBufferAware visualizer logic (SetLogBuffer) remains intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 787728c4-8242-4dd3-a112-7dd167076ab5

📥 Commits

Reviewing files that changed from the base of the PR and between cf30323 and 000edc3.

📒 Files selected for processing (18)
  • cli/cmd/transfer/component-version/cmd.go
  • cli/cmd/transfer/component-version/progress.go
  • cli/cmd/transfer/component-version/progress_test.go
  • cli/docs/reference/ocm_transfer_component-version.md
  • cli/internal/render/progress/bar/animation.go
  • cli/internal/render/progress/bar/animation_test.go
  • cli/internal/render/progress/bar/bar.go
  • cli/internal/render/progress/bar/bar_test.go
  • cli/internal/render/progress/bar/format.go
  • cli/internal/render/progress/bar/options.go
  • cli/internal/render/progress/phase.go
  • cli/internal/render/progress/phase_test.go
  • cli/internal/render/progress/simple/simple.go
  • cli/internal/render/progress/simple/simple_test.go
  • cli/internal/render/progress/slog.go
  • cli/internal/render/progress/slog_test.go
  • cli/internal/render/progress/tracker.go
  • cli/internal/render/progress/tracker_test.go

Comment thread cli/cmd/transfer/component-version/cmd.go Outdated
Comment thread cli/cmd/transfer/component-version/progress.go Outdated
Comment thread cli/internal/render/progress/bar/bar.go Outdated
Comment thread cli/internal/render/progress/bar/bar.go Outdated
Comment thread cli/internal/render/progress/bar/format.go Outdated
@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch 2 times, most recently from 2564455 to 3833133 Compare March 31, 2026 22:42

@jakobmoellerdev jakobmoellerdev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

didn't check this too much but can we make sure that when were not in interactive mode we still get progress updates? in CI we dont have any reasonable output at all right?

@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch from 3833133 to 2b11ad9 Compare April 1, 2026 11:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
cli/cmd/transfer/component-version/cmd.go (1)

255-257: ⚠️ Potential issue | 🟠 Major

Wrap the graph error instead of discarding it.

Line 257 returns a fixed string, so callers lose the original failure reason after phase.Fail(err) renders it. This flattens cancellations and validation errors into the same generic CLI error.

🧾 Suggested fix
 	if err := graph.Process(ctx); err != nil {
 		phase.Fail(err)
-		return fmt.Errorf("graph execution failed")
+		return fmt.Errorf("graph execution failed: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/cmd/transfer/component-version/cmd.go` around lines 255 - 257, The code
currently calls graph.Process(ctx) and calls phase.Fail(err) but then returns a
generic error string, losing the original error; change the return to wrap the
original error so callers see the real failure (e.g., return an error that
includes or wraps err) while still calling phase.Fail(err). Update the return
that follows graph.Process(ctx) to wrap err (reference graph.Process and
phase.Fail in the same block) so the original error is preserved and propagated.
🧹 Nitpick comments (5)
cli/cmd/transfer/component-version/cmd.go (1)

259-260: Redundant tracker.Stop() call.

tracker.Stop() is already deferred on line 177. While Stop() is idempotent (it nil-checks previousLogger), this explicit call is unnecessary and adds noise. Consider removing it or removing the defer if you prefer explicit control.

♻️ Remove redundant call
 	phase.Complete()
-	tracker.Stop()
 
 	slog.DebugContext(ctx, "transfer completed successfully")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/cmd/transfer/component-version/cmd.go` around lines 259 - 260, The
explicit tracker.Stop() call after phase.Complete() is redundant because
tracker.Stop() is already deferred earlier via defer tracker.Stop(); remove the
explicit tracker.Stop() line to avoid duplicate calls (alternatively, if you
prefer explicit control, remove the defer and keep the explicit tracker.Stop(),
but do not keep both). Ensure you only have one stop invocation for
tracker.Stop() and leave phase.Complete() unchanged.
cli/cmd/transfer/component-version/progress.go (1)

44-46: Silently ignoring JSON marshal error.

If json.MarshalIndent fails (e.g., on cyclic structures or unsupported types), specJSON will be nil and ErrorDetail will contain only the info text without the spec dump. Consider logging the marshal error at debug level for troubleshooting.

♻️ Log marshal failures
-		specJSON, _ := json.MarshalIndent(t.Spec.Data, "", "  ")
+		specJSON, marshalErr := json.MarshalIndent(t.Spec.Data, "", "  ")
+		if marshalErr != nil {
+			slog.Debug("failed to marshal transformation spec", "error", marshalErr, "id", t.ID)
+		}
 		evt.ErrorDetail = bar.FramedText(info, string(specJSON), 4)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/cmd/transfer/component-version/progress.go` around lines 44 - 46, The
code silently ignores errors from json.MarshalIndent when building
evt.ErrorDetail from t.Spec.Data; change the snippet to capture the error (err
:= json.MarshalIndent(...)), and if err != nil log the error at debug level
(including the err and the related t.ID/t.Type.Name/t.Type.Version) and use a
clear fallback string (e.g., a short "<json marshal error: ...>" message) for
specJSON so bar.FramedText(info, specJSONString, 4) still contains useful info;
update references to specJSON, json.MarshalIndent, evt.ErrorDetail,
bar.FramedText, info and t.Spec.Data accordingly.
cli/internal/render/progress/tracked.go (2)

49-53: Document the Start-before-Complete/Fail contract.

Complete() and Fail() block on <-p.finished, which is nil until Start() is called. A receive on a nil channel blocks forever. While the intended usage flow is Start → Complete/Fail, consider adding a brief doc comment or a panic-guard to clarify the contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/tracked.go` around lines 49 - 53, The Start()
method initializes p.finished (a channel) but Complete() and Fail() block on
<-p.finished and will deadlock if they are called before Start(); add a short
doc comment to trackedPhase.Start/Complete/Fail stating the required call order
(Start → Complete/Fail) and add a panic-guard in Complete() and Fail() that
checks if p.finished == nil and panics with a clear message (referencing methods
Start, Complete, Fail and the finished channel) so callers get an immediate,
descriptive failure instead of blocking forever.

26-35: Unused ctx field in trackedPhase.

The ctx field is stored (line 28) but never used in the current implementation. If it's intended for future use (e.g., context cancellation in the event loop), consider adding it; otherwise, remove it to avoid confusion.

♻️ If unused, consider removing
 type trackedPhase[E any] struct {
 	vis            Visualizer
-	ctx            context.Context
 	name           string
 	events         <-chan E
 	mapper         func(E) Event
 	total          int
 	finished       chan struct{}
 	errorFormatter func(error) string
 }

And update the factory:

 	return func(t *Tracker, name string) Phase {
 		t.injectLogBuffer(vis)
 		return &trackedPhase[E]{
 			vis:            vis,
-			ctx:            t.ctx,
 			name:           name,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/tracked.go` around lines 26 - 35, The stored ctx
field on trackedPhase is unused; remove the ctx field from the trackedPhase
struct and update the factory/constructor that creates trackedPhase (the
function that builds instances of trackedPhase, e.g., newTrackedPhase or any
factory you added) to stop accepting/passing a context parameter, and remove any
code that references trackedPhase.ctx. Ensure all call sites that constructed
trackedPhase are updated to the new signature and that no remaining references
to ctx remain; alternatively, if you intended cancellation support, instead wire
that ctx into the event loop inside the trackedPhase's processing function to
handle context.Done() – but do not leave the unused ctx field in the struct.
cli/internal/render/progress/tracker.go (1)

67-84: Global slog state manipulation may affect concurrent tests.

NewTracker replaces the global slog.Default() logger (line 82), which could cause issues if tests run in parallel or if multiple trackers are created. For CLI code with single execution this is fine, but consider documenting this limitation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/tracker.go` around lines 67 - 84, NewTracker
currently mutates global slog state via slog.SetDefault (in NewTracker) which
can break parallel tests; change Tracker to avoid touching global state by
either creating and holding a dedicated logger/handler internally (use
newBufferedHandler with t.logBuffer and keep previousLogger for restoration
semantics) or accept an injected *slog.Logger in NewTracker signature so callers
supply a per-instance logger; remove the slog.SetDefault call from NewTracker
(and update any Stop logic to only affect the logger instance) and document the
new behavior on Tracker/NewTracker to preserve test isolation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@cli/cmd/transfer/component-version/cmd.go`:
- Around line 255-257: The code currently calls graph.Process(ctx) and calls
phase.Fail(err) but then returns a generic error string, losing the original
error; change the return to wrap the original error so callers see the real
failure (e.g., return an error that includes or wraps err) while still calling
phase.Fail(err). Update the return that follows graph.Process(ctx) to wrap err
(reference graph.Process and phase.Fail in the same block) so the original error
is preserved and propagated.

---

Nitpick comments:
In `@cli/cmd/transfer/component-version/cmd.go`:
- Around line 259-260: The explicit tracker.Stop() call after phase.Complete()
is redundant because tracker.Stop() is already deferred earlier via defer
tracker.Stop(); remove the explicit tracker.Stop() line to avoid duplicate calls
(alternatively, if you prefer explicit control, remove the defer and keep the
explicit tracker.Stop(), but do not keep both). Ensure you only have one stop
invocation for tracker.Stop() and leave phase.Complete() unchanged.

In `@cli/cmd/transfer/component-version/progress.go`:
- Around line 44-46: The code silently ignores errors from json.MarshalIndent
when building evt.ErrorDetail from t.Spec.Data; change the snippet to capture
the error (err := json.MarshalIndent(...)), and if err != nil log the error at
debug level (including the err and the related t.ID/t.Type.Name/t.Type.Version)
and use a clear fallback string (e.g., a short "<json marshal error: ...>"
message) for specJSON so bar.FramedText(info, specJSONString, 4) still contains
useful info; update references to specJSON, json.MarshalIndent, evt.ErrorDetail,
bar.FramedText, info and t.Spec.Data accordingly.

In `@cli/internal/render/progress/tracked.go`:
- Around line 49-53: The Start() method initializes p.finished (a channel) but
Complete() and Fail() block on <-p.finished and will deadlock if they are called
before Start(); add a short doc comment to trackedPhase.Start/Complete/Fail
stating the required call order (Start → Complete/Fail) and add a panic-guard in
Complete() and Fail() that checks if p.finished == nil and panics with a clear
message (referencing methods Start, Complete, Fail and the finished channel) so
callers get an immediate, descriptive failure instead of blocking forever.
- Around line 26-35: The stored ctx field on trackedPhase is unused; remove the
ctx field from the trackedPhase struct and update the factory/constructor that
creates trackedPhase (the function that builds instances of trackedPhase, e.g.,
newTrackedPhase or any factory you added) to stop accepting/passing a context
parameter, and remove any code that references trackedPhase.ctx. Ensure all call
sites that constructed trackedPhase are updated to the new signature and that no
remaining references to ctx remain; alternatively, if you intended cancellation
support, instead wire that ctx into the event loop inside the trackedPhase's
processing function to handle context.Done() – but do not leave the unused ctx
field in the struct.

In `@cli/internal/render/progress/tracker.go`:
- Around line 67-84: NewTracker currently mutates global slog state via
slog.SetDefault (in NewTracker) which can break parallel tests; change Tracker
to avoid touching global state by either creating and holding a dedicated
logger/handler internally (use newBufferedHandler with t.logBuffer and keep
previousLogger for restoration semantics) or accept an injected *slog.Logger in
NewTracker signature so callers supply a per-instance logger; remove the
slog.SetDefault call from NewTracker (and update any Stop logic to only affect
the logger instance) and document the new behavior on Tracker/NewTracker to
preserve test isolation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8716ebc8-555c-40d7-a2b7-a3eed19dcdbe

📥 Commits

Reviewing files that changed from the base of the PR and between 000edc3 and 2b11ad9.

📒 Files selected for processing (19)
  • cli/cmd/transfer/component-version/cmd.go
  • cli/cmd/transfer/component-version/progress.go
  • cli/cmd/transfer/component-version/progress_test.go
  • cli/internal/render/progress/bar/animation.go
  • cli/internal/render/progress/bar/animation_test.go
  • cli/internal/render/progress/bar/bar.go
  • cli/internal/render/progress/bar/bar_test.go
  • cli/internal/render/progress/bar/basic.go
  • cli/internal/render/progress/bar/format.go
  • cli/internal/render/progress/bar/options.go
  • cli/internal/render/progress/phase_test.go
  • cli/internal/render/progress/simple.go
  • cli/internal/render/progress/simple/simple.go
  • cli/internal/render/progress/simple/simple_test.go
  • cli/internal/render/progress/slog.go
  • cli/internal/render/progress/slog_test.go
  • cli/internal/render/progress/tracked.go
  • cli/internal/render/progress/tracker.go
  • cli/internal/render/progress/tracker_test.go
✅ Files skipped from review due to trivial changes (4)
  • cli/internal/render/progress/bar/animation_test.go
  • cli/internal/render/progress/slog_test.go
  • cli/internal/render/progress/bar/animation.go
  • cli/cmd/transfer/component-version/progress_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • cli/internal/render/progress/tracker_test.go
  • cli/internal/render/progress/slog.go
  • cli/internal/render/progress/simple/simple_test.go
  • cli/internal/render/progress/bar/format.go
  • cli/internal/render/progress/simple/simple.go
  • cli/internal/render/progress/phase_test.go

@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch from 2b11ad9 to abae289 Compare April 1, 2026 16:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
cli/internal/render/progress/tracked.go (1)

24-31: Unused ctx field in trackedPhase.

The ctx field is stored but never referenced in the current implementation. If it's intended for future use (e.g., respecting context cancellation during event processing), consider adding a TODO comment. Otherwise, remove it to avoid confusion.

♻️ Remove unused field if not needed
 type trackedPhase[E, T any] struct {
 	vis      EventVisualizer[T]
-	ctx      context.Context
 	name     string
 	events   <-chan E
 	mapper   func(E) Event[T]
 	finished chan struct{}
 }

And update the factory:

 	return &trackedPhase[E, T]{
 		vis:    vis,
-		ctx:    t.ctx,
 		name:   name,
 		events: events,
 		mapper: mapper,
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/tracked.go` around lines 24 - 31, The stored ctx
field on trackedPhase is unused; either remove it or mark it for future use.
Remove the ctx field from the trackedPhase struct declaration and update any
constructors/factories and call sites that build trackedPhase (e.g., functions
that instantiate trackedPhase or a NewTrackedPhase/trackPhase helper) to stop
passing a context, or alternatively add a clear TODO comment on the ctx field
explaining planned use (e.g., for cancellation during event processing) if you
intend to keep it.
cli/internal/render/progress/tracker.go (1)

55-62: PhaseFactory looks exported, but custom implementations are effectively closed off.

Because the function returns the unexported startable, callers outside this package can use helper-produced factories but cannot define their own. If that is intentional, I’d make PhaseFactory unexported; otherwise expose an exported creation contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/tracker.go` around lines 55 - 62, The exported
type PhaseFactory currently returns the unexported interface startable,
preventing callers outside the package from implementing custom factories;
either make PhaseFactory unexported or change its return type to an exported
contract—e.g., return Phase (or export the interface name like Startable) so
external packages can implement factories. Update the type alias PhaseFactory to
either be unexported (phaseFactory) if you intend to keep startable internal, or
change its signature to return the exported Phase (or rename startable to
Startable and export it) and adjust any code referencing PhaseFactory,
startable, or StartPhase accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/internal/render/progress/tracker.go`:
- Around line 75-77: The shared bytes.Buffer stored in logBuffer is not
thread-safe so concurrent writes from slog.NewTextHandler and reads/drains by
the visualizers cause a data race; fix this by replacing logBuffer with a
synchronized writer type (e.g., define a syncBuffer struct embedding sync.Mutex
and a bytes.Buffer that implements io.Writer) and use that as the argument to
slog.NewTextHandler, and add a locked Drain/ReadAndReset method used by the
visualizers to atomically read and clear the buffer; alternatively add a mutex
field to the tracker (alongside ctx and previousLogger) and ensure all accesses
to logBuffer (both writes from the logger and reads/drains by the visualizers)
acquire the mutex.

---

Nitpick comments:
In `@cli/internal/render/progress/tracked.go`:
- Around line 24-31: The stored ctx field on trackedPhase is unused; either
remove it or mark it for future use. Remove the ctx field from the trackedPhase
struct declaration and update any constructors/factories and call sites that
build trackedPhase (e.g., functions that instantiate trackedPhase or a
NewTrackedPhase/trackPhase helper) to stop passing a context, or alternatively
add a clear TODO comment on the ctx field explaining planned use (e.g., for
cancellation during event processing) if you intend to keep it.

In `@cli/internal/render/progress/tracker.go`:
- Around line 55-62: The exported type PhaseFactory currently returns the
unexported interface startable, preventing callers outside the package from
implementing custom factories; either make PhaseFactory unexported or change its
return type to an exported contract—e.g., return Phase (or export the interface
name like Startable) so external packages can implement factories. Update the
type alias PhaseFactory to either be unexported (phaseFactory) if you intend to
keep startable internal, or change its signature to return the exported Phase
(or rename startable to Startable and export it) and adjust any code referencing
PhaseFactory, startable, or StartPhase accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e3dd3e71-e4de-4c6c-8cad-465cfbf52608

📥 Commits

Reviewing files that changed from the base of the PR and between 2b11ad9 and abae289.

📒 Files selected for processing (22)
  • cli/cmd/transfer/component-version/cmd.go
  • cli/cmd/transfer/component-version/progress.go
  • cli/cmd/transfer/component-version/progress_test.go
  • cli/docs/reference/ocm_transfer_component-version.md
  • cli/internal/render/progress/bar/animation.go
  • cli/internal/render/progress/bar/animation_test.go
  • cli/internal/render/progress/bar/bar.go
  • cli/internal/render/progress/bar/bar_test.go
  • cli/internal/render/progress/bar/basic.go
  • cli/internal/render/progress/bar/format.go
  • cli/internal/render/progress/bar/options.go
  • cli/internal/render/progress/phase_test.go
  • cli/internal/render/progress/simple.go
  • cli/internal/render/progress/simple/simple.go
  • cli/internal/render/progress/simple/simple_test.go
  • cli/internal/render/progress/slog.go
  • cli/internal/render/progress/slog_test.go
  • cli/internal/render/progress/tracked.go
  • cli/internal/render/progress/tracker.go
  • cli/internal/render/progress/tracker_test.go
  • kubernetes/controller/internal/controller/component/component_controller_test.go
  • kubernetes/controller/internal/controller/repository/repository_controller_test.go
✅ Files skipped from review due to trivial changes (6)
  • cli/docs/reference/ocm_transfer_component-version.md
  • kubernetes/controller/internal/controller/component/component_controller_test.go
  • kubernetes/controller/internal/controller/repository/repository_controller_test.go
  • cli/internal/render/progress/slog_test.go
  • cli/internal/render/progress/slog.go
  • cli/internal/render/progress/tracker_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • cli/internal/render/progress/simple.go
  • cli/cmd/transfer/component-version/cmd.go
  • cli/cmd/transfer/component-version/progress_test.go
  • cli/internal/render/progress/bar/animation_test.go
  • cli/internal/render/progress/bar/animation.go
  • cli/cmd/transfer/component-version/progress.go
  • cli/internal/render/progress/simple/simple_test.go

Comment thread cli/internal/render/progress/tracker.go Outdated
@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch from abae289 to ca15f6a Compare April 1, 2026 23:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
cli/internal/render/progress/tracker.go (1)

77-91: ⚠️ Potential issue | 🔴 Critical

Data race on shared *bytes.Buffer remains unaddressed.

The logBuffer is passed to slog.NewTextHandler (via newBufferedHandler) at line 90, which writes to it from any goroutine calling slog.Info() etc. Visualizers read and drain this buffer during rendering. Since bytes.Buffer is not thread-safe and accesses are unprotected, concurrent log writes and UI renders cause a data race.

Consider wrapping the buffer in a synchronized type:

🔒 Proposed fix using a synchronized buffer
+// syncBuffer wraps bytes.Buffer with mutex protection for concurrent access.
+type syncBuffer struct {
+	mu  sync.Mutex
+	buf bytes.Buffer
+}
+
+func (s *syncBuffer) Write(p []byte) (n int, err error) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	return s.buf.Write(p)
+}
+
+func (s *syncBuffer) DrainString() string {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	str := s.buf.String()
+	s.buf.Reset()
+	return str
+}

Then update Tracker to use *syncBuffer instead of *bytes.Buffer, and have visualizers call DrainString() instead of direct buffer access.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/tracker.go` around lines 77 - 91, The
bytes.Buffer used by Tracker (fields logBuffer and set in NewTracker via
newBufferedHandler) is not concurrency-safe; implement a synchronized wrapper
type (e.g., syncBuffer) that embeds/contains a bytes.Buffer and guards
Read/Write/Reset operations with a mutex and exposes a DrainString() method;
change Tracker.logBuffer to *syncBuffer, update NewTracker to create and pass
the syncBuffer to newBufferedHandler (and keep previousLogger logic), and update
any visualizer/renderer code that currently reads/drains the buffer to call
DrainString() instead of accessing the raw bytes.Buffer so all concurrent writes
and drains are mutex-protected.
🧹 Nitpick comments (2)
cli/internal/render/progress/tracker_test.go (1)

46-53: Consider asserting slog state after double-stop.

The test verifies Stop() is idempotent by not panicking, but doesn't assert that slog.Default() remains correctly restored after the second call.

💡 Suggested enhancement
 func TestTracker_StopIsIdempotent(t *testing.T) {
 	originalLogger := slog.Default()
 	defer slog.SetDefault(originalLogger)

 	tracker := NewTracker(t.Context(), os.Stdout)
 	tracker.Stop()
 	tracker.Stop()
+	// Verify logger is still correctly restored after double-stop
+	if IsTerminal(os.Stdout) {
+		assert.Equal(t, originalLogger, slog.Default())
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/tracker_test.go` around lines 46 - 53, The test
TestTracker_StopIsIdempotent should assert that slog.Default() is restored after
calling tracker.Stop() twice: keep the existing originalLogger :=
slog.Default(), call tracker.Stop() twice as now, then add an assertion that the
current slog.Default() equals originalLogger (e.g., using require.Same(t,
originalLogger, slog.Default()) or a t.Fatalf check) to ensure NewTracker /
tracker.Stop restore the global logger correctly; reference the
TestTracker_StopIsIdempotent test, NewTracker, and tracker.Stop to locate where
to add the assertion.
cli/internal/render/progress/bar/animation.go (1)

69-87: Consider prioritizing cancellation in the animation loop.

While renderLocked() includes a guard against rendering after cancellation, the current select statement can still choose a ticker case after done is closed, causing unnecessary callback invocations. Use a nested select with priority pattern to guarantee cancellation is checked first:

 func RunAnimation(done <-chan struct{}, onSpin func(spinFrame, dotFrame int)) {
 	go func() {
 		spinTicker := time.NewTicker(80 * time.Millisecond)
 		dotTicker := time.NewTicker(400 * time.Millisecond)
 		defer spinTicker.Stop()
 		defer dotTicker.Stop()

 		spinFrame, dotFrame := 1, 0
 		for {
+			// Check cancellation before handling any ready tick.
+			select {
+			case <-done:
+				return
+			default:
+			}
+
 			select {
 			case <-done:
 				return
 			case <-dotTicker.C:
 				dotFrame++
 			case <-spinTicker.C:
 				onSpin(spinFrame, dotFrame)
 				spinFrame++
 			}
 		}
 	}()
 }

This is the standard Go pattern for imposing priority in select statements. Per the Go spec, when multiple cases are ready, one is chosen uniformly at random; the nested select with default ensures cancellation wins.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/bar/animation.go` around lines 69 - 87, In
RunAnimation, the select can service ticker cases even after done is closed;
change the loop to use the "priority" nested-select pattern: in the for loop,
first select on <-done with a return and a default branch, and inside the
default branch run a second select that handles dotTicker.C and spinTicker.C and
calls onSpin(spinFrame, dotFrame); this guarantees the done channel is always
checked first and prevents invoking onSpin after cancellation while keeping
spinTicker and dotTicker logic and the spinFrame/dotFrame increments intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@cli/internal/render/progress/tracker.go`:
- Around line 77-91: The bytes.Buffer used by Tracker (fields logBuffer and set
in NewTracker via newBufferedHandler) is not concurrency-safe; implement a
synchronized wrapper type (e.g., syncBuffer) that embeds/contains a bytes.Buffer
and guards Read/Write/Reset operations with a mutex and exposes a DrainString()
method; change Tracker.logBuffer to *syncBuffer, update NewTracker to create and
pass the syncBuffer to newBufferedHandler (and keep previousLogger logic), and
update any visualizer/renderer code that currently reads/drains the buffer to
call DrainString() instead of accessing the raw bytes.Buffer so all concurrent
writes and drains are mutex-protected.

---

Nitpick comments:
In `@cli/internal/render/progress/bar/animation.go`:
- Around line 69-87: In RunAnimation, the select can service ticker cases even
after done is closed; change the loop to use the "priority" nested-select
pattern: in the for loop, first select on <-done with a return and a default
branch, and inside the default branch run a second select that handles
dotTicker.C and spinTicker.C and calls onSpin(spinFrame, dotFrame); this
guarantees the done channel is always checked first and prevents invoking onSpin
after cancellation while keeping spinTicker and dotTicker logic and the
spinFrame/dotFrame increments intact.

In `@cli/internal/render/progress/tracker_test.go`:
- Around line 46-53: The test TestTracker_StopIsIdempotent should assert that
slog.Default() is restored after calling tracker.Stop() twice: keep the existing
originalLogger := slog.Default(), call tracker.Stop() twice as now, then add an
assertion that the current slog.Default() equals originalLogger (e.g., using
require.Same(t, originalLogger, slog.Default()) or a t.Fatalf check) to ensure
NewTracker / tracker.Stop restore the global logger correctly; reference the
TestTracker_StopIsIdempotent test, NewTracker, and tracker.Stop to locate where
to add the assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8cfea40-e37f-4013-96b5-4cf2e9bbdf9e

📥 Commits

Reviewing files that changed from the base of the PR and between abae289 and ca15f6a.

📒 Files selected for processing (23)
  • cli/cmd/transfer/component-version/cmd.go
  • cli/cmd/transfer/component-version/progress.go
  • cli/cmd/transfer/component-version/progress_test.go
  • cli/docs/reference/ocm_transfer_component-version.md
  • cli/internal/render/progress/bar/animation.go
  • cli/internal/render/progress/bar/animation_test.go
  • cli/internal/render/progress/bar/bar.go
  • cli/internal/render/progress/bar/bar_test.go
  • cli/internal/render/progress/bar/basic.go
  • cli/internal/render/progress/bar/format.go
  • cli/internal/render/progress/bar/options.go
  • cli/internal/render/progress/noop.go
  • cli/internal/render/progress/phase_test.go
  • cli/internal/render/progress/simple.go
  • cli/internal/render/progress/simple/simple.go
  • cli/internal/render/progress/simple/simple_test.go
  • cli/internal/render/progress/slog.go
  • cli/internal/render/progress/slog_test.go
  • cli/internal/render/progress/tracked.go
  • cli/internal/render/progress/tracker.go
  • cli/internal/render/progress/tracker_test.go
  • kubernetes/controller/internal/controller/component/component_controller_test.go
  • kubernetes/controller/internal/controller/repository/repository_controller_test.go
💤 Files with no reviewable changes (2)
  • cli/internal/render/progress/simple/simple_test.go
  • cli/internal/render/progress/simple/simple.go
✅ Files skipped from review due to trivial changes (8)
  • kubernetes/controller/internal/controller/repository/repository_controller_test.go
  • cli/docs/reference/ocm_transfer_component-version.md
  • kubernetes/controller/internal/controller/component/component_controller_test.go
  • cli/internal/render/progress/noop.go
  • cli/internal/render/progress/simple.go
  • cli/internal/render/progress/slog_test.go
  • cli/internal/render/progress/bar/animation_test.go
  • cli/internal/render/progress/bar/bar_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • cli/internal/render/progress/slog.go
  • cli/internal/render/progress/tracked.go
  • cli/internal/render/progress/bar/basic.go
  • cli/internal/render/progress/bar/bar.go
  • cli/cmd/transfer/component-version/progress.go
  • cli/cmd/transfer/component-version/cmd.go

@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch from ca15f6a to 543ea54 Compare April 1, 2026 23:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (2)
cli/cmd/transfer/component-version/progress.go (1)

66-69: ⚠️ Potential issue | 🔴 Critical

Derive the display label from Transformation.Spec.

mapEvent() now surfaces this string throughout the tracked UI, but formatTransformationName() still ignores component, version, resourceIdentity, and resource and always returns ID [Type]. That misses the human-readable naming goal and leaves the updated name-formatting coverage failing.

Suggested fix
 func formatTransformationName(t *graphPkg.Transformation) string {
-	return fmt.Sprintf("%s [%s]", t.ID, t.Type.Name)
+	name := t.ID
+
+	if t.Spec != nil {
+		component, _ := t.Spec.Data["component"].(string)
+		version, _ := t.Spec.Data["version"].(string)
+		resourceIdentity, _ := t.Spec.Data["resourceIdentity"].(string)
+		resource, _ := t.Spec.Data["resource"].(string)
+
+		switch {
+		case component != "" && version != "":
+			name = fmt.Sprintf("%s@%s", component, version)
+		case component != "":
+			name = component
+		case resourceIdentity != "":
+			name = resourceIdentity
+		case resource != "":
+			name = resource
+		}
+
+		switch {
+		case resourceIdentity != "" && name != resourceIdentity:
+			name = fmt.Sprintf("%s (%s)", name, resourceIdentity)
+		case resource != "" && name != resource:
+			name = fmt.Sprintf("%s (%s)", name, resource)
+		}
+	}
+
+	if t.Type.Name == "" {
+		return name
+	}
+	return fmt.Sprintf("%s [%s]", name, t.Type.Name)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/cmd/transfer/component-version/progress.go` around lines 66 - 69,
formatTransformationName currently returns only "ID [Type]" and must instead
derive the human-readable label from the Transformation.Spec fields used by
mapEvent; update the function formatTransformationName to build the display
string from t.Spec.component, t.Spec.version, t.Spec.resourceIdentity and
t.Spec.resource (falling back to existing ID and Type.Name when those spec
fields are empty) so the UI shows the same human-friendly label as mapEvent.
cli/internal/render/progress/bar/bar.go (1)

111-129: ⚠️ Potential issue | 🟡 Minor

Print the phase-level error when no item reaches Failed.

End() still only emits details from failed item events. If the phase exits with err != nil before any item transitions to progress.Failed, the user only sees a failed header and no error body.

Suggested fix
 func (v *barVisualizer[T]) End(err error) {
 	v.mu.Lock()
 	defer v.mu.Unlock()
@@
 	for _, event := range v.events {
 		if event.State == progress.Failed {
 			v.renderFailureSummary()
 			return
 		}
 	}
+
+	if err != nil {
+		fmt.Fprint(v.out, "\nErrors:\n")
+		var zero T
+		if v.errorFormatter != nil {
+			if formatted := v.errorFormatter(zero, err); formatted != "" {
+				fmt.Fprintf(v.out, "%s\n", formatted)
+				return
+			}
+		}
+		fmt.Fprintf(v.out, "  %v\n", err)
+	}
 }
🧹 Nitpick comments (6)
cli/internal/render/progress/bar/animation.go (1)

76-76: spinFrame starts at 1, skipping the first spinner frame on initial render.

The spinFrame counter is initialized to 1 at line 76, which means the first call to onSpin will use frame index 1, skipping SpinnerFrames[0]. If the intent is to start with the first frame, initialize to 0 instead.

📝 Suggested fix
-		spinFrame, dotFrame := 1, 0
+		spinFrame, dotFrame := 0, 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/bar/animation.go` at line 76, The spinner
animation starts on the second frame because spinFrame is initialized to 1;
change the initialization of spinFrame to 0 (leave dotFrame as-is) so the first
call to onSpin uses SpinnerFrames[0]; update the variable declaration where
spinFrame is set (refer to spinFrame, dotFrame, onSpin, and SpinnerFrames in
animation.go).
cli/internal/render/progress/tracked.go (1)

51-59: Complete() and Fail() block indefinitely if events channel is never closed.

Both methods wait on <-p.finished, which only completes when run() returns. Since run() uses for evt := range p.events, it will block forever if the events channel is never closed by the producer.

This is a design contract that callers must follow. Consider documenting this requirement on the Tracked function.

📝 Suggested documentation improvement
 // Tracked returns a PhaseFactory for a tracked phase that processes item-level events.
 // E is the raw event type from the source channel.
 // T is the domain type carried in Event[T] for typed error formatting.
+//
+// The events channel must be closed by the producer when all events have been sent;
+// otherwise Complete() and Fail() will block indefinitely.
 func Tracked[E, T any](vis EventVisualizer[T], events <-chan E, mapper func(E) Event[T]) PhaseFactory {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/tracked.go` around lines 51 - 59, Complete() and
Fail() on trackedPhase[E, T] block waiting on <-p.finished because run() loops
with for evt := range p.events and only returns when the events channel is
closed; update the package docs for the Tracked function (and/or its public
constructor) to explicitly state the contract that producers must close the
events channel when done so that run() can exit and p.finished be closed,
referencing trackedPhase.run, trackedPhase.events and the Complete/Fail methods
to make the requirement unambiguous.
cli/cmd/transfer/component-version/progress_test.go (1)

26-33: Consider adding assertion that phase completes without error.

The test starts a phase and calls Complete(), but doesn't verify any observable outcome. Consider capturing output or adding a test visualizer that records calls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/cmd/transfer/component-version/progress_test.go` around lines 26 - 33,
The test TestSimplePhaseLifecycle currently starts a phase via
progress.NewTracker(...), StartPhase("Test phase",
progress.Simple(newSimpleVisualizer(out))) and calls phase.Complete() but makes
no assertions; modify the test to assert observable behavior by either (a)
inspecting the bytes.Buffer out for expected output produced by the simple
visualizer after phase.Complete() (e.g. contains "Test phase" or a completion
marker), or (b) replace newSimpleVisualizer(out) with a test visualizer that
records lifecycle events and assert that it received Start/Complete calls for
"Test phase"; update the test to fail when those expectations are not met and
keep using tracker.Stop() in defer.
cli/internal/render/progress/bar/bar_test.go (1)

317-335: Fragile test setup: manually closing and recreating the done channel.

The pattern of close(v.done) followed by v.done = make(chan struct{}) at lines 322-323 (and similar in other tests) works around the animation goroutine but is fragile. If the internal implementation changes how done is used, these tests will break or behave unexpectedly.

Consider adding a test helper or exposing a way to stop animations for testing purposes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/bar/bar_test.go` around lines 317 - 335, The
test is fragile because it manually closes and recreates barVisualizer.done to
stop the animation goroutine; instead add a deterministic testing hook to
NewBarVisualizer so tests can disable animations (e.g. an option like
WithNoAnimation or WithAnimationDisabled) or expose a Stop/Close method on
barVisualizer that cleanly terminates the animation goroutine, then update
TestBarVisualizer_BeginEnd_Success to construct the visualizer with that option
(via NewBarVisualizer[string](..., WithHeaderIcon(), WithNoAnimation())) or call
the new Stop/Close before asserting, and remove direct manipulation of v.done;
update related tests to use the same hook.
cli/internal/render/progress/bar/format.go (1)

93-114: Documentation example inconsistency with implementation.

The doc comment example shows inconsistent formatting between title and content lines:

  • Line 97: │ Title (with space after bar)
  • Lines 98-99: │line one (no space after bar)

However, the implementation at lines 105 and 110 shows:

  • Title: (bar + space + Reset + line)
  • Content: (bar + Reset + line, no extra space)

The example should reflect this difference or the behavior should be consistent.

📝 Suggested documentation fix to match implementation
 // Example output:
 //
 //	│ Title
-//	│line one
-//	│line two
+//	│ line one
+//	│ line two

Or update the implementation to add a space for content lines too:

 for _, line := range strings.Split(content, "\n") {
-	fmt.Fprintf(&sb, "%s│%s%s\n", color, Reset, line)
+	fmt.Fprintf(&sb, "%s│ %s%s\n", color, Reset, line)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/bar/format.go` around lines 93 - 114, The doc
example for SidebarText is inconsistent with the implementation: title lines are
rendered with a space after the bar while content lines are rendered without
one. Fix by making behavior consistent—either update the comment example to show
content lines without the extra space, or (preferably) change the content
formatting in SidebarText so the content loop uses the same format as the title
(i.e., include the space after the bar), updating the fmt.Fprintf call that
formats content (the loop iterating over strings.Split(content, "\n")) to match
the title format that uses "%s│ %s%s\n" with the same color and Reset usage.
cli/internal/render/progress/tracker_test.go (1)

46-53: Test behavior depends on terminal detection but doesn't skip or handle both cases.

TestTracker_StopIsIdempotent uses os.Stdout but doesn't check IsTerminal(os.Stdout). In a non-terminal environment (like CI), the tracker won't redirect slog, and the test becomes trivial. Consider either skipping in non-terminal mode or using a buffer to test the non-terminal path consistently.

📝 Suggested fix for consistent behavior
 func TestTracker_StopIsIdempotent(t *testing.T) {
+	// Test with non-terminal output for consistent behavior across environments
 	originalLogger := slog.Default()
 	defer slog.SetDefault(originalLogger)

-	tracker := NewTracker(t.Context(), os.Stdout)
+	tracker := NewTracker(t.Context(), &bytes.Buffer{})
 	tracker.Stop()
 	tracker.Stop()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/internal/render/progress/tracker_test.go` around lines 46 - 53,
TestTracker_StopIsIdempotent relies on terminal detection and currently uses
os.Stdout, making the test trivial in non-terminal CI; modify the test
(TestTracker_StopIsIdempotent) to handle both cases by checking
IsTerminal(os.Stdout) and if false create the tracker with a bytes.Buffer (or
io.Discard) instead of os.Stdout so the non-terminal path is exercised,
otherwise keep using os.Stdout; ensure you still restore slog.Default() and call
tracker.Stop() twice to verify idempotence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/internal/render/progress/bar/bar.go`:
- Around line 171-181: Buffered slog lines are not flushed in the final redraw
because renderFinalHeader() (and the final rendering path around lines 273-289)
never calls drainLogBuffer(), so any logs after the last tick remain queued; fix
by invoking v.drainLogBuffer() at the start (or just before restoring the
original logger/output) of renderFinalHeader() and in the corresponding
final-render function covering lines ~273-289 so the buffered lines are printed
before the tracker restores state.

---

Duplicate comments:
In `@cli/cmd/transfer/component-version/progress.go`:
- Around line 66-69: formatTransformationName currently returns only "ID [Type]"
and must instead derive the human-readable label from the Transformation.Spec
fields used by mapEvent; update the function formatTransformationName to build
the display string from t.Spec.component, t.Spec.version,
t.Spec.resourceIdentity and t.Spec.resource (falling back to existing ID and
Type.Name when those spec fields are empty) so the UI shows the same
human-friendly label as mapEvent.

---

Nitpick comments:
In `@cli/cmd/transfer/component-version/progress_test.go`:
- Around line 26-33: The test TestSimplePhaseLifecycle currently starts a phase
via progress.NewTracker(...), StartPhase("Test phase",
progress.Simple(newSimpleVisualizer(out))) and calls phase.Complete() but makes
no assertions; modify the test to assert observable behavior by either (a)
inspecting the bytes.Buffer out for expected output produced by the simple
visualizer after phase.Complete() (e.g. contains "Test phase" or a completion
marker), or (b) replace newSimpleVisualizer(out) with a test visualizer that
records lifecycle events and assert that it received Start/Complete calls for
"Test phase"; update the test to fail when those expectations are not met and
keep using tracker.Stop() in defer.

In `@cli/internal/render/progress/bar/animation.go`:
- Line 76: The spinner animation starts on the second frame because spinFrame is
initialized to 1; change the initialization of spinFrame to 0 (leave dotFrame
as-is) so the first call to onSpin uses SpinnerFrames[0]; update the variable
declaration where spinFrame is set (refer to spinFrame, dotFrame, onSpin, and
SpinnerFrames in animation.go).

In `@cli/internal/render/progress/bar/bar_test.go`:
- Around line 317-335: The test is fragile because it manually closes and
recreates barVisualizer.done to stop the animation goroutine; instead add a
deterministic testing hook to NewBarVisualizer so tests can disable animations
(e.g. an option like WithNoAnimation or WithAnimationDisabled) or expose a
Stop/Close method on barVisualizer that cleanly terminates the animation
goroutine, then update TestBarVisualizer_BeginEnd_Success to construct the
visualizer with that option (via NewBarVisualizer[string](..., WithHeaderIcon(),
WithNoAnimation())) or call the new Stop/Close before asserting, and remove
direct manipulation of v.done; update related tests to use the same hook.

In `@cli/internal/render/progress/bar/format.go`:
- Around line 93-114: The doc example for SidebarText is inconsistent with the
implementation: title lines are rendered with a space after the bar while
content lines are rendered without one. Fix by making behavior consistent—either
update the comment example to show content lines without the extra space, or
(preferably) change the content formatting in SidebarText so the content loop
uses the same format as the title (i.e., include the space after the bar),
updating the fmt.Fprintf call that formats content (the loop iterating over
strings.Split(content, "\n")) to match the title format that uses "%s│ %s%s\n"
with the same color and Reset usage.

In `@cli/internal/render/progress/tracked.go`:
- Around line 51-59: Complete() and Fail() on trackedPhase[E, T] block waiting
on <-p.finished because run() loops with for evt := range p.events and only
returns when the events channel is closed; update the package docs for the
Tracked function (and/or its public constructor) to explicitly state the
contract that producers must close the events channel when done so that run()
can exit and p.finished be closed, referencing trackedPhase.run,
trackedPhase.events and the Complete/Fail methods to make the requirement
unambiguous.

In `@cli/internal/render/progress/tracker_test.go`:
- Around line 46-53: TestTracker_StopIsIdempotent relies on terminal detection
and currently uses os.Stdout, making the test trivial in non-terminal CI; modify
the test (TestTracker_StopIsIdempotent) to handle both cases by checking
IsTerminal(os.Stdout) and if false create the tracker with a bytes.Buffer (or
io.Discard) instead of os.Stdout so the non-terminal path is exercised,
otherwise keep using os.Stdout; ensure you still restore slog.Default() and call
tracker.Stop() twice to verify idempotence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 34ddbd47-979b-409a-ae87-55d8247af7bf

📥 Commits

Reviewing files that changed from the base of the PR and between ca15f6a and 543ea54.

📒 Files selected for processing (23)
  • cli/cmd/transfer/component-version/cmd.go
  • cli/cmd/transfer/component-version/progress.go
  • cli/cmd/transfer/component-version/progress_test.go
  • cli/docs/reference/ocm_transfer_component-version.md
  • cli/internal/render/progress/bar/animation.go
  • cli/internal/render/progress/bar/animation_test.go
  • cli/internal/render/progress/bar/bar.go
  • cli/internal/render/progress/bar/bar_test.go
  • cli/internal/render/progress/bar/basic.go
  • cli/internal/render/progress/bar/format.go
  • cli/internal/render/progress/bar/options.go
  • cli/internal/render/progress/noop.go
  • cli/internal/render/progress/phase_test.go
  • cli/internal/render/progress/simple.go
  • cli/internal/render/progress/simple/simple.go
  • cli/internal/render/progress/simple/simple_test.go
  • cli/internal/render/progress/slog.go
  • cli/internal/render/progress/slog_test.go
  • cli/internal/render/progress/tracked.go
  • cli/internal/render/progress/tracker.go
  • cli/internal/render/progress/tracker_test.go
  • kubernetes/controller/internal/controller/component/component_controller_test.go
  • kubernetes/controller/internal/controller/repository/repository_controller_test.go
💤 Files with no reviewable changes (2)
  • cli/internal/render/progress/simple/simple_test.go
  • cli/internal/render/progress/simple/simple.go
✅ Files skipped from review due to trivial changes (7)
  • cli/docs/reference/ocm_transfer_component-version.md
  • kubernetes/controller/internal/controller/repository/repository_controller_test.go
  • kubernetes/controller/internal/controller/component/component_controller_test.go
  • cli/internal/render/progress/noop.go
  • cli/internal/render/progress/slog_test.go
  • cli/internal/render/progress/bar/animation_test.go
  • cli/internal/render/progress/phase_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • cli/internal/render/progress/simple.go
  • cli/cmd/transfer/component-version/cmd.go
  • cli/internal/render/progress/bar/basic.go

Comment thread cli/internal/render/progress/bar/bar_visualizer.go
@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch from 543ea54 to a4fc63d Compare April 2, 2026 00:08

@jakobmoellerdev jakobmoellerdev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great first stab! I purely reviewed for code maintainability right now, not for functional correctness.

Comment thread cli/cmd/transfer/component-version/cmd.go Outdated
Comment thread cli/cmd/transfer/component-version/cmd.go Outdated
Comment thread cli/internal/render/progress/bar/animation.go Outdated
Comment thread cli/internal/render/progress/bar/animation.go Outdated
Comment thread cli/internal/render/progress/bar/animation_test.go
Comment thread cli/internal/render/progress/bar/basic.go Outdated
Comment thread cli/internal/render/progress/bar/options.go Outdated
Comment thread cli/internal/render/progress/noop.go Outdated
Comment thread cli/internal/render/progress/simple.go Outdated
Comment thread cli/internal/render/progress/tracker.go Outdated
Comment thread cli/cmd/transfer/component-version/cmd.go Outdated
Comment thread cli/cmd/transfer/component-version/progress.go
Comment thread cli/cmd/transfer/component-version/progress.go

@matthiasbruns matthiasbruns left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I love that shining animation - the output looks good

tldr;

  • split logic from rendering
  • try to reuse/merge describe theming

@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch from a4fc63d to ae2e13b Compare April 9, 2026 22:19
@netlify

netlify Bot commented Apr 9, 2026

Copy link
Copy Markdown

Deploy Preview for ocm-website canceled.

Name Link
🔨 Latest commit de1b3fc
🔍 Latest deploy log https://app.netlify.com/projects/ocm-website/deploys/69e617f359947a00080c3305

@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch 2 times, most recently from d49bec2 to 17bdf21 Compare April 10, 2026 13:52
@jakobmoellerdev

Copy link
Copy Markdown
Member

Code Review

Focus: Functional correctness and interfaces


Architecture

The design is clean and well thought out. A few highlights worth calling out:

  • Decoupled concerns: Tracker only manages slog interception; rendering is fully delegated to Visualizer/EventVisualizer[T]. Good separation.
  • No any type assertions: Generics throughout (Event[T], EventVisualizer[T], barVisualizer[T]) are idiomatic Go and avoid unsafe casts.
  • Deadlock prevention: Events channel is only created when IsTerminal() is true — correctly prevents goroutine blocking in non-terminal mode.
  • Progress to stderr: out := cmd.ErrOrStderr() is correct — keeps stdout clean for --dry-run pipe workflows.
  • Thread-safe SyncBuffer: Mutex-guarded buffer for slog interception correctly avoids data races between the animation goroutine and slog writes.
  • Test coverage: Unit tests at multiple levels — visualizer internals, operation lifecycle, flag parsing. Solid.

Issues

MEDIUM — FramedText uses byte length for box sizing

visualizers/format.go:

maxWidth := 0
for _, line := range lines {
    if len(line) > maxWidth {
        maxWidth = len(line)    // byte count, not display width
    }
}
padding := strings.Repeat(" ", maxWidth-len(line))  // same issue

len() returns bytes, not visible characters. For content with multi-byte UTF-8 characters (arbitrary JSON values in transformation specs are a real scenario) the box borders will be misaligned. At minimum use utf8.RuneCountInString(line). For full correctness with CJK or wide characters, golang.org/x/text/width would be needed, but rune count is likely sufficient here.

MEDIUM — Silent drop of Failed event for already-Completed items

bar_visualizer.go — the guard that prevents re-updating terminal states:

if e.State == progress.Completed || e.State == progress.Failed {
    return  // silently ignores
}

If the graph runtime ever emits a Failed event after a Completed event for the same item (e.g. a race, or a retry), the failure is silently dropped and the item stays shown as Completed. If the runtime guarantees this can't happen, a comment explaining that invariant would make the intent clear. If it's not guaranteed, this needs a log or assertion.

LOW — Explicit tracker.Stop() before final log line needs a comment

transfer.goRun calls tracker.Stop() explicitly just before the final slog.DebugContext, with a deferred Stop() as a safety net:

defer r.tracker.Stop()   // safety net for error paths

// ...
r.tracker.Stop()         // restores slog before the debug log
slog.DebugContext(r.ctx, "transfer completed successfully")
return nil

Stop() is idempotent so this works correctly — but the reason the explicit call exists (to ensure the debug log goes to the restored handler, not the buffer) isn't obvious. Worth a short inline comment.

LOW — mapState default returns untyped empty string

progress.go:

default:
    return ""

This is an untyped empty State, not a named sentinel. Downstream switch statements will silently fall through. Suggest:

default:
    return progress.Unknown  // define: Unknown State = ""

or just map to Cancelled with a log if truly unknown.

LOW — Duplicate doc comment on SetLogBuffer

bar_visualizer.go lines 35–37 have the same // SetLogBuffer sets the shared slog buffer from the tracker. comment twice.


Summary

# Severity Issue
1 MEDIUM FramedText box sizing uses len() (bytes) — breaks with multi-byte UTF-8 content
2 MEDIUM Silent drop of Failed events for already-Completed items — needs invariant comment or guard
3 LOW Add comment explaining explicit tracker.Stop() before final slog.DebugContext call
4 LOW mapState default returns untyped "" — use a named Unknown sentinel
5 LOW Duplicate doc comment on SetLogBuffer in bar_visualizer.go

Items 1 and 2 are worth addressing before merge. The rest are minor polish.

@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch 3 times, most recently from 3aedba7 to 9436dec Compare April 11, 2026 22:02
@github-actions github-actions Bot added the component/github-actions Changes on GitHub Actions or within `.github/` directory label Apr 11, 2026
@jakobmoellerdev

Copy link
Copy Markdown
Member

@piotrjanik whats the status of this PR? does it just need spellcheck and a deeper review?

@piotrjanik

piotrjanik commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

@jakobmoellerdev yes, I just need to update spellcheck word list

@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch 2 times, most recently from f29b91e to d4db2cd Compare April 16, 2026 14:27
@piotrjanik piotrjanik enabled auto-merge (squash) April 16, 2026 14:29
@piotrjanik piotrjanik disabled auto-merge April 16, 2026 14:29
@morri-son

Copy link
Copy Markdown
Contributor

all issues from #2132 (comment) have been addressed. lgtm now.

morri-son
morri-son previously approved these changes Apr 16, 2026
<!-- markdownlint-disable MD041 -->

Adds a progress rendering system for the `transfer component-version` command, replacing the previous generic tracker with a streamlined, operation-based API.

**Progress package (`cli/internal/render/progress/`)**
- `Tracker[T]` manages operation lifecycle and slog redirection during terminal rendering
- `StartOperation` supports simple operations (spinner only) and event-tracked operations (progress bar) via `WithEvents` and `WithErrorFormatter`
- `SlogVisualizer[T]` provides automatic fallback for non-terminal environments (CI, piped output)
- slog is buffered during terminal rendering to prevent log output from corrupting the animated UI

**Bar visualizer (`cli/internal/render/progress/bar/`)**
- Single `NewVisualizer[T]` factory handles both simple (total=0, spinner header) and tracked (progress bar with scrolling item log) modes
- Animated spinner with shimmer effect on header text
- Failed items are listed with typed error formatting (spec dump for debugging)

**Transfer command integration**
- Three progress phases: loading transfer spec, resolving component versions, transferring
- Events are fully typed as `Event[*graphPkg.Transformation]` throughout the pipeline
- Error formatter renders transformation failures with error tree and framed spec JSON dump

<!--
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->

Fixes: open-component-model#929

Signed-off-by: Piotr Janik <piotr.janik@sap.com>
@piotrjanik piotrjanik force-pushed the feat/929-log-prefetch-transfer branch from f5a465c to 5493e69 Compare April 17, 2026 09:05
@matthiasbruns

Copy link
Copy Markdown
Contributor

pr desc seems out of sync with the actual changes - e.g. simple.Visualizer

@piotrjanik piotrjanik enabled auto-merge (squash) April 20, 2026 10:29
@piotrjanik piotrjanik merged commit 2b5680a into open-component-model:main Apr 20, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/github-actions Changes on GitHub Actions or within `.github/` directory kind/feature new feature, enhancement, improvement, extension size/l Large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants