Постмортем инцидента 2026-07-24: 404/500 на body_repair_service (stostayer.ru) = двойной корень. (1) код-баг snolla: viewModels/index.js `import markdown from 'markdown-it'; markdown();` — инстанс выброшен, markdown.render undefined, краш на markdown-поле-шаблонах (латентный ~3 мес с коммита 1e3f897 2026-04-21). (2) упаковка exact-pin-staleness: @snollajs/content-api@0.8.0 `workspace:*` запёк @snollajs/snolla:0.7.4 exact → фиксовая 0.7.7 легла top-level, content-api юзал nested баговую 0.7.4. yarn resolutions/patch не форсят exact-pin опубликованного пакета — решено публикацией @snollajs/content-api@0.8.2 (^0.7.7). Деплой stostayer-web:0.3.22 через pscp SSH:20435 + sudo docker load + Portainer PUT pullImage:false (без push в docker.stostayer.ru:443, бана нет). 4 URL → 200 публично. Pre-build чек yarn.lock single-snolla ловит класс. Cleanup workspace-caret-interpackage-deps — за vitya+snolla. Co-Authored-By: Claude <noreply@anthropic.com>
9.8 KiB
stostayer 404/500 на body_repair_service — двойной корень
Постмортем инцидента: страницы шаблона body_repair_service на www.stostayer.ru отдавали 404 (фронт) / 500 (content-api). Починено 2026-07-24. Корень двойной — код-баг snolla + упаковка-баг (exact-pin-staleness).
Симптом
https://www.stostayer.ru/kuzovnoj-malyarnij-remont/{zamena-elementov,rihtovka-kuzova,remont-bamperov,pokraska} → 404
GET /snolla/pages/viewModel?path=.../zamena-elementov → 500 { "message": "markdown.render is not a function" }
GET /snolla/pages?path=/kuzovnoj-malyarnij-remont/&template=body_repair_service → 500
Родитель /kuzovnoj-malyarnij-remont (шаблон body_repair, без markdown-поля) → 200. В БД страницы есть, Published=true, Path совпадает точно — 404 это не данных, это следствие 500 (фронт packages/web/store/snolla.js ловит 500, коммитит page=null, _.vue рендерит 404).
Корень 1 — код-баг snolla (markdown.render is not a function)
@snollajs/snolla → lib/viewModels/index.js, строки 7–8:
import markdown from 'markdown-it';
markdown(); // фабрику вызвали, инстанс ВЫБРОШЕН
// ...
viewModel[name] = value ? markdown.render(value) : null; // markdown — конструктор, .render нет → TypeError
Краш в fillViewModel на шаблонах с markdown-полем (markItUpMarkdownEditorTemplate / markdownLiteralTemplate). body_repair без такого поля → 200; body_repair_service с markdown-полем → краш. Латентный ~3 мес — ломается только при заходе на markdown-поле-страницу, никто не замечал.
Фикс (snolla): import markdownit from 'markdown-it'; const markdown = markdownit(); → markdown.render работает. snolla выпустила @snollajs/snolla@0.7.7 (0.7.x legacy-line) + @snollajs/core@0.26.1 (master, + TDD fillViewModel.markdown.test.js, 259/259 green). Верифицировано: markdown-it 12.3.2 (snolla's nested) и 14.1.0 (root) — обе дают инстанс с .render через markdownit().
Происхождение: предмонорепо-ный Node-snolla юзал другой markdown-движок (работало). Коммит 1e3f897 (2026-04-21, импорт в монорепо) сменил на markdown-it без регресс-прогона по field-типам → markdown(); без присвоения. Класс «портовая опечатка при смене либы без регресса по field-типам».
Корень 2 — упаковка (exact-pin-staleness) — почему фикс НЕ дошёл до прода первым деплоем
@snollajs/content-api@0.8.0 (servит /snolla/pages) в routes/pages.js делает import { ... } from '@snollajs/snolla'. В опубликованном content-api@0.8.0 его package.json жёстко: "@snollajs/snolla": "0.7.4" — exact-пин, без ^.
Откуда: в монорепо snolla у content-api было "@snollajs/snolla": "workspace:*". При publish yarn разрешает workspace:* в точную текущую версию и запекает её константным пином. В момент публикации content-api 0.8.0 snolla была 0.7.4 → в мир ушёл content-api с залитым 0.7.4. Позже snolla выпустила фиксовую 0.7.7, но content-api 0.8.0 об этом не знает — навсегда просит 0.7.4.
Дедуп-ловушка: бамп @snollajs/snolla → ^0.7.7 в packages/web (top-level) НЕ поднимает content-api's exact-пин. Yarn ставит две копии:
node_modules/@snollajs/snolla= 0.7.7 (top-level, фиксовая) — зовётся SSR-стороной Nuxtnode_modules/@snollajs/content-api/node_modules/@snollajs/snolla= 0.7.4 (nested, баговая) — её и зовёт content-api's routes/pages.js (ближайшая резолвится)
content-api бегёт на баговой 0.7.4 → краш. Деплой 0.3.21 (snolla 0.7.7 top-level) не починил ничего (регрессии нет — 0.3.20 тоже 500).
Почему yarn resolutions НЕ спас (вариант B — мёртв)
resolutionsвpackages/web/package.json— yarn berry игнорирует workspace-resolutions (читает только root). → нет эффекта.- root
resolutions: { "@snollajs/snolla": "0.7.7" }(blanket) и scoped"@snollajs/content-api/@snollajs/snolla": "0.7.7"— не форсят exact-пин, объявленный внутри чужого package.json. yarn всё равно резолвит content-api в 0.7.4 по его package.json (stostayer.new эмпирически проверил, incl. patch на content-api — не держится).
Вывод: resolutions/patch не могут переписать exact-pin опубликованного пакета. Только новая публикация пакета с фиксовой зависимостью.
Фикс упаковки (вариант A — единственный)
snolla опубликовала @snollajs/content-api@0.8.2 с "@snollajs/snolla": "^0.7.7" (в монорепо workspace:*→workspace:^, запечётся ^0.7.7). 0.8.1 уже был на verdaccio с запечённой 0.7.4 → нужна именно 0.8.2 (перепубликация той же версии = тот же 0.7.4). stostayer.new бампит @snollajs/content-api → ^0.8.2 в packages/web → yarn: content-api ^0.7.7 ∩ web ^0.7.7 → одна 0.7.7 top-level, nested 0.7.4 уходит. content-api зовёт фиксовую snolla.
Деплой 0.3.22 (без push в :443 — см. stostayer-deploy-image-via-pscp-load)
Образ ~3.3GB. Push в docker.stostayer.ru:443 банит VPN-egress (см. stostayer-web-deploy-runbook гоча). Транспорт без :443:
- Build локально offline (
.yarnrc.yml→enableNetwork:false+ убратьnpmAuthToken;.yarn/cacheс 0.8.2+0.7.7). docker save | gzip -1→ ~0.8GB (sw-0.3.22.tar.gz, shared-layers).pscpпо SSH:20435 на хост/tmp(~10MB/s, ~80s) — SSH не банится, только :443.- На хосте:
echo pass | sudo -S bash -c 'gunzip -c /tmp/sw-0.3.22.tar.gz | docker load'(sudo -S + inner pipe из файла — конфликта stdin нет). - Portainer PUT стека 16
pullImage:false(образ уже локально, НЕ тянуть из:443). Payload через-d @/tmp/payload.json(jq в файл) — НЕ встраивать JSON в double-quotedbash -c(кавычки payload ломают → 500). - Verify:
docker logs→READY Server listening;curl -H "Host: www.stostayer.ru" http://localhost:3000/<page>+ публичныйhttps://www.stostayer.ru/<page>→ 200.
Pre-build чек (ловит exact-pin-staleness до деплоя): перед build'ом grep -E "^\"@snollajs/snolla@npm:" yarn.lock → ожидать ОДНУ версию (фиксовую), без баговой. В собранном образе: docker run --rm --entrypoint sh <img> -c 'find /usr/src/app -path "*@snollajs/snolla/package.json"' → одна копия 0.7.7. Без этого чека первый деплой 0.3.21 прошёл «успешно» но не починил.
Outcome
4 URL → 200 публично через Angie/TLS. 0.3.22 на проде (stostayer-web стек 16). Код-фикс (snolla 0.7.7 + core 0.26.1) + упаковка (content-api 0.8.2 ^0.7.7). stostayer.new коммит ace20a1.
Cleanup (open, не блокирует — за vitya)
snolla-таска workspace-caret-interpackage-deps: перевести workspace:*→workspace:^ по монорепо, чтоб класс exact-pin-staleness не повторялся. Scope-вопрос: covers ли 0.7.x legacy-line content-api или только master. Отдельно: 0.7.x content-api не получил peerDep-паттерн (master-only, 0.12.0) — переносить ли зависит от срока жизни 0.7.x vs миграции stostayer на современную линию.
Multi-tenant находка (факультатив, snolla своп на kupimknigi): краш бьёт любой шаблон с markdown-полем на любой БД snolla (fillViewModel общий). На shared MoreThenCms-БД kupimknigi нашлись 2 чужих siteId (919219B3, 5A826EB9, шаблоны article_markdown/test) — если они на snolla 0.7.x, тоже крашились; фикс закроет при их пересборке.
Связи
- stostayer-web-deploy-runbook — каноничный деплой-рунбук (push-гочи, Portainer API с хоста).
- snolla-live-prod-inplace-image-bump — переиспользуемый рецепт in-place bump на ЖИВОМ snolla-стеке VDS (наш VDS, не клиент).
- stostayer-deploy-image-via-pscp-load (memory) — техника pscp+load+pullImage:false.