Справочник «Стандартизация HTTP API» · модуль 6 · Стандарт и его проверка
Глава 6.4
CI и поиск
breaking changes
Проверки из предыдущих глав дают результат только тогда, когда их нельзя пропустить. В этой главе линтер, тесты и сравнение версий собраны в одну сборку, которая запускается при каждом изменении, а объединение изменения запрещено, пока сборка не прошла.
- Категория
- Стандарт
- Уровень
- Средний
- Связанные главы
- 6.2 Линтер · 6.3 Тесты · 5.2 Breaking changes · 7.1 Шаблон pull request
- Зачем это знать
- Чтобы контракт защищался автоматически, а не силой воли команды
§1Что такое сборка контракта
CI — непрерывная интеграция: набор проверок, который запускается сам при каждом изменении. В учебном проекте это один файл .github/workflows/ci.yml и две задачи внутри него.
name: API contract
on:
push:
branches: [main]
pull_request:Проверка того, что в основной ветке всё в порядке. Значок состояния в README показывает именно её.
Главный случай: изменение проверяется до того, как попадёт в основную ветку.
Разница существенная: проверка после объединения сообщает, что контракт уже сломан, а проверка до объединения не даёт этого сделать.
§2Задача 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=warnpython 3.12, node 22, spectral 6.16.3, действия по номеру. Сборка не должна падать из-за чужого выпуска.предсказуемостьПорядок шагов выбран по времени выполнения: сначала быстрые тесты, потом сверка файла, и только затем установка Node.js для линтера. Если контракт сломан, сборка сообщит об этом через несколько секунд, не тратя время на установку лишнего.
§3Задача breaking-changes
Вторая задача сравнивает контракт из pull request с контрактом в основной ветке — тем самым oasdiff, который мы запускали вручную в главе 5.2.
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Это и есть ответ на вопрос из главы 3.1: сравнивать «то, что было» и «то, что стало», можно только если обе версии лежат в истории. Если описание существует лишь в памяти запущенного сервиса, сравнить его в pull request не с чем.
§4Как сравниваются две версии контракта
- Разработчик меняет код и заново выгружает описание:
python scripts/export_openapi.py. - Открывает pull request. В изменении видны и код, и новое описание — рядом, одним списком правок.
- Сборка достаёт две версии файла
openapi/openapi.yaml: из основной ветки и из изменения. - oasdiff сравнивает их по своему набору правил и печатает список несовместимых изменений.
- Есть ошибки — задача красная, и объединить изменение нельзя, пока автор не вернёт совместимость или не выпустит новую мажорную версию.
Обратите внимание на шаг 2: описание попадает в pull request как обычный текстовый файл, и его правки видны так же, как правки кода. Ревьюер видит не изменение модели в коде, а строку описания: в ответе появилось поле priority типа string. Обсуждать при этом можно сам контракт.
§5Зелёная сборка
§6Красная сборка: настоящий случай
Проверим, что защита работает. В учебном репозитории открыто изменение demo/rename-updated-at: в схеме Ticket поле updated_at переименовано в modified_at. Тесты его не ловят — они переименованы вместе с кодом и проходят. Ловит сравнение описаний.
breaking-changes: сравнение нашло несовместимые изменения. Задача contract при этом проходит — тесты и линтер сравнивают код с описанием, а не текущее описание с прежним.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Защита ветки
Сборка сообщает о результате проверок, но ничего не запрещает. Запрет задаётся настройкой репозитория — защитой основной ветки.
contract и breaking-changes не зелёные.зелёное или ничегоCODEOWNERS назначает обязательного ревьюера на изменения контракта. Подробно — глава 7.2.кто смотритВместе эти четыре настройки дают требуемое свойство: изменить контракт, не пройдя проверок и не показав изменение человеку, невозможно. Без защиты ветки соблюдение всех предыдущих правил остаётся добровольным.
§8Сколько это стоит по времени
| Проверка | Время | Что ловит |
|---|---|---|
pytest | доли секунды | Расхождение поведения и обещаний |
export_openapi.py --check | доли секунды | Устаревший файл описания |
| Spectral | секунды плюс установка Node.js | Нарушения стиля в описании |
| oasdiff | секунды | Несовместимые изменения |
Больше всего времени занимает не выполнение проверок, а установка окружения. Поэтому тесты и сверку файла запускают локально перед отправкой изменения, а полный набор — в сборке. Чем раньше сработала проверка, тем меньше объём исправлений: замечание линтера в своём редакторе исправляют сразу, то же замечание после объединения затрагивает уже чужую работу.
§9Частые ошибки
breaking-changes становится невозможен.§10Проверьте себя
- Чем проверка при pull request принципиально отличается от проверки после объединения?
- Назовите четыре шага задачи
contractи скажите, что ловит каждый. - Почему задача
breaking-changesне запускается при push в основную ветку? - Почему для сравнения версий контракта файл описания обязан лежать в репозитории?
- Задача
contractзелёная, аbreaking-changesкрасная. Что это означает и что делать автору? - Какие четыре настройки защиты ветки нужны, чтобы контракт нельзя было изменить в обход?
contract — при каждом измененииbreaking-changes — только в PRpytestexport_openapi.py --checkspectral lintbase — целевая веткаrevision — изменениеfail-on: ERRтолько через PRобязательные проверкиобязательное одобрениеCODEOWNERSСправочник «Стандартизация HTTP API» · модуль 6 · глава 6.4Макаров Максим Николаевич