Files
wiki/tasks/orchestrator/reports/dev-2026-06-04-deploy-gate-path-fix.md
2026-06-04 13:40:01 +03:00

8.7 KiB
Raw Blame History

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 (стр. 1324):

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, ДО):

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):

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

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.pyimport 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: admin/orchestrator#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.