From f73a5bbb2fabafc6f2f539725396cdc37392c118 Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Mon, 31 Aug 2026 10:07:04 -0500 Subject: [PATCH] Fix disappearing artifacts and guard against empty builds The agent's background build threads called jsonify()/send_file() with no Flask app context, so after a successful build they crashed with 'Working outside of application context', which triggered the error handler that rmtree'd the whole build dir before the controller could download the artifact. Wrap both runners in app.app_context(). Also guard all three layers against empty results: the agent refuses to report success or serve a zip when dist/ has no files, and the scheduler flags a downloaded empty zip as a failure rather than marking the job done. --- agent/build_agent.py | 18 ++++++++++++++---- ctrl/scheduler.py | 7 +++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/agent/build_agent.py b/agent/build_agent.py index 992839b..9599f3e 100755 --- a/agent/build_agent.py +++ b/agent/build_agent.py @@ -261,8 +261,9 @@ def _run_checkout_build_background(job_id, repo_url, repo_dir, start_time): releases it when the work finishes.""" def runner(): try: - subprocess.check_call(['git', 'clone', repo_url, repo_dir]) - install_and_build(repo_dir, job_id, start_time) + with app.app_context(): + subprocess.check_call(['git', 'clone', repo_url, repo_dir]) + install_and_build(repo_dir, job_id, start_time) except Exception as e: print(f"Background checkout/build failed for {job_id}: {e}") system_status['status'] = "ready" @@ -278,7 +279,8 @@ def _run_build_background(job_id, project_path, start_time): the thread releases it when the build finishes.""" def runner(): try: - install_and_build(project_path, job_id, start_time) + with app.app_context(): + install_and_build(project_path, job_id, start_time) except Exception as e: print(f"Background build failed for {job_id}: {e}") _clear_progress(job_id) @@ -444,6 +446,13 @@ def install_and_build(project_path, job_id, start_time): return jsonify({"error": f"Error compiling project: {e}"}), 500 dist_path = os.path.join(project_path, "dist") + if not any(os.path.isfile(os.path.join(root, f)) for root, _, files in os.walk(dist_path) for f in files): + print(f"Error: build for {job_id} reported success but {dist_path} is empty") + system_status['status'] = "ready" + system_status['running_job'] = None + _clear_progress(job_id) + return jsonify({"error": f"Build completed but produced no files in {dist_path}"}), 500 + system_status['status'] = "ready" system_status['running_job'] = None _clear_progress(job_id) @@ -485,7 +494,8 @@ def download_binaries(job_id): job_path = os.path.join(TMP_DIR, BUILD_DIR, job_id) dist_path = os.path.join(job_path, "dist") - if not os.path.exists(dist_path): + if not os.path.exists(dist_path) or not any( + os.path.isfile(os.path.join(root, f)) for root, _, files in os.walk(dist_path) for f in files): return jsonify({"error": f"No binaries found for ID: {job_id}"}), 404 # Create a temporary zip file diff --git a/ctrl/scheduler.py b/ctrl/scheduler.py index 780888d..7b394a4 100644 --- a/ctrl/scheduler.py +++ b/ctrl/scheduler.py @@ -3,6 +3,7 @@ import logging import os import threading import time +import zipfile import requests @@ -225,6 +226,12 @@ class Scheduler: for chunk in resp.iter_content(chunk_size=8192): f.write(chunk) + with zipfile.ZipFile(dest) as zf: + files = [n for n in zf.namelist() if not n.endswith("/")] + if not files: + os.remove(dest) + raise RuntimeError(f"Worker {worker['url']} returned an empty artifact zip for {worker_job_id}") + db.update_job(job_id, artifacts=[fname]) # ---- logging / status -------------------------------------------------