126 lines
8.7 KiB
Markdown
126 lines
8.7 KiB
Markdown
# Dev Report: orchestrator — ложный FAILED деплоя (рассинхрон путей 14-deploy-log.md)
|
||
Дата: 2026-06-04
|
||
Статус: DONE
|
||
|
||
## Задача
|
||
QG `check_deploy_status` ложно заворачивал успешные деплои в FAILED и откатывал
|
||
`deploy → development`. Причина: deployer пишет `14-deploy-log.md` (deploy_status: SUCCESS)
|
||
и мержит артефакты в **main** отдельным PR, а гейт читает лог из **worktree ветки фичи**
|
||
через `_repo_path(repo, branch)` — там лога нет → «Deploy log not found» → FAILED.
|
||
Починить ТОЛЬКО поиск лога деплоя. НЕ трогать merge-gate (`gitea.py`, `current_stage=="deploy"`).
|
||
|
||
## ШАГ 0 — подтверждение по коду (verbatim)
|
||
|
||
### Где гейт читал лог (ДО фикса) — `src/qg/checks.py`
|
||
`_repo_path` (стр. 13–24):
|
||
```python
|
||
def _repo_path(repo: str, branch: str | None = None) -> str:
|
||
if branch:
|
||
wt = get_worktree_path(repo, branch)
|
||
if os.path.isdir(wt):
|
||
return wt
|
||
return os.path.join(settings.repos_dir, repo)
|
||
```
|
||
`check_deploy_status` (стр. ~284, ДО):
|
||
```python
|
||
repo_path = _repo_path(repo, branch)
|
||
log_path = os.path.join(repo_path, f"docs/work-items/{work_item_id}/14-deploy-log.md")
|
||
if not os.path.isfile(log_path):
|
||
return False, "Deploy log not found (14-deploy-log.md)"
|
||
```
|
||
`get_worktree_path` (`src/git_worktree.py:33`):
|
||
```python
|
||
def get_worktree_path(repo: str, branch: str) -> str:
|
||
return os.path.join(settings.worktrees_dir, repo, _safe(branch))
|
||
```
|
||
→ при наличии ветки путь = `/repos/_wt/<repo>/<safe-branch>/docs/work-items/<WI>/14-deploy-log.md`
|
||
(worktree ФИЧИ). Отсутствие файла → текст «Deploy log not found (14-deploy-log.md)» → FAILED.
|
||
|
||
### Где деплоер пишет лог (verbatim подтверждение)
|
||
- `src/usage.py:328`: `"deployer": ("Deploy log", "14-deploy-log.md")` — артефакт деплоера.
|
||
- Промпт деплоера лежит в целевом репо (`.openclaw/agents/deployer.md` в enduro-trails),
|
||
не в orchestrator. Лог пишется и **мержится в main отдельным PR** (для ET-013 — PR #27,
|
||
commit `4e925cc`).
|
||
- **Живое подтверждение в проде:** в shared-клоне `/home/slin/repos/enduro-trails`:
|
||
```
|
||
$ git show origin/main:docs/work-items/ET-013/14-deploy-log.md
|
||
---
|
||
deploy_status: SUCCESS
|
||
version: v0.0.5
|
||
work_item: ET-013
|
||
pr: 26
|
||
merge_commit: be7a052
|
||
```
|
||
При этом в worktree ветки фичи файла нет → отсюда ложный FAILED.
|
||
|
||
**Рассинхрон подтверждён:** пишется/мержится в `origin/main`, читается из worktree фичи.
|
||
|
||
## Выбранный вариант: РЕКОМЕНДУЕМЫЙ (robust, origin/main fallback)
|
||
|
||
Выбран вариант с чтением из `origin/main`, а не «деплоер пишет в worktree», потому что:
|
||
1. Меняет только логику чтения в `check_deploy_status` — ноль изменений в пайплайне
|
||
деплоера/мержа артефактов (меньше риска, ничего нового не ломает).
|
||
2. Переживает текущее поведение «деплоер мержит лог в main отдельным PR» как есть.
|
||
3. Не зависит от состояния worktree (может быть зачищен/переаллоцирован к моменту гейта).
|
||
4. Альтернатива (писать в worktree ДО гейта) потребовала бы менять промпт деплоера в
|
||
целевом репо + порядок merge артефактов — больше поверхности изменений.
|
||
|
||
### Логика (ПОСЛЕ фикса) — порядок поиска: worktree → origin/main → not found
|
||
```python
|
||
repo_path = _repo_path(repo, branch)
|
||
log_path = os.path.join(repo_path, f"docs/work-items/{work_item_id}/14-deploy-log.md")
|
||
if os.path.isfile(log_path):
|
||
...read & _parse_deploy_status(content) # регресс сохранён
|
||
main_content = _deploy_log_from_main(repo, work_item_id) # git fetch origin main + git show
|
||
if main_content is not None:
|
||
return _parse_deploy_status(main_content) # ET-013 fix: SUCCESS в main → PASS
|
||
return False, "Deploy log not found (14-deploy-log.md)"
|
||
```
|
||
`_deploy_log_from_main`: использует shared-клон `settings.repos_dir/<repo>` (НЕ worktree),
|
||
`git -C <clone> fetch origin main` (timeout 30s) + `git show origin/main:docs/work-items/<WI>/14-deploy-log.md`
|
||
(timeout 15s). Любая ошибка git (нет клона / нет `.git` / сеть / нет файла в main) →
|
||
возвращает `None` (деградация к «not found»), **никогда не бросает исключение**.
|
||
Парсинг frontmatter вынесен в `_parse_deploy_status` (читает только `deploy_status:`).
|
||
|
||
## Изменённые файлы
|
||
- `src/qg/checks.py` — `import subprocess`; новые `_parse_deploy_status`,
|
||
`_deploy_log_from_main`; `check_deploy_status` с origin/main-fallback.
|
||
- `tests/test_qg.py` — 5 новых тестов в `TestCheckDeployStatus`.
|
||
|
||
## Тесты
|
||
- worktree `SUCCESS` → PASS (регресс не сломан) — `test_success_verdict_passes` (+short-circuit тест).
|
||
- нет в worktree, `SUCCESS` в origin/main → PASS (**ET-013 fix**) — `test_origin_main_success_passes_when_absent_in_worktree`.
|
||
- `FAILED` в main → FAILED — `test_origin_main_failed_fails`.
|
||
- нет нигде → not found — `test_absent_everywhere_fails`.
|
||
- fetch падает (TimeoutExpired) → деградация без исключения — `test_fetch_failure_degrades_no_exception`.
|
||
- worktree-лог короткозамыкает origin/main lookup — `test_worktree_log_short_circuits_main_lookup`.
|
||
|
||
### Вывод pytest (на проде, в контейнере orchestrator против `/repos/orchestrator`)
|
||
- `tests/test_qg.py`: **33 passed** (было 28 + 5 новых).
|
||
- Полный suite (детерминированно, `-p no:randomly`): **277 passed, 9 failed**.
|
||
- 9 failed = те же off-limits HMAC/401 в `test_webhooks.py` (НЕ чинить).
|
||
- Baseline на чистом дереве: **272 passed, 9 failed**. Дельта = +5 (мои тесты), 0 новых падений.
|
||
- Списки FAILED у baseline и у фикса **идентичны** (сравнил отсортированно).
|
||
- Прим.: один прогон без `no:randomly` дал «10 failed/276 passed» — это flaky-ordering
|
||
в 401-webhook-тестах, не связано с фиксом (детерминированный прогон стабильно 9+277).
|
||
|
||
## Результат
|
||
- Ветка `fix/deploy-gate-log-path` от актуального origin/main.
|
||
- Commit: `4e4cc6c`.
|
||
- **PR #23** в Gitea: https://git.mva154.duckdns.org/admin/orchestrator/pulls/23 (НЕ смержен, на ревью).
|
||
- Push в main НЕ делал. merge-gate `gitea.py` НЕ тронут. Off-limits (PLANE_STATES,
|
||
set_issue_done, launcher:475, HMAC, queue, usage_comment, cost_usd, трекер, миграции) — не тронуты.
|
||
- Живая валидация ET-013: `_deploy_log_from_main("enduro-trails","ET-013")` фетчит и
|
||
читает `deploy_status: SUCCESS` из origin/main → теперь PASS (раньше был ложный FAILED).
|
||
|
||
## Путь до/после (verbatim)
|
||
- **ДО (читал):** `_repo_path(repo, branch)` → `/repos/_wt/<repo>/<safe-branch>/docs/work-items/<WI>/14-deploy-log.md` (worktree фичи; файла нет).
|
||
- **Деплоер писал:** `docs/work-items/<WI>/14-deploy-log.md` → мержил в `origin/main` отдельным PR.
|
||
- **ПОСЛЕ (читает):** worktree (как раньше) → если нет, fallback `git show origin/main:docs/work-items/<WI>/14-deploy-log.md` на shared-клоне `settings.repos_dir/<repo>` → если нет нигде, «not found».
|
||
|
||
## Проблемы и решения
|
||
- Тесты не в образе контейнера (в `/app` запечён только `src/`). Прогонял в контейнере
|
||
против смонтированного host-репо: `docker exec orchestrator sh -c 'cd /repos/orchestrator && python -m pytest ...'`.
|
||
- Кажущийся +1 fail в одном прогоне — flaky-ordering 401-webhook тестов; подтверждено
|
||
детерминированным `-p no:randomly` (стабильно 9 failed) и идентичными списками FAILED.
|