From 67c90f10eb1622432bd402daba9213b1fb4f4bc3 Mon Sep 17 00:00:00 2001 From: Simone Date: Sun, 17 May 2026 17:47:17 +0200 Subject: [PATCH] fix(build): flush guest build output + put tail in failure message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a non-zero build the guest stdout/stderr was written unflushed then a ClickException raised, so the cause was lost (only "build command failed (exit 1)" surfaced). _report_build_output now emits the output with markers, flushes both streams, and returns the last lines which are appended to the failure message — diagnosable even if act_runner buffering drops the body. Applied to Linux + Windows. Co-Authored-By: Claude Opus 4.7 --- src/ci_orchestrator/commands/build.py | 34 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/ci_orchestrator/commands/build.py b/src/ci_orchestrator/commands/build.py index 2ac3e6a..432c3ac 100644 --- a/src/ci_orchestrator/commands/build.py +++ b/src/ci_orchestrator/commands/build.py @@ -98,6 +98,25 @@ def _sh_quote(value: str) -> str: return shlex.quote(value) +def _report_build_output(stdout: str, stderr: str) -> str: + """Emit guest build stdout/stderr to the CI log, flushed. + + Returns the last few lines for the failure message so the cause is + visible even if stream buffering hides the body on a non-zero exit. + """ + click.echo("[build run] ----- build output -----") + if stdout: + sys.stdout.write(stdout if stdout.endswith("\n") else stdout + "\n") + sys.stdout.flush() + if stderr: + sys.stderr.write(stderr if stderr.endswith("\n") else stderr + "\n") + sys.stderr.flush() + click.echo("[build run] ----- end build output -----") + combined = (stderr or "").strip() or (stdout or "").strip() + lines = combined.splitlines() + return " | ".join(lines[-5:]) if lines else "(no build output)" + + # ---------------------------------------------------------------- Linux flow @@ -173,14 +192,10 @@ def _linux_build( full_cmd = f"{env_prefix}cd {_sh_quote(workdir)} && {cmd}" click.echo(f"[build run] running build (env vars redacted): cd {workdir} && {cmd}") result = t.run(full_cmd, check=False) - # Stream guest stdout/stderr after the fact (paramiko buffers the channel). - if result.stdout: - sys.stdout.write(result.stdout) - if result.stderr: - sys.stderr.write(result.stderr) + tail = _report_build_output(result.stdout, result.stderr) if result.returncode != 0: raise click.ClickException( - f"build command failed (exit {result.returncode})" + f"build command failed (exit {result.returncode}): {tail}" ) if skip_artifact: @@ -333,13 +348,10 @@ def _windows_build( "if ($null -eq $LASTEXITCODE) { exit 0 } else { exit $LASTEXITCODE }" ) result = t.run(full_ps, check=False) - if result.stdout: - sys.stdout.write(result.stdout) - if result.stderr: - sys.stderr.write(result.stderr) + tail = _report_build_output(result.stdout, result.stderr) if result.returncode != 0: raise click.ClickException( - f"build command failed (exit {result.returncode})" + f"build command failed (exit {result.returncode}): {tail}" ) if skip_artifact: