Публикация дерева категорий — операторский запуск
Публикация черновика дерева категорий (POST /api/v1/admin/catalog/categories/builds/{id}/publish)
больше не выполняется по HTTP. Начиная с блокера C3
(.superpowers/sdd/2026-08-09-analog-rules-carry-forward/critical-c3-report.md),
эндпоинт всегда отвечает 501 с понятным текстом отказа, а сама публикация
выполняется только версионированным операционным джобом category-tree-publish
в GitLab CI.
Почему так
Publish — многоминутная транзакция: она активирует узлы дерева и мигрирует
привязки характеристик, с бюджетом TAXONOMY_PUBLISH_STATEMENT_TIMEOUT
(default 8 минут). Это дольше:
API_HTTP_WRITE_TIMEOUT(60с) на самом api-server;proxy_read_timeout 60sна edge-nginx перед ним.
При обрыве на этом бюджете HTTP-клиент получает разрыв соединения, но транзакция на сервере продолжает выполняться — оператор не узнаёт, дошла публикация до конца или нет, и в каком состоянии осталось дерево.
Как запустить
- Получить
build_idчерновика:GET /api/v1/admin/catalog/categories/builds?status=draft(или из ответаPOST /builds, которым черновик был создан). - Открыть в GitLab пайплайн нужного коммита (main, тот же SHA, что уже
раскатан выкладкой —
deploy-db-vpsв этом пайплайне должен быть зелёным). - Найти ручную джобу
category-tree-publish(стадияrelease) и запустить её, задав переменнуюTAXONOMY_BUILD_IDравной UUID черновика из шага 1. Пустое значение джоба ловит до ssh на DB-VPS и явно отказывает — не уходит в базу с мусорным аргументом. - Прочитать лог джобы: она печатает SHA образа
(
tracium/worker:$CI_COMMIT_SHA),build_idи исход публикации — строку отчётаcategory-tree-publish, что и в CLI (published build ... — N nodes activated (tree vK; ...; analog rules carry enabled=... carried=N)), либо ошибку с кодом выхода ≠ 0, если публикация не удалась.
Джоба ручная (when: manual), без allow_failure — красный джоб означает
настоящий провал публикации, а не «temporarily disabled» галочку. У неё нет
зависимости от CI_DB_BUILDER_ENABLED: образ уже гарантирован джобой
build-workers, из того же релизного образа воркеров и того же
$CI_COMMIT_SHA, что уехал в deploy-db-vps.
Что бинарник трогает на хосте
Джоба SSH’ится на DB-VPS и запускает разовый контейнер тем же приёмом, каким
выкладка гоняет миграции (deploy-db-vps.sh) и каким пилот семейств запускает
family-axis-rebuild (см. product-family-pilot-ups.md, шаг 7):
cd /srv/tracium/infra/deploy/docker
COMPOSE="docker compose --env-file /srv/tracium/infra/.env \
--env-file /srv/tracium/infra/deploy/.env"
$COMPOSE run --rm --no-deps -T \
--entrypoint /usr/local/bin/category-tree-publish \
category-classifier --build-id=<uuid> --by=<кто>Контейнер поднят под category-classifier, а не под произвольным сервисом:
именно в его environment: в deploy/docker/docker-compose.yml проброшен
TAXONOMY_PUBLISH_STATEMENT_TIMEOUT (и там же — ключи переноса) — запуск под
другим сервисом (например, supplier-sync) молча потерял бы эти переменные, и
публикация словила бы дефолты кода, а не настроенные значения (тот же класс
отказа, что закрывал C4/C5).
Перенос правил аналогов публикацией НЕ выполняется
ANALOG_RULES_CARRY_ON_PUBLISH_ENABLED по умолчанию false: перенос правил
подбора на новое дерево — самостоятельная горячая операция со своей метрикой и
своим окном, а не побочный эффект публикации. Порядок —
analog-rules-carry.md.
Практическое следствие для этого runbook’а: публикация не изменит покрытие
правилами и не создаст запись в analog_rule_coverage_reports. Если
покрытие ожидалось — оно не появится, и искать причину в публикации не надо.
Шаг ПОСЛЕ публикации: перезапустить prod-health-check
Секция P (покрытие правил подбора аналогов) считается только в джобе
prod-health-check, а она идёт в стадии deploy — то есть до стадии
release, в которой публикуется дерево. Значит сразу после публикации
health-check покажет числа предыдущего состояния до следующего пайплайна
main.
Перезапустите prod-health-check вручную и прочитайте секцию P.
Что означают три состояния секции:
| Вывод | Смысл |
|---|---|
⚠ отчёта для tree_version=N нет | штатно, пока перенос (analog-rules-carry) не выполнялся на этой версии дерева |
✗ покрытие ниже предыдущего | покрытие упало — разбираться до следующей публикации |
✓ покрытие N/M, прямых целей K | два числа расходятся из-за наследования правил, и это нормально |
Откат
Публикация обратима на уровне дерева: POST .../taxonomy/rollback/{tree_version}
по HTTP либо тот же бинарник с --rollback --to-version=<предыдущая версия>.
Откат двигает указатель назад и не требует пересборки черновика.
Что откат НЕ делает: если перенос когда-либо выполнялся отдельной операцией,
его carried-строки остаются на своих категориях — идентификаторы категорий
между версиями стабильны, а строки живут в category_analog_rules, а не в
дереве. Снимаются они своим откатом по run_id, а не откатом дерева
(analog-rules-carry.md).
Публикация как таковая горячих строк не пишет: перенос из неё выключен
(см. выше), поэтому отдельная точка восстановления под category_analog_rules
здесь не требуется — она требуется под операцию переноса.
Прочие ручки хендлера не меняются
GET .../builds, GET .../builds/{id}, GET .../taxonomy/diff/{from}/{to},
POST .../taxonomy/rollback/{tree_version} и POST .../builds/{id}/discard
по-прежнему работают по HTTP как раньше — они быстрые и не упираются в
60-секундный бюджет. Джоб category-tree-publish покрывает только Publish.
Ручной запуск CLI напрямую (эскалация)
Если GitLab недоступен, тот же бинарник можно запустить вручную с той же
командой, что использует джоба — обязательно тем же тегом образа, что
сейчас раскатан (docker compose config | grep image на category-classifier
подтвердит текущий SHA). Запуск с произвольного/устаревшего образа — ровно та
проблема, ради устранения которой публикатор переехал из отдельно собираемого
tools-образа в общий релизный.
Связано
.superpowers/sdd/2026-08-09-analog-rules-carry-forward/critical-c3-report.mddocs/docs/40-operations/runbooks/product-family-pilot-ups.md(тот же приём запуска разовой команды из runtime-образа)backend/cmd/category-tree-publish/main.go