fix(ci): fleet-branch push triggers + dispatcher claim prefilter (#463)

* fix(ci): fleet-branch push triggers close the absent-check gap; dispatcher claim prefilter

PROVEN with API receipts: when the PM squash-merges a subtask PR into a
branch that is itself another PR's head (GitService.merge_pull_request →
GitHub's Merge API), the pull_request synchronize webhook fires
unreliably (1 of 3 in the live sample) while plain push events fired
100% — so PR heads sat with ABSENT required checks that three review
rounds mistook for green. CI, CodeQL, e2e-smoke, and panel-ci now also
trigger on push to the fleet's branch types, deduped by a concurrency
group keyed on head_ref||ref_name so a branch that is also a PR head
never double-runs.

Dispatcher churn: _route_unassigned_pm_task consults the claim guards'
own predicate (TaskService.is_pending_claim_blocked, a public wrapper —
no duplicated SQL) before routing, so dependency- or sequence-held
tasks skip the tick with zero HTTP claim round-trips; fails open so a
DB hiccup degrades to the old behavior.

* chore(docs): reflow hard-wrapped prose inherited from the six-PR merge train

* chore(foundation): regenerate lifecycle artifacts; reflow inherited prose

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-11 09:21:15 +02:00
committed by GitHub
co-authored by Renn F
parent 786e6ffc3c
commit 20110debab
8 changed files with 354 additions and 1 deletions
@@ -1510,6 +1510,69 @@ async def test_claim_blocked_by_sequence_names_distinct_reason(
assert "seq-0 blocker" in reason
# ---------------------------------------------------------------------------
# is_pending_claim_blocked — the public dispatch-time probe the orchestrator
# fetch filter uses to skip a doomed claim attempt (churn reduction).
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_is_pending_claim_blocked_true_for_lower_sequence_sibling(
task_setup: dict, db_session: AsyncSession
) -> None:
svc = task_setup["svc"]
parent = await svc.create(_req(task_setup, title="parent"))
seq0 = await svc.create(
_req(task_setup, title="seq-0 blocker", parent_task_id=parent.id, sequence=0)
)
seq1 = await svc.create(
_req(task_setup, title="seq-1", parent_task_id=parent.id, sequence=1)
)
seq0.status = TaskStatus.IN_PROGRESS
await db_session.flush()
assert await svc.is_pending_claim_blocked(seq1.id) is True
seq0.status = TaskStatus.COMPLETED
await db_session.flush()
assert await svc.is_pending_claim_blocked(seq1.id) is False
@pytest.mark.asyncio
async def test_is_pending_claim_blocked_true_for_unmet_dependency(
task_setup: dict, db_session: AsyncSession
) -> None:
svc = task_setup["svc"]
dep = await svc.create(_req(task_setup, title="dependency"))
task = await svc.create(_req(task_setup, title="dependent"))
await db_session.flush()
await svc.add_dependency(task.id, dep.id)
assert await svc.is_pending_claim_blocked(task.id) is True
dep.status = TaskStatus.COMPLETED
await db_session.flush()
assert await svc.is_pending_claim_blocked(task.id) is False
@pytest.mark.asyncio
async def test_is_pending_claim_blocked_false_for_clear_task(
task_setup: dict,
) -> None:
"""A ready task (no dependency edge, sequence 0, no parent) is never held."""
svc = task_setup["svc"]
task = await svc.create(_req(task_setup, title="ready"))
assert await svc.is_pending_claim_blocked(task.id) is False
@pytest.mark.asyncio
async def test_is_pending_claim_blocked_false_for_missing_task(
task_setup: dict,
) -> None:
svc = task_setup["svc"]
assert await svc.is_pending_claim_blocked(uuid4()) is False
@pytest.mark.asyncio
async def test_claim_batch_wave_blocked_by_all_wave0_siblings_no_edges(
task_setup: dict, db_session: AsyncSession