@intentius/sql-yodeler 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/README.md +28 -26
  2. package/dist/bin.js +15 -0
  3. package/dist/bin.js.map +1 -1
  4. package/dist/chant/apply.d.ts +9 -1
  5. package/dist/chant/apply.js +13 -1
  6. package/dist/chant/apply.js.map +1 -1
  7. package/dist/chant/build.js +7 -0
  8. package/dist/chant/build.js.map +1 -1
  9. package/dist/chant/drift.d.ts +69 -1
  10. package/dist/chant/drift.js +152 -5
  11. package/dist/chant/drift.js.map +1 -1
  12. package/dist/chant/runnable.d.ts +19 -0
  13. package/dist/chant/runnable.js +47 -0
  14. package/dist/chant/runnable.js.map +1 -0
  15. package/dist/ci/render.d.ts +289 -0
  16. package/dist/ci/render.js +688 -0
  17. package/dist/ci/render.js.map +1 -0
  18. package/dist/ci/replay.d.ts +17 -0
  19. package/dist/ci/replay.js +69 -0
  20. package/dist/ci/replay.js.map +1 -0
  21. package/dist/cli.d.ts +4 -0
  22. package/dist/cli.js +40 -0
  23. package/dist/cli.js.map +1 -1
  24. package/dist/command.d.ts +6 -0
  25. package/dist/command.js.map +1 -1
  26. package/dist/commands/apply.d.ts +113 -0
  27. package/dist/commands/apply.js +376 -49
  28. package/dist/commands/apply.js.map +1 -1
  29. package/dist/commands/approve.d.ts +5 -0
  30. package/dist/commands/approve.js +155 -0
  31. package/dist/commands/approve.js.map +1 -0
  32. package/dist/commands/checkpoint.d.ts +3 -0
  33. package/dist/commands/checkpoint.js +78 -0
  34. package/dist/commands/checkpoint.js.map +1 -0
  35. package/dist/commands/ci.d.ts +3 -0
  36. package/dist/commands/ci.js +118 -0
  37. package/dist/commands/ci.js.map +1 -0
  38. package/dist/commands/docs.d.ts +11 -0
  39. package/dist/commands/docs.js +108 -0
  40. package/dist/commands/docs.js.map +1 -0
  41. package/dist/commands/drift.d.ts +23 -0
  42. package/dist/commands/drift.js +263 -18
  43. package/dist/commands/drift.js.map +1 -1
  44. package/dist/commands/index.js +16 -0
  45. package/dist/commands/index.js.map +1 -1
  46. package/dist/commands/init.d.ts +3 -0
  47. package/dist/commands/init.js +15 -11
  48. package/dist/commands/init.js.map +1 -1
  49. package/dist/commands/lint.js +25 -8
  50. package/dist/commands/lint.js.map +1 -1
  51. package/dist/commands/mcp.d.ts +3 -0
  52. package/dist/commands/mcp.js +50 -0
  53. package/dist/commands/mcp.js.map +1 -0
  54. package/dist/commands/new.d.ts +2 -0
  55. package/dist/commands/new.js +134 -10
  56. package/dist/commands/new.js.map +1 -1
  57. package/dist/commands/override.d.ts +3 -0
  58. package/dist/commands/override.js +150 -0
  59. package/dist/commands/override.js.map +1 -0
  60. package/dist/commands/plan.js +43 -27
  61. package/dist/commands/plan.js.map +1 -1
  62. package/dist/commands/repair.d.ts +11 -0
  63. package/dist/commands/repair.js +15 -7
  64. package/dist/commands/repair.js.map +1 -1
  65. package/dist/commands/report.d.ts +13 -0
  66. package/dist/commands/report.js +121 -0
  67. package/dist/commands/report.js.map +1 -0
  68. package/dist/commands/revert.d.ts +20 -0
  69. package/dist/commands/revert.js +246 -0
  70. package/dist/commands/revert.js.map +1 -0
  71. package/dist/commands/status.js +44 -17
  72. package/dist/commands/status.js.map +1 -1
  73. package/dist/config/base.d.ts +69 -0
  74. package/dist/config/base.js +121 -0
  75. package/dist/config/base.js.map +1 -0
  76. package/dist/config/define.d.ts +223 -3
  77. package/dist/config/define.js +3 -1
  78. package/dist/config/define.js.map +1 -1
  79. package/dist/config/index.d.ts +2 -0
  80. package/dist/config/index.js +2 -0
  81. package/dist/config/index.js.map +1 -1
  82. package/dist/config/load.d.ts +1 -1
  83. package/dist/config/load.js +150 -5
  84. package/dist/config/load.js.map +1 -1
  85. package/dist/config/settings.d.ts +18 -2
  86. package/dist/config/settings.js +40 -6
  87. package/dist/config/settings.js.map +1 -1
  88. package/dist/env-args.d.ts +6 -0
  89. package/dist/env-args.js +31 -1
  90. package/dist/env-args.js.map +1 -1
  91. package/dist/exit.d.ts +20 -0
  92. package/dist/exit.js +28 -0
  93. package/dist/exit.js.map +1 -0
  94. package/dist/history/client.d.ts +8 -0
  95. package/dist/history/client.js +24 -1
  96. package/dist/history/client.js.map +1 -1
  97. package/dist/history/lock.d.ts +5 -1
  98. package/dist/history/lock.js +8 -2
  99. package/dist/history/lock.js.map +1 -1
  100. package/dist/history/postgres/ddl.d.ts +18 -0
  101. package/dist/history/postgres/ddl.js +27 -2
  102. package/dist/history/postgres/ddl.js.map +1 -1
  103. package/dist/history/postgres/store.d.ts +2 -0
  104. package/dist/history/postgres/store.js +7 -1
  105. package/dist/history/postgres/store.js.map +1 -1
  106. package/dist/history/rows.d.ts +16 -3
  107. package/dist/history/rows.js +30 -1
  108. package/dist/history/rows.js.map +1 -1
  109. package/dist/history/store.d.ts +2 -0
  110. package/dist/history/store.js +2 -2
  111. package/dist/history/store.js.map +1 -1
  112. package/dist/index.d.ts +4 -1
  113. package/dist/index.js +1 -0
  114. package/dist/index.js.map +1 -1
  115. package/dist/init/adopt.js +8 -3
  116. package/dist/init/adopt.js.map +1 -1
  117. package/dist/init/live.d.ts +11 -0
  118. package/dist/init/live.js +25 -2
  119. package/dist/init/live.js.map +1 -1
  120. package/dist/lint/check.d.ts +8 -1
  121. package/dist/lint/check.js +38 -4
  122. package/dist/lint/check.js.map +1 -1
  123. package/dist/lint/report.js +1 -1
  124. package/dist/lint/report.js.map +1 -1
  125. package/dist/lint/routines.d.ts +26 -0
  126. package/dist/lint/routines.js +65 -0
  127. package/dist/lint/routines.js.map +1 -0
  128. package/dist/lint/rules.d.ts +16 -4
  129. package/dist/lint/rules.js +9 -4
  130. package/dist/lint/rules.js.map +1 -1
  131. package/dist/mcp/server.d.ts +40 -0
  132. package/dist/mcp/server.js +106 -0
  133. package/dist/mcp/server.js.map +1 -0
  134. package/dist/mcp/tools.d.ts +72 -0
  135. package/dist/mcp/tools.js +178 -0
  136. package/dist/mcp/tools.js.map +1 -0
  137. package/dist/migrations/checkpoint.d.ts +45 -0
  138. package/dist/migrations/checkpoint.js +110 -0
  139. package/dist/migrations/checkpoint.js.map +1 -0
  140. package/dist/migrations/checks.d.ts +28 -0
  141. package/dist/migrations/checks.js +97 -0
  142. package/dist/migrations/checks.js.map +1 -0
  143. package/dist/migrations/create.d.ts +3 -1
  144. package/dist/migrations/create.js +3 -0
  145. package/dist/migrations/create.js.map +1 -1
  146. package/dist/migrations/directory.js +4 -0
  147. package/dist/migrations/directory.js.map +1 -1
  148. package/dist/migrations/document.d.ts +33 -0
  149. package/dist/migrations/document.js +2 -0
  150. package/dist/migrations/document.js.map +1 -1
  151. package/dist/migrations/index.d.ts +3 -0
  152. package/dist/migrations/index.js +3 -0
  153. package/dist/migrations/index.js.map +1 -1
  154. package/dist/migrations/routines.d.ts +31 -0
  155. package/dist/migrations/routines.js +55 -0
  156. package/dist/migrations/routines.js.map +1 -0
  157. package/dist/notify/index.d.ts +124 -0
  158. package/dist/notify/index.js +210 -0
  159. package/dist/notify/index.js.map +1 -0
  160. package/dist/notify/verify.d.ts +53 -0
  161. package/dist/notify/verify.js +71 -0
  162. package/dist/notify/verify.js.map +1 -0
  163. package/dist/pr/forge.d.ts +4 -0
  164. package/dist/pr/forge.js +21 -7
  165. package/dist/pr/forge.js.map +1 -1
  166. package/dist/pr/issue.d.ts +68 -0
  167. package/dist/pr/issue.js +107 -0
  168. package/dist/pr/issue.js.map +1 -0
  169. package/dist/pr/model.d.ts +12 -1
  170. package/dist/pr/model.js +44 -6
  171. package/dist/pr/model.js.map +1 -1
  172. package/dist/pr/render.d.ts +3 -1
  173. package/dist/pr/render.js +63 -4
  174. package/dist/pr/render.js.map +1 -1
  175. package/dist/project.d.ts +19 -2
  176. package/dist/project.js +33 -1
  177. package/dist/project.js.map +1 -1
  178. package/dist/replay/index.d.ts +3 -0
  179. package/dist/replay/index.js +3 -0
  180. package/dist/replay/index.js.map +1 -1
  181. package/dist/replay/replay.d.ts +9 -1
  182. package/dist/replay/replay.js +63 -32
  183. package/dist/replay/replay.js.map +1 -1
  184. package/dist/replay/report.js +1 -1
  185. package/dist/replay/report.js.map +1 -1
  186. package/dist/replay/shared.d.ts +28 -0
  187. package/dist/replay/shared.js +77 -0
  188. package/dist/replay/shared.js.map +1 -0
  189. package/dist/report/audit.d.ts +138 -0
  190. package/dist/report/audit.js +202 -0
  191. package/dist/report/audit.js.map +1 -0
  192. package/dist/report/jsonl.d.ts +21 -0
  193. package/dist/report/jsonl.js +52 -0
  194. package/dist/report/jsonl.js.map +1 -0
  195. package/dist/report/model.d.ts +68 -0
  196. package/dist/report/model.js +127 -0
  197. package/dist/report/model.js.map +1 -0
  198. package/dist/report/render.d.ts +18 -0
  199. package/dist/report/render.js +127 -0
  200. package/dist/report/render.js.map +1 -0
  201. package/dist/schema-docs/render.d.ts +49 -0
  202. package/dist/schema-docs/render.js +359 -0
  203. package/dist/schema-docs/render.js.map +1 -0
  204. package/dist/schemas.d.ts +37 -0
  205. package/dist/schemas.js +41 -0
  206. package/dist/schemas.js.map +1 -0
  207. package/dist/server.d.ts +40 -0
  208. package/dist/server.js +87 -0
  209. package/dist/server.js.map +1 -0
  210. package/dist/sources/ddl.d.ts +53 -0
  211. package/dist/sources/ddl.js +347 -0
  212. package/dist/sources/ddl.js.map +1 -0
  213. package/dist/sources/index.d.ts +69 -0
  214. package/dist/sources/index.js +170 -0
  215. package/dist/sources/index.js.map +1 -0
  216. package/dist/steps/backfill.d.ts +21 -17
  217. package/dist/steps/backfill.js +20 -20
  218. package/dist/steps/backfill.js.map +1 -1
  219. package/dist/steps/index.d.ts +2 -2
  220. package/dist/steps/index.js +3 -3
  221. package/dist/steps/index.js.map +1 -1
  222. package/dist/steps/postgres-migration.d.ts +35 -25
  223. package/dist/steps/postgres-migration.js +42 -29
  224. package/dist/steps/postgres-migration.js.map +1 -1
  225. package/dist/steps/rebuild.d.ts +15 -15
  226. package/dist/steps/rebuild.js +17 -24
  227. package/dist/steps/rebuild.js.map +1 -1
  228. package/dist/tenants/index.d.ts +40 -0
  229. package/dist/tenants/index.js +122 -0
  230. package/dist/tenants/index.js.map +1 -0
  231. package/dist/tenants/target.d.ts +23 -0
  232. package/dist/tenants/target.js +29 -0
  233. package/dist/tenants/target.js.map +1 -0
  234. package/dist/versioned/checks.d.ts +29 -0
  235. package/dist/versioned/checks.js +54 -0
  236. package/dist/versioned/checks.js.map +1 -0
  237. package/dist/versioned/clickhouse.js +8 -0
  238. package/dist/versioned/clickhouse.js.map +1 -1
  239. package/dist/versioned/driver.d.ts +10 -0
  240. package/dist/versioned/engine.d.ts +61 -1
  241. package/dist/versioned/engine.js +109 -6
  242. package/dist/versioned/engine.js.map +1 -1
  243. package/dist/versioned/explain.js +7 -2
  244. package/dist/versioned/explain.js.map +1 -1
  245. package/dist/versioned/index.d.ts +10 -0
  246. package/dist/versioned/index.js +25 -1
  247. package/dist/versioned/index.js.map +1 -1
  248. package/dist/versioned/op.d.ts +16 -1
  249. package/dist/versioned/op.js +43 -6
  250. package/dist/versioned/op.js.map +1 -1
  251. package/dist/versioned/policy.d.ts +98 -0
  252. package/dist/versioned/policy.js +212 -0
  253. package/dist/versioned/policy.js.map +1 -0
  254. package/dist/versioned/postgres/driver.d.ts +3 -1
  255. package/dist/versioned/postgres/driver.js +30 -3
  256. package/dist/versioned/postgres/driver.js.map +1 -1
  257. package/dist/versioned/postgres/waits.d.ts +46 -0
  258. package/dist/versioned/postgres/waits.js +78 -0
  259. package/dist/versioned/postgres/waits.js.map +1 -0
  260. package/dist/versioned/report.d.ts +3 -0
  261. package/dist/versioned/report.js +13 -1
  262. package/dist/versioned/report.js.map +1 -1
  263. package/dist/versioned/revert.d.ts +128 -0
  264. package/dist/versioned/revert.js +251 -0
  265. package/dist/versioned/revert.js.map +1 -0
  266. package/dist/versioned/stand-down.d.ts +30 -0
  267. package/dist/versioned/stand-down.js +56 -0
  268. package/dist/versioned/stand-down.js.map +1 -0
  269. package/dist/versioned/state.d.ts +25 -0
  270. package/dist/versioned/state.js +36 -3
  271. package/dist/versioned/state.js.map +1 -1
  272. package/dist/waves/index.d.ts +179 -0
  273. package/dist/waves/index.js +268 -0
  274. package/dist/waves/index.js.map +1 -0
  275. package/dist/waves/promotion.d.ts +56 -0
  276. package/dist/waves/promotion.js +108 -0
  277. package/dist/waves/promotion.js.map +1 -0
  278. package/dist/waves/record.d.ts +24 -0
  279. package/dist/waves/record.js +20 -0
  280. package/dist/waves/record.js.map +1 -0
  281. package/package.json +22 -8
  282. package/schemas/apply.schema.json +428 -0
  283. package/schemas/checkpoint.schema.json +19 -0
  284. package/schemas/common.schema.json +213 -0
  285. package/schemas/drift.schema.json +61 -0
  286. package/schemas/event.schema.json +36 -0
  287. package/schemas/init.schema.json +31 -0
  288. package/schemas/lint-rules.schema.json +54 -0
  289. package/schemas/lint.schema.json +294 -0
  290. package/schemas/mcp.schema.json +70 -0
  291. package/schemas/new.schema.json +71 -0
  292. package/schemas/plan.schema.json +489 -0
  293. package/schemas/rebase.schema.json +54 -0
  294. package/schemas/repair.schema.json +20 -0
  295. package/schemas/report.schema.json +114 -0
  296. package/schemas/revert.schema.json +69 -0
  297. package/schemas/status.schema.json +371 -0
@@ -4,11 +4,17 @@ import { loadProject, resolveEnvironment, YodelError } from "../project.js";
4
4
  import { buildSchema } from "../chant/build.js";
5
5
  import { declaredChanges, planEnvironment, planExitCode, planJson, renderPlan, PLAN_NEEDS_OP } from "../chant/plan.js";
6
6
  import { findApplyOp, GATED, runApplyOp } from "../chant/apply.js";
7
- import { readMigrations } from "../migrations/index.js";
7
+ import { runnable } from "../chant/runnable.js";
8
8
  import { migrationSilences } from "../lint/index.js";
9
9
  import { askedAgainLine, describeMove, explainAgainst, latestApproval } from "../versioned/explain.js";
10
- import { declarativeTopologyRefusal } from "../config/settings.js";
11
- import { ALLOW_OUT_OF_ORDER_ENV, APPROVED_PLAN_ENV, currentDigest, executeApply, findMigrateOp, gitLedger, verifyApproval, hasMigrations, inspect, migrationsDir, closeVersioned, openVersioned, renderStatus, runMigrateOp, selectToApply, statusJson, statusReport, } from "../versioned/index.js";
10
+ import { declarativeTopologyRefusal, LOCK_WAIT_ENV, parseDuration } from "../config/settings.js";
11
+ import { policyAtBase } from "../config/base.js";
12
+ import { newerCommit, STAND_DOWN_ENV, standDownFromEnv, standDownLine } from "../versioned/stand-down.js";
13
+ import { describeServer, probeServer } from "../server.js";
14
+ import { eventFromRun, sendEvent } from "../notify/index.js";
15
+ import { EXIT_OUTSTANDING, EXIT_REFUSED } from "../exit.js";
16
+ import { enforcePromotion, forecastWave, planWave, readWavePlan, verifyWaveDecision, WAVES_SPEC_FILE, writeWavePlan } from "../waves/index.js";
17
+ import { ALLOW_OUT_OF_ORDER_ENV, APPROVED_PLAN_ENV, checksDue, checksRefusal, describeResults, judgePlan, judgeRevert, policyRefusal, runChecks, currentDigest, currentRevert, executeApply, executeRevert, revertFromEnv, findMigrateOp, gitLedger, verifyApproval, hasMigrations, inspect, readChain, recordStepExit, closeVersioned, openVersioned, renderStatus, runMigrateOp, selectToApply, statusJson, statusReport, } from "../versioned/index.js";
12
18
  /** When the gate's newest approval does not hold: what moved since (or that approval was asked for again), in one line. */
13
19
  async function movedSinceApproval(project, venv, ordered, op, digest) {
14
20
  try {
@@ -31,7 +37,18 @@ async function movedSinceApproval(project, venv, ordered, op, digest) {
31
37
  return undefined;
32
38
  }
33
39
  }
34
- const usage = "usage: yodel apply <env> [--json] [--dir <project>] [--op <name>] [--allow-out-of-order] [--plan]";
40
+ /** The document `yodel apply --json` prints (schemas/apply.schema.json). */
41
+ export function applyJson(doc) {
42
+ if (doc.mode === "versioned")
43
+ return { ...statusJson(doc.report), mode: "versioned", ...doc.outcome };
44
+ if (doc.mode === "execute")
45
+ return { environment: doc.environment, status: doc.result.stoodDown ? "stood-down" : "applied", planDigest: doc.planDigest, ...(doc.wavePlanDigest ? { wavePlanDigest: doc.wavePlanDigest } : {}), ...doc.result };
46
+ if (doc.mode === "wave-plan")
47
+ return { ...doc.plan };
48
+ return { environment: doc.environment, dialect: doc.dialect, ...(doc.server ? { server: doc.server } : {}), op: doc.op, ...doc.outcome };
49
+ }
50
+ const usage = "usage: yodel apply <env> [--json] [--dir <project>] [--op <name>] [--allow-out-of-order] [--plan] [--lock-wait <duration>] [--stand-down]";
51
+ const waveModes = ["wave-plan", "wave-apply"];
35
52
  const help = `${usage}
36
53
 
37
54
  Two paths, chosen by the project:
@@ -46,7 +63,9 @@ Two paths, chosen by the project:
46
63
  environment (chant run <op>), whose gate is bound to the plan digest: the
47
64
  pending migrations' checksums, the topology, the history and the live
48
65
  schema. A run that stops at the gate prints the chant approve command;
49
- approve, then run yodel apply again. If anything the digest covers changes after
66
+ approve, then run yodel apply again. At a terminal, yodel approve <env>
67
+ approves the same plan without the digest to copy: yodel approve <env>,
68
+ then yodel apply <env>. If anything the digest covers changes after
50
69
  approval, the gate asks again.
51
70
 
52
71
  A migration that failed part way resumes at the statement that failed; a
@@ -59,6 +78,24 @@ Two paths, chosen by the project:
59
78
  pending migration the chain puts before an applied one is out of order
60
79
  and is refused unless --allow-out-of-order; it is never skipped.
61
80
 
81
+ The policy in yodel.config.ts (policy.rules) is judged over the plan
82
+ before the gate and again under the lock: a rule that denies it refuses
83
+ (exit 4) naming the rule, unless someone policy.overriders lists recorded
84
+ an override for exactly this plan digest (yodel override <env> --rule <id>
85
+ --reason <text>). The history records the overrides a run relied on.
86
+
87
+ A migration's pre-migration checks (checks in its migration.json, read-only
88
+ queries) run against the environment: the first pending migration's before
89
+ the gate, and every migration's under the lock just before its first
90
+ statement. One that fails refuses (exit 4), naming it, and nothing of that
91
+ migration is applied.
92
+
93
+ <env> can be one tenant of a tenant set, <env>/<tenant> (yodel.config.ts
94
+ environments.<env>.tenants): the environment's profile on the tenant's
95
+ database or schema, each statement with the template's name replaced by
96
+ the tenant's, and a history of its own. A tenant set is applied tenant by
97
+ tenant, or by its wave, which gates once for every tenant.
98
+
62
99
  - Declarative: no migrations. Plans the declared schema against the live
63
100
  database, then runs the project's ApplyOp for the environment behind its
64
101
  plan-bound gate.
@@ -69,16 +106,37 @@ Options:
69
106
  --allow-out-of-order versioned: apply out-of-order migrations too
70
107
  --plan versioned: print the pending migrations and the plan
71
108
  digest, apply nothing
109
+ --lock-wait <d> versioned: when another apply holds the lock, wait up
110
+ to <d> (90, 30s, 10m, 1h) for it, naming the holder,
111
+ before giving up with exit 5. Default: yodel.config.ts
112
+ lockWait (seconds), else YODEL_LOCK_WAIT, else 0
113
+ --stand-down versioned: when the branch this run is for has a newer
114
+ commit on origin (GITHUB_REF_NAME, CI_COMMIT_BRANCH,
115
+ else the checked-out branch), apply nothing and exit 0:
116
+ that commit's own apply applies this environment.
117
+ Asked before the lock and while waiting for it. Also
118
+ YODEL_STAND_DOWN=1
72
119
  -C, --dir <dir> the project directory (default: the working directory)
73
120
 
74
121
  The migrations Op runs two steps of its own, not for direct use:
75
122
  --digest print the plan digest alone (the Op's Plan step)
76
- --execute apply under the lock, if the digest is still the one in ${APPROVED_PLAN_ENV}
77
- and chant's gate ledger holds an approval of the Op's gate for it
78
- (the Op's Apply step)
123
+ --execute apply under the lock, if the digest is still the one in
124
+ ${APPROVED_PLAN_ENV} and chant's gate ledger holds an approval
125
+ of the Op's gate for it (the Op's Apply step)
126
+
127
+ The apply pipeline's wave jobs (chant run wave, ${WAVES_SPEC_FILE}) run two more:
128
+ --wave-plan <file> write the environment's wave plan to <file>: the
129
+ plan digest, the gate policy at the base commit, and
130
+ whether a pending step is destructive
131
+ --wave-apply <file> apply the plan in <file> under the lock, if it is
132
+ still the plan now and chant's decision for the
133
+ wave lets it through (an approval of the wave's
134
+ digest, or a policy at the base commit that needs none)
135
+ --spec <file> the waves spec (default ${WAVES_SPEC_FILE})
79
136
 
80
137
  Exit codes:
81
- 0 applied, or nothing to apply
138
+ 0 applied, or nothing to apply, or (--stand-down) stood down for a newer
139
+ commit, applying nothing
82
140
  2 a change needs its own Op (declarative), or a pending migration has a
83
141
  manual step or an Op step apply does not run (versioned: it runs
84
142
  ClickHouseRebuildOp in materialized-view mode and backfill steps on
@@ -86,9 +144,13 @@ Exit codes:
86
144
  nothing was applied
87
145
  3 stopped at the gate, waiting for approval of this plan
88
146
  4 versioned: refused, nothing applied: a checksum mismatch, an applied
89
- migration gone from the directory, or an out-of-order migration
90
- without --allow-out-of-order
91
- 5 versioned: another apply holds the lock
147
+ migration gone from the directory, an out-of-order migration
148
+ without --allow-out-of-order, a pre-migration check that failed, a
149
+ policy rule that denies the plan with no override, or a pending
150
+ migration the environment this one requires (yodel.config.ts
151
+ waves[].requires, default the wave before) does not record as applied
152
+ with the same checksum
153
+ 5 versioned: another apply holds the lock (after --lock-wait, if given)
92
154
  1 anything else failed: a statement (the history records where; rerun to
93
155
  resume), a plan that moved since approval, a bad config
94
156
  `;
@@ -107,6 +169,11 @@ function parseApplyArgs(args) {
107
169
  plan: { type: "boolean" },
108
170
  digest: { type: "boolean" },
109
171
  execute: { type: "boolean" },
172
+ "lock-wait": { type: "string" },
173
+ "stand-down": { type: "boolean" },
174
+ "wave-plan": { type: "string" },
175
+ "wave-apply": { type: "string" },
176
+ spec: { type: "string" },
110
177
  },
111
178
  });
112
179
  }
@@ -118,7 +185,8 @@ function parseApplyArgs(args) {
118
185
  if (env === undefined || rest.length > 0)
119
186
  throw new YodelError(usage);
120
187
  const flag = (k) => values[k] === true;
121
- const modes = ["plan", "digest", "execute"].filter(flag);
188
+ const str = (k) => (typeof values[k] === "string" ? values[k] : undefined);
189
+ const modes = [...["plan", "digest", "execute"].filter(flag), ...waveModes.filter((k) => str(k) !== undefined)];
122
190
  if (modes.length > 1)
123
191
  throw new YodelError(`--${modes.join(" and --")} cannot be combined\n${usage}`);
124
192
  return {
@@ -130,47 +198,196 @@ function parseApplyArgs(args) {
130
198
  plan: flag("plan"),
131
199
  digest: flag("digest"),
132
200
  execute: flag("execute"),
201
+ lockWaitMs: typeof values["lock-wait"] === "string" ? parseDuration(values["lock-wait"], "--lock-wait") : undefined,
202
+ standDown: flag("stand-down") || standDownFromEnv(process.env),
203
+ wavePlan: str("wave-plan"),
204
+ waveApply: str("wave-apply"),
205
+ spec: str("spec") ?? WAVES_SPEC_FILE,
133
206
  };
134
207
  }
135
208
  /** `yodel apply <env>`; the exit codes are in `help`. */
136
209
  export const apply = {
137
210
  name: "apply",
138
- summary: "bring an environment to the declared schema or the pending migrations",
211
+ summary: "bring an environment to its declared schema or pending migrations",
139
212
  implemented: true,
140
- run: (args, io) => guarded("apply", io, args, help, async () => {
141
- const a = parseApplyArgs(args);
142
- const project = await loadProject(a.dir);
143
- const env = resolveEnvironment(project, a.env);
144
- if (hasMigrations(project))
145
- return versionedApply(project, env, a, io);
146
- if (a.plan || a.digest || a.execute || a.allowOutOfOrder) {
147
- throw new YodelError(`--plan, --digest, --execute and --allow-out-of-order are for versioned migrations, and ${project.dir} has no migrations/ directory with a migration`);
148
- }
149
- const refusal = project.dialect === "clickhouse" ? declarativeTopologyRefusal(project, env.name) : undefined;
150
- if (refusal)
151
- throw refusal;
152
- return declarativeApply(project, env, a, io);
153
- }),
213
+ run: async (args, io) => {
214
+ const code = await guarded("apply", io, args, help, async () => {
215
+ const a = parseApplyArgs(args);
216
+ const project = await loadProject(a.dir);
217
+ const env = resolveEnvironment(project, a.env);
218
+ if (!env.tenant && project.yodel?.config.environments?.[env.name]?.tenants) {
219
+ throw new YodelError(`${env.name} is a tenant set (yodel.config.ts environments.${env.name}.tenants): apply a tenant, yodel apply ${env.name}/<tenant>, or every tenant through ${env.name}'s wave in the apply pipeline`);
220
+ }
221
+ if (hasMigrations(project))
222
+ return versionedApply(project, env, a, io);
223
+ if (a.plan || a.digest || a.execute || a.allowOutOfOrder || a.lockWaitMs !== undefined || args.includes("--stand-down") || a.wavePlan !== undefined || a.waveApply !== undefined) {
224
+ throw new YodelError(`--plan, --digest, --execute, --wave-plan, --wave-apply, --allow-out-of-order, --lock-wait and --stand-down are for versioned migrations, and ${project.dir} has no migrations/ directory with a migration`);
225
+ }
226
+ const refusal = project.dialect === "clickhouse" ? declarativeTopologyRefusal(project, env.name) : undefined;
227
+ if (refusal)
228
+ throw refusal;
229
+ return declarativeApply(project, env, a, io);
230
+ });
231
+ if (args.includes("--execute"))
232
+ recordStepExit(code);
233
+ return code;
234
+ },
154
235
  };
155
236
  async function versionedApply(project, env, a, io) {
156
- const ordered = readMigrations(migrationsDir(project));
237
+ const ordered = readChain(project);
157
238
  const venv = await openVersioned(project, env);
239
+ const outcome = {};
240
+ let code;
158
241
  try {
159
- return await versionedApplyWith(project, env, a, io, ordered, venv);
242
+ code = await versionedApplyWith(project, env, a, io, ordered, venv, outcome);
160
243
  }
161
- finally {
244
+ catch (e) {
245
+ // The wave modes run inside chant run wave, which keeps no outcome of its own: a refusal or failure there is told here (#87).
246
+ if (a.wavePlan !== undefined || a.waveApply !== undefined)
247
+ outcome.doc = waveFailure(env.name, e);
162
248
  await closeVersioned(venv);
249
+ if (outcome.doc)
250
+ await sendEvent(project.yodel?.config.notify, eventFromRun("apply", outcome.doc), { io, verb: "apply" });
251
+ throw e;
163
252
  }
253
+ await closeVersioned(venv);
254
+ // Waiting, refused or failed: the webhook in yodel.config.ts notify hears of it (#61).
255
+ if (outcome.doc)
256
+ await sendEvent(project.yodel?.config.notify, eventFromRun("apply", outcome.doc), { io, verb: "apply" });
257
+ return code;
258
+ }
259
+ /** The outcome a wave mode's error is: refused (exit 4, or 3 for a wave with no decision that lets it through; nothing applied) or failed. */
260
+ export function waveFailure(environment, e) {
261
+ const message = e instanceof Error ? e.message : String(e);
262
+ const exitCode = e instanceof YodelError ? e.exitCode : 1;
263
+ return exitCode === EXIT_REFUSED || exitCode === EXIT_OUTSTANDING ? { environment, status: "refused", refusal: { message } } : { environment, status: "failed", exitCode, reason: message };
164
264
  }
165
- async function versionedApplyWith(project, env, a, io, ordered, venv) {
265
+ async function versionedApplyWith(project, env, a, io, ordered, venv, outcome = {}) {
266
+ // yodel revert (#55) runs this Op with the migration to revert in the environment: its Plan and Apply steps plan and run that instead.
267
+ const revert = a.digest || a.execute ? revertFromEnv(process.env, project.dir) : undefined;
268
+ if (revert && a.digest) {
269
+ io.out(`${(await currentRevert(venv, ordered, revert.id, revert.step)).digest}\n`);
270
+ return 0;
271
+ }
272
+ if (revert && a.execute) {
273
+ const approved = process.env[APPROVED_PLAN_ENV];
274
+ if (!approved)
275
+ throw new YodelError(`--execute reverts only the plan its Op's gate approved, and ${APPROVED_PLAN_ENV} is not set; run yodel revert ${env.name} ${revert.id}`);
276
+ const op = await findMigrateOp(project, env, a.op);
277
+ const ledger = await gitLedger(project.dir);
278
+ // The policy over the revert (#97), as the base commit has it (#73), as yodel revert judged it before the gate.
279
+ const { policy: basePolicy, note: policyNote } = await policyAtBase(project);
280
+ if (policyNote)
281
+ io.err(`${policyNote}\n`);
282
+ const plan = await executeRevert(venv, ordered, {
283
+ id: revert.id,
284
+ step: revert.step,
285
+ approvedDigest: approved,
286
+ ...(a.lockWaitMs !== undefined ? { lockWaitMs: a.lockWaitMs } : {}),
287
+ approve: async (digest) => {
288
+ const by = await verifyApproval(ledger, op, digest);
289
+ io.err(`approval: ${op.name} / ${op.gate} for ${digest}, by ${by.resolvedBy} at ${by.timestamp}\n`);
290
+ },
291
+ ...(basePolicy
292
+ ? {
293
+ policy: async (digest, planned) => {
294
+ const results = await judgeRevert({ policy: basePolicy, environment: env.name, dialect: project.dialect, plan: planned, digest, ledger: () => Promise.resolve({ port: ledger, op }) });
295
+ const refusal = policyRefusal(results, env.name, digest, "under the lock", { id: revert.id, step: revert.step?.file });
296
+ if (refusal)
297
+ throw refusal;
298
+ for (const line of describeResults(results))
299
+ io.err(`${line}\n`);
300
+ return results.flatMap((r) => (r.override ? [r.override] : []));
301
+ },
302
+ }
303
+ : {}),
304
+ log: (l) => { io.err(`${l}\n`); },
305
+ });
306
+ io.out(`Reverted: ${plan.migration.id}, back to ${plan.to ?? "an empty schema"}\n`);
307
+ return 0;
308
+ }
166
309
  if (a.digest) {
167
310
  const { status } = await inspect(venv, ordered);
168
311
  const toApply = selectToApply(status, a.allowOutOfOrder);
169
312
  io.out(`${await currentDigest(venv, status, toApply)}\n`);
170
313
  return 0;
171
314
  }
315
+ if (a.wavePlan !== undefined) {
316
+ const plan = await writeWavePlan(project, venv, ordered, a.wavePlan, { allowOutOfOrder: a.allowOutOfOrder });
317
+ io.err(`${env.name}: ${plan.empty ? "nothing to apply" : `${String(plan.pending.length)} pending (${plan.pending.join(", ")})`}; gate ${plan.gate} at ${plan.base.slice(0, 12)}` +
318
+ `${plan.destructive ? `; destructive: ${plan.destructiveSteps.map((d) => `${d.migration} step ${String(d.step)} (${d.why})`).join(", ")}` : ""}; plan ${plan.planDigest}\n`);
319
+ // chant decides the wave right after this; tell the webhook now when it will wait (#87), since chant sends nothing.
320
+ const forecast = await forecastWave(process.cwd(), a.spec, plan);
321
+ if (forecast?.waits) {
322
+ // A tenant set's wave (#56) is approved once for every tenant, with chant approve.
323
+ const how = env.tenant ? `for every tenant of ${env.tenant.env}. Approve it with: ` : `. Approve it with yodel approve ${env.name}, or: `;
324
+ io.err(`${env.name}: wave ${String(forecast.wave)} waits for an approval of ${forecast.digest} (gate ${forecast.policy} at ${forecast.base.slice(0, 12)})${how}${forecast.approve}\n`);
325
+ outcome.doc = { environment: env.name, status: "gated", pending: plan.pending.map((id) => ({ id })), gatePlanDigest: forecast.digest, approve: forecast.approve };
326
+ }
327
+ if (a.json)
328
+ io.out(`${JSON.stringify(applyJson({ mode: "wave-plan", plan }), null, 2)}\n`);
329
+ return 0;
330
+ }
331
+ if (a.waveApply !== undefined) {
332
+ const planned = readWavePlan(a.waveApply);
333
+ const { policy: basePolicy, note: policyNote } = await policyAtBase(project);
334
+ if (policyNote)
335
+ io.err(`${policyNote}\n`);
336
+ if (planned.environment !== env.name)
337
+ throw new YodelError(`refused: ${a.waveApply} is ${planned.environment}'s plan, not ${env.name}'s; nothing was applied`, EXIT_REFUSED);
338
+ const cwd = process.cwd();
339
+ let wave;
340
+ const result = await executeApply(venv, ordered, {
341
+ runNote: () => (wave ? { wave } : undefined),
342
+ approvedDigest: planned.migrations,
343
+ allowOutOfOrder: a.allowOutOfOrder,
344
+ ...(a.lockWaitMs !== undefined ? { lockWaitMs: a.lockWaitMs } : {}),
345
+ ...(a.standDown ? { standDown: standingDown(project, env) } : {}),
346
+ ...(basePolicy
347
+ ? {
348
+ policy: async (digest, toApply) => {
349
+ const ledger = async () => {
350
+ try {
351
+ return { port: await gitLedger(project.dir), op: await findMigrateOp(project, env, a.op) };
352
+ }
353
+ catch {
354
+ return undefined;
355
+ }
356
+ };
357
+ const results = await judgePlan({ policy: basePolicy, environment: env.name, dialect: project.dialect, toApply, digest, ledger });
358
+ const refusal = policyRefusal(results, env.name, digest, "under the lock");
359
+ if (refusal)
360
+ throw refusal;
361
+ for (const line of describeResults(results))
362
+ io.err(`${line}\n`);
363
+ return results.flatMap((r) => (r.override ? [r.override] : []));
364
+ },
365
+ }
366
+ : {}),
367
+ approve: async () => {
368
+ // Under the lock: the wave plan now, and chant's decision for it.
369
+ const now = await planWave(project, venv, ordered, { allowOutOfOrder: a.allowOutOfOrder });
370
+ if (now.planDigest !== planned.planDigest) {
371
+ throw new YodelError(`refused: ${env.name}'s wave plan moved since the wave was decided (decided ${planned.planDigest}, now ${now.planDigest}); nothing was applied. Run the wave again.`, 1);
372
+ }
373
+ const verified = await verifyWaveDecision(cwd, a.spec, env.name, planned.planDigest);
374
+ wave = verified.wave;
375
+ io.err(`${verified.line}\n`);
376
+ },
377
+ log: (l) => { io.err(`${l}\n`); },
378
+ silences: migrationSilences,
379
+ });
380
+ if (a.json)
381
+ io.out(`${JSON.stringify(applyJson({ mode: "execute", environment: env.name, planDigest: planned.migrations, wavePlanDigest: planned.planDigest, result }), null, 2)}\n`);
382
+ else
383
+ io.out(result.stoodDown ? `${result.stoodDown}\n` : result.applied.length === 0 ? "Nothing to apply.\n" : `Applied: ${result.applied.join(", ")}\n`);
384
+ return 0;
385
+ }
172
386
  if (a.execute) {
173
387
  const approved = process.env[APPROVED_PLAN_ENV];
388
+ const { policy: basePolicy, note: policyNote } = await policyAtBase(project);
389
+ if (policyNote)
390
+ io.err(`${policyNote}\n`);
174
391
  if (!approved)
175
392
  throw new YodelError(`--execute applies only the plan its Op's gate approved, and ${APPROVED_PLAN_ENV} is not set; run yodel apply ${env.name}`);
176
393
  // The env var is only the Op's hand-off; the approval itself is checked in chant's gate ledger, under the lock.
@@ -179,23 +396,41 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
179
396
  const result = await executeApply(venv, ordered, {
180
397
  approvedDigest: approved,
181
398
  allowOutOfOrder: a.allowOutOfOrder,
399
+ ...(a.lockWaitMs !== undefined ? { lockWaitMs: a.lockWaitMs } : {}),
400
+ ...(a.standDown ? { standDown: standingDown(project, env) } : {}),
182
401
  approve: async (digest) => {
402
+ // Under the lock: every pending migration ran first where this environment requires (exit 4 otherwise).
403
+ await enforcePromotion(project, env.name, ordered, (await inspect(venv, ordered)).status.pending);
183
404
  const by = await verifyApproval(ledger, op, digest);
184
405
  io.err(`approval: ${op.name} / ${op.gate} for ${digest}, by ${by.resolvedBy} at ${by.timestamp}\n`);
185
406
  },
186
407
  log: (l) => { io.err(`${l}\n`); },
187
408
  silences: migrationSilences,
409
+ ...(basePolicy
410
+ ? {
411
+ policy: async (digest, toApply) => {
412
+ const results = await judgePlan({ policy: basePolicy, environment: env.name, dialect: project.dialect, toApply, digest, ledger: () => Promise.resolve({ port: ledger, op }) });
413
+ const refusal = policyRefusal(results, env.name, digest, "under the lock");
414
+ if (refusal)
415
+ throw refusal;
416
+ for (const line of describeResults(results))
417
+ io.err(`${line}\n`);
418
+ return results.flatMap((r) => (r.override ? [r.override] : []));
419
+ },
420
+ }
421
+ : {}),
188
422
  });
189
423
  if (a.json)
190
- io.out(`${JSON.stringify({ environment: env.name, status: "applied", planDigest: approved, ...result }, null, 2)}\n`);
424
+ io.out(`${JSON.stringify(applyJson({ mode: "execute", environment: env.name, planDigest: approved, result }), null, 2)}\n`);
191
425
  else
192
- io.out(result.applied.length === 0 ? "Nothing to apply.\n" : `Applied: ${result.applied.join(", ")}\n`);
426
+ io.out(result.stoodDown ? `${result.stoodDown}\n` : result.applied.length === 0 ? "Nothing to apply.\n" : `Applied: ${result.applied.join(", ")}\n`);
193
427
  return 0;
194
428
  }
195
429
  const report = await statusReport(venv, ordered, { allowOutOfOrder: a.allowOutOfOrder });
196
430
  const emit = (doc, code) => {
431
+ outcome.doc = applyJson({ mode: "versioned", report, outcome: doc });
197
432
  if (a.json)
198
- io.out(`${JSON.stringify({ ...statusJson(report), mode: "versioned", ...doc }, null, 2)}\n`);
433
+ io.out(`${JSON.stringify(outcome.doc, null, 2)}\n`);
199
434
  return code;
200
435
  };
201
436
  if (a.plan) {
@@ -210,38 +445,112 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
210
445
  }
211
446
  if (report.status.pending.length === 0) {
212
447
  if (!a.json)
213
- io.out(`${env.name} is up to date: every migration is applied.\n`);
448
+ io.out(`${env.name}${report.server ? ` (${describeServer(report.server)})` : ""} is up to date: every migration is applied.\n`);
214
449
  return emit({ status: "unchanged" }, 0);
215
450
  }
451
+ const standDown = a.standDown ? standingDown(project, env) : undefined;
452
+ const stood = await standDown?.();
453
+ if (stood) {
454
+ if (!a.json)
455
+ io.out(`${stood}\n`);
456
+ return emit({ status: "stood-down", stoodDown: stood }, 0);
457
+ }
458
+ // Before the gate: a pending migration the required environment has not applied refuses, and nothing is asked.
459
+ try {
460
+ await enforcePromotion(project, env.name, ordered, report.status.pending);
461
+ }
462
+ catch (e) {
463
+ if (!(e instanceof YodelError) || e.exitCode !== EXIT_REFUSED)
464
+ throw e;
465
+ if (!a.json)
466
+ io.err(`yodel apply: ${e.message}\n`);
467
+ return emit({ status: "refused", refusal: { exitCode: EXIT_REFUSED, message: e.message } }, EXIT_REFUSED);
468
+ }
469
+ // The first pending migration's checks (#52) read the data as it is now: one that fails refuses before anyone is asked to approve. The Apply step runs every migration's again under the lock.
470
+ const first = report.status.pending[0];
471
+ let checked;
472
+ if (first && checksDue(first)) {
473
+ const outcomes = await runChecks(venv.driver, first.migration);
474
+ checked = { migration: first.id, checks: outcomes };
475
+ const refusal = checksRefusal(first.migration, outcomes, "before the gate");
476
+ if (refusal) {
477
+ if (!a.json)
478
+ io.err(`yodel apply: ${refusal.message}\n`);
479
+ return emit({ status: "refused", preChecks: checked }, refusal.exitCode);
480
+ }
481
+ }
482
+ const op = await findMigrateOp(project, env, a.op);
483
+ // The policy (#53), as the base commit has it (#73): a rule that denies this plan, with no override recorded for its digest, refuses before the gate.
484
+ const basePolicy = await policyAtBase(project);
485
+ if (basePolicy.note && !a.json)
486
+ io.err(`${basePolicy.note}\n`);
487
+ const policy = await judgePlan({
488
+ policy: basePolicy.policy,
489
+ environment: env.name,
490
+ dialect: project.dialect,
491
+ toApply: report.status.pending,
492
+ digest: report.digest,
493
+ ledger: async () => ({ port: await gitLedger(project.dir), op }),
494
+ });
495
+ const denied = policyRefusal(policy, env.name, report.digest, "before the gate");
496
+ if (denied) {
497
+ if (!a.json)
498
+ io.err(`yodel apply: ${denied.message}\n`);
499
+ return emit({ status: "denied", policy }, denied.exitCode);
500
+ }
216
501
  if (!a.json)
217
502
  io.out(renderStatus(report));
218
- const op = await findMigrateOp(project, env, a.op);
503
+ if (!a.json && checked)
504
+ for (const o of checked.checks)
505
+ io.out(`check ${o.name} of ${checked.migration} passed (${o.detail}); it runs again under the lock before the first statement\n`);
506
+ if (!a.json)
507
+ for (const line of describeResults(policy))
508
+ io.out(`${line}\n`);
219
509
  if (!a.json)
220
510
  io.out(`\nRunning ${op.name} (${op.filePath})\n`);
221
- const run = await runMigrateOp(project, op, { allowOutOfOrder: a.allowOutOfOrder, onOutput: (t) => { io.err(t); } });
511
+ // The Op's Apply step (yodel apply --execute) takes these from its environment.
512
+ const childEnv = {
513
+ ...process.env,
514
+ ...(a.lockWaitMs !== undefined ? { [LOCK_WAIT_ENV]: `${String(a.lockWaitMs)}ms` } : {}),
515
+ ...(a.standDown ? { [STAND_DOWN_ENV]: "1" } : {}),
516
+ };
517
+ const run = await runMigrateOp(project, op, { allowOutOfOrder: a.allowOutOfOrder, env: childEnv, onOutput: (t) => { io.err(t); } });
222
518
  if (run.status === "gated") {
223
519
  const stale = await movedSinceApproval(project, venv, ordered, op, run.planDigest ?? report.digest);
224
520
  if (stale && !a.json)
225
521
  io.out(`\n${stale.line}\n`);
226
522
  if (!a.json) {
227
523
  io.out(`\n${op.name} is waiting at gate "${op.gate}" for approval of this plan${run.planDigest ? ` (${run.planDigest})` : ""}.\n`);
228
- if (run.approve)
229
- io.out(`Approve it with:\n ${run.approve}\nthen run yodel apply ${env.name} again.\n`);
524
+ if (run.approve) {
525
+ io.out(`Approve it with:\n ${runnable(run.approve, project.dir)}\n` +
526
+ `or, at a terminal, with ${runnable(`yodel approve ${env.name}`, project.dir)}, which shows the plan and asks you first;\n` +
527
+ `then run ${runnable(`yodel apply ${env.name}`, project.dir)} again.\n`);
528
+ }
230
529
  }
231
530
  return emit({ status: "gated", op: op.name, gate: op.gate, ...(stale ? { moved: stale.moved } : {}), ...(run.approve ? { approve: run.approve } : {}), ...(run.planDigest ? { gatePlanDigest: run.planDigest } : {}) }, GATED);
232
531
  }
233
532
  const after = await statusReport(venv, ordered, { allowOutOfOrder: a.allowOutOfOrder });
234
533
  if (run.status === "failed") {
534
+ // The Apply step's own code (5 for the lock, 4 for a refusal) is what a script retries or stops on; chant run reports any failed step as 1.
535
+ const code = run.stepExit !== undefined && run.stepExit !== 0 ? run.stepExit : 1;
235
536
  if (!a.json) {
236
- io.err(`yodel apply: chant run ${op.name} failed (exit ${String(run.code)})\n`);
537
+ io.err(`yodel apply: chant run ${op.name} failed (exit ${String(run.code)})${run.stepExit ? `: its Apply step, yodel apply ${env.name} --execute, exited ${String(run.stepExit)}` : ""}\n`);
237
538
  io.out(`\n${renderStatus(after)}`);
238
539
  }
540
+ outcome.doc = applyJson({ mode: "versioned", report: after, outcome: { status: "failed", op: op.name, exitCode: code, chantExitCode: run.code } });
239
541
  if (a.json)
240
- io.out(`${JSON.stringify({ ...statusJson(after), mode: "versioned", status: "failed", op: op.name, exitCode: run.code }, null, 2)}\n`);
241
- return 1;
542
+ io.out(`${JSON.stringify(outcome.doc, null, 2)}\n`);
543
+ return code;
544
+ }
545
+ // The Apply step stood down (a newer commit came while it waited for the lock): what it applied is nothing, and that is not a failure.
546
+ const stoodLate = after.status.pending.length > 0 ? await standDown?.() : undefined;
547
+ if (stoodLate) {
548
+ if (!a.json)
549
+ io.out(`\n${stoodLate}\n`);
550
+ return emit({ status: "stood-down", stoodDown: stoodLate, op: op.name }, 0);
242
551
  }
243
552
  if (a.json) {
244
- io.out(`${JSON.stringify({ ...statusJson(after), mode: "versioned", status: "applied", op: op.name, appliedNow: report.status.pending.map((p) => p.id) }, null, 2)}\n`);
553
+ io.out(`${JSON.stringify(applyJson({ mode: "versioned", report: after, outcome: { status: "applied", op: op.name, appliedNow: report.status.pending.map((p) => p.id) } }), null, 2)}\n`);
245
554
  }
246
555
  else {
247
556
  io.out(`\nApplied: ${report.status.pending.map((p) => p.id).join(", ")}.\n`);
@@ -249,18 +558,29 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
249
558
  return after.status.pending.length === 0 ? 0 : 1;
250
559
  }
251
560
  async function declarativeApply(project, env, a, io) {
252
- const json = a.json;
253
561
  const op = await findApplyOp(project, env, a.op);
562
+ let server;
563
+ let printed;
254
564
  const report = (doc, code) => {
255
- if (json)
256
- io.out(`${JSON.stringify({ environment: env.name, dialect: project.dialect, op: op.name, ...doc }, null, 2)}\n`);
565
+ printed = applyJson({ mode: "declarative", environment: env.name, dialect: project.dialect, server, op: op.name, outcome: doc });
566
+ if (a.json)
567
+ io.out(`${JSON.stringify(printed, null, 2)}\n`);
257
568
  return code;
258
569
  };
570
+ const code = await declarativeRun(project, env, a, io, op, report, (s) => (server = s));
571
+ if (printed)
572
+ await sendEvent(project.yodel?.config.notify, eventFromRun("apply", printed), { io, verb: "apply" });
573
+ return code;
574
+ }
575
+ async function declarativeRun(project, env, a, io, op, report, setServer) {
576
+ const json = a.json;
259
577
  const build = await buildSchema(project);
260
578
  try {
261
579
  const before = await planEnvironment(project, env, build);
580
+ const server = await probeServer(project, env);
581
+ setServer(server);
262
582
  if (!json)
263
- io.out(`${await renderPlan(before, `Plan for ${env.name} (${project.dialect}, sql.profiles.${env.name})`)}\n`);
583
+ io.out(`${await renderPlan(before, `Plan for ${env.name} (${server ? describeServer(server) : project.dialect}, sql.profiles.${env.name})`)}\n`);
264
584
  if (planExitCode(before) === PLAN_NEEDS_OP) {
265
585
  if (!json)
266
586
  io.err(`yodel apply: refused: ${String(before.refused.length)} change(s) need their own Op; nothing was applied\n`);
@@ -278,7 +598,7 @@ async function declarativeApply(project, env, a, io) {
278
598
  if (!json) {
279
599
  io.out(`\n${op.name} is waiting at gate "${op.gate}" for approval of this plan.\n`);
280
600
  if (run.approve)
281
- io.out(`Approve it with:\n ${run.approve}\nthen run yodel apply ${env.name} again.\n`);
601
+ io.out(`Approve it with:\n ${runnable(run.approve, project.dir)}\nthen run ${runnable(`yodel apply ${env.name}`, project.dir)} again.\n`);
282
602
  }
283
603
  return report({ status: "gated", gate: op.gate, ...(run.approve ? { approve: run.approve } : {}), ...(run.planDigest ? { planDigest: run.planDigest } : {}), plan: planJson(before) }, GATED);
284
604
  }
@@ -303,4 +623,11 @@ async function declarativeApply(project, env, a, io) {
303
623
  build.dispose();
304
624
  }
305
625
  }
626
+ /** `--stand-down`'s question: the line to stand down with, when a newer commit on the run's branch will apply `env`. */
627
+ function standingDown(project, env) {
628
+ return async () => {
629
+ const n = await newerCommit(project.dir);
630
+ return n ? standDownLine(env.name, n) : undefined;
631
+ };
632
+ }
306
633
  //# sourceMappingURL=apply.js.map