У обновления зависимостей два режима, и их почти всегда путают. В первом ничего не случилось: вышла новая версия, можно обновиться сегодня, можно через месяц — ничего не давит. Во втором случилось: в библиотеке из вашего lock-файла нашли уязвимость, и теперь чужой график диктует ваш. Политика обновлений нужна ровно для одного: чтобы решение «обновлять или нет» не принималось каждый раз заново, по настроению и по близости релиза.
Большинство политик при этом не работает. Их пишут, кладут в вики и тихо не соблюдают — не потому, что команда безответственная, а потому, что политика обещает поведение, не меняя его стоимость. Соблюдают ту политику, в которой соблюдать дешевле, чем нарушать. Дальше — как собрать такую руками.
Тупик: матрица дедлайнов и «день обновлений»
Самый очевидный первый ход — документ с матрицей срочности: критические уязвимости закрываем за столько-то, высокие — за столько-то, остальное — раз в квартал. Второй ход той же природы — «день обновлений»: одним коммитом подтягивается всё накопившееся.
Оба ломаются об одно и то же.
Дедлайн — это обещание о будущем поведении. В момент, когда пора обновляться, всё, что делало обновление дорогим, стоит на месте: тесты не покрывают то, как вы используете библиотеку, релизная ветка заморожена, мажор тянет за собой цепочку транзитивных зависимостей. Обещание не убирает ни одного из этих препятствий — оно назначает виноватого. Дальше предсказуемо: первое нарушение, второе, исключения становятся нормой, и документ умирает молча. Отмену такой политики никто не объявляет — она просто перестаёт существовать.
У матрицы есть и вторая беда: критичность назначает автор уведомления, и назначает её среднему пользователю пакета, а не вам. «Критическая» уязвимость в ветке кода, которую вы не вызываете, конкурирует за тот же день с настоящей проблемой. Чужая шкала не переживает встречи с вашим бэклогом.
«День обновлений» ломается арифметикой партии. Обновления в партии не локализуются: сборка падает — и непонятно, какое из изменений виновато; откатывать приходится всё, потом раскатывать заново по одному. Стоимость партии растёт быстрее её размера. Чем дольше день откладывали, тем больше партия; чем больше партия, тем неохотнее за неё берутся — и тем вероятнее, что она пересекает мажорные границы, а мажор — это чтение заметок о миграции, а не нажатие кнопки.
Общий корень обоих тупиков: обновление воспринимается как событие, которое надо назначить в календаре, а не поток, который надо удешевить. Пока стоимость не изменилась, любой календарь — декорация.
Два потока: рутина и безопасность
Рабочая политика начинается с разделения потоков: у них разные триггеры, разная цена ошибки и разные процедуры решения.
Рутина. Триггер — вышла новая версия. Цель — сделать обновление скучным, а скучное — частым и мелким. Ставите бота, который следит за новыми версиями и открывает pull-реквесты; какой именно — неважно, это стандартный класс инструментов. Патчи и минорные версии идут пачкой, мажоры — по одному, с чтением заметок о миграции: стоимость мажора живёт в чтении, а не в мерже. Пачка — тоже решение: группировать стоит то, что тестируется вместе, а не всё подряд; бот, сваливающий в один реквест всё накопившееся, возвращает вас к арифметике партии.
Частое и мелкое дешевле редкого и крупного — это не дисциплина ради дисциплины, а арифметика. Чем больше прыжок между версиями, тем больше изменений вы принимаете за раз и тем труднее найти то, которое сломало поведение. Мелкий прыжок локализует проблему в крошечном диффе. Отставание к тому же копится с процентами: экосистема движется, транзитивные зависимости требуют друг от друга всё более свежих версий, и цена наверстывания растёт быстрее, чем само отставание.
Автоматический мерж — условная опция, а не догма. Включать его можно при двух выполненных условиях: тесты покрывают интеграционную поверхность — то, как вы реально используете библиотеку, а не строчки кода вообще, — и откат быстрый. Нарушено любое из них, и авто-мерж превращает красную сборку в продакшен-инцидент: это не экономия труда, а перенос того же труда в худшее место. Ручной мерж по зелёной сборке медленнее, зато без сюрпризов.
Безопасность. Триггер — уведомление об уязвимости. Политика здесь — не срок, а процедура из трёх вопросов:
- Есть ли пакет у нас и где он используется? На это отвечает перечень цепочки поставки — тот самый, который кто-то реально читает. Без него первый вопрос съедает часы, с ним — минуты; о том, как такой перечень устроить, у нас есть отдельный материал.
- Досягаем ли уязвимый путь из того, что у вас открыто наружу? Уведомление описывает конкретную ветку кода. Если вы её не вызываете или до неё не добраться снаружи — события не произошло. Проверка обычно дешёвая: из перечня видно, где живёт пакет, а дальше — вызываете ли вы ту самую функцию.
- Что дешевле: обновление или обход? Иногда уязвимый функционал отключается конфигурацией, и это дешевле, чем перепрыгивать через мажор под давлением.
Ответы раскладывают уведомления по потокам. Недостижимый путь — в рутину: обновитесь в порядке очереди, но обновитесь, потому что оценка досягаемости быстро устаревает. Достижимый и открытый снаружи — прерывание: обновление прыгает через очередь. Если само обновление — большой мажор, обход делается сейчас, а мажор уходит в плановый поток с датой в регистре исключений.
И про сроки. Ваш настоящий срок реакции не пишется в политике — он измеряется в конвейере. Замерьте время от решения «обновляем» до работающего продакшена для обычного патча. Это и есть честный срок; обещание в документе, которое быстрее него, сработает как ложь в первом же инциденте. Хотите обещать быстрее — укорачивайте конвейер: тесты, деплой, откат. Политика тут ни при чём. Тот же замер — честный ответ на вопрос «когда закроете», заданный в неудобный момент.
Порог отставания и видимые исключения
Поток — ещё не политика, нужна проверяемая граница. Выберите порог отставания — например, «не отстаём от последнего мажора дольше, чем на ваш обычный релизный цикл», — и главное условие: порог проверяет машина. Порог, который проверяет человек, не проверяется никто.
Калибровка без лишних цифр: смотрите, как распределяются нарушения. Кучкуются на одних и тех же зависимостях — это кандидаты в исключения. Размазаны равномерно — поток буксует: бот выключен, мержи лежат, конвейер мешает. Порог, нарушаемый постоянно, — не порог, а шум: поднимите его или почините поток, но не оставляйте мигать красным. Привыкание к красному и есть смерть политики.
Исключение — легальный способ нарушить, и у него три обязательных поля: причина, владелец, дата пересмотра. Исключение без даты — не исключение, а дрейф с подписью. Обход уязвимости из предыдущего раздела живёт здесь же: обход — это заём, а не решение, и у заёма есть срок возврата.
Порядок действий
- Измерьте конвейер: обновите одну рутинную зависимость, доведите до продакшена и запишите, где застряли. Не получается даже с одним патчем — у вас проблема не с политикой, а с тестами или откатом; чините сначала это.
- Включите бот на патчи и миноры. Авто-мерж — только при условиях из раздела выше, иначе мержите руками по зелёной сборке.
- Мажоры — по одному, с чтением заметок о миграции. Если мажор тянет цепочку транзитивных обновлений, это уже не рутина, а маленький проект: заведите задачу и оцените её как задачу.
- Пропишите процедуру для уведомлений: три вопроса и человек, отвечающий за каждый — обычно владелец перечня зависимостей и тот, кто отвечает за внешний контур.
- Поставьте машинный порог отставания и регистр исключений с датами.
- Через релизный цикл вернитесь к пункту 1 и сравните время. Падает или хотя бы не растёт — политика работает. Растёт — вы поддерживаете документ, а не поток.
Позиция редакции
Мы считаем, что политика обновления зависимостей — это конфигурация конвейера, а текст — её краткая запись: два потока, три вопроса, машинный порог, регистр исключений с датами. Если текст длиннее этого, в нём, скорее всего, обещания вместо механизмов.
Мы неправы в двух случаях. Первый: маленький граф зависимостей без транзитивной паутины, редкие релизы, продукт в режиме поддержки. Тогда настройка потока дороже ручных обновлений по списку, и разумнее список с календарным напоминанием, а не бот. Второй: продукт заморожен по решению. Тогда политики нет вовсе — есть абзац «не обновляем, и вот почему».
Если неясно, к какому случаю вы ближе, — начните с измерения времени одного обновления. Это дешевле, чем полгода жизни под политикой, которую никто не соблюдает.