chore(contracts): update contract flow

This commit is contained in:
2026-10-24 11:55:27 +03:00
parent 4aadc8a9d8
commit bfb43104d8
2 changed files with 84 additions and 4 deletions

View File

@@ -0,0 +1,63 @@
name: "Contract sync: flutter manifest -> eatvio-web"
# Автоматический перенос манифеста API-контракта из eatvio_app в eatvio-web.
# Триггер: push в main с изменениями api_contract/**.
# Результат: PR в eatvio-web с обновлённым tests/Fixtures/Contract/flutter_model_contract.json.
# Далее CI eatvio-web сам прогонит FlutterApiContractTest и покажет,
# выполняет ли бэкенд требования новых моделей.
on:
push:
branches:
- main
paths:
- "api_contract/**"
jobs:
sync-manifest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Open PR in eatvio-web with updated manifest
env:
TOKEN: ${{ secrets.CONTRACT_SYNC_TOKEN }} # PAT с write на eatvio/eatvio-web
API: http://gitea.office.local:3000/api/v1
BACKEND_REPO: eatvio/eatvio-web
SRC: api_contract/flutter_model_contract.json
DST: tests/Fixtures/Contract/flutter_model_contract.json
run: |
set -e
SHORT_SHA="${GITHUB_SHA::8}"
BRANCH="contract-sync/manifest-${SHORT_SHA}"
auth="Authorization: token ${TOKEN}"
DEFAULT_BRANCH=$(curl -sf -H "$auth" "${API}/repos/${BACKEND_REPO}" | grep -o '"default_branch":"[^"]*"' | head -1 | cut -d'"' -f4)
# Файл не изменился относительно предыдущего синка — выходим тихо.
REMOTE_SHA=$(curl -sf -H "$auth" "${API}/repos/${BACKEND_REPO}/contents/${DST}?ref=${DEFAULT_BRANCH}" | grep -o '"sha":"[^"]*"' | head -1 | cut -d'"' -f4)
LOCAL_SHA=$(git hash-object -- "${SRC}")
if [ "${REMOTE_SHA}" = "${LOCAL_SHA}" ]; then
echo "Manifest already in sync with ${BACKEND_REPO}."
exit 0
fi
# 1. Ветка от дефолтной (если уже существует — переиспользуем).
curl -sf -X POST -H "$auth" -H "Content-Type: application/json" \
-d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"${DEFAULT_BRANCH}\"}" \
"${API}/repos/${BACKEND_REPO}/branches" || echo "branch ${BRANCH} already exists"
# 2. Обновить файл (contents API требует sha текущего файла).
CONTENT_B64=$(base64 -w0 "${SRC}")
curl -sf -X PUT -H "$auth" -H "Content-Type: application/json" \
-d "{\"content\":\"${CONTENT_B64}\",\"message\":\"chore(contract): sync flutter manifest (${SHORT_SHA})\",\"sha\":\"${REMOTE_SHA}\",\"branch\":\"${BRANCH}\"}" \
"${API}/repos/${BACKEND_REPO}/contents/${DST}" > /dev/null
# 3. PR (если уже открыт — пропускаем).
curl -sf -X POST -H "$auth" -H "Content-Type: application/json" \
-d "{\"title\":\"[contract-sync] flutter manifest ${SHORT_SHA}\",\"head\":\"${BRANCH}\",\"base\":\"${DEFAULT_BRANCH}\",\"body\":\"Автоматический синк манифеста из eatvio_app@${GITHUB_SHA}.\\n\\nCI бэкенда прогонит FlutterApiContractTest — если бэкенд не выполняет новые требования моделей, тест упадёт на этом PR.\"}" \
"${API}/repos/${BACKEND_REPO}/pulls" > /dev/null \
|| echo "PR already exists for ${BRANCH}"
echo "Sync PR opened: ${BACKEND_REPO} <- ${BRANCH}"

View File

@@ -40,10 +40,27 @@
## Автоматика
- eatvio_app, локально: `make export-api-contract` / `make check-api-contract`.
- eatvio_app, CI: шаг `API contract manifest check` в `.gitea/workflows/test.yml`.
- eatvio-web, локально (если рядом лежит копия eatvio_app): `make sync-flutter-contract`.
- eatvio-web, CI: `tests/Feature/Contract/FlutterApiContractTest.php` в общем прогоне.
| Где | Что | Когда срабатывает |
|---|---|---|
| eatvio_app, CI | шаг `API contract manifest check` (test.yml) | каждый PR: манифест обязан быть регенерирован из исходников |
| eatvio_app, CI | `contract-sync.yml` | push в main с изменениями `api_contract/**` → авто-PR в eatvio-web с новой копией манифеста |
| eatvio_app, локально | `make export-api-contract` / `make check-api-contract` | руками при работе над моделями |
| eatvio-web, CI | `FlutterApiContractTest` (12 тестов) | каждый PR: worst-case сериализация ресурсов ↔ манифест |
| eatvio-web, CI | `contract-drift-check.yaml` | еженощно: сравнение локальной копии манифеста с eatvio_app@main, при дрейфе — issue |
| eatvio-web, локально | `make sync-flutter-contract` | руками, если авто-PR не пришёл |
Секреты: `CONTRACT_SYNC_TOKEN` — PAT с write на eatvio/eatvio-web (для contract-sync) и read eatvio/eatvio_app + issues на eatvio/eatvio-web (для drift-check).
## Как команды держат друг друга в рабочем состоянии (три слоя)
1. **Предотвращение (до мержа).** Контрактные тесты на обеих сторонах: flutter не смержит модели без регенерации манифеста; бэкенд не смержит ресурс, отдающий null/не тот тип там, где приложение требует non-null.
2. **Синхронизация (при мерже).** Авто-PR из `contract-sync.yml` приносит новую копию манифеста в бэкенд; CI бэкенда на этом PR показывает, выполняет ли сервер новые требования. Ночной drift-check ловит всё, что проскочило мимо автоматики.
3. **Обнаружение (после релиза).** Sentry: группа `TypeError ... in _$XxxFromJson` (проект flutter) = нарушение контракта в проде. Такой issue должен ссылаться на контрактную пару и закрываться фиксом ресурса + контрак тестом.
Ритуалы:
- Меняешь модель в приложении → `make export-api-contract` в том же PR; манифест уедет в бэкенд автоматически.
- Меняешь API-ресурс → прогоняй контрактный тест; красный = либо делай совместимо (дефолты), либо mobile-first релиз.
- WARNING генератора о коллизии имён моделей (`Step`, `Users`, ...) — повод переименовать Dart-классы: под одним именем могут скрываться разные контракты.
## Формат манифеста