Перейти к основному содержимому

ADR 0034 — Документация, публикуемая по ступеням, в подкаталоге Pages

Контекст​

Документация существовала опубликованной только из main. docs-deploy.yml запускался по push в main, собирал сайт и передавал артефакт actions/deploy-pages.

Это оставляло зазор во всём конвейере: между merge в dev и финальным продвижением никто не может прочитать документацию этого состояния, не клонируя репозиторий и не запуская pnpm docs:start. А именно там она нужнее всего — в приёмочном тестировании, чтобы проверить, что изменилось; в dev, чтобы проверить новый документ с применённым сайтом, а не с сырым Markdown из diff.

docs-check.yml уже собирает сайт в каждом PR, но он выбрасывает сборку: его вердикт — «собирается без битых ссылок», никогда не «опубликовано где-то, где я могу это открыть».

На решение повлиял второй факт. 117 операционных страниц справочника API вышли сломанными в релизах v1.0.0 и v1.0.1, и никто этого не видел два месяца работы — потому что единственным способом увидеть было открыть опубликованный сайт, который появлялся только после того, как изменение прошло три ступени. Публиковать каждую ступень — не удобство: это сокращение расстояния между написанием и просмотром.

Решение​

Каждая постоянная ветка публикуется в собственном месте, в том же GitHub Pages:

ступеньURLиндексируется поисковиком
mainhttps://daneiel.github.io/brabo/✅
qahttps://daneiel.github.io/brabo/qa/❌
devhttps://daneiel.github.io/brabo/dev/❌

main не меняет URL — существующие ссылки продолжают работать. Это было требованием, а не следствием: сломать URL документации ради получения предпросмотра значило бы обменять правильное на удобное.

Последствия, которые нужно записать​

1. Публикация становится прямым push, и это третье исключение​

actions/deploy-pages публикует артефакт как весь сайт и не умеет обновлять часть дерева. С ним публикация dev в подкаталоге означала бы пересборку всех трёх ступеней при каждом push любой из них. Поэтому публикация переходит на peaceiris/actions-gh-pages, который коммитит в ветку gh-pages.

Это открывает третье исключение из единственной двери PR, рядом с тегами и .release/gate.json, — и оно зафиксировано в политике веток, потому что незадокументированное исключение становится прецедентом.

Что делает это исключение отличным от двух других и более приемлемым: gh-pages не является веткой кода. Ничто в ней не является источником, всё генерируется из docs/ и website/, и её полное удаление не теряет информации — следующий push её пересобирает. Её git log — это запись каждой публикации, с датой и sha источника.

2. main собирает дерево, а не сохраняет по умолчанию​

main публикуется в корень. Публикация в корень с удалением того, что там было, унесла бы /dev/ и /qa/ вместе: каждый production-деплой обрушивал бы две другие ступени до их следующего push.

Выход — не keep_files: true. Это никогда ничего не удаляет, и страница, удалённая из репозитория, осталась бы опубликованной навсегда — проблема сирот, которую генератор справочника API уже решает с помощью clean-api-docs. Обменять «исчезает то, что не должно» на «остаётся то, что не должно» — не исправление.

Поэтому дерево собирается: перед публикацией job main приносит /dev/ и /qa/ из gh-pages внутрь сборки и публикует полный набор с keep_files: false. Тогда семантика становится точной — то, чего нет в новом дереве, не должно быть опубликовано.

3. baseUrl перестаёт быть константой​

baseUrl входит в каждый URL ресурса, который испускает Docusaurus. Сайт, отдаваемый из /brabo/dev/ с baseUrl: '/brabo/', загружает HTML и больше ничего: CSS, JS и поиск дают 404, а страница выглядит сломанной без ошибки. Поэтому baseUrl начинает браться из DOCS_BASE_URL, с production-значением по умолчанию — pnpm docs:build без какой-либо переменной продолжает производить ровно то, что всегда производил.

4. noIndex и локальный поиск взаимно гасились, и это пришлось обнаружить​

dev и qa — то же содержимое, что production, на другой стадии зрелости. Будучи проиндексированными Google, они конкурировали бы с настоящей документацией, и тот, кто пришёл бы через поиск, читал бы непроверенную версию, не замечая этого. Отсюда noIndex: true вне main.

Только @easyops-cn/docusaurus-search-local отбрасывает каждую страницу, у которой есть <meta name="robots" content="noindex"> (parse.js, комментарий «Unlisted content»), — ровно то, что испускает noIndex. Измеренный эффект: индекс в 666 байт с documents: [], и поле поиска, отвечающее «No results» на любой термин. Ступени публиковались бы с мёртвым поиском, и симптом не проявился бы ни в одной сборке.

Опция плагина forceIgnoreNoIndex: true решает это, и решение стоит объяснить, потому что оно выглядит противоречием: noIndex говорит с ВНЕШНИМ поисковиком; forceIgnoreNoIndex говорит с ЛОКАЛЬНЫМ индексом. Хотеть, чтобы ступени были вне Google, не значит хотеть ступени без поиска.

Измерено в трёх режимах после исправления: 2318 проиндексированных документов в production, dev и qa, с meta robots, присутствующим только в двух последних.

5. «По ветке, которая прошла» не потребовалось механизма​

push в постоянную ветку происходит только через merge PR, а ruleset требует обязательных checks перед merge. Триггер push уже И ЕСТЬ «прошло» — не существует пути, при котором этот workflow опубликует код, не прошедший конвейер. Запись этого здесь не даёт кому-нибудь позже добавить проверку «checks прошли?», которая была бы избыточной и создавала бы впечатление, что без неё была бы дыра.

6. Gate отрисовки выполняется до публикации​

api-render-check.mjs (см. documentation-workflow) выполняется в docs-deploy помимо docs-check. Это шаг, который публикует: если страница не отрисовывается, лучше не публиковать, чем публиковать сломанной. Именно отсутствие этого позволило двум релизам выйти с мёртвым справочником API.

Рассмотренные альтернативы​

Артефакт на запуск, без URL. Каждая ступень загружала бы сайт как artifact Actions, скачиваемый в zip. Ноль исключений push, ноль изменений в Settings — и ноль пользы для реального сценария, который — открыть ссылку и посмотреть. Скачивать и распаковывать, чтобы проверить страницу, — достаточное трение, чтобы никто этого не делал.

Предпросмотр на PR. Полезнее при ревью, чем сайт на ветку, и та же стоимость механизма. Не был отвергнут по существу: остаётся возможным дополнением, а механизм этого ADR (подкаталог + DOCS_BASE_URL) — ровно то, что ему понадобилось бы. Перевесило то, что зазор, описанный в Контексте, находится между ступенями, а не внутри PR — у PR уже есть docs-check, собирающий сайт.

Оставить actions/deploy-pages и пересобирать три ступени при каждом push. Без исключения push, без gh-pages. Но каждый push в dev пересобирал бы qa и main из других веток — три checkout и три сборки на публикацию, и деплой dev, способный переопубликовать main с содержимым, которого никто не просил. Связанность между ступенями — противоположность того, ради чего существует весь конвейер.

Последствия​

  • Ручное действие пользователя: сменить источник Pages в Settings с «GitHub Actions» на ветку gh-pages / root. Это в Rulesets, вместе с остальным, что применяется вручную.
  • gh-pages не входит в rulesets постоянных веток: она не постоянная, и боту нужно в неё пушить.
  • Окружение github-pages перестаёт использоваться потоком. Оно существует, потому что Pages его создаёт, а не потому, что проект его объявил, — CLAUDE.md сохраняет правило не создавать Environments.
  • Кэш Rspack начинает ключеваться по ступени: baseUrl входит в bundle, поэтому кэш dev не годится для main. Без этого три инвалидировали бы друг друга по очереди, и кэш стал бы украшением.
  • Три ступени, пишущие в одну gh-pages, нуждаются в сериализации: concurrency workflow становится на весь репозиторий, а не на ветку.