Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions src/operations_center/entrypoints/custodian_sweep/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down
17 changes: 17 additions & 0 deletions tests/test_custodian_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading