feat(self-heal): scope CI signal to a workflow + warn on missing target

Two hardening fixes from the gap review:
- Optional self_heal_ci_workflow scopes the CI signal to one workflow file
  (the workflow-scoped Actions endpoint). Without it, "latest completed run
  across all workflows" could miss a red CI run masked by a later passing
  workflow, or false-trigger on a non-CI workflow — unreliable on a
  multi-workflow repo.
- The loop logs a warning when self-heal is armed but self_heal_project_slug
  is unset, so a misconfiguration isn't mistaken for "all green".

Tests cover the workflow-scoped endpoint.
This commit is contained in:
Renn F
2026-06-17 21:58:48 +02:00
parent 7ef7d8414e
commit 33fa21d00a
5 changed files with 61 additions and 10 deletions
@@ -241,3 +241,18 @@ async def test_get_latest_ci_conclusion_none_when_no_runs() -> None:
patch("roboco.services.git.httpx.AsyncClient", return_value=client),
):
assert await svc.get_latest_ci_conclusion("roboco") is None
@pytest.mark.asyncio
async def test_get_latest_ci_conclusion_scopes_to_workflow() -> None:
# With a workflow file given, hit the workflow-scoped endpoint (precise signal).
svc = _service()
client = _client(_resp(200, json_payload={"workflow_runs": [_run("success")]}))
with (
_patch_project_ci(),
patch("roboco.services.git.httpx.AsyncClient", return_value=client),
):
out = await svc.get_latest_ci_conclusion("roboco", workflow="ci.yml")
assert out is not None
assert out["conclusion"] == "success"
assert client.get.await_args.args[0].endswith("/actions/workflows/ci.yml/runs")