Просмотр исходного кода

Merge pull request #34112 from overleaf/dn0-migration-check-predeploy-hook

Add predeploy migration gate for Mongo-bundling services

GitOrigin-RevId: d9eb192ea32b5328fb24fd453ddb1370f373858e
Daniel Kontšek 2 месяцев назад
Родитель
Сommit
2570b6559d
1 измененных файлов с 111 добавлено и 0 удалено
  1. 111 0
      tools/migrations/scripts/check_blocking.mjs

+ 111 - 0
tools/migrations/scripts/check_blocking.mjs

@@ -0,0 +1,111 @@
+// Predeploy migration gate.
+//
+// Run as a Cloud Deploy predeploy hook (a K8s Job using the service's released image).
+// Blocks the rollout when there are pending migrations the image expects to have been applied.
+//
+// Exit codes:
+//   0  nothing pending, or all pending migrations are non-blocking
+//   1  one or more blocking pending migrations - rollout must not proceed
+//   2  the check itself failed (e.g. Mongo unreachable) - fail closed
+//
+// A migration is non-blocking when either:
+//   - it is tagged "nonblocking" (preferred; safe to set on an already-applied migration), or
+//   - its filename ends with "_nonBlocking" before the extension,
+//     e.g. 20260101000000_backfill_thing_nonBlocking.mjs.
+
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import east from 'east'
+
+const { MigrationManager } = east
+
+const NON_BLOCKING_SUFFIX = '_nonBlocking'
+const NON_BLOCKING_TAG = 'nonblocking'
+const TAG = process.env.MIGRATION_TAG || 'saas'
+
+const scriptDir = path.dirname(fileURLToPath(import.meta.url))
+const migrationsDir = path.resolve(scriptDir, '..')
+
+function skipRequested() {
+  const value = process.env.SKIP_MIGRATION_CHECK
+  return Boolean(value) && value !== '0' && value !== 'false'
+}
+
+async function findBlockingMigrations() {
+  // east resolves .eastrc, the migrations dir and the adapter relative to the
+  // working directory (same as `npm run migrations`). chdir so the check works
+  // regardless of how the container invokes it.
+  process.chdir(migrationsDir)
+  // The adapter aborts unless a tag is passed on argv; we pass the tag to east
+  // directly, so disable that CLI-only guard.
+  process.env.SKIP_TAG_CHECK = '1'
+
+  const manager = new MigrationManager()
+  await manager.configure({ esModules: true })
+  await manager.connect()
+
+  try {
+    const pending = await manager.getMigrationNames({ status: 'new' })
+    const candidates = pending.filter(
+      name => !name.endsWith(NON_BLOCKING_SUFFIX)
+    )
+    const nonBlocking = pending.filter(name =>
+      name.endsWith(NON_BLOCKING_SUFFIX)
+    )
+    const blocking = candidates.length
+      ? await manager.getMigrationNames({
+          migrations: candidates,
+          tag: `${TAG} & !${NON_BLOCKING_TAG}`,
+        })
+      : []
+
+    return { pending, nonBlocking, blocking }
+  } finally {
+    await manager.disconnect()
+  }
+}
+
+async function main() {
+  if (skipRequested()) {
+    console.warn(
+      `SKIP_MIGRATION_CHECK is set ("${process.env.SKIP_MIGRATION_CHECK}") - ` +
+        'bypassing the pending-migration gate. Only use this for incident recovery.'
+    )
+    return 0
+  }
+
+  const { pending, nonBlocking, blocking } = await findBlockingMigrations()
+
+  if (nonBlocking.length) {
+    console.log(
+      `Ignoring ${nonBlocking.length} non-blocking pending migration(s):\n` +
+        nonBlocking.map(name => `  - ${name}`).join('\n')
+    )
+  }
+
+  if (blocking.length === 0) {
+    console.log(
+      pending.length === 0
+        ? `No pending "${TAG}" migrations. Safe to deploy.`
+        : `No blocking pending "${TAG}" migrations. Safe to deploy.`
+    )
+    return 0
+  }
+
+  console.error(
+    `${blocking.length} blocking pending "${TAG}" migration(s) must be applied before deploying:\n` +
+      blocking.map(name => `  - ${name}`).join('\n') +
+      `\n\nApply them, then retry the rollout. To intentionally ship a non-blocking ` +
+      `migration, add the "${NON_BLOCKING_TAG}" tag (preferred — safe on already-applied ` +
+      `migrations) or rename the file with the "${NON_BLOCKING_SUFFIX}" suffix.`
+  )
+  return 1
+}
+
+main()
+  .then(code => process.exit(code))
+  .catch(err => {
+    console.error('Migration check failed; blocking rollout (fail closed):')
+    console.error(err)
+    process.exit(2)
+  })