headroomlabs-ai/headroom · error · SystemExit

output directory must be empty to avoid stale artifacts: {pa

Error message

output directory must be empty to avoid stale artifacts: {path}

What it means

Same guard as the Python smoke's ensure_empty_dir(), but in the top-level orchestrator scripts/release_smoke_all.py: it creates the run's output directory and refuses to continue if it is non-empty, so a run's summary never mixes artifacts from different runs.

Source

Thrown at scripts/release_smoke_all.py:50

        return tomllib.load(fh)["project"]["version"]


def quote_arg(value: str | os.PathLike[str]) -> str:
    text = str(value)
    if re.fullmatch(r"[A-Za-z0-9_./:=\\-]+", text):
        return text
    return f'"{text.replace(chr(34), chr(34) * 2)}"'


def run(args: list[str | os.PathLike[str]]) -> None:
    print("\n> " + " ".join(quote_arg(arg) for arg in args), flush=True)
    subprocess.run([str(arg) for arg in args], cwd=ROOT, check=True)


def ensure_empty_dir(path: Path) -> None:
    path.mkdir(parents=True, exist_ok=True)
    if any(path.iterdir()):
        raise SystemExit(f"output directory must be empty to avoid stale artifacts: {path}")


def parse_args() -> argparse.Namespace:
    version = load_project_version()
    stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    default_out = ROOT / "release-assets-local" / f"all-{version}-{stamp}"

    parser = argparse.ArgumentParser(description="Run local npm and Python release smokes.")
    parser.add_argument("--out", type=Path, default=default_out)
    parser.add_argument("--python", default=sys.executable)
    parser.add_argument("--node", default=shutil.which("node") or "node")
    parser.add_argument(
        "--python-release",
        action="store_true",
        help="Run the Python smoke with maturin --release instead of the faster ci profile.",
    )
    parser.add_argument("--skip-npm", action="store_true")
    parser.add_argument("--skip-python", action="store_true")

View on GitHub (pinned to 322425c43b)

Solutions

  1. Drop `--out` and let the script use its timestamped default directory.
  2. Or remove/rename the existing directory before rerunning: `rm -rf <out_dir>`.
  3. In CI, use a unique directory per attempt (`out-$GITHUB_RUN_ATTEMPT`) instead of a fixed path.

Example fix

# before
python scripts/release_smoke_all.py --out ./release-out
# second run fails

# after
python scripts/release_smoke_all.py  # default: release-assets-local/all-<version>-<stamp>
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
import sys

def ensure_runnable(out_dir: Path, skip_npm: bool, skip_python: bool) -> None:
    if skip_npm and skip_python:
        raise SystemExit("nothing to run: at least one smoke must be enabled")
    out_dir.mkdir(parents=True, exist_ok=True)
    if any(out_dir.iterdir()):
        raise SystemExit(f"stale output dir: {out_dir}")

Prevention

When it happens

Trigger: Passing `--out <dir>` that already contains files. The default output path embeds version + a `%Y%m%d-%H%M%S` timestamp, so the default essentially never collides — hitting this error almost always means an explicit `--out` was reused.

Common situations: Scripting repeated smokes with a fixed `--out ./out`; CI caching the output directory between attempts; a prior run crashing after writing partial results.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/838d95322d67774c. Report an issue: GitHub.