From ce5e68f3574951db8a6f1b600a4d2876bbf76359 Mon Sep 17 00:00:00 2001 From: Simone Date: Sun, 17 May 2026 16:12:50 +0200 Subject: [PATCH] =?UTF-8?q?fix(build):=20Linux=20artifact=20staging=20?= =?UTF-8?q?=E2=80=94=20absolute=20source=20+=20don't=20wipe=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs made the Linux smoke "succeed" with an empty artifact: - artifact_source was always joined as workdir + '/' + src, so an absolute source (/opt/ci/output) became /opt/ci/build//opt/ci/output and was never found — only a non-fatal WARNING was logged. - `rm -rf output_dir` ran unconditionally; when the build writes directly into the collect dir (the smoke case: output == source), it deleted the build output before collection. Resolve absolute vs workdir-relative; when the source is the output dir, collect in place without wiping; a missing/empty source is now a hard failure instead of a silent empty artifact. Co-Authored-By: Claude Opus 4.7 --- src/ci_orchestrator/commands/build.py | 43 ++++++++++++++++++++------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/ci_orchestrator/commands/build.py b/src/ci_orchestrator/commands/build.py index 0b44650..28581b1 100644 --- a/src/ci_orchestrator/commands/build.py +++ b/src/ci_orchestrator/commands/build.py @@ -180,17 +180,40 @@ def _linux_build( click.echo("[build run] artifact packaging skipped (--skip-artifact).") return - t.run(f"rm -rf {_sh_quote(output_dir)} && mkdir -p {_sh_quote(output_dir)}") + # Resolve the artifact source: absolute path as-is, else relative + # to the build workdir. (artifact_source may be absolute, e.g. + # /opt/ci/output; the old `workdir + '/' + src` produced + # /opt/ci/build//opt/ci/output and silently found nothing.) src = artifact_source or "dist" - cp_cmd = ( - f"if [ -d {_sh_quote(workdir + '/' + src)} ]; then " - f"cp -r {_sh_quote(workdir + '/' + src + '/.')} {_sh_quote(output_dir + '/')}; " - f"elif [ -e {_sh_quote(workdir + '/' + src)} ]; then " - f"cp {_sh_quote(workdir + '/' + src)} {_sh_quote(output_dir + '/')}; " - f"else echo '[build run] WARNING: artifact source not found: {src}' >&2; fi" - ) - t.run(cp_cmd) - click.echo(f"[build run] artifact staged at {output_dir}") + srcpath = src.rstrip("/") if src.startswith("/") else workdir.rstrip("/") + "/" + src + out = output_dir.rstrip("/") + + if srcpath == out or srcpath.startswith(out + "/"): + # The build wrote directly into the collect dir; do NOT wipe it + # (that deleted the build output) and nothing to copy. Fail + # loudly if it is missing or empty rather than collecting an + # empty artifact. + click.echo( + f"[build run] artifact source is the output dir ({out}); collecting in place" + ) + t.run( + f"if [ ! -d {_sh_quote(out)} ] || " + f'[ -z "$(ls -A {_sh_quote(out)} 2>/dev/null)" ]; then ' + f"echo '[build run] artifact source not found or empty: {out}' >&2; " + f"exit 1; fi" + ) + else: + t.run(f"rm -rf {_sh_quote(out)} && mkdir -p {_sh_quote(out)}") + cp_cmd = ( + f"if [ -d {_sh_quote(srcpath)} ]; then " + f"cp -r {_sh_quote(srcpath + '/.')} {_sh_quote(out + '/')}; " + f"elif [ -e {_sh_quote(srcpath)} ]; then " + f"cp {_sh_quote(srcpath)} {_sh_quote(out + '/')}; " + f"else echo '[build run] artifact source not found: {srcpath}' >&2; " + f"exit 1; fi" + ) + t.run(cp_cmd) + click.echo(f"[build run] artifact staged at {out}") # ---------------------------------------------------------------- Windows flow