Декомпозиция Pipeline: от симлинк-хака до пяти независимых деплоев
· 4 min read
Документационный пайплайн Flude годами существовал в режиме типичного внутреннего тулинга. Он просто работал. Никто в здравом уме не хотел его трогать. Python-пакет engine генерировал свой API-справочник через костыльный симлинк-хак, о котором мы уже упоминали в эпизоде про GitHub Actions. Платформа диктовала жесткие условия. Бесплатный тариф GitHub Pages разрешает публиковать сайты исключительно из открытых репозиториев. При этом сам код движка обязан был оставаться приватным до релиза. Нам приходилось выкручиваться. В CI запускалась команда ln -s ../engine ./user-docs/engine. Открытый репозиторий user-docs молча собирал сайт из чужих исходников. Flude пережевывал файлы на лету, отдавая наружу готовый статический результат из легального места.
Схема действительно работала. Заодно она создавала бетонную архитектурную связность. Ради публикации одной строчки в API-справочнике приватному engine приходилось сливать исходники в соседний открытый репозиторий. Проект user-docs тащил на себе чужую сборку. Движок невозможно было собрать или протестировать отдельно. Вся цепочка запускалась строго изнутри документационного репозитория. При каждом прогоне он заново выкачивал мегабайты чужого кода. Похожая проблема отравляла жизнь и design-docs. Деплой этих документов на GitHub Pages управлялся централизованно из огромного umbrella-репозитория Pipeline. Доступ шел по SSH-ключу деплоя в совершенно другой проект.
Площадка без лишних вопросов

Рассматривались два пути спасения engine. Один из вариантов предлагал собирать распарсенное промежуточное представление в виде версионированного артефакта. Например, запаковать всё в GitHub Release или обычный wheel-архив. Нижестоящие репозитории забирали бы этот пакет и рендерили у себя. Звучит чисто. На практике это сулило ад с версионированием схемы, которого нам пока удавалось избегать. В итоге победил самый прямолинейный подход. Движок сам парсит, рендерит и деплоит собственный справочник из родного CI в собственном репозитории. Никаких промежуточных контрактов. Никаких публикаций артефактов.
Оставалось преодолеть старое ограничение GitHub Pages на бесплатном тарифе. Раз площадка требует публичности, значит нужно менять площадку публикации. Мы перешли на Cloudflare Pages. Этот сервис тоже бесплатный, деплоит готовые артефакты сборки и вообще игнорирует видимость исходного репозитория. Теперь engine спокойно публикует API-справочник, оставаясь невидимым для посторонних. Миграция свелась к банальному копированию файлов Doxyfile, hugo-site и sidebar.toml напрямую в движок. Сгенерированные директории content/ и public/ отправились в .gitignore, перестав висеть мертвым грузом в истории коммитов.
Эту же логику мы раскатали дальше по конвейеру. Проекты design-docs и user-docs переехали на Cloudflare Pages. Каждый получил собственный deploy.yml. GitHub Actions предоставляет нативный доступ на чтение к репозиторию запуска, поэтому ключи развертывания больше не требуются. Экшен cloudflare/pages-action@v1 к тому моменту успел отправиться в архив. Все пять проектов сразу перевели на свежий wrangler-action.
Один токен на всех

Тут гладкий план разбился о реальность. Для деплоя требуется CF_API_TOKEN. Первая мысль была стандартной: выпустить узко ограниченный токен под каждый репозиторий с минимальными правами. Оказалось, Cloudflare запрещает привязывать API-токен к конкретному проекту Pages. Права выдаются только на весь аккаунт целиком. Любой «ограниченный» токен имел бы полный доступ ко всей нашей инфраструктуре. Более того, Cloudflare показывает значение ключа ровно один раз при создании. Забыли скопировать — прощайтесь с ним. Для получения нового значения приходится делать Roll. Это действие мгновенно убивает старый ключ везде, где он успел прописаться.
Процесс превратился в скоростной забег. Мы делаем Roll один раз. Затем за несколько минут разносим свежее значение по всем пяти репозиториям. Всё делается исключительно через интерактивный промпт. Никаких скриптов, файлов или сообщений в чате. Секреты не должны оседать в логах. Пять независимых репозиториев теперь делят один токен и одно окно ротации.
Аудит счетов и мертвых душ

Когда деплои наконец позеленели, мы задались резонным вопросом. Какая часть этой махины реально нужна? Аудит выявил кучу исторического мусора. Триггер repository_dispatch от каждого субмодуля перезапускал интеграционные тесты в главном репозитории. Проблема заключалась в том, что он срабатывал до обновления гитлинка. Тесты гоняли старое, неизмененное состояние. Мы заменили это недоразумение на обычный запуск по расписанию раз в шесть часов на джобе обновления версий. Скорость реакции не пострадала, ведь итоговый PR никогда не сливался автоматически.
Матрица языковых тестов в Pipeline оказалась дословной копией того, что уже выполнялось внутри CI движка. Причем оригинал имел более жесткий порог покрытия. Джоба проверки собранного сайта тихо тестировала архитектуру, которая давно умерла в продакшене. Она сливала вывод Hugo с деревом VitePress для проверки битых ссылок. На деле мы давно деплоили два независимых проекта на разных доменах. Между ними осталась обычная внешняя ссылка. Подобные вещи никогда не всплывают при чтении красивых архитектурных схем. Они находятся только копанием в логах реальных воркфлоу.
Последняя нить

Оставалась крошечная межрепозиторная зацепка. Шаг отправки алертов при падениях CI вытягивал общий Telegram-нотификатор из Pipeline прямо во время выполнения. Для этого требовался еще один секретный токен. Из-за такой мелочи вроде бы независимые проекты продолжали дергать родительский монорепозиторий. Пришлось продублировать файл экшена в каждый проект руками. Пять копий вместо одного красивого источника правды. Такова реальная цена изоляции.
Вы сейчас читаете этот блог, который собран по тому же паттерну. Никаких deploy-ключей. Своя копия нотификатора. Полностью автономная архитектура развертывания на мощностях Cloudflare.
В следующей статье расскажем, как мы искали публичное имя для документации. Спойлер: рабочее название оказалось намертво занято злыми корпоративными юристами, и нам пришлось перебрать кучу странных доменов.