Модуль 6 · Стандарт и его проверка

Справочник «Стандартизация HTTP API» · модуль 6 · Стандарт и его проверка

Глава 6.4

6.4

CI и поиск
breaking changes

Проверки из предыдущих глав дают результат только тогда, когда их нельзя пропустить. В этой главе линтер, тесты и сравнение версий собраны в одну сборку, которая запускается при каждом изменении, а объединение изменения запрещено, пока сборка не прошла.

Категория
Стандарт
Уровень
Средний
Связанные главы
6.2 Линтер · 6.3 Тесты · 5.2 Breaking changes · 7.1 Шаблон pull request
Зачем это знать
Чтобы контракт защищался автоматически, а не силой воли команды

§1Что такое сборка контракта

CI — непрерывная интеграция: набор проверок, который запускается сам при каждом изменении. В учебном проекте это один файл .github/workflows/ci.yml и две задачи внутри него.

.github/workflows/ci.yml · когда запускается
name: API contract

on:
  push:
    branches: [main]
  pull_request:
При push в main

Проверка того, что в основной ветке всё в порядке. Значок состояния в README показывает именно её.

При каждом pull request

Главный случай: изменение проверяется до того, как попадёт в основную ветку.

Разница существенная: проверка после объединения сообщает, что контракт уже сломан, а проверка до объединения не даёт этого сделать.

§2Задача contract: четыре проверки

.github/workflows/ci.yml · задача contract
jobs:
  contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Contract and behaviour tests
        run: pytest

      - name: OpenAPI files match the code
        run: python scripts/export_openapi.py --check

      - uses: actions/setup-node@v4
        with:
          node-version: "22"

      - name: Spectral lint
        run: npx --yes @stoplight/spectral-cli@6.16.3 lint openapi/openapi.yaml --fail-severity=warn
pytest31 тест из главы 6.3: описание, поведение, механизмы. Падает — контракт разошёлся с обещаниями.поведение
export_openapi.py --checkСверяет выгруженный файл с описанием из кода и падает, если они разошлись. Та же задача из главы 3.1, вынесенная в сборку.совпадение
Spectral lintПравила стиля из главы 6.2, порог — предупреждение.стиль
Закреплённые версииpython 3.12, node 22, spectral 6.16.3, действия по номеру. Сборка не должна падать из-за чужого выпуска.предсказуемость

Порядок шагов выбран по времени выполнения: сначала быстрые тесты, потом сверка файла, и только затем установка Node.js для линтера. Если контракт сломан, сборка сообщит об этом через несколько секунд, не тратя время на установку лишнего.

§3Задача breaking-changes

Вторая задача сравнивает контракт из pull request с контрактом в основной ветке — тем самым oasdiff, который мы запускали вручную в главе 5.2.

.github/workflows/ci.yml · задача breaking-changes
  breaking-changes:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v5
      - run: git fetch --depth=1 origin ${{ github.base_ref }}

      - name: oasdiff breaking
        uses: oasdiff/oasdiff-action/breaking@v0.1.17
        with:
          base: "origin/${{ github.base_ref }}:openapi/openapi.yaml"
          revision: "HEAD:openapi/openapi.yaml"
          fail-on: ERR
          review: false
if: pull_requestПри push в основную ветку сравнение не выполняется: эта ветка сама служит точкой отсчёта.когда
baseОписание в целевой ветке — то, что клиенты видят сегодня.было
revisionОписание в изменении — то, что они увидят после объединения.станет
fail-on: ERRПадать на несовместимых изменениях. Предупреждения не роняют сборку.порог
permissions: contents: readЗадаче нужно только чтение. Права выдаются минимальные — принцип из модуля 7.права
Зачем нужен файл описания в репозитории

Это и есть ответ на вопрос из главы 3.1: сравнивать «то, что было» и «то, что стало», можно только если обе версии лежат в истории. Если описание существует лишь в памяти запущенного сервиса, сравнить его в pull request не с чем.

§4Как сравниваются две версии контракта

  1. Разработчик меняет код и заново выгружает описание: python scripts/export_openapi.py.
  2. Открывает pull request. В изменении видны и код, и новое описание — рядом, одним списком правок.
  3. Сборка достаёт две версии файла openapi/openapi.yaml: из основной ветки и из изменения.
  4. oasdiff сравнивает их по своему набору правил и печатает список несовместимых изменений.
  5. Есть ошибки — задача красная, и объединить изменение нельзя, пока автор не вернёт совместимость или не выпустит новую мажорную версию.

Обратите внимание на шаг 2: описание попадает в pull request как обычный текстовый файл, и его правки видны так же, как правки кода. Ревьюер видит не изменение модели в коде, а строку описания: в ответе появилось поле priority типа string. Обсуждать при этом можно сам контракт.

§5Зелёная сборка

Вкладка Actions репозитория: успешные запуски сборки API contract в основной ветке
История запусков. Каждая строка — одно изменение основной ветки. Зелёная отметка означает, что на тот момент контракт проходил все проверки: тесты, сверку файла и линтер.
Блок проверок в pull request: задачи contract и breaking-changes с их состоянием
Проверки в pull request. Обе задачи видны прямо в обсуждении изменения, рядом с кнопкой объединения. Ревьюеру не нужно ничего запускать вручную — состояние проверок видно до объединения.

§6Красная сборка: настоящий случай

Проверим, что защита работает. В учебном репозитории открыто изменение demo/rename-updated-at: в схеме Ticket поле updated_at переименовано в modified_at. Тесты его не ловят — они переименованы вместе с кодом и проходят. Ловит сравнение описаний.

Вкладка Actions: задача breaking-changes завершилась с ошибкой на демонстрационном pull request
Задача упала. Красный крест у breaking-changes: сравнение нашло несовместимые изменения. Задача contract при этом проходит — тесты и линтер сравнивают код с описанием, а не текущее описание с прежним.
журнал задачи breaking-changes, запуск 35027958937
running oasdiff breaking...
  base: origin/main:openapi/openapi.yaml, revision: HEAD:openapi/openapi.yaml, fail_on: ERR

##[error] in API GET /api/v1/tickets
         removed the required property `items/items/updated_at` from the response with the `200` status
##[error] in API POST /api/v1/tickets
         removed the required property `updated_at` from the response with the `201` status
##[error] in API GET /api/v1/tickets/open
         removed the required property `items/items/updated_at` from the response with the `200` status
##[error] in API GET /api/v1/tickets/{ticket_id}
         removed the required property `updated_at` from the response with the `200` status
##[error] in API PATCH /api/v1/tickets/{ticket_id}
         removed the required property `updated_at` from the response with the `200` status

Пять ошибок на одно переименование — по числу операций, в ответе которых встречается схема Ticket. Формулировка везде одна: removed the required property. Для клиента переименование и есть удаление: код, который читал ticket["updated_at"], перестанет находить это поле, и появление рядом modified_at ему не поможет.

Разработчик видит этот вывод в самом pull request, до объединения. Дальше у него три пути: вернуть прежнее имя поля, признать изменение мажорным и выпускать /api/v2 или добавить новое поле рядом со старым и объявить старое устаревшим, как описано в главе 5.3. Варианта «объединить и посмотреть, что будет» среди них нет.

Как читать красную сборку

Упавшая проверка означает, что нарушение найдено до того, как оно дошло до клиентов. Опаснее обратный случай — зелёная сборка при нарушенном контракте; его предотвращают, дописывая тесты и правила линтера.

§7Защита ветки

Сборка сообщает о результате проверок, но ничего не запрещает. Запрет задаётся настройкой репозитория — защитой основной ветки.

Запрет прямой записи в mainИзменения попадают только через pull request. Нельзя обойти проверки, записав напрямую.только через PR
Обязательные проверкиКнопка объединения заблокирована, пока задачи contract и breaking-changes не зелёные.зелёное или ничего
Обязательное одобрениеНужен просмотр человеком. Машина проверяет соблюдение правил, человек — смысл изменения.ревью
Владельцы кодаФайл CODEOWNERS назначает обязательного ревьюера на изменения контракта. Подробно — глава 7.2.кто смотрит

Вместе эти четыре настройки дают требуемое свойство: изменить контракт, не пройдя проверок и не показав изменение человеку, невозможно. Без защиты ветки соблюдение всех предыдущих правил остаётся добровольным.

§8Сколько это стоит по времени

ПроверкаВремяЧто ловит
pytestдоли секундыРасхождение поведения и обещаний
export_openapi.py --checkдоли секундыУстаревший файл описания
Spectralсекунды плюс установка Node.jsНарушения стиля в описании
oasdiffсекундыНесовместимые изменения

Больше всего времени занимает не выполнение проверок, а установка окружения. Поэтому тесты и сверку файла запускают локально перед отправкой изменения, а полный набор — в сборке. Чем раньше сработала проверка, тем меньше объём исправлений: замечание линтера в своём редакторе исправляют сразу, то же замечание после объединения затрагивает уже чужую работу.

§9Частые ошибки

Сборка есть, защиты ветки нетПроверки можно обойти прямой записью в основную ветку — и однажды обойдут.
Проверки запускаются только после объединенияСообщают о поломке вместо того, чтобы её предотвратить.
Незакреплённые версии инструментовСборка падает сама по себе, команда перестаёт доверять красному цвету.
Описание не лежит в репозиторииСравнивать в pull request нечего, и breaking-changes становится невозможен.
Отключить упавшую проверку, чтобы «разблокировать» изменениеЕдинственный способ гарантированно потерять пользу от всего построенного.
Считать зелёную сборку одобрениемМашина проверяет правила, человек — смысл. Нужны оба.

§10Проверьте себя

  1. Чем проверка при pull request принципиально отличается от проверки после объединения?
  2. Назовите четыре шага задачи contract и скажите, что ловит каждый.
  3. Почему задача breaking-changes не запускается при push в основную ветку?
  4. Почему для сравнения версий контракта файл описания обязан лежать в репозитории?
  5. Задача contract зелёная, а breaking-changes красная. Что это означает и что делать автору?
  6. Какие четыре настройки защиты ветки нужны, чтобы контракт нельзя было изменить в обход?
Две задачиcontract — при каждом измененииbreaking-changes — только в PR
В задаче contractpytestexport_openapi.py --checkspectral lint
Сравнениеbase — целевая веткаrevision — изменениеfail-on: ERR
Защита веткитолько через PRобязательные проверкиобязательное одобрениеCODEOWNERS

Справочник «Стандартизация HTTP API» · модуль 6 · глава 6.4Макаров Максим Николаевич

Кадр