check_blocking.mjs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. // Predeploy migration gate.
  2. //
  3. // Run as a Cloud Deploy predeploy hook (a K8s Job using the service's released image).
  4. // Blocks the rollout when there are pending migrations the image expects to have been applied.
  5. //
  6. // Exit codes:
  7. // 0 nothing pending, or all pending migrations are non-blocking
  8. // 1 one or more blocking pending migrations - rollout must not proceed
  9. // 2 the check itself failed (e.g. Mongo unreachable) - fail closed
  10. //
  11. // A migration is non-blocking when either:
  12. // - it is tagged "nonblocking" (preferred; safe to set on an already-applied migration), or
  13. // - its filename ends with "_nonBlocking" before the extension,
  14. // e.g. 20260101000000_backfill_thing_nonBlocking.mjs.
  15. import path from 'node:path'
  16. import { fileURLToPath } from 'node:url'
  17. import east from 'east'
  18. const { MigrationManager } = east
  19. const NON_BLOCKING_SUFFIX = '_nonBlocking'
  20. const NON_BLOCKING_TAG = 'nonblocking'
  21. const TAG = process.env.MIGRATION_TAG || 'saas'
  22. const scriptDir = path.dirname(fileURLToPath(import.meta.url))
  23. const migrationsDir = path.resolve(scriptDir, '..')
  24. function skipRequested() {
  25. const value = process.env.SKIP_MIGRATION_CHECK
  26. return Boolean(value) && value !== '0' && value !== 'false'
  27. }
  28. async function findBlockingMigrations() {
  29. // east resolves .eastrc, the migrations dir and the adapter relative to the
  30. // working directory (same as `npm run migrations`). chdir so the check works
  31. // regardless of how the container invokes it.
  32. process.chdir(migrationsDir)
  33. // The adapter aborts unless a tag is passed on argv; we pass the tag to east
  34. // directly, so disable that CLI-only guard.
  35. process.env.SKIP_TAG_CHECK = '1'
  36. const manager = new MigrationManager()
  37. await manager.configure({ esModules: true })
  38. await manager.connect()
  39. try {
  40. const pending = await manager.getMigrationNames({ status: 'new' })
  41. const candidates = pending.filter(
  42. name => !name.endsWith(NON_BLOCKING_SUFFIX)
  43. )
  44. const nonBlocking = pending.filter(name =>
  45. name.endsWith(NON_BLOCKING_SUFFIX)
  46. )
  47. const blocking = candidates.length
  48. ? await manager.getMigrationNames({
  49. migrations: candidates,
  50. tag: `${TAG} & !${NON_BLOCKING_TAG}`,
  51. })
  52. : []
  53. return { pending, nonBlocking, blocking }
  54. } finally {
  55. await manager.disconnect()
  56. }
  57. }
  58. async function main() {
  59. if (skipRequested()) {
  60. console.warn(
  61. `SKIP_MIGRATION_CHECK is set ("${process.env.SKIP_MIGRATION_CHECK}") - ` +
  62. 'bypassing the pending-migration gate. Only use this for incident recovery.'
  63. )
  64. return 0
  65. }
  66. const { pending, nonBlocking, blocking } = await findBlockingMigrations()
  67. if (nonBlocking.length) {
  68. console.log(
  69. `Ignoring ${nonBlocking.length} non-blocking pending migration(s):\n` +
  70. nonBlocking.map(name => ` - ${name}`).join('\n')
  71. )
  72. }
  73. if (blocking.length === 0) {
  74. console.log(
  75. pending.length === 0
  76. ? `No pending "${TAG}" migrations. Safe to deploy.`
  77. : `No blocking pending "${TAG}" migrations. Safe to deploy.`
  78. )
  79. return 0
  80. }
  81. console.error(
  82. `${blocking.length} blocking pending "${TAG}" migration(s) must be applied before deploying:\n` +
  83. blocking.map(name => ` - ${name}`).join('\n') +
  84. `\n\nApply them, then retry the rollout. To intentionally ship a non-blocking ` +
  85. `migration, add the "${NON_BLOCKING_TAG}" tag (preferred — safe on already-applied ` +
  86. `migrations) or rename the file with the "${NON_BLOCKING_SUFFIX}" suffix.`
  87. )
  88. return 1
  89. }
  90. main()
  91. .then(code => process.exit(code))
  92. .catch(err => {
  93. console.error('Migration check failed; blocking rollout (fail closed):')
  94. console.error(err)
  95. process.exit(2)
  96. })