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

Closed
opened 2026-08-11 17:18:02 +03:00 by slaid098 · 0 comments
Owner

Контекст

Зачем: после re-run failed jobs того же SHA оракул _forgejo_ci_rollup собирает и старый failed run, и новый success run для одного workflow → _classify_rollup видит FAILURE первым → возвращает NOT_DONE → pipeline блокируется. PR с фикс-коммитом, равным HEAD PR (или после re-run), не может пройти CI фазу. Зафиксировано на PR#46 (fix-коммит 2e490cb431 = HEAD PR, в API mix runs: старый failed + новый success).

Контекст: _forgejo_ci_rollup в .opencode/scripts/pipeline-status.py:161-195 — единственный фильтр r.get("head_sha") != sha: continue (line 178). Дедупликации по workflow нет. API /actions/tasks?limit=50 возвращает последние 50 runs (по id descending), включая устаревшие failed runs от re-runs с тем же head_sha. _classify_rollup (pipeline-status.py:717-742) работает с плоским списком conclusions — первый FAILURE → NOT_DONE, игнорирует новый SUCCESS.

ADR-054 (docs/decisions/054-pr-122-pipeline-status-statuscheckrollup.md:44-47) ЯВНО отверг альтернативу (C) "SHA + all workflows" именно по причине "выбор правильного run из нескольких". После миграции на Forgejo (ADR-092/093) statusCheckRollup исчез, reimplementation в _forgejo_ci_rollup повторил ровно ту проблему, которую ADR-054 называл причиной отказа от (C).

Тестов на re-run scenario НЕТ: tests/test_pipeline_status_ci.py мокают выход _forgejo_ci_rollup (готовый JSON rollup), а не саму функцию. Тест test_ci_partial_failure (lines 445-466) — про 2 РАЗНЫХ check'а, не про 2 runs одного workflow одного SHA. Тестов _forgejo_ci_rollup вообще нет.

Re-run semantics Forgejo/GitHub Actions: "Re-run failed jobs" / "Re-run all jobs" создаёт НОВЫЙ run с тем же head_sha, старый run остаётся в финальном статусе FAILURE. cancel-in-progress: true в .github/workflows/ci.yml:13 работает только на новый push (новый commit), не на re-run.

Задача

  1. В .opencode/scripts/pipeline-status.py функция _forgejo_ci_rollup (lines 161-195) — после фильтра по head_sha (line 178) добавить дедупликацию "latest run per workflow":
    • Группировать отфильтрованные runs по полю name (имя workflow; проверить что Forgejo /actions/tasks возвращает это поле — если нет, использовать workflow_id или комбинацию). Скорее всего API возвращает runs отсортированными по id descending (стандарт GitHub Actions) — тогда первый match per workflow = latest.
    • В каждой группе оставить только run с наибольшим id (поле id точно есть, используется в _forgejo_run_view line 398).
    • В rollup (lines 176-194) добавлять только эти "latest per workflow" runs.
  2. Добавить тест test_forgejo_ci_rollup_rerun_same_sha в tests/test_pipeline_status_ci.py — мокает _forgejo_request чтобы вернуть 2 runs одного workflow с одинаковым head_sha: old (id=100, status=failure, conclusion=failure) + new (id=200, status=success, conclusion=success). Ожидаемый verdict: DONE (CI green, latest run success). Мокировать через subprocess.run patch (как существующие test_forgejo_* тесты в tests/test_pipeline_status.py:1281-1462).
  3. Обновить ADR-054 (docs/decisions/054-pr-122-pipeline-status-statuscheckrollup.md) — добавить секцию "Update YYYY-MM-DD: after Forgejo migration, _forgejo_ci_rollup deduplicates runs by workflow name (latest by id) to handle re-run scenario" с ссылкой на этот PR. ИЛИ создать новый ADR (номер определи через ls docs/decisions/ — следующий свободный NNN-pr-<this_pr>-forgejo-ci-rollup-dedup.md).

Контракты

  • _forgejo_ci_rollup(repo, sha) возвращает tuple[int, str, str] — без изменений в signature.
  • При re-run (old failed + new success, один SHA, один workflow) → verdict DONE (CI green).
  • При реальном failed (один run, failed) → verdict NOT_DONE (без изменений).
  • При 2 разных workflows (ci.yml failed + always-ci.yml success) → verdict NOT_DONE (без изменений — это разные checks, не re-run).
  • При in_progress run → verdict AMBIGUOUS (без изменений — polling).
  • API endpoint: GET /api/v1/repos/{repo}/actions/tasks?limit=50 — без изменений (server-side фильтрация НЕ добавляется, дедупликация client-side).

Инварианты

  • Дедупликация ТОЛЬКО по workflow name (или workflow_id если name отсутствует). НЕ по head_branch (он одинаковый для re-runs) и НЕ по event (он одинаковый для re-runs).
  • Latest = max id (числовое сравнение через int(r.get("id", 0))).
  • Если поле name отсутствует в API ответе — fallback на workflow_id, потом на id (каждый run сам по себе, без дедупликации — корректно для edge case).
  • Существующая логика mapping status/conclusion → rollup entry (lines 180-194) НЕ меняется — меняется только то, какие runs попадают в цикл.

Граничные случаи

  • API возвращает пустой workflow_runs: [] → rollup пустой → _classify_rollup возвращает AMBIGUOUS "no checks found in rollup" (без изменений).
  • Все runs с другим head_sha (нет matching) → rollup пустой → AMBIGUOUS (без изменений).
  • Re-run создал run с status=in_progress (ещё не завершился) → latest per workflow = in_progress → AMBIGUOUS "CI in progress" (правильно — ждём завершения).
  • 2 runs с одинаковым id (невозможно в API, но defensive) → первый побеждает (dict insert order).
  • Поле name есть, но пустая строка → fallback на workflow_id, потом на id.

Влияние на связанные компоненты

  • pipeline-status.ts (.opencode/tools/pipeline-status.ts) — без изменений (TS wrapper только spawnSync'ит Python).
  • tests/test_pipeline_status_ci.py — добавить test_forgejo_ci_rollup_rerun_same_sha (мок _forgejo_request через monkeypatch ps._forgejo_request).
  • tests/test_pipeline_status.py — существующие 9 test_forgejo_* (lines 1281-1462) тестируют _forgejo_request HTTP-слой, НЕ затрагиваются (дедупликация поверх, не в HTTP-слое).
  • run-pipeline/SKILL.md Template D (fix_ci) — без изменений (поведение оракула улучшается, протокол тот же).
  • ADR-054 (docs/decisions/054-pr-122-pipeline-status-statuscheckrollup.md) — добавить update note ИЛИ создать новый ADR (supersedes portions of ADR-054 re: Forgejo migration).

Вне scope

  • ❌ Server-side фильтрация по head_sha параметром API (если Forgejo поддерживает — отдельный PR; сейчас client-side достаточно).
  • ❌ Фильтр по event (pull_request vs push vs schedule) — не нужен, фильтр по SHA + workflow покрывает.
  • ❌ Локальный ruff gate в create-pr.ts — отдельный issue (предотвращает попадание в CI, этот issue — починка оракула).
  • ❌ Утечка веток pr-N — отдельный issue.

Критерии приемки

  • Re-run scenario (old failed + new success, один SHA, один workflow) → pipeline-status возвращает DONE для CI фазы
  • Реальный failed (один run, failed) → NOT_DONE (без регрессии)
  • 2 разных workflows (один failed, один success) → NOT_DONE (без регрессии)
  • In-progress run → AMBIGUOUS (без регрессии)
  • Тест test_forgejo_ci_rollup_rerun_same_sha проходит
  • Существующие tests/test_pipeline_status_ci.py + tests/test_pipeline_status.py не ломаются
  • ruff format --check . && ruff check . && mypy .opencode/scripts/pipeline-status.py — green
  • ADR обновлён (054 update note) или создан новый ADR
## Контекст Зачем: после re-run failed jobs того же SHA оракул `_forgejo_ci_rollup` собирает и старый failed run, и новый success run для одного workflow → `_classify_rollup` видит FAILURE первым → возвращает NOT_DONE → pipeline блокируется. PR с фикс-коммитом, равным HEAD PR (или после re-run), не может пройти CI фазу. Зафиксировано на PR#46 (fix-коммит 2e490cb43126 = HEAD PR, в API mix runs: старый failed + новый success). Контекст: `_forgejo_ci_rollup` в `.opencode/scripts/pipeline-status.py:161-195` — единственный фильтр `r.get("head_sha") != sha: continue` (line 178). Дедупликации по workflow нет. API `/actions/tasks?limit=50` возвращает последние 50 runs (по id descending), включая устаревшие failed runs от re-runs с тем же `head_sha`. `_classify_rollup` (`pipeline-status.py:717-742`) работает с плоским списком conclusions — первый FAILURE → NOT_DONE, игнорирует новый SUCCESS. ADR-054 (`docs/decisions/054-pr-122-pipeline-status-statuscheckrollup.md:44-47`) ЯВНО отверг альтернативу (C) "SHA + all workflows" именно по причине "выбор правильного run из нескольких". После миграции на Forgejo (ADR-092/093) `statusCheckRollup` исчез, reimplementation в `_forgejo_ci_rollup` повторил ровно ту проблему, которую ADR-054 называл причиной отказа от (C). Тестов на re-run scenario НЕТ: `tests/test_pipeline_status_ci.py` мокают выход `_forgejo_ci_rollup` (готовый JSON rollup), а не саму функцию. Тест `test_ci_partial_failure` (lines 445-466) — про 2 РАЗНЫХ check'а, не про 2 runs одного workflow одного SHA. Тестов `_forgejo_ci_rollup` вообще нет. Re-run semantics Forgejo/GitHub Actions: "Re-run failed jobs" / "Re-run all jobs" создаёт НОВЫЙ run с тем же `head_sha`, старый run остаётся в финальном статусе FAILURE. `cancel-in-progress: true` в `.github/workflows/ci.yml:13` работает только на новый push (новый commit), не на re-run. ## Задача 1. В `.opencode/scripts/pipeline-status.py` функция `_forgejo_ci_rollup` (lines 161-195) — после фильтра по `head_sha` (line 178) добавить дедупликацию "latest run per workflow": - Группировать отфильтрованные runs по полю `name` (имя workflow; проверить что Forgejo `/actions/tasks` возвращает это поле — если нет, использовать `workflow_id` или комбинацию). Скорее всего API возвращает runs отсортированными по id descending (стандарт GitHub Actions) — тогда первый match per workflow = latest. - В каждой группе оставить только run с наибольшим `id` (поле `id` точно есть, используется в `_forgejo_run_view` line 398). - В rollup (lines 176-194) добавлять только эти "latest per workflow" runs. 2. Добавить тест `test_forgejo_ci_rollup_rerun_same_sha` в `tests/test_pipeline_status_ci.py` — мокает `_forgejo_request` чтобы вернуть 2 runs одного workflow с одинаковым `head_sha`: old (id=100, status=failure, conclusion=failure) + new (id=200, status=success, conclusion=success). Ожидаемый verdict: DONE (CI green, latest run success). Мокировать через `subprocess.run` patch (как существующие `test_forgejo_*` тесты в `tests/test_pipeline_status.py:1281-1462`). 3. Обновить ADR-054 (`docs/decisions/054-pr-122-pipeline-status-statuscheckrollup.md`) — добавить секцию "Update YYYY-MM-DD: after Forgejo migration, `_forgejo_ci_rollup` deduplicates runs by workflow name (latest by id) to handle re-run scenario" с ссылкой на этот PR. ИЛИ создать новый ADR (номер определи через `ls docs/decisions/` — следующий свободный `NNN-pr-<this_pr>-forgejo-ci-rollup-dedup.md`). ## Контракты - `_forgejo_ci_rollup(repo, sha)` возвращает `tuple[int, str, str]` — без изменений в signature. - При re-run (old failed + new success, один SHA, один workflow) → verdict DONE (CI green). - При реальном failed (один run, failed) → verdict NOT_DONE (без изменений). - При 2 разных workflows (ci.yml failed + always-ci.yml success) → verdict NOT_DONE (без изменений — это разные checks, не re-run). - При in_progress run → verdict AMBIGUOUS (без изменений — polling). - API endpoint: `GET /api/v1/repos/{repo}/actions/tasks?limit=50` — без изменений (server-side фильтрация НЕ добавляется, дедупликация client-side). ## Инварианты - Дедупликация ТОЛЬКО по workflow name (или workflow_id если name отсутствует). НЕ по `head_branch` (он одинаковый для re-runs) и НЕ по `event` (он одинаковый для re-runs). - Latest = max `id` (числовое сравнение через `int(r.get("id", 0))`). - Если поле `name` отсутствует в API ответе — fallback на `workflow_id`, потом на `id` (каждый run сам по себе, без дедупликации — корректно для edge case). - Существующая логика mapping status/conclusion → rollup entry (lines 180-194) НЕ меняется — меняется только то, какие runs попадают в цикл. ## Граничные случаи - API возвращает пустой `workflow_runs: []` → rollup пустой → `_classify_rollup` возвращает AMBIGUOUS "no checks found in rollup" (без изменений). - Все runs с другим `head_sha` (нет matching) → rollup пустой → AMBIGUOUS (без изменений). - Re-run создал run с status=in_progress (ещё не завершился) → latest per workflow = in_progress → AMBIGUOUS "CI in progress" (правильно — ждём завершения). - 2 runs с одинаковым `id` (невозможно в API, но defensive) → первый побеждает (dict insert order). - Поле `name` есть, но пустая строка → fallback на `workflow_id`, потом на `id`. ## Влияние на связанные компоненты - `pipeline-status.ts` (`.opencode/tools/pipeline-status.ts`) — без изменений (TS wrapper только spawnSync'ит Python). - `tests/test_pipeline_status_ci.py` — добавить `test_forgejo_ci_rollup_rerun_same_sha` (мок `_forgejo_request` через monkeypatch `ps._forgejo_request`). - `tests/test_pipeline_status.py` — существующие 9 `test_forgejo_*` (lines 1281-1462) тестируют `_forgejo_request` HTTP-слой, НЕ затрагиваются (дедупликация поверх, не в HTTP-слое). - `run-pipeline/SKILL.md` Template D (fix_ci) — без изменений (поведение оракула улучшается, протокол тот же). - ADR-054 (`docs/decisions/054-pr-122-pipeline-status-statuscheckrollup.md`) — добавить update note ИЛИ создать новый ADR (supersedes portions of ADR-054 re: Forgejo migration). ## Вне scope - ❌ Server-side фильтрация по `head_sha` параметром API (если Forgejo поддерживает — отдельный PR; сейчас client-side достаточно). - ❌ Фильтр по `event` (pull_request vs push vs schedule) — не нужен, фильтр по SHA + workflow покрывает. - ❌ Локальный ruff gate в `create-pr.ts` — отдельный issue (предотвращает попадание в CI, этот issue — починка оракула). - ❌ Утечка веток `pr-N` — отдельный issue. ## Критерии приемки - [ ] Re-run scenario (old failed + new success, один SHA, один workflow) → `pipeline-status` возвращает DONE для CI фазы - [ ] Реальный failed (один run, failed) → NOT_DONE (без регрессии) - [ ] 2 разных workflows (один failed, один success) → NOT_DONE (без регрессии) - [ ] In-progress run → AMBIGUOUS (без регрессии) - [ ] Тест `test_forgejo_ci_rollup_rerun_same_sha` проходит - [ ] Существующие `tests/test_pipeline_status_ci.py` + `tests/test_pipeline_status.py` не ломаются - [ ] `ruff format --check . && ruff check . && mypy .opencode/scripts/pipeline-status.py` — green - [ ] ADR обновлён (054 update note) или создан новый ADR
Sign in to join this conversation.
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#48
No description provided.