Summary
Running apcdeploy status interactively (stderr is a TTY) always corrupts its own output: the last line of the deployment status table is erased and replaced by a duplicate of the Targets summary row.
Details
internal/status/executor.go finalises the Targets row and then renders the status table, while tg.Close() is deferred:
tg.Done(id, summarizeDeployment(deploymentInfo)) // executor.go:98
display.DeploymentStatus(e.reporter, ...) // executor.go:106 — Header + Table on stderr
return nil // deferred tg.Close() fires here (executor.go:61)
ttyTargets.Close() (internal/cli/targets_tty.go:205-228) unconditionally performs one final redraw(). redraw() (targets_tty.go:91-97) emits \033[<N>A\r\033[J — cursor up N rows (N = number of Targets rows), then clear to end of screen — assuming the cursor still sits directly below the Targets block. After display.DeploymentStatus has printed the Header and Table, the cursor is below the table instead, so the final redraw moves up into the table, deletes its last line (e.g. the Duration row), and prints the row summary there a second time.
The animation goroutine (targets_tty.go:122-141) also keeps redrawing every ~120 ms until Close, so any tick that fires between Done and Close corrupts the table the same way.
The comment at executor.go:99-105 ("In TTY mode the Targets renderer has already finalised by the time the table prints, so the two views stack cleanly") does not hold — Done() finalises the row state, but the renderer stays active until Close().
rollback already avoids this exact hazard by rendering display.DeploymentStatus before opening the Targets block (internal/rollback/executor.go:99-102).
Reproduction
Run apcdeploy status -c apcdeploy.yml in a terminal against any profile with a completed deployment. The table's last line is replaced by a duplicated ✓ COMPLETE — v<N> ... row. Reproduces every time.
Fix direction
Close the Targets block before rendering the table: either call display.DeploymentStatus after tg.Close() (drop the defer and close explicitly), or render the table before opening Targets like rollback does.
Summary
Running
apcdeploy statusinteractively (stderr is a TTY) always corrupts its own output: the last line of the deployment status table is erased and replaced by a duplicate of the Targets summary row.Details
internal/status/executor.gofinalises the Targets row and then renders the status table, whiletg.Close()is deferred:ttyTargets.Close()(internal/cli/targets_tty.go:205-228) unconditionally performs one finalredraw().redraw()(targets_tty.go:91-97) emits\033[<N>A\r\033[J— cursor up N rows (N = number of Targets rows), then clear to end of screen — assuming the cursor still sits directly below the Targets block. Afterdisplay.DeploymentStatushas printed the Header and Table, the cursor is below the table instead, so the final redraw moves up into the table, deletes its last line (e.g. theDurationrow), and prints the row summary there a second time.The animation goroutine (
targets_tty.go:122-141) also keeps redrawing every ~120 ms until Close, so any tick that fires betweenDoneandClosecorrupts the table the same way.The comment at
executor.go:99-105("In TTY mode the Targets renderer has already finalised by the time the table prints, so the two views stack cleanly") does not hold —Done()finalises the row state, but the renderer stays active untilClose().rollbackalready avoids this exact hazard by renderingdisplay.DeploymentStatusbefore opening the Targets block (internal/rollback/executor.go:99-102).Reproduction
Run
apcdeploy status -c apcdeploy.ymlin a terminal against any profile with a completed deployment. The table's last line is replaced by a duplicated✓ COMPLETE — v<N> ...row. Reproduces every time.Fix direction
Close the Targets block before rendering the table: either call
display.DeploymentStatusaftertg.Close()(drop thedeferand close explicitly), or render the table before opening Targets likerollbackdoes.