disaster-restore вернул тарболлы но древний/пустой packument →
publish свежей версии EEXISTS 409. Fix: снять только коллизирующий
целевой .tgz (backup first), republish; НЕ rm -rf каталог.
Применено к @snollajs/{data,mailer,numbering,content-api}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.6 KiB
title, type, tags, sources, updated
| title | type | tags | sources | updated | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Verdaccio disaster-restore desync — tarball present, packument stale → EEXISTS 409 | concept |
|
2026-06-11 |
Verdaccio disaster-restore desync — 409 при publish свежей версии
Симптом
После disaster-restore verdaccio из бэкапа: npm/yarn publish <pkg>@<newver> падает с
409 "this package is already present", хотя в реестре этой версии (по GET) нет —
dist-tags.latest показывает древнюю версию. На некоторых пакетах GET вообще 404.
Root cause: packument ↔ tarball рассинхрон
Restore вернул в /storage/.../<pkg>/:
- все
.tgzтарболлы — до текущих версий включительно (data-0.9.0.tgzфизически на диске); - но
package.json(packument-индекс) — древний (versions{}знает только 0.1.x/0.2.x,dist-tags.latest=0.2.0), потому что метадата восстановилась из старого слоя бэкапа либо была затёрта сорваннымunpublish.
Verdaccio publish-flow:
- merge новой версии в packument → OK (версии нет в индексе);
- запись
.tgzна диск → файл уже существует (с restore) →EEXISTS→ surface как409 this package is already present.
Лог-отпечаток:
PUT /@snollajs%2Fdata
http <-- 409 ... error: this package is already present
Подвид: пустой packument после сорванного unpublish
unpublish пишет пустой packument-stub (161 байт, versions:[], _rev:""), затем пытается
rmdir каталог → ENOTEMPTY (тарболлы на месте) → 500. Итог: GET 404 (пустой индекс),
publish 409 (тарболл цел). Каталог «полупустой»: индекс мёртв, бинарники живы.
Fix: хирургия, НЕ rm -rf каталога
rm -rf /storage/.../<pkg> снимет 409, но уничтожит всю историю версий (тарболлы —
locally-published, _distfiles:{} ⇒ единственная копия, см. verdaccio-prune-semantics).
Правильно — удалить только коллизирующий целевой .tgz под версию, которую republish'ат:
# backup сначала
sudo tar czf /tmp/verdaccio-rescue-$(date -u +%Y%m%dT%H%M%SZ).tar.gz <pkg-dirs>
# снять коллизию — ровно целевой тарболл, остальное не трогать
sudo rm /opt/stacks/verdaccio/storage/@scope/<pkg>/<pkg>-<targetver>.tgz
# → republish <pkg>@<targetver> из source проходит, packument регенерится корректно
Рестарт контейнера не нужен: local-storage читает packument/тарболл с диска пер-реквест;
packument не трогаем, только убираем лишний .tgz, которого в индексе и так нет.
Trade-off: промежуточные версии (тарболлы на диске, но не в индексе после republish) остаются
orphaned — безвредны, не ставятся по pin, GC их не трогает (locally-published). Если нужна полная
история в индексе — отдельный скрипт-reconstruct packument из всех on-disk тарболлов
(tar xzO package/package.json per tgz + shasum/integrity) — heavier, по умолчанию не делаем.
Применено 2026-06-11
@snollajs/{data@0.9.0, mailer@0.7.4, numbering@0.7.4, content-api@0.8.1} — удалены 4
коллизирующих тарболла, бэкап /tmp/verdaccio-snollajs-rescue-20260611T140801Z.tar.gz на
../entities/vds-kzntsv. Republish делегирован владельцу пакетов (сессия snolla).
Контекст: восстановление verdaccio после wipe (см. также verdaccio-token-lifecycle — другой
класс отказа того же restore-эпизода).
Связанные
- verdaccio-prune-semantics — locally-published vs proxied, почему
rmтарболла = permanent loss - verdaccio-token-lifecycle — restart trap / ephemeral secret (соседний симптом того же restore)
- ../entities/vds-kzntsv — хост