fix(pipeline-status): dedup forgejo ci runs by workflow on same SHA #51

Merged
slaid098 merged 3 commits from fix/pipeline-status/forgejo-ci-rollup-dedup into main 2026-08-11 17:28:47 +03:00
Owner

Что сделано

  • pipeline-status.py:_forgejo_ci_rollup — после фильтра по head_sha добавлена дедупликация «latest run per workflow» (max id), вынесенная в helper-функции _forgejo_workflow_key (ключ = name → workflow_id → id) и _forgejo_latest_runs_per_workflow. Mapping status/conclusion → rollup entry не меняется — меняется только то, какие runs попадают в цикл.
  • tests/test_pipeline_status_ci.py — добавлено 6 тестов _forgejo_ci_rollup_* (мок ps._forgejo_request): re-run same SHA+workflow (old failed id=100 + new success id=200 → DONE), real single failure → NOT_DONE, two different workflows (one fails) → NOT_DONE, re-run in_progress → AMBIGUOUS, other SHA filtered → AMBIGUOUS, workflow_id fallback при отсутствии name → DONE.
  • docs/decisions/096-forgejo-ci-rollup-dedup.md — новый ADR (supersedes portions of ADR-054 re: Forgejo migration): фиксирует причину (re-run создаёт новый run, старый остаётся FAILURE), решение (client-side dedup max id per workflow), отвергнутые альтернативы (server-side SHA filter, фильтр по event, ослабление _classify_rollup).

Почему

После re-run failed jobs того же SHA оракул _forgejo_ci_rollup собирал и старый failed run, и новый success run для одного workflow. _classify_rollup плоско итерирует conclusions — первый FAILURE → NOT_DONE, новый SUCCESS игнорируется → pipeline блокируется на CI-фазе даже после успешного re-run. Зафиксировано на PR#46 (fix-коммит 2e490cb431 = HEAD PR, в API mix runs: старый failed + новый success). ADR-054 ЯВНО отверг альтернативу (C) «SHA + all workflows» на GitHub именно по причине «выбор правильного run из нескольких», но после миграции на Forgejo statusCheckRollup исчез, а reimplementation повторил ту же проблему — без GitHub-side агрегации, которая её решала. Дедупликация max id per workflow решает «который run» на client-side.

Watch out

  • Дедуп ключ = name (workflow name), НЕ head_branch/event — они одинаковы для re-runs одного SHA. Если в будущем понадобится различать runs по event (pull_request vs push) — это отдельная фича, текущий контракт покрывает re-run.
  • API ordering: код НЕ зависит от того что /actions/tasks возвращает runs по id desc — явно выбирает max id. Если Forgejo изменит порядок, поведение сохранится.
  • limit=50: при >50 runs на SHA+workflow дедуп всё равно корректен (берёт max id из того что вернулось), но старые runs могут выпасть из окна — это ограничение API, не дедупа. Server-side SHA filter (отдельный PR) снял бы это ограничение.
  • mypy strict: добавлен from typing import Any + типизация новых helper-функций как dict[str, Any]. 6 пре-существующих dict-без-args ошибок mypy на master сохранены (не добавлены этим PR) — это в pipeline-status.py lines 94, 218, 274, 378, 393, 396 (функции _forgejo_request, _forgejo_pr_view и др.).
  • mccabe: _forgejo_ci_rollup была 11 (>10) после inline-дедупа — вынес логику в 2 helper-функции, теперь green.

Pending

  • Server-side фильтрация по head_sha параметром API (если Forgejo поддерживает) — отдельный PR, сейчас client-side достаточно.
  • Локальный ruff gate в create-pr.ts (предотвращает попадание в CI) — отдельный issue.
  • Утечка веток pr-N — отдельный issue.

Closes #48

Closes #48

## Что сделано - `pipeline-status.py:_forgejo_ci_rollup` — после фильтра по `head_sha` добавлена дедупликация «latest run per workflow» (max `id`), вынесенная в helper-функции `_forgejo_workflow_key` (ключ = `name` → `workflow_id` → `id`) и `_forgejo_latest_runs_per_workflow`. Mapping status/conclusion → rollup entry не меняется — меняется только то, какие runs попадают в цикл. - `tests/test_pipeline_status_ci.py` — добавлено 6 тестов `_forgejo_ci_rollup_*` (мок `ps._forgejo_request`): re-run same SHA+workflow (old failed id=100 + new success id=200 → DONE), real single failure → NOT_DONE, two different workflows (one fails) → NOT_DONE, re-run in_progress → AMBIGUOUS, other SHA filtered → AMBIGUOUS, `workflow_id` fallback при отсутствии `name` → DONE. - `docs/decisions/096-forgejo-ci-rollup-dedup.md` — новый ADR (supersedes portions of ADR-054 re: Forgejo migration): фиксирует причину (re-run создаёт новый run, старый остаётся FAILURE), решение (client-side dedup max `id` per workflow), отвергнутые альтернативы (server-side SHA filter, фильтр по `event`, ослабление `_classify_rollup`). ## Почему После re-run failed jobs того же SHA оракул `_forgejo_ci_rollup` собирал и старый failed run, и новый success run для одного workflow. `_classify_rollup` плоско итерирует conclusions — первый FAILURE → NOT_DONE, новый SUCCESS игнорируется → pipeline блокируется на CI-фазе даже после успешного re-run. Зафиксировано на PR#46 (fix-коммит 2e490cb43126 = HEAD PR, в API mix runs: старый failed + новый success). ADR-054 ЯВНО отверг альтернативу (C) «SHA + all workflows» на GitHub именно по причине «выбор правильного run из нескольких», но после миграции на Forgejo `statusCheckRollup` исчез, а reimplementation повторил ту же проблему — без GitHub-side агрегации, которая её решала. Дедупликация max `id` per workflow решает «который run» на client-side. ## Watch out - Дедуп ключ = `name` (workflow name), НЕ `head_branch`/`event` — они одинаковы для re-runs одного SHA. Если в будущем понадобится различать runs по `event` (pull_request vs push) — это отдельная фича, текущий контракт покрывает re-run. - API ordering: код НЕ зависит от того что `/actions/tasks` возвращает runs по `id` desc — явно выбирает max `id`. Если Forgejo изменит порядок, поведение сохранится. - `limit=50`: при >50 runs на SHA+workflow дедуп всё равно корректен (берёт max `id` из того что вернулось), но старые runs могут выпасть из окна — это ограничение API, не дедупа. Server-side SHA filter (отдельный PR) снял бы это ограничение. - mypy strict: добавлен `from typing import Any` + типизация новых helper-функций как `dict[str, Any]`. 6 пре-существующих `dict`-без-args ошибок mypy на master сохранены (не добавлены этим PR) — это в `pipeline-status.py` lines 94, 218, 274, 378, 393, 396 (функции `_forgejo_request`, `_forgejo_pr_view` и др.). - mccabe: `_forgejo_ci_rollup` была 11 (>10) после inline-дедупа — вынес логику в 2 helper-функции, теперь green. ## Pending - Server-side фильтрация по `head_sha` параметром API (если Forgejo поддерживает) — отдельный PR, сейчас client-side достаточно. - Локальный ruff gate в `create-pr.ts` (предотвращает попадание в CI) — отдельный issue. - Утечка веток `pr-N` — отдельный issue. Closes #48 Closes #48
docs(adr): add ADR-096 forgejo ci rollup re-run dedup
All checks were successful
CI (always) / bootstrap (pull_request) Successful in 14s
CI / bootstrap (pull_request) Successful in 21s
CI / lint (pull_request) Successful in 31s
CI / complexity (pull_request) Successful in 31s
CI / typecheck (pull_request) Successful in 31s
CI / test (3.13) (pull_request) Successful in 1m40s
bbc85ea4c5
Author
Owner

Code Review Summary

Чистый, хорошо декомпозированный fix: дедупликация re-run'ов Forgejo CI по workflow (max id per workflow key) решает реальный баг с блокировкой pipeline после успешного re-run (PR#46). 6 новых тестов покрывают все сценарии, ADR-096 корректно фиксирует решение и контекст.

Positives

  • Декомпозиция: логика дедупа вынесена в 2 helper-функции (_forgejo_workflow_key 15 строк, _forgejo_latest_runs_per_workflow 16 строк) — mccabe _forgejo_ci_rollup остался в норме, функции <50 строк.
  • Defensive coding: int(r.get("id", 0) or 0) корректно обрабатывает None/пустую строку; .strip() на name; явный max id вместо зависимости от API ordering.
  • Fallback chain в _forgejo_workflow_key: name → workflow_id → id (каждый run сам по себе) — покрывает все варианты API-ответа, тест test_forgejo_ci_rollup_workflow_id_fallback это верифицирует.
  • Тесты: 6 тестов покрывают re-run success, real failure (no regression), two distinct workflows (partial-failure preserved), in_progress (AMBIGUOUS poll), other-SHA filtered, workflow_id fallback. Имена описательные, assertions осмысленные.
  • ADR-096: корректно supersedes portions of ADR-054, описывает контекст (PR#46), решение, 4 отвергнутые альтернативы, последствия. Ссылки на ADR-092/093 согласованы.
  • Контракт сохранён: _forgejo_ci_rollup return-тип и statusCheckRollup JSON не изменились — run-pipeline/SKILL.md Template D (fix_ci) не требует обновления, oracle просто начинает работать корректно.
  • PR hygiene: title conventional commits, body ## Что сделано/## Почему/## Watch out/## Pending заполнены осмысленно, Closes #48, branch name descriptive.
  • Quality gates: ruff All checks passed; mypy 6 pre-existing dict errors (не добавлены PR — подтверждено сравнением с main); xenon rank B для новых функций; 29/29 тестов прошли.

Suggestions (info, not blocking)

  • pipeline-status.py:190 [robustness] int(r.get("id", 0) or 0) — если API вернёт id как float (теоретически), int() сработает, но если как нечисловую строку — ValueError. Текущий контракт Forgejo API возвращает id как int, так что это гипотетический edge case. Можно ужесточить через try/except, но YAGNI для текущего сценария.
  • pipeline-status.py:211 [info] limit=50 — при >50 runs на SHA+workflow дедуп всё равно корректен (берёт max id из того что вернулось), но старые runs могут выпасть из окна. Это ограничение API, не дедупа — корректно отмечено в PR body ## Watch out и ADR-096 (server-side SHA filter отложен в отдельный PR).
  • test_pipeline_status_ci.py:650 [style] _forgejo_rollup_runs — helper без префикса _test может выглядеть как production-функция, но _ prefix + расположение в test-файле достаточно сигнализируют. Не блокер.

Verdict: APPROVE

## Code Review Summary Чистый, хорошо декомпозированный fix: дедупликация re-run'ов Forgejo CI по workflow (max `id` per workflow key) решает реальный баг с блокировкой pipeline после успешного re-run (PR#46). 6 новых тестов покрывают все сценарии, ADR-096 корректно фиксирует решение и контекст. ### Positives - **Декомпозиция**: логика дедупа вынесена в 2 helper-функции (`_forgejo_workflow_key` 15 строк, `_forgejo_latest_runs_per_workflow` 16 строк) — mccabe `_forgejo_ci_rollup` остался в норме, функции <50 строк. - **Defensive coding**: `int(r.get("id", 0) or 0)` корректно обрабатывает None/пустую строку; `.strip()` на `name`; явный `max id` вместо зависимости от API ordering. - **Fallback chain в `_forgejo_workflow_key`**: `name` → `workflow_id` → `id` (каждый run сам по себе) — покрывает все варианты API-ответа, тест `test_forgejo_ci_rollup_workflow_id_fallback` это верифицирует. - **Тесты**: 6 тестов покрывают re-run success, real failure (no regression), two distinct workflows (partial-failure preserved), in_progress (AMBIGUOUS poll), other-SHA filtered, workflow_id fallback. Имена описательные, assertions осмысленные. - **ADR-096**: корректно supersedes portions of ADR-054, описывает контекст (PR#46), решение, 4 отвергнутые альтернативы, последствия. Ссылки на ADR-092/093 согласованы. - **Контракт сохранён**: `_forgejo_ci_rollup` return-тип и `statusCheckRollup` JSON не изменились — `run-pipeline/SKILL.md` Template D (fix_ci) не требует обновления, oracle просто начинает работать корректно. - **PR hygiene**: title conventional commits, body `## Что сделано`/`## Почему`/`## Watch out`/`## Pending` заполнены осмысленно, Closes #48, branch name descriptive. - **Quality gates**: ruff All checks passed; mypy 6 pre-existing `dict` errors (не добавлены PR — подтверждено сравнением с main); xenon rank B для новых функций; 29/29 тестов прошли. ### Suggestions (info, not blocking) - **pipeline-status.py:190** [robustness] `int(r.get("id", 0) or 0)` — если API вернёт `id` как float (теоретически), `int()` сработает, но если как нечисловую строку — `ValueError`. Текущий контракт Forgejo API возвращает `id` как int, так что это гипотетический edge case. Можно ужесточить через `try/except`, но YAGNI для текущего сценария. - **pipeline-status.py:211** [info] `limit=50` — при >50 runs на SHA+workflow дедуп всё равно корректен (берёт max `id` из того что вернулось), но старые runs могут выпасть из окна. Это ограничение API, не дедупа — корректно отмечено в PR body `## Watch out` и ADR-096 (server-side SHA filter отложен в отдельный PR). - **test_pipeline_status_ci.py:650** [style] `_forgejo_rollup_runs` — helper без префикса `_test` может выглядеть как production-функция, но `_` prefix + расположение в test-файле достаточно сигнализируют. Не блокер. ### Verdict: APPROVE
slaid098 deleted branch fix/pipeline-status/forgejo-ci-rollup-dedup 2026-08-11 17:28:47 +03:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/opencode-config!51
No description provided.