What's wrong
A board row opened with crate::ui::progress::start(...) has no guarantee of a matching progress_line(...) (which is what closes it via Board::finish, internal/ui/progress/board.rs:142). Several call sites across up and down return, or fall through, on a code path that never closes the row it opened — so that row sits on the Creating/Removing/Pulling/Stopping spinner forever, even though the underlying operation is done (successfully reused, successfully skipped, or genuinely failed).
This isn't one bug in one command — it's the same missing-close shape recurring at six sites across the two commands the live board covers most:
| Command |
File:line |
Row opened as |
Branch that never closes it |
up |
internal/engine/network/mod.rs:70-85 |
Network … Creating |
Err(e) if e.is_already_exists() => {} |
up |
internal/engine/volume/mod.rs:68-82 |
Volume … Creating |
Err(e) if e.is_already_exists() => {} |
up |
internal/engine/build/pull.rs:256-303 |
Image … Pulling |
Some(e) => Err(...) on the failed-pull path |
down |
internal/engine/lifecycle/mod.rs:653-661 |
Network … Removing |
Ok(false) (nothing to remove) and Err(e) (removal failed — only tracing::warn!) |
down |
internal/engine/lifecycle/mod.rs:694-702 |
Volume … Removing |
same two arms as the network case above |
down |
internal/engine/lifecycle/parallel.rs:348-395 |
Container … Stopping |
Err(e) if e.is_status(404) (already gone) and the genuine-failure Err(e) arm |
The container-create side of up (internal/engine/lifecycle/mod.rs:486-528) already gets this right — its "already exists" / "up to date" branches call progress_line("Container", ..., "Exists") or "Running" before returning, so that row always resolves. That's the pattern the other six sites are missing.
Why it matters
Most of these are the common path, not an edge case: any redeploy that reuses an existing network or volume hits it every time, and down hits it on a container/network/volume that's already gone (partial teardown, re-running down after a failure). The board ends up looking hung exactly when nothing is wrong, and — worse — the genuine-failure arms (down's network/volume removal, container stop/remove) also leave the row spinning while the error is only a tracing::warn! line or the final process error, with nothing on the board itself pointing at which resource failed.
Repro (generic)
Any compose file where two services share a network, run up once to create it, then up again:
==> Starting
⠋ Network app_shared_net Creating 0.0s
✔ Container app_web Running
The network row never reaches a checkmark. Same shape on down when a network/volume/container from a previous partial run is already gone.
What's worth deciding before fixing
Patching each of the six sites with an honest closing verb ("Exists", "Absent", "Failed", …) fixes what's observed today, but the pattern will recur — progress::start still has no enforced matching close, so a seventh call site can reintroduce this the same way. Worth deciding whether the fix is per-site (fast, matches the existing "Exists"/"Running" precedent) or structural — something that makes an orphaned Working row impossible by construction (e.g. a guard returned by start() that force-closes on drop with a fallback verb if nothing closed it explicitly). Leaving that call for whoever picks this up rather than presupposing it here.
Impact
Cosmetic on the success/already-there arms — the operations themselves are correct. Worse than cosmetic on the failure arms in down, where the board actively hides that something went wrong.
What's wrong
A board row opened with
crate::ui::progress::start(...)has no guarantee of a matchingprogress_line(...)(which is what closes it viaBoard::finish,internal/ui/progress/board.rs:142). Several call sites acrossupanddownreturn, or fall through, on a code path that never closes the row it opened — so that row sits on theCreating/Removing/Pulling/Stoppingspinner forever, even though the underlying operation is done (successfully reused, successfully skipped, or genuinely failed).This isn't one bug in one command — it's the same missing-close shape recurring at six sites across the two commands the live board covers most:
upinternal/engine/network/mod.rs:70-85Err(e) if e.is_already_exists() => {}upinternal/engine/volume/mod.rs:68-82Err(e) if e.is_already_exists() => {}upinternal/engine/build/pull.rs:256-303Some(e) => Err(...)on the failed-pull pathdowninternal/engine/lifecycle/mod.rs:653-661Ok(false)(nothing to remove) andErr(e)(removal failed — onlytracing::warn!)downinternal/engine/lifecycle/mod.rs:694-702downinternal/engine/lifecycle/parallel.rs:348-395Err(e) if e.is_status(404)(already gone) and the genuine-failureErr(e)armThe container-create side of
up(internal/engine/lifecycle/mod.rs:486-528) already gets this right — its "already exists" / "up to date" branches callprogress_line("Container", ..., "Exists")or"Running"before returning, so that row always resolves. That's the pattern the other six sites are missing.Why it matters
Most of these are the common path, not an edge case: any redeploy that reuses an existing network or volume hits it every time, and
downhits it on a container/network/volume that's already gone (partial teardown, re-runningdownafter a failure). The board ends up looking hung exactly when nothing is wrong, and — worse — the genuine-failure arms (down's network/volume removal, container stop/remove) also leave the row spinning while the error is only atracing::warn!line or the final process error, with nothing on the board itself pointing at which resource failed.Repro (generic)
Any compose file where two services share a network, run
uponce to create it, thenupagain:The network row never reaches a checkmark. Same shape on
downwhen a network/volume/container from a previous partial run is already gone.What's worth deciding before fixing
Patching each of the six sites with an honest closing verb (
"Exists","Absent","Failed", …) fixes what's observed today, but the pattern will recur —progress::startstill has no enforced matching close, so a seventh call site can reintroduce this the same way. Worth deciding whether the fix is per-site (fast, matches the existing "Exists"/"Running" precedent) or structural — something that makes an orphanedWorkingrow impossible by construction (e.g. a guard returned bystart()that force-closes on drop with a fallback verb if nothing closed it explicitly). Leaving that call for whoever picks this up rather than presupposing it here.Impact
Cosmetic on the success/already-there arms — the operations themselves are correct. Worse than cosmetic on the failure arms in
down, where the board actively hides that something went wrong.