architect(ET): auto-commit from architect run_id=381
This commit is contained in:
@@ -140,25 +140,44 @@ merge-в-main вообще**. Detached host-деплой лишь retag'ал о
|
||||
- **Merge в Phase C (после рестарта), НЕ в Phase B** — finalizer restart-surviving (claim воркером
|
||||
нового контейнера, re-drive reaper'ом), merge физически строго ПОСЛЕ рестарта прода → рестарт его
|
||||
не убивает (G3 «шаг, переживающий рестарт»; постмортем-урок №3).
|
||||
- **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (no-op повтор, ORCH-065) → иначе
|
||||
Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Никогда push/force-push в `main`.
|
||||
- **Верификатор `merge_gate.verify_merged_to_main`** — `PR.merged==true` ИЛИ
|
||||
`git merge-base --is-ancestor <validated_sha> origin/main` (`validated_revision` — тот же якорь,
|
||||
что у ORCH-058). never-raise → `False`.
|
||||
- **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (idempotency no-op повтор) → иначе
|
||||
Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Выбор PR строго по `head.ref==branch`
|
||||
И `base.ref=="main"`. Никогда push/force-push в `main`.
|
||||
- **Верификатор `merge_gate.verify_merged_to_main` (семантика ORCH-073, FR-1):** подтверждение —
|
||||
**ТОЛЬКО** `git merge-base --is-ancestor <validated_sha> origin/main` (`validated_revision` —
|
||||
якорь ORCH-058). PR-флаг `pr_already_merged` **больше НЕ подтверждает merge** (удалён из verify):
|
||||
он понижен до idempotency-guard `merge_pr` и засчитывает merged PR лишь при `head.ref==branch`
|
||||
И `base.ref=="main"` (исключает авто docs-PR). Пустой SHA / git-ошибка → `False` (fail-closed),
|
||||
never-raise.
|
||||
- **Регресс-гард целостности `main` (ORCH-073, FR-5):** `merge_gate.check_main_regression` в
|
||||
`_handle_merge_verify` ПОСЛЕ подтверждённого SHA-в-main и ДО `done` проверяет, что `origin/main`
|
||||
содержит декларативный набор маркеров ранее-merged задач (`MAIN_REGRESSION_MARKERS`,
|
||||
`git grep -c <marker> origin/main -- <path>` > 0). Маркер отсутствует → alert «main regressed» +
|
||||
HOLD (НЕ `done`, ALERT-only). Fail-open на git-ошибке грепа (регресс — только при `count==0`).
|
||||
Kill-switch `regression_guard_enabled`; non-self → no-op. Набор — append-only константа,
|
||||
значимая задача дописывает свой маркер.
|
||||
- **Не подтверждено → alert «deploy succeeded but not merged» (Telegram+Plane) + HOLD**
|
||||
(`set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на `development` — not-merged есть
|
||||
инфра-дефект, реакция ALERT-only как ORCH-021 self-hosting). Подтверждено → штатный `deploy →
|
||||
done` + `merged_to_main: true` во frontmatter `14-deploy-log.md` (`deploy_status:` нетронут).
|
||||
- **Защита от CHANGELOG-затирания (ORCH-073, FR-4):** корневой `.gitattributes` с
|
||||
`CHANGELOG.md merge=union` → правки `## [Unreleased]` авто-сливаются при `auto_rebase_onto_main`
|
||||
без конфликта, ветка не откатывается в `development` и не тащит устаревший код-сосед. `docs/**`
|
||||
под union НЕ ставится (union только для append-only).
|
||||
- **Условность как ORCH-35/43/58:** `merge_verify_enabled` (kill-switch, дефолт `true`) +
|
||||
`merge_verify_repos` (пусто → только self-hosting); non-self — no-op, merge остаётся за `deployer`.
|
||||
never-raise; идемпотентность (`pr_already_merged`, INV-5); ручной approve сохранён (`Confirm Deploy`).
|
||||
never-raise; идемпотентность по **SHA-в-main** (INV-4, не «любой merged PR»); ручной approve
|
||||
сохранён (`Confirm Deploy`).
|
||||
- **Инварианты:** `STAGE_TRANSITIONS`, `check_deploy_status`/`_parse_deploy_status`, реестр
|
||||
`QG_CHECKS` (под-гейт — врезка в `advance_stage`, НЕ новый зарегистрированный QG), схема БД,
|
||||
БАГ-8, terminal-sync, merge-gate, image-freshness, exit-коды хука — **без изменений**.
|
||||
Диагностика фантома — runbook `docs/operations/PHANTOM_MERGE_RUNBOOK.md` (4 проверки постмортема).
|
||||
|
||||
Подробнее: [adr-0013](adr/adr-0013-merge-verify-gate.md), детально —
|
||||
`docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`.
|
||||
Подробнее: [adr-0013](adr/adr-0013-merge-verify-gate.md) +
|
||||
[adr-0014](adr/adr-0014-merge-verify-sha-source-of-truth.md) (amends 0013 — SHA-в-main как
|
||||
единственный критерий + регресс-гард, ORCH-073); детально —
|
||||
`docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`,
|
||||
`docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`.
|
||||
|
||||
### Post-deploy наблюдение прода + реакция на деградацию (ORCH-021 — реализовано)
|
||||
Конвейер заканчивался на `deploy → done` и **забывал про прод**: «успех» = health-check
|
||||
|
||||
@@ -17,11 +17,15 @@ Per-work-item решения живут в `docs/work-items/<id>/06-adr/ADR-NNN-
|
||||
| adr-0009 | Толерантность staging-вердикта к инфраструктурным FAIL | accepted | 2026-06-07 | ORCH-061 |
|
||||
| adr-0010 | Post-deploy мониторинг прода + реакция на деградацию | proposed | 2026-06-07 | ORCH-021 |
|
||||
| adr-0011 | Job-reaper + проактивный реклейм merge-lease | accepted | 2026-06-07 | ORCH-065 |
|
||||
| adr-0012 | Security-гейт (secrets/deps) | accepted | 2026-06-08 | ORCH-022 |
|
||||
| adr-0013 | Merge-в-main + пост-деплой верификация как условие `done` | accepted | 2026-06-08 | ORCH-071 |
|
||||
| adr-0014 | SHA-в-main — единственный критерий merge-verify + регресс-гард | accepted | 2026-06-08 | ORCH-073 |
|
||||
|
||||
> ⚠️ Историческая коллизия: номер `0007` занят двумя файлами —
|
||||
> `adr-0007-reconciler.md` (ORCH-053) и `adr-0007-executable-self-deploy.md`
|
||||
> (ORCH-036). Оба accepted; для новых сквозных ADR использовать следующий
|
||||
> свободный номер (текущий максимум — `0011`).
|
||||
> свободный номер (текущий максимум — `0014`).
|
||||
> adr-0014 **amends** adr-0013 (меняет критерий merge-verify на «SHA-в-main»).
|
||||
|
||||
## Формат
|
||||
**Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# adr-0014: SHA-в-main — единственный критерий merge-verify + регресс-гард целостности `main`
|
||||
|
||||
- **Статус:** accepted
|
||||
- **Дата:** 2026-06-08
|
||||
- **Задача:** ORCH-073 (BUG CRITICAL — эрозия `main`)
|
||||
- **Amends:** [adr-0013](adr-0013-merge-verify-gate.md) (ORCH-071) — меняет КРИТЕРИЙ подтверждения merge.
|
||||
- **Детальный ADR:** `docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`
|
||||
- **Постмортем:** `docs/history/LESSONS_2026-06-08_phantom-merge.md`
|
||||
|
||||
## Контекст
|
||||
|
||||
adr-0013 (ORCH-071) ввёл под-гейт merge-verify на ребре `deploy → done`, но допускал
|
||||
подтверждение merge по **ИЛИ-критерию**: `verify_merged_to_main` возвращал `True`, если
|
||||
`pr_already_merged(repo, branch)` **ЛИБО** SHA — предок `origin/main`. `pr_already_merged`
|
||||
засчитывал **любой** merged PR ветки, включая авто docs-PR (staging/deploy-логи). У одной
|
||||
feature-ветки в `main` сливались только docs-PR, а code-PR — нет → `pr_already_merged`=`True` →
|
||||
verify `CONFIRMED` → `done`, хотя кода в `main` не было. Накопительно потеряны ORCH-067 (ссылки
|
||||
`plane_issue_link`) и ORCH-069 (`qg0_title_max`). Вторичный усилитель — CHANGELOG-ребейзы,
|
||||
откатывающие ветку и тащащие устаревший код-сосед. Восстановление кода (G1) выполнено вручную
|
||||
restore-PR #76; этот ADR устраняет корень навсегда.
|
||||
|
||||
## Решение
|
||||
|
||||
1. **SHA-в-main — единственный критерий (FR-1).** `verify_merged_to_main(repo, branch, sha)`
|
||||
подтверждает merge **ТОЛЬКО** прямым фактом `git merge-base --is-ancestor <sha> origin/main`
|
||||
(после `git fetch origin main`). OR-ветка `pr_already_merged` **удалена** из верификатора.
|
||||
Пустой `sha` / любая git-ошибка → `False` (fail-closed: alert + HOLD). never-raise (INV-1).
|
||||
2. **`pr_already_merged` → idempotency-guard, различающий code-PR/docs-PR (FR-2).** Засчитывает
|
||||
merged PR только при `head.ref==<feature-branch>` И `base.ref=="main"` (явный фильтр в цикле,
|
||||
не ненадёжный query-параметр `head`). Используется лишь как защита `merge_pr` от второго merge,
|
||||
НЕ как подтверждение `done`.
|
||||
3. **`merge_pr` сливает именно code-ветку (FR-3).** Выбор открытого PR по `head.ref==branch` И
|
||||
`base.ref=="main"`; merge только Gitea `POST /pulls/{index}/merge`, никогда push/force-push в
|
||||
`main`. Источник истины «слилось» — FR-1.
|
||||
4. **Регресс-гард целостности `main` (FR-5).** Новая `merge_gate.check_main_regression`,
|
||||
вызываемая в `_handle_merge_verify` ПОСЛЕ подтверждённого SHA-в-main и ДО `done`: проверяет, что
|
||||
`origin/main` содержит **декларативный набор маркеров** ключевых функций ранее-merged задач
|
||||
(`git grep -c <marker> origin/main -- <path>` > 0). Маркер отсутствует → **alert «main
|
||||
regressed» + HOLD** (НЕ `done`, БЕЗ авто-отката на `development` — инфра-дефект, ALERT-only как
|
||||
ORCH-021/071). Набор — append-only константа `MAIN_REGRESSION_MARKERS` в `merge_gate.py`
|
||||
(расширяется каждой значимой задачей). **Fail-open** на git-ошибке самого грепа (регресс
|
||||
утверждается только при детерминированном `count==0`); первичный фейл-клозед — SHA-в-main.
|
||||
Kill-switch `regression_guard_enabled` (дефолт `true`); non-self → no-op.
|
||||
5. **`.gitattributes CHANGELOG.md merge=union` (FR-4).** В корне репо; авто-слияние правок
|
||||
`## [Unreleased]` без конфликта → `auto_rebase_onto_main` не откатывает ветку и не тащит
|
||||
устаревший код-сосед. `docs/**/*.md` под union **НЕ** ставится (union только для append-only;
|
||||
доки переписываются построчно).
|
||||
|
||||
## Инварианты
|
||||
|
||||
never-raise на verify/merge/регресс-гарде (ошибка → alert/HOLD, не падение); прод 8500 не
|
||||
рестартится/не падает в рамках merge; merge только Gitea PR-API без force-push в `main`; ручной
|
||||
`Confirm Deploy` (ORCH-059) сохранён; идемпотентность по «SHA-в-main», а не по «любому merged PR»;
|
||||
non-self репо (enduro) — merge/verify/регресс-гард без изменений. `STAGE_TRANSITIONS`, реестр
|
||||
`QG_CHECKS`, `check_deploy_status`, схема БД, внешние HTTP-эндпоинты — **без изменений**.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- Сохранить PR-флаг как со-критерий verify (с фильтром head/base) — отклонено: PR можно слить и
|
||||
тут же откатить ребейзом-соседом; надёжен только факт «SHA в main».
|
||||
- `docs/**/*.md merge=union` — отклонено: тихая дубликация строк в переписываемых доках.
|
||||
- Регресс-гард с авто-откатом / хранением маркеров в БД/Plane — отклонено (Не-цель «не менять
|
||||
схему БД/Plane»; реакция ALERT-only).
|
||||
- Fail-closed на marker-grep — отклонено: ложный HOLD при git-сбое; marker-grep вторичен.
|
||||
|
||||
## Последствия
|
||||
|
||||
Невозможно «`done` + прод задеплоен, а code-PR не в `main`». Ложно-зелёный по docs-PR устранён в
|
||||
корне. CHANGELOG-конфликты больше не откатывают ветку. Регресс соседнего кода ловится отдельным
|
||||
гардом. Минус: при недоступной Gitea/git verify консервативно `False` → возможен ложный HOLD+alert
|
||||
(снимается повтором; fail-closed для `done` приоритетен). Набор маркеров требует дисциплины —
|
||||
значимая задача дописывает свой маркер.
|
||||
|
||||
## Связи
|
||||
|
||||
- Amends adr-0013 (ORCH-071), наследует adr-0006 (merge-gate), adr-0011 (job-reaper/lease).
|
||||
- Детально: `docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`.
|
||||
@@ -0,0 +1,214 @@
|
||||
# ADR-001 (ORCH-073): SHA-в-main как единственный критерий merge-verify + регресс-гард + `.gitattributes`
|
||||
|
||||
- **Статус:** Accepted
|
||||
- **Дата:** 2026-06-08
|
||||
- **Задача:** ORCH-073 (BUG CRITICAL — эрозия `main`)
|
||||
- **Связь:** усиливает/чинит ORCH-071 (merge-verify под-гейт). Сквозной аналог — `docs/architecture/adr/adr-0014-merge-verify-sha-source-of-truth.md` (amends adr-0013).
|
||||
- **Источники:** `01-brd.md` (root-cause git-аудит 08.06), `02-trz.md` (FR-1…FR-5), `03-acceptance-criteria.md` (AC-1…AC-11).
|
||||
|
||||
## Контекст
|
||||
|
||||
Код «задеплоенных» и переведённых в `done` задач **ORCH-067** (`plane_issue_link`, кликабельные
|
||||
ссылки, tracker bump) и **ORCH-069** (`qg0_title_max`) физически отсутствовал в `origin/main`,
|
||||
хотя обе прошли весь конвейер, Confirm Deploy, merge-verify `CONFIRMED` и стали `done`. В `main`
|
||||
попадали только их **docs-коммиты** (staging/deploy-логи через отдельные авто docs-PR), но НЕ
|
||||
код feature-веток. Внешнее проявление (нашёл Слава, 08.06): в карточке Telegram сырой номер
|
||||
задачи вместо кликабельной ссылки — код ссылок есть в ветке ORCH-067, но не в `main`.
|
||||
|
||||
### Root cause (G4 audit) — подтверждён git-аудитом, НЕ гипотеза
|
||||
|
||||
1. **`verify_merged_to_main` подтверждает merge по ложному признаку.** Возвращает `True`, если
|
||||
`pr_already_merged(repo, branch)` **ЛИБО** `git merge-base --is-ancestor <sha> origin/main`.
|
||||
OR-ветка `pr_already_merged` — и есть дыра.
|
||||
2. **`pr_already_merged` засчитывает ЛЮБОЙ merged PR.** `GET /pulls?state=all&head=<branch>` и
|
||||
`True`, если **хоть один** PR `merged==True`. Параметр `head` у Gitea для одиночной строки-ветки
|
||||
фильтрует ненадёжно → в выборку попадают авто docs-PR (staging/deploy-логи) с других веток
|
||||
(`docs/*`). Сливается docs-PR → `pr_already_merged`=`True` → `verify_merged_to_main`=`True` →
|
||||
merge-verify `CONFIRMED` → `done`, хотя **code-PR НЕ слит**. Ложно-зелёный.
|
||||
3. **CHANGELOG-ребейзы — вторичный усилитель.** `auto_rebase_onto_main` при конфликте
|
||||
`CHANGELOG.md` откатывает `deploy-staging → development`; повторный ребейз ветки от старого
|
||||
`main` несёт устаревшие версии соседних файлов, которые при merge тихо затирают код-сосед
|
||||
(фантом-эффект как в ORCH-071, без конфликт-маркеров).
|
||||
|
||||
**G1 (восстановление кода) выполнено вручную** restore-PR #76 — `git grep` подтверждает в
|
||||
`origin/main` одновременно `plane_issue_link` (8), `qg0_title_max` (3+2), `verify_merged_to_main`
|
||||
(4). ORCH-073 фиксирует это в AC-1 и устраняет корень навсегда (FR-1…FR-5).
|
||||
|
||||
## Решение
|
||||
|
||||
Меняется **семантика merge-verify** (под-гейт ребра `deploy → done`, врезка `_handle_merge_verify`
|
||||
в `advance_stage`, введён ORCH-071). `STAGE_TRANSITIONS`, реестр `QG_CHECKS`,
|
||||
`check_deploy_status`/`_parse_deploy_status`, merge-gate (`check_branch_mergeable`),
|
||||
image-freshness, схема БД (`src/db.py`) — **НЕ меняются**. Внешние HTTP-эндпоинты `src/main.py` —
|
||||
**НЕ меняются**.
|
||||
|
||||
### Р-1 (FR-1, ядро) — `verify_merged_to_main`: SHA-в-main — единственный критерий
|
||||
|
||||
Подтверждение merge — **ТОЛЬКО** прямой факт «deployed commit является предком `origin/main`»:
|
||||
|
||||
```
|
||||
verify_merged_to_main(repo, branch, sha) -> bool:
|
||||
if not sha: # пустой SHA -> неопределённо
|
||||
log warning; return False # fail-closed (alert + HOLD)
|
||||
git fetch origin main (timeout merge_verify_timeout_s)
|
||||
rc = git merge-base --is-ancestor <sha> origin/main
|
||||
return rc == 0
|
||||
```
|
||||
|
||||
- **OR-ветка `pr_already_merged` удаляется** из `verify_merged_to_main`. PR-флаг больше **не
|
||||
подтверждает** merge.
|
||||
- Пустой `sha` → `False` (fail-closed: alert + HOLD), как сейчас.
|
||||
- never-raise: любая git-ошибка → `False` (INV-1) — фейл-клозед для `done`.
|
||||
|
||||
> Дизайн-выбор: вариант (б) из ТЗ §2 FR-2 — единственный источник истины «merged/done» — это
|
||||
> SHA-в-main. PR-флаги остаются только как **idempotency-guard** в `merge_pr` (Р-3), не как
|
||||
> подтверждение.
|
||||
|
||||
### Р-2 (FR-2/G2) — `pr_already_merged`: различает code-PR и docs-PR
|
||||
|
||||
`pr_already_merged` понижается до **idempotency-guard для `merge_pr`** (не источник истины для
|
||||
`done`). Но guard обязан быть **корректным**: «слит ли именно code-PR ЭТОЙ ветки», иначе merged
|
||||
docs-PR заставил бы `merge_pr` ошибочно сделать no-op и пропустить реальный merge кода.
|
||||
Поэтому в цикле явный фильтр (НЕ полагаться на ненадёжный query-параметр `head`):
|
||||
|
||||
```
|
||||
for pr in resp.json():
|
||||
if pr.merged is True
|
||||
and pr.head.ref == branch # код именно этой feature-ветки
|
||||
and pr.base.ref == "main": # таргет — main, не docs-база
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
- Исключает авто docs-PR (другой `head.ref`, напр. `docs/*`) и PR на не-`main` базу.
|
||||
- never-raise → `False` (консервативно).
|
||||
- Поведение для non-self репо (enduro) не меняется (INV-5) — `merge_pr`/verify для них как раньше.
|
||||
|
||||
### Р-3 (FR-3/G2) — `merge_pr`: сливает именно code-ветку
|
||||
|
||||
`merge_pr` уже выбирает открытый PR по `head.ref==branch`; добавляется фильтр `base.ref=="main"`
|
||||
при выборе PR (защита от слияния PR на чужую базу). Idempotency-guard `pr_already_merged` (Р-2,
|
||||
теперь корректный) перед merge оставляем — повторный прогон не делает второй POST. Merge —
|
||||
ТОЛЬКО Gitea `POST /pulls/{index}/merge`, никогда push/force-push в `main` (INV-2). После merge
|
||||
единственный источник истины «слилось» — FR-1 (SHA-в-main), его проверяет `_handle_merge_verify`.
|
||||
|
||||
### Р-4 (FR-5/G5) — регресс-гард целостности `main` (защита навсегда)
|
||||
|
||||
Новая детерминированная (no-LLM) функция в `merge_gate.py`, вызывается в `_handle_merge_verify`
|
||||
**ПОСЛЕ** подтверждённого SHA-в-main (Р-1) и **ДО** `update_task_stage(done)`:
|
||||
|
||||
```
|
||||
check_main_regression(repo, branch) -> tuple[bool, str]
|
||||
# ok=True -> регресса нет (набор маркеров цел) -> пропустить к done
|
||||
# ok=False -> маркер отсутствует -> "main regressed: <task/marker> missing"
|
||||
```
|
||||
|
||||
**Декларативный набор маркеров** — константа в `merge_gate.py` (append-only, расширяется каждой
|
||||
будущей задачей; НЕ БД, НЕ Plane — Не-цель):
|
||||
|
||||
```python
|
||||
MAIN_REGRESSION_MARKERS = [
|
||||
# (task, marker_substring, path)
|
||||
("ORCH-067", "plane_issue_link", "src/notifications.py"),
|
||||
("ORCH-069", "qg0_title_max", "src/config.py"),
|
||||
("ORCH-071", "verify_merged_to_main", "src/merge_gate.py"),
|
||||
("ORCH-073", "check_main_regression", "src/merge_gate.py"),
|
||||
]
|
||||
```
|
||||
|
||||
Проверка (в worktree после `git fetch origin main`): для каждого маркера
|
||||
`git grep -c <marker> origin/main -- <path>`; счётчик `0` → регресс.
|
||||
|
||||
- **Реакция при регрессе: ALERT-only + HOLD** (`set_issue_blocked` + Telegram + Plane-коммент
|
||||
«main regressed: code of `<task>` missing»), задача **НЕ `done`**, остаётся на `deploy`. БЕЗ
|
||||
авто-отката на `development` (это инфра-дефект, не код-фолт), симметрично not-merged ветке
|
||||
ORCH-071.
|
||||
- **Fail-OPEN на инфра-ошибке грепа** (намеренный trade-off): любая git/OS-ошибка самого грепа →
|
||||
`(True, "guard inconclusive: …")` → НЕ блокировать `done`. Обоснование: первичный фейл-клозед
|
||||
гейт — это SHA-в-main (Р-1); вторичный marker-grep не должен давать ложный HOLD на git-сбое.
|
||||
«Регресс» утверждается только при **детерминированном `count==0`**, не при «не смог определить».
|
||||
- never-raise (INV-1). Kill-switch — новый `regression_guard_enabled` (дефолт `true`,
|
||||
переиспользует область self-hosting через `merge_verify_applies`). Non-self репо — no-op (INV-5).
|
||||
|
||||
### Р-5 (FR-4/G4 корень) — `.gitattributes` с `merge=union`
|
||||
|
||||
В корне репозитория новый файл `.gitattributes`:
|
||||
|
||||
```
|
||||
CHANGELOG.md merge=union
|
||||
```
|
||||
|
||||
- `merge=union` — встроенный git-драйвер, доп. конфиг хоста не требуется; проверяется
|
||||
`git check-attr merge CHANGELOG.md` → `merge: union`.
|
||||
- Эффект: при `auto_rebase_onto_main` правки `## [Unreleased]` авто-сливаются (обе записи
|
||||
сохраняются) без конфликт-маркера → ветка не откатывается в `development` и не тащит устаревшие
|
||||
версии соседних файлов.
|
||||
- **Решено НЕ добавлять `docs/**/*.md merge=union`:** union годится только для строго
|
||||
append-only файлов; docs-артефакты (README, ADR, internals) регулярно **переписываются**
|
||||
построчно — union там тихо задублировал бы строки. Ограничиваемся `CHANGELOG.md`.
|
||||
- Оговорка о самозагрузке: задача, ВПЕРВЫЕ вносящая `.gitattributes`, при собственном ребейзе
|
||||
ещё не получает эффект union (атрибут попадёт в `main` только после её merge). Это допустимо —
|
||||
гард действует для всех последующих задач.
|
||||
|
||||
## Конфигурация
|
||||
|
||||
| Ключ | Дефолт | Назначение |
|
||||
|---|---|---|
|
||||
| `merge_verify_enabled` (есть) | `true` | kill-switch всего под-гейта |
|
||||
| `merge_verify_repos` (есть) | `""` | CSV; пусто → только self-hosting |
|
||||
| `merge_pr_timeout_s` / `merge_verify_timeout_s` (есть) | `60` | таймауты Gitea/git |
|
||||
| `regression_guard_enabled` (новый) | `true` | kill-switch регресс-гарда (Р-4); non-self → no-op |
|
||||
|
||||
Новый ключ задокументировать в `.env.example`. Дефолты безопасны (для non-self — no-op).
|
||||
|
||||
## Сигнатуры (внутренние; внешний API не меняется)
|
||||
|
||||
- `verify_merged_to_main(repo, branch, sha) -> bool` — семантика меняется (Р-1), сигнатура та же.
|
||||
- `pr_already_merged(repo, branch) -> bool` — назначение/фильтр уточняются (Р-2), сигнатура та же.
|
||||
- `merge_pr(repo, branch) -> tuple[bool, str]` — фильтр `base==main` (Р-3), сигнатура та же.
|
||||
- `check_main_regression(repo, branch) -> tuple[bool, str]` — **новая**, never-raise, fail-open.
|
||||
- `merge_verify_status()` — допустимо дополнить счётчиком регресс-алертов (read-only, не источник истины).
|
||||
|
||||
## Инварианты
|
||||
|
||||
- **INV-1** never-raise: ошибка верификации → alert/HOLD, не падение конвейера.
|
||||
- **INV-2** self-hosting safety: прод 8500 не падает/не рестартится в рамках merge; merge только
|
||||
Gitea PR-API, без force-push в `main`.
|
||||
- **INV-3** ручной `Confirm Deploy` (ORCH-059) сохранён.
|
||||
- **INV-4** идемпотентность опирается на «SHA-в-main», а не на «любой merged PR».
|
||||
- **INV-5** обратная совместимость non-self (enduro): merge/verify/регресс-гард — no-op.
|
||||
|
||||
## Альтернативы (отклонены)
|
||||
|
||||
1. **Оставить `pr_already_merged` как со-критерий verify, но фильтровать по `head/base`** —
|
||||
отклонено: PR-флаг всё равно слабее факта «SHA в main» (PR можно слить и тут же откатить
|
||||
ребейзом-соседом). Единственный надёжный критерий — предок-`main`. PR-флаг → только idempotency.
|
||||
2. **`docs/**/*.md merge=union`** — отклонено (см. Р-5): тихая дубликация строк в переписываемых
|
||||
доках.
|
||||
3. **Регресс-гард с авто-откатом на `development`** — отклонено: регресс соседнего кода —
|
||||
инфра-дефект merge, не код-фолт текущей задачи; реакция ALERT-only + HOLD (как ORCH-021/071).
|
||||
4. **Хранить набор маркеров в БД/Plane** — отклонено (Не-цель «не менять схему БД/Plane»);
|
||||
декларативная append-only константа в коде проще и версионируется вместе с фиксом.
|
||||
5. **Fail-closed на marker-grep** — отклонено: дало бы ложный HOLD при git-сбое; первичный
|
||||
фейл-клозед — SHA-в-main (Р-1), marker-grep вторичен → fail-open.
|
||||
|
||||
## Последствия
|
||||
|
||||
- **Плюс:** невозможно «`done` + прод задеплоен, а code-PR не в `main`» — единственный критерий
|
||||
`done` теперь «SHA-в-main». Ложно-зелёный по docs-PR устранён в корне (Р-1+Р-2+Р-3).
|
||||
- **Плюс:** CHANGELOG-конфликты больше не откатывают ветку и не тащат устаревший код-сосед (Р-5).
|
||||
- **Плюс:** регресс-гард ловит откат соседнего кода даже если SHA-в-main прошёл (Р-4).
|
||||
- **Минус:** при недоступной Gitea/git verify консервативно `False` → возможен ложный HOLD+alert
|
||||
(снимается повтором; fail-closed для `done` приоритетен). Регресс-гард при git-сбое наоборот
|
||||
fail-open (не блокирует) — осознанный trade-off, SHA-в-main остаётся первичным гейтом.
|
||||
- **Минус:** набор маркеров требует дисциплины — каждая значимая задача дописывает свой маркер
|
||||
(иначе гард его не защитит). Документируется в `CLAUDE.md`/README.
|
||||
|
||||
## Связи
|
||||
|
||||
- Amends: `docs/architecture/adr/adr-0013-merge-verify-gate.md` (ORCH-071) — меняет критерий verify.
|
||||
- Сквозной: `docs/architecture/adr/adr-0014-merge-verify-sha-source-of-truth.md`.
|
||||
- Постмортем: `docs/history/LESSONS_2026-06-08_phantom-merge.md`, runbook
|
||||
`docs/operations/PHANTOM_MERGE_RUNBOOK.md`.
|
||||
- AC: AC-1 (G1 markers), AC-2/AC-3 (Р-1/Р-2), AC-4 (Р-5), AC-5 (Р-4), AC-6 (happy-path),
|
||||
AC-7 (idempotency), AC-8/AC-9 (docs+audit), AC-10 (staging), AC-11 (self-hosting safety).
|
||||
32
docs/work-items/ORCH-073/07-infra-requirements.md
Normal file
32
docs/work-items/ORCH-073/07-infra-requirements.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 07 — Инфра-требования: ORCH-073
|
||||
|
||||
## Топология
|
||||
**Без изменений.** Один сервер (mva154), prod `orchestrator` (8500), staging
|
||||
`orchestrator-staging` (8501), общая SQLite, общая очередь. Новых контейнеров/портов/сервисов нет.
|
||||
|
||||
## Git / worktree
|
||||
- Новый корневой файл **`.gitattributes`** (`CHANGELOG.md merge=union`). Драйвер `union` —
|
||||
встроенный в git, **доп. конфигурация хоста НЕ требуется**.
|
||||
- Проверка применения в worktree агентов: `git check-attr merge CHANGELOG.md` → `merge: union`.
|
||||
Атрибут действует при 3-way merge/rebase, когда `.gitattributes` присутствует в дереве
|
||||
(`auto_rebase_onto_main` выполняет `git rebase origin/main` в per-branch worktree).
|
||||
- Самозагрузка: первая задача с `.gitattributes` своего ребейза не ускоряет (атрибут попадёт в
|
||||
`main` после её merge); эффект — для последующих задач. Допустимо.
|
||||
- Регресс-гард (`check_main_regression`) использует уже существующий per-branch worktree
|
||||
(`ensure_worktree` + `git fetch origin main` + `git grep origin/main`). Новых клонов/worktree нет.
|
||||
|
||||
## Сеть / внешние интеграции
|
||||
- Те же Gitea-эндпоинты: `GET /pulls`, `POST /pulls/{index}/merge`. Новых внешних вызовов нет.
|
||||
- Telegram/Plane — существующие хелперы alert (`send_telegram`, `set_issue_blocked`,
|
||||
`plane_add_comment`). Новых интеграций нет.
|
||||
|
||||
## Деплой self (self-hosting safety)
|
||||
- Прод-контейнер `orchestrator` (8500) **НЕ рестартить/не ронять** в рамках задачи.
|
||||
- Обязательный staging-гейт (8501) перед прод-деплоем; прод-деплой — только переводом на
|
||||
`Confirm Deploy` (ORCH-059). Ручной гейт не меняется.
|
||||
- Merge — только Gitea PR-API, без force-push в `main`.
|
||||
|
||||
## Конфигурация (хост `.env` / `.env.example`)
|
||||
- Новый ключ `regression_guard_enabled` (дефолт `true`) — задокументировать в `.env.example`.
|
||||
- Существующие `merge_verify_enabled`/`merge_verify_repos`/`merge_pr_timeout_s`/
|
||||
`merge_verify_timeout_s` — переиспользуются, без изменений значений.
|
||||
23
docs/work-items/ORCH-073/08-data-requirements.md
Normal file
23
docs/work-items/ORCH-073/08-data-requirements.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# 08 — Требования к данным/схеме БД: ORCH-073
|
||||
|
||||
## Схема БД
|
||||
**Без изменений.** `src/db.py` не трогается (Не-цель BRD §5, ТЗ §4). Новых таблиц/колонок/
|
||||
миграций нет.
|
||||
|
||||
## Источник истины merge-verify
|
||||
- Подтверждение `done` опирается **только на git** (`origin/main`: `git merge-base
|
||||
--is-ancestor <sha> origin/main`), НЕ на состояние БД и НЕ на Plane-статусы.
|
||||
- Регресс-гард (`check_main_regression`) опирается на `git grep origin/main` по декларативному
|
||||
набору маркеров — **не на БД**.
|
||||
- Набор маркеров `MAIN_REGRESSION_MARKERS` — **append-only константа в коде** (`src/merge_gate.py`),
|
||||
версионируется вместе с фиксом. Сознательно НЕ в БД и НЕ в Plane (Не-цель).
|
||||
|
||||
## Состояние в БД (читается, не меняется)
|
||||
- `tasks.stage` — переходы через существующий `update_task_stage`/`advance_stage`; HOLD = задача
|
||||
остаётся на `deploy` (не записывается `done`). Семантика та же, что у ORCH-071.
|
||||
- Счётчики `_MERGE_VERIFY_COUNTERS` — **in-process**, не БД; read-only через `GET /queue`.
|
||||
Допустимо дополнить счётчиком регресс-алертов (наблюдаемость, не источник истины).
|
||||
|
||||
## Plane
|
||||
**Без изменений** (Не-цель). Используются существующие сеттеры (`set_issue_blocked`,
|
||||
`plane_add_comment`) для alert/HOLD. Новых статусов/маппингов нет.
|
||||
19
docs/work-items/ORCH-073/10-tech-risks.md
Normal file
19
docs/work-items/ORCH-073/10-tech-risks.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 10 — Технические риски: ORCH-073
|
||||
|
||||
| # | Риск | Вероятность | Влияние | Митигация |
|
||||
|---|------|-------------|---------|-----------|
|
||||
| R-1 | **Ложный HOLD на сбое Gitea/git** — verify консервативно `False` при недоступности → задача не доходит до `done`, нужен повтор. | средняя | среднее | Осознанный fail-closed для `done` (приоритет: не дать ложно-зелёный). Снимается re-drive (reaper/reconciler/re-approve). Документировано в ADR «Последствия». |
|
||||
| R-2 | **`pr_already_merged` всё ещё ловит docs-PR** при иной структуре head/base в Gitea (cross-repo `owner:branch`). | низкая | высокое (возврат бага) | Явный фильтр в цикле `head.ref==branch И base.ref=="main"` (не полагаться на query-param). Тест AC-2/AC-3 мокает merged docs-PR и проверяет, что verify=`False`. |
|
||||
| R-3 | **Регресс-гард fail-open пропустит реальный регресс** во время git-сбоя грепа. | низкая | среднее | Первичный гейт `done` — SHA-в-main (fail-closed). Marker-grep вторичен; «регресс» — только при детерминированном `count==0`. Trade-off зафиксирован в ADR. |
|
||||
| R-4 | **Набор маркеров устаревает/неполный** — будущая задача не добавила свой маркер → гард её не защищает. | средняя | среднее | Append-only константа в коде + правило в `CLAUDE.md`/README «значимая задача дописывает маркер». Reviewer проверяет. Не регресс существующего поведения (только недозащита нового). |
|
||||
| R-5 | **`merge=union` тихо дублирует строки** при применении к не-append-only файлам. | низкая | среднее | Union строго ограничен `CHANGELOG.md`; `docs/**` под union НЕ ставится (решение Р-5 ADR). |
|
||||
| R-6 | **Самозагрузка `.gitattributes`** — первая задача не получает эффект union на своём ребейзе. | высокая (одноразово) | низкое | Принято: атрибут попадёт в `main` после merge ORCH-073, действует для последующих задач. Для самой ORCH-073 CHANGELOG-конфликт разрешается вручную при необходимости. |
|
||||
| R-7 | **Ложный «main regressed» при легитимном рефакторе**, переименовавшем маркер-функцию. | низкая | среднее | Маркеры выбираются как стабильные публичные имена; при намеренном переименовании задача обновляет `MAIN_REGRESSION_MARKERS` в том же PR (правило документации). |
|
||||
| R-8 | **Регресс на non-self репо (enduro)** из-за нового кода. | низкая | высокое | Вся врезка под `merge_verify_applies` (kill-switch + self-hosting scope); регресс-гард — отдельный `regression_guard_enabled`; non-self → no-op (INV-5). Тест AC-6 (enduro no-op). |
|
||||
| R-9 | **Self-hosting: рестарт/падение прода** при ошибке в merge_gate. | низкая | высокое (групповой риск) | never-raise контракт (INV-1); merge только PR-API без force-push; staging-гейт обязателен; прод не рестартится в рамках merge. Тест AC-11. |
|
||||
|
||||
## Сводный вывод
|
||||
Изменения локализованы в `src/merge_gate.py` + врезка в `_handle_merge_verify`
|
||||
(`src/stage_engine.py`) + новый ключ конфигурации + корневой `.gitattributes`. Схема БД, Plane,
|
||||
внешние HTTP-эндпоинты, реестр QG, `STAGE_TRANSITIONS` — не затронуты. Главный остаточный риск —
|
||||
ложный HOLD на инфра-сбое (R-1), сознательно принят ради устранения ложно-зелёного merge-verify.
|
||||
Reference in New Issue
Block a user