diff --git a/.gitea/workflows/contract-sync.yml b/.gitea/workflows/contract-sync.yml new file mode 100644 index 00000000..e17fd550 --- /dev/null +++ b/.gitea/workflows/contract-sync.yml @@ -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}" diff --git a/api_contract/README.md b/api_contract/README.md index d7b36cb4..0798d0fd 100644 --- a/api_contract/README.md +++ b/api_contract/README.md @@ -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-классы: под одним именем могут скрываться разные контракты. ## Формат манифеста