Stream update progress into a bar on the stack's row (0.42.0)
CI / build-and-push (push) Successful in 1m55s

Update ran as a blocking POST with nothing to show but a spinner, so the
status added in 86c67df could only sit above the table as a banner.

Adds /ws/update/{stack_id}, streaming `compose pull` then `up -d` with
--progress json, and feeds it through the existing DeployTracker — the
same weighting the deploy console uses. The result renders as a progress
bar inside the stack's own row: percentage, phase label, and layer/byte
detail. Non-streaming actions (start/stop/restart/pull/down) reuse the
bar in its indeterminate form, so every row action looks consistent.

compose_service gains _stream_phase, shared by stream_up and the new
stream_update; a failed pull short-circuits before `up`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pMmFFkdfxkoYjcEcpZTa5
This commit is contained in:
menzelj
2026-08-31 00:33:34 +02:00
co-authored by Claude Opus 5
parent 86c67dfcea
commit 1e8d4248fd
11 changed files with 392 additions and 56 deletions
+42 -9
View File
@@ -387,17 +387,16 @@ async def supports_json_progress() -> bool:
return _json_progress
async def stream_up(stack_id: str, override: Optional[str] = None):
"""Run `compose up -d` streaming combined output, so the deploy console can
show image-pull and container-create progress live.
Yields ``("log", line)`` for each output line, then ``("done", returncode)``.
"""
async def _stream_phase(
stack_id: str, args: list[str], override: Optional[str], json_progress: bool
):
"""One compose subcommand, streamed. Yields ``("log", line)`` per output
line, then ``("rc", returncode)`` exactly once."""
cmd = _compose_base_cmd(stack_id, override)
if await supports_json_progress():
if json_progress:
# Global flag, must precede the subcommand.
cmd += ["--progress", "json"]
cmd += ["up", "-d", "--remove-orphans"]
cmd += args
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
@@ -407,7 +406,41 @@ async def stream_up(stack_id: str, override: Optional[str] = None):
async for raw in proc.stdout:
yield ("log", raw.decode("utf-8", "replace").rstrip("\n"))
await proc.wait()
yield ("done", proc.returncode)
yield ("rc", proc.returncode)
async def stream_up(stack_id: str, override: Optional[str] = None):
"""Run `compose up -d` streaming combined output, so the deploy console can
show image-pull and container-create progress live.
Yields ``("log", line)`` for each output line, then ``("done", returncode)``.
"""
json_progress = await supports_json_progress()
async for kind, payload in _stream_phase(
stack_id, ["up", "-d", "--remove-orphans"], override, json_progress
):
yield ("done", payload) if kind == "rc" else ("log", payload)
async def stream_update(stack_id: str, override: Optional[str] = None):
"""Run `compose pull` then `compose up -d`, streaming both phases, so the
stacks list can show real update progress instead of a spinner.
Yields ``("log", line)`` for each output line of either phase, then
``("done", returncode)`` once. A failed pull short-circuits: recreating
containers on images that never came down would only make things worse.
"""
json_progress = await supports_json_progress()
rc = 0
for args in (["pull"], ["up", "-d", "--remove-orphans"]):
async for kind, payload in _stream_phase(stack_id, args, override, json_progress):
if kind == "log":
yield ("log", payload)
else:
rc = payload
if rc != 0:
break
yield ("done", rc)
# Convenience lifecycle wrappers ------------------------------------------------