diff --git a/src/operations_center/entrypoints/custodian_sweep/main.py b/src/operations_center/entrypoints/custodian_sweep/main.py index a63d6b0a..5839a8d6 100644 --- a/src/operations_center/entrypoints/custodian_sweep/main.py +++ b/src/operations_center/entrypoints/custodian_sweep/main.py @@ -29,10 +29,10 @@ import shutil import subprocess import sys +import threading from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass, field from datetime import UTC, datetime -from functools import partial from pathlib import Path from typing import Any @@ -135,10 +135,28 @@ def _run_custodian_audit(target: _RepoTarget, *, timeout_seconds: int) -> _RepoS def _run_custodian_audits( targets: list[_RepoTarget], *, jobs: int, timeout_seconds: int ) -> list[_RepoSweep]: - """Run repo audits with bounded parallelism while preserving target order.""" + """Run repo audits with bounded parallelism while preserving target order. + + Prints one flushed progress line per completed repo to stderr — the sweep + can legitimately take minutes across many repos, and without interim + output a bounded probe (e.g. a `timeout 90` health check) is + indistinguishable from a genuine hang. + """ if not targets: return [] - runner = partial(_run_custodian_audit, timeout_seconds=timeout_seconds) + total = len(targets) + done_count = 0 + lock = threading.Lock() + + def runner(target: _RepoTarget) -> _RepoSweep: + nonlocal done_count + result = _run_custodian_audit(target, timeout_seconds=timeout_seconds) + with lock: + done_count += 1 + n = done_count + print(f"[custodian-sweep] {n}/{total} {target.repo_key} done", file=sys.stderr, flush=True) + return result + max_workers = max(1, min(jobs, len(targets))) if max_workers == 1: return [runner(target) for target in targets] diff --git a/tests/test_custodian_sweep.py b/tests/test_custodian_sweep.py index 29739c88..339ad1e9 100644 --- a/tests/test_custodian_sweep.py +++ b/tests/test_custodian_sweep.py @@ -258,6 +258,23 @@ def test_run_custodian_audits_falls_back_to_serial_when_jobs_is_one(monkeypatch) assert [sweep.repo_key for sweep in sweeps] == ["A", "B"] +def test_run_custodian_audits_prints_progress_per_repo(monkeypatch, capsys) -> None: + targets = [_RepoTarget("A", Path("/tmp/a")), _RepoTarget("B", Path("/tmp/b"))] + + monkeypatch.setattr( + sweep_module, + "_run_custodian_audit", + lambda target, *, timeout_seconds: _RepoSweep(repo_key=target.repo_key), + ) + + sweeps = _run_custodian_audits(targets, jobs=1, timeout_seconds=20) + + assert [sweep.repo_key for sweep in sweeps] == ["A", "B"] + err = capsys.readouterr().err + assert "1/2 A done" in err + assert "2/2 B done" in err + + def test_main_uses_safer_default_timeout(monkeypatch, tmp_path: Path, capsys) -> None: config_path = tmp_path / "config.yaml" config_path.write_text("ignored: true\n", encoding="utf-8")