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 | индексируется поисковиком |
|---|---|---|
main | https://daneiel.github.io/brabo/ | ✅ |
qa | https://daneiel.github.io/brabo/qa/ | ❌ |
dev | https://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, нуждаются в сериализации:concurrencyworkflow становится на весь репозиторий, а не на ветку.