В статье «Публичное описание как проверка инженерного решения» я писал, как восстановление фактического порядка событий помогло найти отсутствующую зависимость в CI/CD-процессе.

После этого появилась другая проблема. Один раз написать точную документацию недостаточно. Следующее изменение пайплайна может снова сделать её неверной.

В DocOps-проекте я описал CI в каноническом документе: когда запускаются разные пайплайны, какие проверки обязательны, как связаны задания и какие действия остаются ручными. Затем часть этих утверждений вынес в контрактные тесты.

Идея простая: если документация обещает конкретное поведение системы, некоторые такие обещания можно проверять как любой другой контракт.

Документация устаревает не из-за лени

У CI-конфигурации и её описания разный жизненный цикл.

.gitlab-ci.yml исполняется. Если изменение синтаксически некорректно или ломает проверяемый сценарий, это можно обнаружить автоматически.

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

Для автора изменения задача при этом действительно может выглядеть завершённой. Он исправил CI, проверил результат и получил зелёный пайплайн. Документация просто не входила в критерий готовности.

Так появляется расхождение документации с фактическим поведением системы.

Пытаться решить его только правилом «не забывай обновлять документацию» ненадёжно. Человек должен каждый раз помнить, какие документы зависят от конкретного изменения конфигурации.

Часть этой связи можно сделать машинной.

Контракт — это не весь текст документа

Первая опасная идея здесь — попытаться автоматически доказать, что документация целиком соответствует реализации.

Это быстро сделает проверки хрупкими.

Порядок абзацев, формулировки, примеры и пояснения не являются частью поведения CI. Если тесты начнут фиксировать их, обычная редактура будет ломать сборку, а набор проверок превратится в снимок Markdown-файла.

Поэтому я выделил только утверждения, от которых зависит модель пайплайна:

  • какие существенные сценарии запуска существуют
  • какие задания входят в обязательный путь проверки
  • какие зависимости определяют порядок выполнения
  • какие задания являются ручными или допускают ошибку
  • где находится каноническое описание CI
  • какие связанные файлы должны ссылаться именно на него

Это уже не редакционные детали.

Если документация говорит, что сборка начинается только после обязательных проверок, а конфигурация позволяет начать её раньше, документация описывает другую систему.

Если README объявляет один документ каноническим, а ссылка ведёт на другой файл, контракт тоже нарушен.

Остальной текст остаётся текстом. Его не нужно превращать в тест.

Контрактный тест делает изменение гарантии видимым

Без такой проверки изменение CI может выглядеть так:

изменить пайплайн
→ проверить поведение
→ получить зелёный результат
→ завершить задачу

Контрактный тест добавляет ещё одну границу:

изменить пайплайн
→ проверить поведение
→ проверить документированные гарантии
→ сохранить контракт или явно изменить его

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

Если одна из них исчезнет случайно, проверка станет красной.

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

Проверка делает расхождение видимым. Решение о том, какой вариант теперь является правильным, всё равно принимает человек.

В этом для меня и оказалась основная ценность подхода. Существенная часть документации перестала быть файлом, который желательно вспомнить после изменения CI. Несовпадение между описанным и фактическим поведением стало обычной регрессией, которую можно увидеть до merge.

Зелёный контрактный тест тоже может врать

Контрактный тест не становится правильным только потому, что называется контрактным.

Это хорошо проявилось на простой проверке ссылки.

Один из тестов должен был гарантировать, что README ведёт именно на канонический документ о CI. Первая версия фактически проверяла, что в ссылке встречается ожидаемый путь.

Такой тест выглядел разумно и был зелёным.

Но строка вроде:

docs/ci-pipeline.md.invalid

тоже удовлетворяла этой проверке.

То есть тест подтверждал не тот контракт, который был заявлен. README мог вести не на канонический документ, а проверка всё равно считала результат корректным.

После добавления регрессионного сценария проверку пришлось сделать точнее. Допустимые варианты пути нормализуются, фрагмент ссылки отделяется, а путь сравнивается с каноническим целиком.

Этот дефект небольшой, но хорошо показывает границу подхода.

Фраза «у нас есть тест документации» сама по себе ничего не доказывает. Нужно понимать, какое нарушение способно сделать этот тест красным.

Если проверка утверждает, что ссылка ведёт на конкретный документ, подмена его похожим путём должна приводить к ошибке.

Контрактный тест не защищает сам себя

Есть ещё одна граница, которую легко пропустить.

Автор изменения может не обновить документацию, а изменить ожидаемое значение в тесте. Может ослабить проверку. Может удалить её совсем.

Никакой контрактный тест не способен запретить изменение самого себя.

Поэтому он не гарантирует синхронность документации автоматически. Его задача скромнее: случайное изменение поведения перестаёт проходить незаметно.

Если меняется сам тест, это уже отдельный сигнал на ревью. Изменение проверяемого контракта должно рассматриваться так же внимательно, как изменение реализации.

Для намеренного изменения я ожидаю согласованного diff:

поведение системы
+
проверяемый контракт
+
каноническое описание

Не обязательно должны измениться все три части. Если документированная гарантия сохранилась, документацию трогать не нужно. Если изменилась сама гарантия, изменение теста без соответствующего изменения описания требует объяснения.

Машинная проверка не принимает это решение за ревьюера. Она делает место изменения явным.

Не нужно писать второй парсер GitLab CI

У контрактных тестов есть противоположная опасность: начать моделировать в них весь .gitlab-ci.yml.

Тогда рядом с настоящим пайплайном появляется его вторая реализация, которую тоже нужно поддерживать.

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

Я стараюсь проводить границу через наблюдаемое обещание документации.

Добавление внутренней вспомогательной команды может ничего не менять для читателя документа.

Удаление обязательной проверки меняет.

Перестановка технических деталей внутри задания может быть несущественной.

Изменение зависимости, после которого сборка больше не ждёт обязательную проверку, меняет модель пайплайна.

То же относится к условиям запуска, ручным заданиям и допустимым ошибкам, если документация делает на их основе конкретное обещание.

Хороший контрактный тест знает о реализации ровно столько, сколько нужно для проверки этого обещания.

В правилах своих проектов я сейчас формулирую тот же принцип проще: проверять наименьший устойчивый наблюдаемый контракт.

Сначала нужно выбрать утверждения

Чтобы применить этот подход, не нужен универсальный фреймворк для документации.

Полезнее открыть описание собственного CI и найти несколько предложений, ошибка в которых действительно изменит понимание процесса.

Например:

Утверждение:
сборка начинается только после обязательных проверок

Источник поведения:
зависимости заданий в CI-конфигурации

Проверка:
обязательные зависимости действительно присутствуют

Или:

Утверждение:
задание запускается вручную и не блокирует обычный пайплайн

Источник поведения:
фактическая конфигурация задания для этого сценария

Проверка:
задание сохраняет документированные свойства

Ещё один вариант:

Утверждение:
README ведёт на каноническое описание CI

Источник поведения:
ссылка в README

Проверка:
нормализованный путь точно совпадает с каноническим

Для начала достаточно нескольких таких гарантий.

Хороший признак — изменение соответствующего поведения делает тест красным.

Плохой — перестановка абзацев или безобидная редактура текста тоже ломает проверку.

Не вся документация должна быть исполняемой

Контрактные тесты хорошо работают там, где текст описывает проверяемое поведение системы.

Они плохо подходят для объяснения причин решения, рекомендаций, примеров и обычной прозы. Попытка проверить такие фрагменты машинно даст больше хрупкости, чем пользы.

Поэтому «документация как контракт» для меня не означает превращение Markdown в ещё один язык программирования.

Смысл уже.

Если документ обещает конкретное поведение системы и это поведение можно проверить устойчивым способом, такую проверку стоит поставить рядом с остальными регрессиями.

Она не гарантирует, что документация всегда будет полной и правильной. Не мешает намеренно изменить или удалить сам тест. Не заменяет ревью.

Зато закрывает другой класс ошибок: система уже изменилась, а её каноническое описание продолжает уверенно рассказывать о предыдущей версии.