fix(controller): stop migration failures being masked as success

Backfill-миграция V2.3.2 падала на testnet TS-ошибкой компиляции
(mongo.db возможно undefined, strictNullChecks), но deploy-скрипт
(blue-green-deploy.sh) рапортовал успех — 2.3.2 не появилась в
таблице migrations, extensions.capital_program_doc_data_hash осталась
пустой, а Semaphore-лог показывал "Database migrations completed
successfully".

Причина ложноположительного результата — не баг деплой-скрипта, а
Sentry: в production Sentry.init() (enabled: config.env==='production')
ставит process.on('unhandledRejection', ...) с дефолтным mode:'warn',
который перехватывает необработанный reject из bootstrap()'а --migrate
ветки и не роняет процесс — в отличие от штатного поведения голого
Node. В dev (Sentry выключен) тот же баг воспроизводимо валит процесс
exit 1; на testnet — exit 0. Подтверждено локальным воспроизведением
обеих версий на идентичном коде.

Фикс на двух уровнях:
1. index.ts: явный try/catch + process.exit(1) вокруг --migrate и
   --migrations-only веток. Не полагаемся на ambient unhandled-rejection
   поведение (Sentry/Node/версия) — exit code теперь детерминирован
   независимо от окружения.
2. V2.3.2: mongo.db действительно типизирован как возможно undefined —
   явная runtime-проверка с throw вместо каста.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
coopops
2026-07-19 16:57:59 +00:00
parent ba6d3d1310
commit 1b457b8f87
2 changed files with 26 additions and 5 deletions
@@ -72,6 +72,9 @@ export default {
try { try {
mongo = await mongoose.createConnection(config.mongoose.url).asPromise(); mongo = await mongoose.createConnection(config.mongoose.url).asPromise();
const db = mongo.db; const db = mongo.db;
if (!db) {
throw new Error('Подключение к MongoDB установлено, но db не инициализирована');
}
const collection = db.collection(COLLECTION_NAME); const collection = db.collection(COLLECTION_NAME);
await collection.createIndex({ hash: 1 }, { unique: true }); await collection.createIndex({ hash: 1 }, { unique: true });
await collection.updateOne( await collection.updateOne(
+23 -5
View File
@@ -62,15 +62,33 @@ async function bootstrap() {
// Проверяем, был ли запущен режим миграций // Проверяем, был ли запущен режим миграций
const args = process.argv.slice(2); const args = process.argv.slice(2);
if (args.includes('--migrate')) { if (args.includes('--migrate')) {
await migrateData(); try {
process.exit(0); await migrateData();
process.exit(0);
} catch (error) {
// migrateData() уже логирует причину; здесь важен только ненулевой
// exit code. В production Sentry.init() ставит свой глобальный
// process.on('unhandledRejection', ...) с дефолтным mode: 'warn' —
// он перехватывает необработанный reject этого await и НЕ роняет
// процесс (в отличие от штатного поведения Node без Sentry). Без
// явного process.exit(1) здесь deploy-скрипт получал ложноположительный
// exit code 0 при реально упавшей миграции (кейс V2.3.2 на testnet
// 2026-07-19 — TS-ошибка компиляции миграции проглатывалась именно так).
logger.error('Процесс миграции завершился с ошибкой, выходим с кодом 1', error);
process.exit(1);
}
} }
// Проверяем, был ли запущен режим только миграций (для обратной совместимости) // Проверяем, был ли запущен режим только миграций (для обратной совместимости)
if (args.includes('--migrations-only')) { if (args.includes('--migrations-only')) {
await migrateData(); try {
logger.info('Режим только миграций - миграции выполнены, сервер не будет запущен'); await migrateData();
process.exit(0); logger.info('Режим только миграций - миграции выполнены, сервер не будет запущен');
process.exit(0);
} catch (error) {
logger.error('Процесс миграции завершился с ошибкой, выходим с кодом 1', error);
process.exit(1);
}
} }
// Подключение к MongoDB // Подключение к MongoDB