check_blocking.mjs 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  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 it is tagged "nonblocking".
  12. import path from 'node:path'
  13. import { fileURLToPath } from 'node:url'
  14. import east from 'east'
  15. const { MigrationManager } = east
  16. const NON_BLOCKING_TAG = 'nonblocking'
  17. const TAG = process.env.MIGRATION_TAG || 'saas'
  18. const scriptDir = path.dirname(fileURLToPath(import.meta.url))
  19. const migrationsDir = path.resolve(scriptDir, '..')
  20. function skipRequested() {
  21. const value = process.env.SKIP_MIGRATION_CHECK
  22. return Boolean(value) && value !== '0' && value !== 'false'
  23. }
  24. async function findBlockingMigrations() {
  25. // east resolves .eastrc, the migrations dir and the adapter relative to the
  26. // working directory (same as `npm run migrations`). chdir so the check works
  27. // regardless of how the container invokes it.
  28. process.chdir(migrationsDir)
  29. // The adapter aborts unless a tag is passed on argv; we pass the tag to east
  30. // directly, so disable that CLI-only guard.
  31. process.env.SKIP_TAG_CHECK = '1'
  32. const manager = new MigrationManager()
  33. await manager.configure({ esModules: true })
  34. await manager.connect()
  35. try {
  36. const pending = await manager.getMigrationNames({ status: 'new' })
  37. const blocking = pending.length
  38. ? await manager.getMigrationNames({
  39. migrations: pending,
  40. tag: `${TAG} & !${NON_BLOCKING_TAG}`,
  41. })
  42. : []
  43. return { pending, blocking }
  44. } finally {
  45. await manager.disconnect()
  46. }
  47. }
  48. async function main() {
  49. if (skipRequested()) {
  50. console.warn(
  51. `SKIP_MIGRATION_CHECK is set ("${process.env.SKIP_MIGRATION_CHECK}") - ` +
  52. 'bypassing the pending-migration gate. Only use this for incident recovery.'
  53. )
  54. return 0
  55. }
  56. const { pending, blocking } = await findBlockingMigrations()
  57. if (blocking.length === 0) {
  58. console.log(
  59. pending.length === 0
  60. ? `No pending "${TAG}" migrations. Safe to deploy.`
  61. : `No blocking pending "${TAG}" migrations. Safe to deploy.`
  62. )
  63. return 0
  64. }
  65. console.error(
  66. `${blocking.length} blocking pending "${TAG}" migration(s) must be applied before deploying:\n` +
  67. blocking.map(name => ` - ${name}`).join('\n') +
  68. `\n\nApply them, then retry the rollout. To intentionally ship a non-blocking ` +
  69. `migration, add the "${NON_BLOCKING_TAG}" tag.`
  70. )
  71. return 1
  72. }
  73. main()
  74. .then(code => process.exit(code))
  75. .catch(err => {
  76. console.error('Migration check failed; blocking rollout (fail closed):')
  77. console.error(err)
  78. process.exit(2)
  79. })