Полные стенды SourceCraft
Оглавление · SourceCraft CI и доставка
Интеграция влита в SourceCraft main через PR №11–14. Контроллер и холодный runtime
обновлены на VPS из main da61602. Полный цикл test и PR прошёл живую приёмку:
CI, контент, HTTPS, холодный запуск и отзыв сертификатов.
Main разворачивается на web.test.gheilt.mxsource.xyz, собственный PR — на
web.pr-N.gheilt.mxsource.xyz. У каждого также cms, media, storybook, docs.
Свои PostgreSQL, Garage, Strapi, доступы и volumes. Production сохраняет прежние адреса и существующий pipeline до отдельного переключения.
Почему пока сохраняем полный стек
На этом этапе каждый PR, test и staging получает собственные Strapi, PostgreSQL
и Garage, включая PR с изменениями только интерфейса. Автоматического выбора
light/full и чтения общей test CMS в текущем pipeline нет. Переход на общие
PostgreSQL/Garage и перенос существующих баз и бакетов отложены.
Это осознанный выбор для учебного проекта: отдельный стек проще исследовать, восстанавливать и удалять вместе со стендом. Общие процессы уменьшили бы число контейнеров, но потребовали бы управления отдельными ролями, базами, бакетами, ключами и проверки владельцев при уборке. Сначала используем уже реализованные TTL, холодный режим и лимит одновременно работающих стендов, описанные ниже.
Все стеки работают на одном VPS: отдельные контейнеры и тома не обеспечивают HA и не изолируют потребление CPU, RAM и диска. К дальнейшему сокращению процессов вернёмся при измеренной нехватке ресурсов: сравним работающий и холодный стенды, потребление отдельных контейнеров и доступные ресурсы хоста. Само число контейнеров не доказывает необходимость общего хранилища.
Для сохранения этой схемы миграция данных и изменение production ACL не требуются. Ранее предложенные grants для общего хранения не входят в этот этап.
Первый запуск копирует только опубликованные документы и используемые media текущей production CMS.
Черновики, пользователи, токены и webhook не копируются. Исходный внешний сайт не используется.
Медиа читаются напрямую из текущего Garage через внутреннюю сеть и фиксированный Host
штатным http.request: Node.js fetch этот заголовок не передаёт;
исходные публичные URL сохраняются как ключи снимка, затем импорт меняет их на адреса стенда.
Повторный push сохраняет контент и редакторские изменения стенда.
Повторное обновление test данными production — отдельная операция, а не результат push или обновления образов. Проект процедуры разбирает выбор между сохранением test-правок и reset после backup, валидацию снимка, проверку импорта и откат. Готовой команды повторного reset/sync пока нет.
Экспорт: pnpm cms:snapshot --destination var/content-import/published.
Проверка снимка: pnpm cms:snapshot --dry-run --destination var/content-import/published.
Старая внешняя команда переименована в cms:snapshot:external и не входит в pipeline стендов.
Импорт published snapshot требует выделенное окружение, поддерживает dry-run и отказывается перезаписывать существующие редакторские записи.
Авторитет состояния и доставка
SourceCraft CI публикует готовые digest-манифесты и отдельный снимок веток/PR в существующий release repository YC. VPS использует свою pull-only учётку и outgoing HTTPS; персональный PAT SourceCraft на VPS не хранится. Control workflow срабатывает при main push и по расписанию каждые 10 минут. Приёмник проверяет полное чтение страниц API, собственный repository, source/target PR, SHA и свежесть control не более 20 минут. Неизвестное или устаревшее состояние не разрешает обновление/удаление стенда.
PR CI должен проверить точный head и актуальный main base; чужие/fork PR выполняют обычные проверки без privileged publication. Проверки готовности включают публичные HTTP endpoints стенда. Перед merge агент сверяет head/base, CI, живой стенд и обсуждения; branch protection тарифом не заменяем. Агент работает в собственном worktree и ветке от свежего SourceCraft main.
TTL и холодный режим
PR удаляется после закрытия/merge или 12 часов без нового head. Повтор CI и HTTP обращения lease не продлевают. Контроллер обрабатывает свежие сведения о закрытии; при расписании отсутствие события закрытия обнаруживается в следующем 10-минутном цикле плюс опрос VPS. После TTL удаляются контейнеры, сети, volumes, снимок, локальные доступы и маршруты данного PR. Новый commit открытого PR создаёт новый стенд с текущим опубликованным контентом.
PR/test/staging охлаждаются через 30 минут без HTTP к любому из пяти доменов. Холодный режим сохраняет данные и digest. HTTP отвечает 503/Retry-After на время запуска и будит сохранённый релиз без сборки или импорта. POST и загрузки не повторяются автоматически. Production всегда включён. Одновременно работают не более четырёх nonproduction стендов; LRU охлаждает наименее недавно посещённый допустимый стенд. Все изменения state/power, start/stop и deployment используют общие locks и повторную проверку поколения/lease.
Основной state: /opt/gheilt/preview-state/<id>.json; provider отделяет SourceCraft от legacy GitHub.
Power state: /opt/gheilt/preview-runtime/<id>.json. Чужой provider не получает право заменить существующий id.
Runtime: user-unit atmanki-preview-runtime.service; Caddy получает только Unix socket, Docker socket ему не передаётся.
Релизные ветки, staging и blue-green production — следующий этап интеграции, их завершение этой главой не утверждается.
Проверка версии и отзыв сертификатов
/api/health сохраняет JSON { status: "ok" } и возвращает X-App-Release с SHA сборки.
CI ждёт именно свой SHA, а не просто HTTP 200 предыдущего стенда.
PR-образы собираются с временной CMS-фикстурой без production-секретов; после импорта на VPS
webhook инвалидирует страницы, и сайт начинает читать собственную CMS и media.
Privileged publisher загружается из текущего main, а не из файлов PR.
После удаления маршрутов всех пяти адресов PR сертификаты попадают в долговечную очередь. ACME использует ключ конкретного сертификата и проверяет, что все SAN относятся к этому PR. Недоступность Let’s Encrypt оставляет задание для повторного опроса. После подтверждённого отзыва удаляются только соответствующие leaf-файлы. Caddy перезапускается для очистки кеша сертификатов; это кратковременно прерывает проксирование, контейнеры production-приложений и их данные продолжают работать. Охлаждение не удаляет и не отзывает сертификаты.
Если CA не достигает HTTP/TLS порта VPS, применяется DNS-01 через ограниченный YC-провайдер Caddy. Образ proxy собирается в CI; отдельный ключ DNS доступен только Caddy. Изменение способа проверки домена не отменяет отзыв сертификатов закрытых PR.
Установка контроллера
Оператор доставляет проверенные tools из main в /opt/gheilt/preview-tools/<SHA>;
PR-файлы на VPS не исполняются. Под deploy-пользователем запускается
scripts/deploy/preview-runtime-install.sh; административно —
scripts/deploy/sourcecraft-environments-install.sh /opt/gheilt/preview-tools/<SHA>.
Он заменяет команду существующего sourcecraft-receive.service, сохраняя таймер опроса раз в две минуты.
Authorized key остаётся в /opt/gheilt/sourcecraft/pull-key.json, control и готовность —
в приватном /opt/gheilt/sourcecraft/state. После обновления tools оператор повторяет установку: автоматическое
обновление исполняемых скриптов с веток PR запрещено.
Ошибка развёртывания одного стенда оставляет его для повторного опроса и не блокирует закрытие других PR.
Обновление получает только четыре application images из YC Registry; установленные
PostgreSQL и Garage не обновляются попутно и не требуют повторного доступа к Docker Hub.
Если их ещё нет на хосте, первоначальная подготовка VPS должна установить эти образы.
Контроллер сохраняет отдельную запись активности каждого PR ещё до появления образов.
При первом наблюдении используется дата commit из API SourceCraft; повторная сборка не меняет её.
При следующем изменении head записывается новое наблюдение, включая возврат A → B → A.
Новый head продлевает lease один раз, даже если его проверки ещё не прошли;
до успешной сборки продолжает обслуживаться предыдущая проверенная версия.
Повторное наблюдение того же head, повтор CI и обращения по HTTP срок не меняют.
PR-проверки и публикация выполняются в разных tasks: SourceCraft запускает их на разных workers,
публикация зависит от успешной check-task. От проверок не передаются workspace или environment.
В чистой privileged task checkout удаляет Git credentials; скрипты PR на хосте не запускаются,
Docker получает только фиктивный read-token. Доверенные publisher-файлы читаются из текущего main
в свежем полном clone; если main успел измениться и его object ещё отсутствует, сборка завершается
и требует свежего запуска. Автоматический PR использует конфигурацию из main.
Manual --cfg-commit с feature допустим только для workflow checks, без выдачи IAM.
Контракты платформы: tasks и отдельные workers, триггеры из main.
В живой приёмке обнаружено: переменная, записанная в SOURCECRAFT_ENV, не сработала
как динамическое условие if. Gate теперь пишет eligible в SOURCECRAFT_OUTPUT;
публикация проверяет статус gate-кубика. Чужой/stale PR получает checks-only,
собственный PR обязан пройти финальную проверку полного capsule и публичной готовности.
Отсутствующий gate output (например, ошибка API) или пропущенная публикация завершают CI ошибкой.
Живая приёмка полного цикла
Для проверки создаётся отдельный собственный PR от текущего main. Его автоматический CI
должен собрать четыре образа, опубликовать capsule и дождаться публичного /api/health
с X-App-Release, равным head этого PR. Успешных проверок исходников недостаточно.
Затем проверяются пять HTTPS-адресов, отдельные тома CMS и медиа, сохранение редакторской
правки при следующем commit, охлаждение и пробуждение по HTTP. Закрытие PR должно удалить
его ресурсы и обработать отзыв сертификатов всех пяти доменов.
6 октября 2026 года проверены импорт 200 опубликованных документов и 897 media, публичные адреса test и PR №13, сохранение черновой редакторской правки при обновлении, HTTP-пробуждение и удаление PR после merge с фактическим отзывом пяти сертификатов. Входящие HTTP-01/TLS-ALPN-01 проверки нового IP обходятся DNS-01; проверка доверия HTTPS проходит. Подробности и границы проверки — в отчёте о живой приёмке.
Staging release-ветки
В следующем этапе тот же полный runtime обслуживает staging для release/X.Y.
Controller выбирает самый новый проверенный staging-run-N с текущим head;
поздний запуск старого SHA не заслоняет актуальный capsule. Смена линии требует
новой проверки, даже если commit совпадает. Import marker, секреты и редакторские
правки staging сохраняются при обновлении образов.
После проверок web SHA/content-health, CMS, Storybook, docs и текущего media
controller под deploy.lock записывает приватный staging-ready.json:
branch/head/images/generation/checkedAt. Повторный poll готового поколения не
будит охлаждённый стенд. Отказ записи receipt повторяет проверку; он не даёт
права promotion. Пока изменения не интегрированы и не проверены живым CI,
этот раздел описывает реализуемый контракт, а не опубликованный staging.
Параллельная загрузка админки и gate
Каждый запрос к стенду проходит через Unix-сокет dispatcher, включая JS-чанки
админки Strapi. Поэтому успешный /_health ещё не проверяет загрузку панели.
В приёмке открываем /admin/ и одновременно загружаем её чанки: ответы должны
быть успешными, без 502 и ошибки dynamic import.
Очередь ожидающих соединений dispatcher равна 128. Стандартная очередь Python
из пяти соединений оказалась недостаточной для браузерной загрузки: Caddy
получал connect: resource temporarily unavailable и возвращал 502 ещё до CMS.
Регрессионный тест проверяет пакет соединений с настоящим Unix-сокетом.
При таком сбое сверяем журналы Caddy и dispatcher, а также прямую отдачу чанков
CMS; увеличение CPU или перезапуск Strapi не исправляют малую очередь gate.
Временное исправление на VPS хранится в пользовательском systemd drop-in
~/.config/systemd/user/atmanki-preview-runtime.service.d/20-unix-backlog.conf: он
задаёт очередь 128 перед запуском существующего trusted runtime. Основной unit и
application images остаются прежними. После установки trusted controller с
Server.request_queue_size=128 этот drop-in можно удалить, выполнить
systemctl --user daemon-reload и перезапустить atmanki-preview-runtime.service.
Удаление drop-in до обновления controller возвращает прежнюю очередь из пяти.