ci: гейты наблюдаемых свойств перед publish — image-smoke, migration-order на push, allowlist fail-closed, property-тесты #476

Open
opened 2026-07-11 00:13:40 +03:00 by agent_vscode · 0 comments
Collaborator

Мотивация

Ретроспектива мержей 22.06–10.07 показала повторяющийся класс промаха: локальная логика верна, интеграционное свойство не проверено. Каждый инцидент-класс двух недель проскочил в прод именно потому, что проверялся юнит чистой функции, а не наблюдаемое свойство системы:

  • #361 — миграция «задним числом» уронила старт прода (гейт migration-order работает только на pull_request, прямой push его обходит);
  • #353 — re2 сломал сборку/рантайм Docker-образа (образ в publish собирается, но ни разу не запускается — e2e гоняются из исходников, а не из артефакта, который тянет watchtower);
  • #452 — immutable-заголовок статики был инертен в проде (дефолт @fastify/static перезаписывал setHeaders; юнит покрывал чистую функцию);
  • #172 — allowlist внешних MCP-тулов 7 дней не применялся вообще (jsonb-строка вместо массива); семантика до сих пор fail-open: corrupt-значение → «все тулы разрешены», пустой массив нормализуется в null — «запретить все» невыразимо;
  • #435 — кэш CollabSession с hit-rate 0% ушёл в прод: юнит-тесты гоняли фиксированный токен, свойство «ключ кэша стабилен под реальной вариативностью токена» не проверялось.

Состав (один PR)

1. Boot-smoke Docker-образа в publish (.github/workflows/develop.yml + scripts/ci/image-smoke.sh)

В job publish добавить services (postgres pgvector + redis, как у e2e-mcp) и перед push: build с load: true (кэш-хит из параллельного build-job), запуск контейнера с --network host, проверки:

  • S1: /api/health → 200 за 120s (стартовый мигратор + boot);
  • S2: POST /api/auth/setup → 2xx;
  • S3: GET / → 200 и содержит /assets/ (клиентский dist внутри образа);
  • S4: первый /assets/*.js из HTML → curl -sI -H 'Accept-Encoding: br'cache-control: …immutable и content-encoding: br.

Fail любой проверки = образ не пушится. На failure — docker logs.

2. Гейт migration-order на push (.github/workflows/test.yml)

База сравнения по событию: PR — origin/${base_ref} (как сейчас); push — ${{ github.event.before }}; before == 0000… → skip с notice; force-push (git cat-file -e падает) → fail-closed с указанием прогнать workflow_dispatch после ручной проверки.

3. Allowlist внешних MCP: fail-closed + сквозной тест enforcement

  • Запись: [] больше НЕ нормализуется в null — «запретить все тулы» выразимо (apps/server/src/database/utils.ts);
  • Чтение: corrupt/неразборный tool_allowlistdeny-all (пустой набор) + error-лог вместо null=«всё разрешено» (ai-mcp-server.repo.ts);
  • Int-тест цепочки repo → mcp-clients.service → toolset против stub HTTP MCP-сервера (2 тула): ["alpha"] → только alpha; null → оба; legacy double-encoded → только alpha; corrupt → ноль; [] → ноль.

4. Два property-теста

  • Fixpoint конвертера через живой сервер (packages/mcp/test-e2e.mjs): exportPageMarkdown → updatePageMarkdown → exportPageMarkdown → byte-equal;
  • Стабильность ключа CollabSession (packages/mcp/test/unit/): mock-transport считает connect'ы; две мутации одной identity с разными свежими токенами → ровно один connect (закрепляет урок #435/#439).

Критерий приёмки

Каждый инцидент-класс (#361, #353, #452, #172, #435) имеет именованную проверку, падающую до push образа :develop.

Вне scope (отдельно)

Hit-rate метрики/алерты для тихих деградаций под реальным трафиком (паттерн #403), post-deploy curl против env-дрейфа, «upgrade-path»-тест миграций на БД с историей.

## Мотивация Ретроспектива мержей 22.06–10.07 показала повторяющийся класс промаха: **локальная логика верна, интеграционное свойство не проверено**. Каждый инцидент-класс двух недель проскочил в прод именно потому, что проверялся юнит чистой функции, а не наблюдаемое свойство системы: - **#361** — миграция «задним числом» уронила старт прода (гейт `migration-order` работает только на `pull_request`, прямой push его обходит); - **#353** — re2 сломал сборку/рантайм Docker-образа (образ в `publish` собирается, но ни разу не запускается — e2e гоняются из исходников, а не из артефакта, который тянет watchtower); - **#452** — immutable-заголовок статики был инертен в проде (дефолт `@fastify/static` перезаписывал `setHeaders`; юнит покрывал чистую функцию); - **#172** — allowlist внешних MCP-тулов 7 дней не применялся вообще (jsonb-строка вместо массива); семантика до сих пор fail-open: corrupt-значение → «все тулы разрешены», пустой массив нормализуется в `null` — «запретить все» невыразимо; - **#435** — кэш CollabSession с hit-rate 0% ушёл в прод: юнит-тесты гоняли фиксированный токен, свойство «ключ кэша стабилен под реальной вариативностью токена» не проверялось. ## Состав (один PR) ### 1. Boot-smoke Docker-образа в `publish` (`.github/workflows/develop.yml` + `scripts/ci/image-smoke.sh`) В job `publish` добавить services (postgres pgvector + redis, как у `e2e-mcp`) и перед push: build с `load: true` (кэш-хит из параллельного build-job), запуск контейнера с `--network host`, проверки: - S1: `/api/health` → 200 за 120s (стартовый мигратор + boot); - S2: `POST /api/auth/setup` → 2xx; - S3: `GET /` → 200 и содержит `/assets/` (клиентский dist внутри образа); - S4: первый `/assets/*.js` из HTML → `curl -sI -H 'Accept-Encoding: br'` → `cache-control: …immutable` и `content-encoding: br`. Fail любой проверки = образ не пушится. На failure — `docker logs`. ### 2. Гейт `migration-order` на push (`.github/workflows/test.yml`) База сравнения по событию: PR — `origin/${base_ref}` (как сейчас); push — `${{ github.event.before }}`; `before == 0000…` → skip с notice; force-push (`git cat-file -e` падает) → **fail-closed** с указанием прогнать `workflow_dispatch` после ручной проверки. ### 3. Allowlist внешних MCP: fail-closed + сквозной тест enforcement - Запись: `[]` больше НЕ нормализуется в `null` — «запретить все тулы» выразимо (`apps/server/src/database/utils.ts`); - Чтение: corrupt/неразборный `tool_allowlist` → **deny-all** (пустой набор) + error-лог вместо `null`=«всё разрешено» (`ai-mcp-server.repo.ts`); - Int-тест цепочки repo → mcp-clients.service → toolset против stub HTTP MCP-сервера (2 тула): `["alpha"]` → только alpha; `null` → оба; legacy double-encoded → только alpha; corrupt → ноль; `[]` → ноль. ### 4. Два property-теста - **Fixpoint конвертера через живой сервер** (`packages/mcp/test-e2e.mjs`): `exportPageMarkdown → updatePageMarkdown → exportPageMarkdown` → byte-equal; - **Стабильность ключа CollabSession** (`packages/mcp/test/unit/`): mock-transport считает connect'ы; две мутации одной identity с разными свежими токенами → ровно один connect (закрепляет урок #435/#439). ## Критерий приёмки Каждый инцидент-класс (#361, #353, #452, #172, #435) имеет именованную проверку, падающую **до** push образа `:develop`. ## Вне scope (отдельно) Hit-rate метрики/алерты для тихих деградаций под реальным трафиком (паттерн #403), post-deploy curl против env-дрейфа, «upgrade-path»-тест миграций на БД с историей.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#476