@intentius/sql-yodeler 0.2.0 → 0.4.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 (308) hide show
  1. package/README.md +3 -3
  2. package/dist/apply-steps/index.d.ts +65 -0
  3. package/dist/apply-steps/index.js +106 -0
  4. package/dist/apply-steps/index.js.map +1 -0
  5. package/dist/approval-mode/index.d.ts +78 -0
  6. package/dist/approval-mode/index.js +105 -0
  7. package/dist/approval-mode/index.js.map +1 -0
  8. package/dist/chant/access.d.ts +87 -0
  9. package/dist/chant/access.js +169 -0
  10. package/dist/chant/access.js.map +1 -0
  11. package/dist/chant/build.js +7 -0
  12. package/dist/chant/build.js.map +1 -1
  13. package/dist/chant/drift.d.ts +17 -1
  14. package/dist/chant/drift.js +55 -2
  15. package/dist/chant/drift.js.map +1 -1
  16. package/dist/chant/plan.d.ts +18 -2
  17. package/dist/chant/plan.js +21 -3
  18. package/dist/chant/plan.js.map +1 -1
  19. package/dist/ci/render.d.ts +333 -0
  20. package/dist/ci/render.js +885 -0
  21. package/dist/ci/render.js.map +1 -0
  22. package/dist/ci/replay.d.ts +17 -0
  23. package/dist/ci/replay.js +69 -0
  24. package/dist/ci/replay.js.map +1 -0
  25. package/dist/cleanup/index.d.ts +86 -0
  26. package/dist/cleanup/index.js +207 -0
  27. package/dist/cleanup/index.js.map +1 -0
  28. package/dist/commands/apply.d.ts +113 -0
  29. package/dist/commands/apply.js +370 -30
  30. package/dist/commands/apply.js.map +1 -1
  31. package/dist/commands/approve.d.ts +13 -0
  32. package/dist/commands/approve.js +239 -13
  33. package/dist/commands/approve.js.map +1 -1
  34. package/dist/commands/checkpoint.d.ts +3 -0
  35. package/dist/commands/checkpoint.js +78 -0
  36. package/dist/commands/checkpoint.js.map +1 -0
  37. package/dist/commands/ci.d.ts +3 -0
  38. package/dist/commands/ci.js +130 -0
  39. package/dist/commands/ci.js.map +1 -0
  40. package/dist/commands/cleanup.d.ts +13 -0
  41. package/dist/commands/cleanup.js +138 -0
  42. package/dist/commands/cleanup.js.map +1 -0
  43. package/dist/commands/config.d.ts +3 -0
  44. package/dist/commands/config.js +136 -0
  45. package/dist/commands/config.js.map +1 -0
  46. package/dist/commands/docs.d.ts +11 -0
  47. package/dist/commands/docs.js +108 -0
  48. package/dist/commands/docs.js.map +1 -0
  49. package/dist/commands/drift.d.ts +13 -1
  50. package/dist/commands/drift.js +52 -5
  51. package/dist/commands/drift.js.map +1 -1
  52. package/dist/commands/index.js +16 -0
  53. package/dist/commands/index.js.map +1 -1
  54. package/dist/commands/init.js +1 -1
  55. package/dist/commands/init.js.map +1 -1
  56. package/dist/commands/lint.js +39 -7
  57. package/dist/commands/lint.js.map +1 -1
  58. package/dist/commands/new.js +133 -34
  59. package/dist/commands/new.js.map +1 -1
  60. package/dist/commands/override.js +48 -16
  61. package/dist/commands/override.js.map +1 -1
  62. package/dist/commands/plan.js +47 -6
  63. package/dist/commands/plan.js.map +1 -1
  64. package/dist/commands/report.d.ts +13 -0
  65. package/dist/commands/report.js +121 -0
  66. package/dist/commands/report.js.map +1 -0
  67. package/dist/commands/revert.d.ts +20 -0
  68. package/dist/commands/revert.js +246 -0
  69. package/dist/commands/revert.js.map +1 -0
  70. package/dist/commands/status.js +32 -5
  71. package/dist/commands/status.js.map +1 -1
  72. package/dist/commands/test.d.ts +3 -0
  73. package/dist/commands/test.js +164 -0
  74. package/dist/commands/test.js.map +1 -0
  75. package/dist/config/base.d.ts +69 -0
  76. package/dist/config/base.js +121 -0
  77. package/dist/config/base.js.map +1 -0
  78. package/dist/config/define.d.ts +240 -3
  79. package/dist/config/define.js +4 -0
  80. package/dist/config/define.js.map +1 -1
  81. package/dist/config/index.d.ts +2 -0
  82. package/dist/config/index.js +2 -0
  83. package/dist/config/index.js.map +1 -1
  84. package/dist/config/load.d.ts +1 -1
  85. package/dist/config/load.js +156 -7
  86. package/dist/config/load.js.map +1 -1
  87. package/dist/config/settings.d.ts +22 -1
  88. package/dist/config/settings.js +34 -2
  89. package/dist/config/settings.js.map +1 -1
  90. package/dist/credentials.d.ts +119 -0
  91. package/dist/credentials.js +119 -0
  92. package/dist/credentials.js.map +1 -0
  93. package/dist/exit.js +1 -1
  94. package/dist/exit.js.map +1 -1
  95. package/dist/history/postgres/ddl.d.ts +18 -0
  96. package/dist/history/postgres/ddl.js +27 -2
  97. package/dist/history/postgres/ddl.js.map +1 -1
  98. package/dist/history/postgres/store.d.ts +2 -0
  99. package/dist/history/postgres/store.js +7 -1
  100. package/dist/history/postgres/store.js.map +1 -1
  101. package/dist/history/rows.d.ts +16 -3
  102. package/dist/history/rows.js +30 -1
  103. package/dist/history/rows.js.map +1 -1
  104. package/dist/history/store.d.ts +2 -0
  105. package/dist/history/store.js +2 -2
  106. package/dist/history/store.js.map +1 -1
  107. package/dist/index.d.ts +8 -1
  108. package/dist/index.js +3 -0
  109. package/dist/index.js.map +1 -1
  110. package/dist/init/adopt.js +1 -1
  111. package/dist/init/adopt.js.map +1 -1
  112. package/dist/init/clickhouse-access.d.ts +64 -0
  113. package/dist/init/clickhouse-access.js +211 -0
  114. package/dist/init/clickhouse-access.js.map +1 -0
  115. package/dist/init/live.d.ts +4 -0
  116. package/dist/init/live.js +16 -0
  117. package/dist/init/live.js.map +1 -1
  118. package/dist/lint/access.d.ts +18 -0
  119. package/dist/lint/access.js +34 -0
  120. package/dist/lint/access.js.map +1 -0
  121. package/dist/lint/check.js +38 -6
  122. package/dist/lint/check.js.map +1 -1
  123. package/dist/lint/config.d.ts +7 -1
  124. package/dist/lint/config.js +11 -5
  125. package/dist/lint/config.js.map +1 -1
  126. package/dist/lint/index.d.ts +2 -0
  127. package/dist/lint/index.js +2 -0
  128. package/dist/lint/index.js.map +1 -1
  129. package/dist/lint/naming.d.ts +62 -0
  130. package/dist/lint/naming.js +138 -0
  131. package/dist/lint/naming.js.map +1 -0
  132. package/dist/lint/report.js +1 -1
  133. package/dist/lint/report.js.map +1 -1
  134. package/dist/lint/routines.d.ts +26 -0
  135. package/dist/lint/routines.js +65 -0
  136. package/dist/lint/routines.js.map +1 -0
  137. package/dist/lint/rules.d.ts +51 -6
  138. package/dist/lint/rules.js +23 -6
  139. package/dist/lint/rules.js.map +1 -1
  140. package/dist/lint/schema-checks.d.ts +63 -0
  141. package/dist/lint/schema-checks.js +128 -0
  142. package/dist/lint/schema-checks.js.map +1 -0
  143. package/dist/lint/statements.d.ts +22 -1
  144. package/dist/lint/statements.js +40 -1
  145. package/dist/lint/statements.js.map +1 -1
  146. package/dist/mcp/tools.d.ts +1 -1
  147. package/dist/mcp/tools.js +1 -1
  148. package/dist/mcp/tools.js.map +1 -1
  149. package/dist/migrations/checkpoint.d.ts +45 -0
  150. package/dist/migrations/checkpoint.js +108 -0
  151. package/dist/migrations/checkpoint.js.map +1 -0
  152. package/dist/migrations/checks.d.ts +19 -1
  153. package/dist/migrations/checks.js +15 -0
  154. package/dist/migrations/checks.js.map +1 -1
  155. package/dist/migrations/create.d.ts +13 -4
  156. package/dist/migrations/create.js +24 -7
  157. package/dist/migrations/create.js.map +1 -1
  158. package/dist/migrations/document.d.ts +10 -0
  159. package/dist/migrations/document.js +1 -0
  160. package/dist/migrations/document.js.map +1 -1
  161. package/dist/migrations/index.d.ts +2 -0
  162. package/dist/migrations/index.js +2 -0
  163. package/dist/migrations/index.js.map +1 -1
  164. package/dist/migrations/routines.d.ts +21 -0
  165. package/dist/migrations/routines.js +30 -0
  166. package/dist/migrations/routines.js.map +1 -0
  167. package/dist/notify/index.d.ts +124 -0
  168. package/dist/notify/index.js +210 -0
  169. package/dist/notify/index.js.map +1 -0
  170. package/dist/notify/verify.d.ts +53 -0
  171. package/dist/notify/verify.js +71 -0
  172. package/dist/notify/verify.js.map +1 -0
  173. package/dist/pr/model.d.ts +7 -1
  174. package/dist/pr/model.js +29 -7
  175. package/dist/pr/model.js.map +1 -1
  176. package/dist/pr/render.d.ts +4 -1
  177. package/dist/pr/render.js +49 -9
  178. package/dist/pr/render.js.map +1 -1
  179. package/dist/pr/tenants.d.ts +37 -0
  180. package/dist/pr/tenants.js +138 -0
  181. package/dist/pr/tenants.js.map +1 -0
  182. package/dist/probe.d.ts +27 -0
  183. package/dist/probe.js +106 -0
  184. package/dist/probe.js.map +1 -0
  185. package/dist/project.d.ts +35 -2
  186. package/dist/project.js +62 -2
  187. package/dist/project.js.map +1 -1
  188. package/dist/replay/clickhouse.d.ts +31 -0
  189. package/dist/replay/clickhouse.js +103 -6
  190. package/dist/replay/clickhouse.js.map +1 -1
  191. package/dist/replay/replay.d.ts +14 -3
  192. package/dist/replay/replay.js +62 -31
  193. package/dist/replay/replay.js.map +1 -1
  194. package/dist/replay/report.js +1 -1
  195. package/dist/replay/report.js.map +1 -1
  196. package/dist/report/audit.d.ts +138 -0
  197. package/dist/report/audit.js +219 -0
  198. package/dist/report/audit.js.map +1 -0
  199. package/dist/report/jsonl.d.ts +21 -0
  200. package/dist/report/jsonl.js +52 -0
  201. package/dist/report/jsonl.js.map +1 -0
  202. package/dist/report/model.d.ts +68 -0
  203. package/dist/report/model.js +127 -0
  204. package/dist/report/model.js.map +1 -0
  205. package/dist/report/render.d.ts +18 -0
  206. package/dist/report/render.js +127 -0
  207. package/dist/report/render.js.map +1 -0
  208. package/dist/schema-docs/render.d.ts +49 -0
  209. package/dist/schema-docs/render.js +359 -0
  210. package/dist/schema-docs/render.js.map +1 -0
  211. package/dist/schemas.d.ts +7 -2
  212. package/dist/schemas.js +7 -2
  213. package/dist/schemas.js.map +1 -1
  214. package/dist/sources/ddl.d.ts +48 -0
  215. package/dist/sources/ddl.js +53 -0
  216. package/dist/sources/ddl.js.map +1 -0
  217. package/dist/sources/index.d.ts +69 -0
  218. package/dist/sources/index.js +170 -0
  219. package/dist/sources/index.js.map +1 -0
  220. package/dist/steps/backfill.d.ts +40 -5
  221. package/dist/steps/backfill.js +128 -8
  222. package/dist/steps/backfill.js.map +1 -1
  223. package/dist/steps/index.d.ts +19 -1
  224. package/dist/steps/index.js +34 -7
  225. package/dist/steps/index.js.map +1 -1
  226. package/dist/steps/kinds.d.ts +42 -4
  227. package/dist/steps/kinds.js +88 -14
  228. package/dist/steps/kinds.js.map +1 -1
  229. package/dist/steps/postgres-migration.d.ts +38 -25
  230. package/dist/steps/postgres-migration.js +49 -32
  231. package/dist/steps/postgres-migration.js.map +1 -1
  232. package/dist/steps/rebuild.d.ts +15 -6
  233. package/dist/steps/rebuild.js +12 -9
  234. package/dist/steps/rebuild.js.map +1 -1
  235. package/dist/tenants/index.d.ts +41 -0
  236. package/dist/tenants/index.js +121 -0
  237. package/dist/tenants/index.js.map +1 -0
  238. package/dist/tenants/target.d.ts +23 -0
  239. package/dist/tenants/target.js +29 -0
  240. package/dist/tenants/target.js.map +1 -0
  241. package/dist/tenants/wave.d.ts +128 -0
  242. package/dist/tenants/wave.js +254 -0
  243. package/dist/tenants/wave.js.map +1 -0
  244. package/dist/testing/define.d.ts +62 -0
  245. package/dist/testing/define.js +32 -0
  246. package/dist/testing/define.js.map +1 -0
  247. package/dist/testing/files.d.ts +5 -0
  248. package/dist/testing/files.js +18 -0
  249. package/dist/testing/files.js.map +1 -0
  250. package/dist/testing/report.d.ts +14 -0
  251. package/dist/testing/report.js +83 -0
  252. package/dist/testing/report.js.map +1 -0
  253. package/dist/testing/run.d.ts +104 -0
  254. package/dist/testing/run.js +328 -0
  255. package/dist/testing/run.js.map +1 -0
  256. package/dist/versioned/approval.d.ts +6 -0
  257. package/dist/versioned/approval.js +11 -5
  258. package/dist/versioned/approval.js.map +1 -1
  259. package/dist/versioned/checks.d.ts +8 -2
  260. package/dist/versioned/checks.js +34 -13
  261. package/dist/versioned/checks.js.map +1 -1
  262. package/dist/versioned/driver.d.ts +2 -2
  263. package/dist/versioned/engine.d.ts +16 -0
  264. package/dist/versioned/engine.js +20 -6
  265. package/dist/versioned/engine.js.map +1 -1
  266. package/dist/versioned/explain.d.ts +2 -0
  267. package/dist/versioned/explain.js +3 -1
  268. package/dist/versioned/explain.js.map +1 -1
  269. package/dist/versioned/index.d.ts +1 -0
  270. package/dist/versioned/index.js +1 -0
  271. package/dist/versioned/index.js.map +1 -1
  272. package/dist/versioned/op.d.ts +6 -1
  273. package/dist/versioned/op.js +7 -2
  274. package/dist/versioned/op.js.map +1 -1
  275. package/dist/versioned/policy.d.ts +33 -2
  276. package/dist/versioned/policy.js +59 -8
  277. package/dist/versioned/policy.js.map +1 -1
  278. package/dist/versioned/report.js +9 -0
  279. package/dist/versioned/report.js.map +1 -1
  280. package/dist/versioned/revert.d.ts +128 -0
  281. package/dist/versioned/revert.js +251 -0
  282. package/dist/versioned/revert.js.map +1 -0
  283. package/dist/versioned/state.d.ts +25 -0
  284. package/dist/versioned/state.js +37 -4
  285. package/dist/versioned/state.js.map +1 -1
  286. package/dist/waves/index.d.ts +224 -0
  287. package/dist/waves/index.js +353 -0
  288. package/dist/waves/index.js.map +1 -0
  289. package/dist/waves/promotion.d.ts +56 -0
  290. package/dist/waves/promotion.js +108 -0
  291. package/dist/waves/promotion.js.map +1 -0
  292. package/dist/waves/record.d.ts +34 -0
  293. package/dist/waves/record.js +20 -0
  294. package/dist/waves/record.js.map +1 -0
  295. package/package.json +11 -7
  296. package/schemas/apply.schema.json +162 -3
  297. package/schemas/checkpoint.schema.json +19 -0
  298. package/schemas/cleanup.schema.json +40 -0
  299. package/schemas/drift.schema.json +26 -1
  300. package/schemas/event.schema.json +36 -0
  301. package/schemas/lint-rules.schema.json +4 -0
  302. package/schemas/lint.schema.json +4 -0
  303. package/schemas/new.schema.json +41 -3
  304. package/schemas/plan.schema.json +157 -0
  305. package/schemas/report.schema.json +114 -0
  306. package/schemas/revert.schema.json +69 -0
  307. package/schemas/status.schema.json +64 -3
  308. package/schemas/test.schema.json +41 -0
package/README.md CHANGED
@@ -22,7 +22,7 @@ npx yodel apply dev
22
22
 
23
23
  For Postgres, use `templates/postgres` with `--param name=app-schema --param schema=app`.
24
24
 
25
- Each template gives `chant.config.ts` with a profile per environment (`dev`, `prod`), `yodel.config.ts`, a migrate Op per environment (`yodel apply` behind a gate bound to the plan digest), a `WatchOp` per environment for drift on a schedule, and pipelines for GitHub Actions, GitLab CI and Forgejo Actions: `yodel lint` and the plan comment on pull requests with read-only credentials, `yodel apply` on a push to main with the writer, and the drift watch on its cron with the reader. `scripts/ci.ts` in the project renders the pipelines again after an environment is added (`npm run ci`). The parameters are in each template's `chant.template.json`; the template's README covers the credentials and each forge's setup.
25
+ Each template gives `chant.config.ts` with a profile per environment (`dev`, `prod`), `yodel.config.ts`, a migrate Op per environment (`yodel apply` behind a gate bound to the plan digest), a `WatchOp` per environment for drift on a schedule, and pipelines for GitHub Actions, GitLab CI and Forgejo Actions: `yodel lint` and the plan comment on pull requests with read-only credentials, `yodel apply` on a push to main with the writer, and the drift watch on its cron with the reader. `yodel ci` (`npm run ci`) renders the pipelines from the installed package, so upgrading yodel and rendering again brings its pipeline fixes, and the project's own jobs, declared in a module `yodel.config.ts` `ci.jobs` names, are kept. The parameters are in each template's `chant.template.json`; the template's README covers the credentials and each forge's setup.
26
26
 
27
27
  ## Documentation
28
28
 
@@ -92,8 +92,8 @@ export const op = Op({
92
92
  `yodel lint` checks the migrations directory offline, with no server, so it runs in CI on every pull request. Every rule is free. Exit 0 when there is no error (warnings and silenced findings do not fail), 3 when there is at least one, 1 when lint cannot run; `--json` prints the findings.
93
93
 
94
94
  - Integrity, always errors: `fork` (two migrations follow the same parent; `yodel rebase`), `directory` (a bad name or file, a missing parent, a cycle, another dialect), `checksum` (a migration's files changed since it was written), `unreadable-object` (an object in a recorded schema, or a step, the sql lexicon cannot read or classify; never skipped).
95
- - From the classifier's classes: `destructive` (a drop), `pg-rewrite` (a rewrite or full scan under ACCESS EXCLUSIVE), `pg-lock` (writes blocked for a whole index build or scan: no `CONCURRENTLY`, a constraint validated in place), `ch-mutation` (a mutation rewriting parts), `ch-rebuild` (a `ClickHouseRebuildOp` step).
96
- - `refused-step` (a step `yodel apply` will not run), `migration-sql` (migration.sql does not hold a statement migration.json records), `silence` (a bad or unused `yodel:allow` line).
95
+ - From the classifier's classes: `destructive` (a drop), `pg-rewrite` (a rewrite or full scan under ACCESS EXCLUSIVE), `pg-lock` (writes blocked for a whole index build or scan: no `CONCURRENTLY`, a constraint validated in place), `pg-signature` (a function or procedure dropped and created with another signature), `pg-access` (a privilege revoked, a policy dropped, row-level security turned off), `pg-rls-not-forced` (a warning: row-level security enabled but not forced), `ch-mutation` (a mutation rewriting parts), `ch-rebuild` (a `ClickHouseRebuildOp` step), `ch-access` (a privilege or role revoked).
96
+ - `refused-step` (a step `yodel apply` will not run), `pg-routine-in-use` (a function dropped while a declared trigger still executes it), `migration-sql` (migration.sql does not hold a statement migration.json records), `silence` (a bad or unused `yodel:allow` line).
97
97
 
98
98
  A line above a statement in its `migration.sql` silences one of the classifier rules for that statement:
99
99
 
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Steps around apply (#64): per environment, `yodel.config.ts`
3
+ * `environments.<env>.steps.before` and `.after`, SQL checks or commands
4
+ * run around the versioned apply.
5
+ *
6
+ * - Read at the base commit (`../config/base.ts`), as the plan policy is:
7
+ * a pull request that adds, removes or loosens a step is run under the
8
+ * steps the branch had. A checkout with no base to read runs none.
9
+ * - `before` steps run under the lock before the first migration. SQL
10
+ * checks also run when the run is planned (the wave plan, or `yodel apply`
11
+ * before the gate), since they only read; a command, which may have side
12
+ * effects, runs once per apply, under the lock, and the plan only shows
13
+ * that it will (#107). One that fails refuses the run (exit 4) unless its
14
+ * `onFailure` is `approve`: then the wave is held for a person, whatever
15
+ * its gate, and the plan they approve says which steps failed.
16
+ * - `after` steps run under the lock once the last migration's statements
17
+ * succeeded. One that fails fails the run (exit 1); the migrations stay
18
+ * applied.
19
+ * - Every outcome is in the history: the `before` steps' on each
20
+ * migration's `started` row, the `after` steps' on the last migration's
21
+ * `succeeded` row (`note.steps`), and `yodel report` lists them.
22
+ */
23
+ import type { ApplyStep, ApplySteps } from "../config/define.js";
24
+ import { YodelError, type Project } from "../project.js";
25
+ import type { VersionedDriver } from "../versioned/driver.js";
26
+ export type StepWhen = "before" | "after";
27
+ /** One step's outcome, as the history records it. */
28
+ export interface StepOutcome {
29
+ name: string;
30
+ when: StepWhen;
31
+ kind: "sql" | "command";
32
+ passed: boolean;
33
+ /** What the query returned, or how the command exited, in a few words. */
34
+ detail: string;
35
+ onFailure: "fail" | "approve";
36
+ /** A command not run because the run was only planned: it runs under the lock. */
37
+ deferred?: true;
38
+ }
39
+ /** The steps that apply to `env`: the base commit's, and a note when the working tree's differ. */
40
+ export interface StepsAtBase {
41
+ steps: ApplySteps | undefined;
42
+ sha?: string;
43
+ note?: string;
44
+ }
45
+ /** `env`'s steps from yodel.config.ts at the base commit. */
46
+ export declare function stepsAtBase(project: Pick<Project, "dir" | "yodel">, env: string, processEnv?: NodeJS.ProcessEnv): Promise<StepsAtBase>;
47
+ /**
48
+ * Runs `list` in order and returns each outcome; a failing step does not
49
+ * stop the ones after it. With `commands: "plan"` (a run being planned, not
50
+ * applied) a command is not run: its outcome says it runs under the lock.
51
+ */
52
+ export declare function runApplySteps(driver: Pick<VersionedDriver, "readCheck">, list: readonly ApplyStep[], when: StepWhen, ctx: {
53
+ dir: string;
54
+ env: string;
55
+ processEnv?: NodeJS.ProcessEnv;
56
+ commands?: "run" | "plan";
57
+ }): Promise<StepOutcome[]>;
58
+ /** One line for an outcome. */
59
+ export declare const describeStep: (o: StepOutcome) => string;
60
+ /** The `before` steps that failed with `onFailure: "approve"`: the run waits for a person. */
61
+ export declare const heldSteps: (outcomes: readonly StepOutcome[]) => string[];
62
+ /** The refusal for a step that failed with `onFailure: "fail"`, or undefined. */
63
+ export declare function stepsRefusal(env: string, outcomes: readonly StepOutcome[], when: string): YodelError | undefined;
64
+ /** The steps a history row's note records, if any. */
65
+ export declare function stepsOfNote(note: string): StepOutcome[];
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Steps around apply (#64): per environment, `yodel.config.ts`
3
+ * `environments.<env>.steps.before` and `.after`, SQL checks or commands
4
+ * run around the versioned apply.
5
+ *
6
+ * - Read at the base commit (`../config/base.ts`), as the plan policy is:
7
+ * a pull request that adds, removes or loosens a step is run under the
8
+ * steps the branch had. A checkout with no base to read runs none.
9
+ * - `before` steps run under the lock before the first migration. SQL
10
+ * checks also run when the run is planned (the wave plan, or `yodel apply`
11
+ * before the gate), since they only read; a command, which may have side
12
+ * effects, runs once per apply, under the lock, and the plan only shows
13
+ * that it will (#107). One that fails refuses the run (exit 4) unless its
14
+ * `onFailure` is `approve`: then the wave is held for a person, whatever
15
+ * its gate, and the plan they approve says which steps failed.
16
+ * - `after` steps run under the lock once the last migration's statements
17
+ * succeeded. One that fails fails the run (exit 1); the migrations stay
18
+ * applied.
19
+ * - Every outcome is in the history: the `before` steps' on each
20
+ * migration's `started` row, the `after` steps' on the last migration's
21
+ * `succeeded` row (`note.steps`), and `yodel report` lists them.
22
+ */
23
+ import { spawnSync } from "node:child_process";
24
+ import { loadYodelConfigAtBase } from "../config/base.js";
25
+ import { EXIT_REFUSED } from "../exit.js";
26
+ import { judgeCheck } from "../migrations/checks.js";
27
+ import { YodelError } from "../project.js";
28
+ /** `env`'s steps from yodel.config.ts at the base commit. */
29
+ export async function stepsAtBase(project, env, processEnv = process.env) {
30
+ const tree = project.yodel?.config.environments?.[env]?.steps;
31
+ let at;
32
+ try {
33
+ at = await loadYodelConfigAtBase(project.dir, processEnv);
34
+ }
35
+ catch (e) {
36
+ const why = e instanceof Error ? (e.message.split("\n")[0] ?? "") : String(e);
37
+ return { steps: undefined, ...(tree ? { note: `steps: yodel.config.ts could not be read at the base commit (${why}), so no step runs; the working tree's are not used` } : {}) };
38
+ }
39
+ const steps = at.config?.environments?.[env]?.steps;
40
+ const differs = JSON.stringify(tree ?? null) !== JSON.stringify(steps ?? null);
41
+ return { steps, sha: at.sha, ...(differs ? { note: `steps: ${env}'s steps are read at the base commit ${at.sha.slice(0, 12)}, not from this change` } : {}) };
42
+ }
43
+ const lastLine = (text) => text.trim().split("\n").at(-1)?.slice(0, 200) ?? "";
44
+ /**
45
+ * Runs `list` in order and returns each outcome; a failing step does not
46
+ * stop the ones after it. With `commands: "plan"` (a run being planned, not
47
+ * applied) a command is not run: its outcome says it runs under the lock.
48
+ */
49
+ export async function runApplySteps(driver, list, when, ctx) {
50
+ const out = [];
51
+ for (const step of list) {
52
+ const onFailure = step.onFailure ?? "fail";
53
+ if (step.sql !== undefined) {
54
+ try {
55
+ const judged = judgeCheck({ name: step.name, sql: step.sql, ...(step.expect !== undefined ? { expect: step.expect } : {}) }, await driver.readCheck(step.sql, {}));
56
+ out.push({ name: step.name, when, kind: "sql", passed: judged.passed, detail: judged.detail, onFailure });
57
+ }
58
+ catch (e) {
59
+ out.push({ name: step.name, when, kind: "sql", passed: false, detail: `the query failed: ${(e instanceof Error ? e.message : String(e)).split("\n")[0] ?? ""}`, onFailure });
60
+ }
61
+ continue;
62
+ }
63
+ if (ctx.commands === "plan") {
64
+ out.push({ name: step.name, when, kind: "command", passed: true, detail: "a command: it runs under the lock, not when the run is planned", onFailure, deferred: true });
65
+ continue;
66
+ }
67
+ const ran = spawnSync("sh", ["-c", step.command ?? ""], {
68
+ cwd: ctx.dir,
69
+ encoding: "utf8",
70
+ env: { ...(ctx.processEnv ?? process.env), YODEL_ENV: ctx.env, YODEL_STEP: when },
71
+ timeout: 10 * 60_000,
72
+ });
73
+ const said = lastLine(`${ran.stdout}\n${ran.stderr}`);
74
+ const code = ran.status ?? 1;
75
+ out.push({ name: step.name, when, kind: "command", passed: code === 0 && !ran.error, detail: `exit ${ran.error ? "(did not run)" : String(code)}${said ? `: ${said}` : ""}`, onFailure });
76
+ }
77
+ return out;
78
+ }
79
+ /** One line for an outcome. */
80
+ export const describeStep = (o) => o.deferred ? `${o.when} step ${o.name} will run under the lock (${o.detail})` : `${o.when} step ${o.name} ${o.passed ? "passed" : o.onFailure === "approve" ? "failed, held for approval" : "failed"} (${o.detail})`;
81
+ /** The `before` steps that failed with `onFailure: "approve"`: the run waits for a person. */
82
+ export const heldSteps = (outcomes) => outcomes.filter((o) => !o.passed && o.when === "before" && o.onFailure === "approve").map((o) => o.name);
83
+ /** The refusal for a step that failed with `onFailure: "fail"`, or undefined. */
84
+ export function stepsRefusal(env, outcomes, when) {
85
+ const failed = outcomes.filter((o) => !o.passed && (o.when === "after" || o.onFailure === "fail"));
86
+ if (failed.length === 0)
87
+ return undefined;
88
+ const after = failed.every((o) => o.when === "after");
89
+ return new YodelError(after
90
+ ? `${env}: the migrations are applied, and an after step failed ${when}:\n${failed.map((o) => ` ${describeStep(o)}`).join("\n")}`
91
+ : `refused: a before step of ${env} failed ${when}; nothing was applied:\n${failed.map((o) => ` ${describeStep(o)}`).join("\n")}\n` +
92
+ `Fix the cause and run again, or, for a check a person may wave through, give the step onFailure: "approve" (at the base commit).`, after ? 1 : EXIT_REFUSED);
93
+ }
94
+ /** The steps a history row's note records, if any. */
95
+ export function stepsOfNote(note) {
96
+ if (!note.includes('"steps"'))
97
+ return [];
98
+ try {
99
+ const steps = JSON.parse(note).steps;
100
+ return Array.isArray(steps) ? steps : [];
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ }
106
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/apply-steps/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Approval modes (#48): which approvals of a gate's plan digest count.
3
+ *
4
+ * - `ledger` (the default): any `chant approve` of the digest on
5
+ * `chant/lifecycle`. Anyone who can push to that branch can approve, in
6
+ * anyone's name.
7
+ * - `pr-review` (a wave's only): also the review of the pull request that
8
+ * merged the commit being applied: an approving review of its head by a
9
+ * writer other than its author approves the digest that head planned,
10
+ * which the pull request's pipeline records (`chant run wave
11
+ * --record-plans`). chant judges it, in `chant run wave`, and writes the
12
+ * approval it makes of the review to the ledger.
13
+ * - `sealed`: only an approval sealed with `chant approve --sign` (`yodel
14
+ * approve --sign`) by a key the signers file lists for its approver. The
15
+ * file is `.chant/allowed_signers` at the repository's root (or the path
16
+ * `.chant/trust.json` names), read at the base commit, so a key the change
17
+ * itself adds does not count for it.
18
+ *
19
+ * A wave's mode is `waves[].approval`, which yodel ci writes into the waves
20
+ * spec, where chant reads it at the base commit with the gate policy. The
21
+ * migrations Op's gate (yodel apply <env> outside the waves) takes
22
+ * `environments.<env>.approval`, read from yodel.config.ts at the base
23
+ * commit; without it, `sealed` when the environment's wave is sealed, else
24
+ * `ledger`. yodel checks it itself where it checks an approval
25
+ * (`--execute`, `--wave-apply`), so an unsealed approval chant's gate step
26
+ * let through still applies nothing.
27
+ */
28
+ import type { GateLedgerPort } from "@intentius/chant/op/gate";
29
+ import type { ApprovalMode, EnvironmentApprovalMode, YodelConfig } from "../config/define.js";
30
+ export type { ApprovalMode, EnvironmentApprovalMode };
31
+ /**
32
+ * Set (to `1`) by `yodel ci` on the pull request job that records the plans
33
+ * each head makes for a `pr-review` wave, `chant run wave --record-plans`:
34
+ * `yodel apply <env> --wave-plan` then plans the wave as it will be after the
35
+ * merge, once the environments it requires have applied what is pending.
36
+ * `--wave-apply` never reads it.
37
+ */
38
+ export declare const RECORD_PLANS_ENV = "YODEL_WAVE_RECORD_PLANS";
39
+ /** Where chant reads the signers when `.chant/trust.json` names no other path. */
40
+ export declare const SIGNERS_FILE = ".chant/allowed_signers";
41
+ /** `env`'s wave's mode in `config`; `ledger` when no wave names it or the wave sets none. */
42
+ export declare function waveApproval(config: YodelConfig | undefined, env: string): ApprovalMode;
43
+ /** The migrations Op's mode for `env` in `config`: the environment's, else `sealed` when its wave is, else `ledger`. */
44
+ export declare function migrationsApproval(config: YodelConfig | undefined, env: string): EnvironmentApprovalMode;
45
+ /** The migrations Op's mode as the base commit has it. */
46
+ export interface ApprovalAtBase {
47
+ mode: EnvironmentApprovalMode;
48
+ /** The commit the signers file is read at, when the base could be read. */
49
+ base?: string;
50
+ /** Why the working tree's mode is not the one that applies, when they differ. */
51
+ note?: string;
52
+ }
53
+ /**
54
+ * `env`'s migrations Op mode from yodel.config.ts at the base commit, so a
55
+ * change cannot loosen the mode that gates its own merge. A checkout with no
56
+ * base to read (no git, a repository's first commit) takes the working
57
+ * tree's, and a sealed one then has no signers file at base to verify
58
+ * against, so nothing counts.
59
+ */
60
+ export declare function migrationsApprovalAtBase(project: {
61
+ dir: string;
62
+ yodel?: {
63
+ config: YodelConfig;
64
+ } | undefined;
65
+ }, env: string, processEnv?: NodeJS.ProcessEnv): Promise<ApprovalAtBase>;
66
+ /**
67
+ * `port`, with every gate it is asked for also sealed: an approval counts
68
+ * only when its seal verifies, for the approver it names, against the
69
+ * signers file at `base` (and the workspace's own rule, when it has one,
70
+ * accepts it too). No signers file at base, nothing counts.
71
+ */
72
+ export declare function sealedPort(port: GateLedgerPort, cwd: string, base: string | undefined): Promise<GateLedgerPort>;
73
+ /** `port` as `mode` judges approvals: sealed at `base`, or as it is. */
74
+ export declare function portFor(port: GateLedgerPort, mode: ApprovalMode, cwd: string, base: string | undefined): Promise<GateLedgerPort>;
75
+ /** What a printed approve command needs for `mode`: `--sign` under sealed. */
76
+ export declare const signFlag: (mode: ApprovalMode) => string;
77
+ /** One sentence on which approvals count under `mode`, for the plan and the pull request comment. */
78
+ export declare function describeApprovalMode(mode: ApprovalMode, code?: (s: string) => string): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Approval modes (#48): which approvals of a gate's plan digest count.
3
+ *
4
+ * - `ledger` (the default): any `chant approve` of the digest on
5
+ * `chant/lifecycle`. Anyone who can push to that branch can approve, in
6
+ * anyone's name.
7
+ * - `pr-review` (a wave's only): also the review of the pull request that
8
+ * merged the commit being applied: an approving review of its head by a
9
+ * writer other than its author approves the digest that head planned,
10
+ * which the pull request's pipeline records (`chant run wave
11
+ * --record-plans`). chant judges it, in `chant run wave`, and writes the
12
+ * approval it makes of the review to the ledger.
13
+ * - `sealed`: only an approval sealed with `chant approve --sign` (`yodel
14
+ * approve --sign`) by a key the signers file lists for its approver. The
15
+ * file is `.chant/allowed_signers` at the repository's root (or the path
16
+ * `.chant/trust.json` names), read at the base commit, so a key the change
17
+ * itself adds does not count for it.
18
+ *
19
+ * A wave's mode is `waves[].approval`, which yodel ci writes into the waves
20
+ * spec, where chant reads it at the base commit with the gate policy. The
21
+ * migrations Op's gate (yodel apply <env> outside the waves) takes
22
+ * `environments.<env>.approval`, read from yodel.config.ts at the base
23
+ * commit; without it, `sealed` when the environment's wave is sealed, else
24
+ * `ledger`. yodel checks it itself where it checks an approval
25
+ * (`--execute`, `--wave-apply`), so an unsealed approval chant's gate step
26
+ * let through still applies nothing.
27
+ */
28
+ import { loadYodelConfigAtBase } from "../config/base.js";
29
+ import { baseEnvironment } from "../tenants/target.js";
30
+ /**
31
+ * Set (to `1`) by `yodel ci` on the pull request job that records the plans
32
+ * each head makes for a `pr-review` wave, `chant run wave --record-plans`:
33
+ * `yodel apply <env> --wave-plan` then plans the wave as it will be after the
34
+ * merge, once the environments it requires have applied what is pending.
35
+ * `--wave-apply` never reads it.
36
+ */
37
+ export const RECORD_PLANS_ENV = "YODEL_WAVE_RECORD_PLANS";
38
+ /** Where chant reads the signers when `.chant/trust.json` names no other path. */
39
+ export const SIGNERS_FILE = ".chant/allowed_signers";
40
+ /** `env`'s wave's mode in `config`; `ledger` when no wave names it or the wave sets none. */
41
+ export function waveApproval(config, env) {
42
+ return config?.waves?.find((w) => w.env === baseEnvironment(env))?.approval ?? "ledger";
43
+ }
44
+ /** The migrations Op's mode for `env` in `config`: the environment's, else `sealed` when its wave is, else `ledger`. */
45
+ export function migrationsApproval(config, env) {
46
+ const own = config?.environments?.[baseEnvironment(env)]?.approval;
47
+ if (own)
48
+ return own;
49
+ return waveApproval(config, env) === "sealed" ? "sealed" : "ledger";
50
+ }
51
+ /**
52
+ * `env`'s migrations Op mode from yodel.config.ts at the base commit, so a
53
+ * change cannot loosen the mode that gates its own merge. A checkout with no
54
+ * base to read (no git, a repository's first commit) takes the working
55
+ * tree's, and a sealed one then has no signers file at base to verify
56
+ * against, so nothing counts.
57
+ */
58
+ export async function migrationsApprovalAtBase(project, env, processEnv = process.env) {
59
+ const tree = migrationsApproval(project.yodel?.config, env);
60
+ let at;
61
+ try {
62
+ at = await loadYodelConfigAtBase(project.dir, processEnv);
63
+ }
64
+ catch (e) {
65
+ const why = e instanceof Error ? (e.message.split("\n")[0] ?? "") : String(e);
66
+ return { mode: tree, ...(tree !== "ledger" ? { note: `approval: yodel.config.ts could not be read at the base commit (${why}), so ${env}'s approval mode is the working tree's, ${tree}` } : {}) };
67
+ }
68
+ const mode = migrationsApproval(at.config, env);
69
+ return { mode, base: at.sha, ...(mode !== tree ? { note: `approval: ${env}'s approvals count as ${mode}, as yodel.config.ts has it at the base commit ${at.sha.slice(0, 12)}, not ${tree} as this change has it` } : {}) };
70
+ }
71
+ /**
72
+ * `port`, with every gate it is asked for also sealed: an approval counts
73
+ * only when its seal verifies, for the approver it names, against the
74
+ * signers file at `base` (and the workspace's own rule, when it has one,
75
+ * accepts it too). No signers file at base, nothing counts.
76
+ */
77
+ export async function sealedPort(port, cwd, base) {
78
+ const review = await import("@intentius/chant/op/gate-review");
79
+ const sealed = (gate) => base === undefined
80
+ ? { requirement: { gate, class: null }, refuses: () => `the gate is sealed, and there is no base commit to read the signers file (${SIGNERS_FILE}) at` }
81
+ : review.sealedApprovalRule(gate, review.trustPolicyAt(cwd, base));
82
+ return {
83
+ ...port,
84
+ read: (op) => port.read(op),
85
+ async approvalRule(gate) {
86
+ const own = (await port.approvalRule?.(gate)) ?? null;
87
+ return review.bothRules(own, sealed(gate));
88
+ },
89
+ };
90
+ }
91
+ /** `port` as `mode` judges approvals: sealed at `base`, or as it is. */
92
+ export async function portFor(port, mode, cwd, base) {
93
+ return mode === "sealed" ? sealedPort(port, cwd, base) : port;
94
+ }
95
+ /** What a printed approve command needs for `mode`: `--sign` under sealed. */
96
+ export const signFlag = (mode) => (mode === "sealed" ? " --sign" : "");
97
+ /** One sentence on which approvals count under `mode`, for the plan and the pull request comment. */
98
+ export function describeApprovalMode(mode, code = (s) => s) {
99
+ if (mode === "sealed")
100
+ return `Approvals count only sealed (approval ${code("sealed")}): ${code("chant approve --sign")} or ${code("yodel approve --sign")}, with a key ${code(SIGNERS_FILE)} at the base commit lists for the approver.`;
101
+ if (mode === "pr-review")
102
+ return `Approval ${code("pr-review")}: an approving review of this pull request's head, by a writer other than its author, approves the plan that head records; ${code("chant approve")} of the digest counts too.`;
103
+ return `Approval ${code("ledger")}: any ${code("chant approve")} of the digest counts.`;
104
+ }
105
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/approval-mode/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Access control under review. On Postgres (#45): row-level security,
3
+ * policies, the roles a schema declares, grants and default privileges, as
4
+ * chant's sql lexicon plans and applies them where `sql.profiles.<env>.access`
5
+ * is on (yodel.config.ts's `environments.<env>.access` sets it per
6
+ * environment). On ClickHouse (#46): users, roles, row policies and grants,
7
+ * which chant plans whenever the build declares them; yodel refuses the
8
+ * declarative path where the environment does not manage access
9
+ * ({@link clickhouseAccessRefusal}).
10
+ *
11
+ * The plan already lists each access change with its rule and class. This
12
+ * module picks them out so `yodel plan`, `yodel new`, lint and the pull
13
+ * request comment can show them together, and names the changes that take
14
+ * access away: a privilege revoked (SQLPG297), a policy dropped (SQLPG292),
15
+ * row-level security turned off or no longer forced (SQLPG293), and on
16
+ * ClickHouse a privilege or role revoked (SQLCH274). A revoke no `REVOKE`
17
+ * declaration asks for takes away a privilege granted by hand (or one whose
18
+ * declaration was deleted) from the role that holds it, so it is called out
19
+ * on its own. ClickHouse takes no `REVOKE` declaration: a grantee's grant
20
+ * declarations are all it holds, so every revoke there is one of these.
21
+ */
22
+ import type { ClassifiedChange } from "@intentius/chant-lexicon-sql/core/classifier";
23
+ import type { MigrationStep } from "@intentius/chant-lexicon-sql/migration-statements";
24
+ import type { BuildOutput } from "./build.js";
25
+ import { YodelError, type Project } from "../project.js";
26
+ /** chant's access rules: policies, row-level security, roles, grants, revokes, default privileges; on ClickHouse role settings, user clauses, row policies, grants and revokes. */
27
+ export declare const ACCESS_RULES: ReadonlySet<string>;
28
+ /** The entity types of ClickHouse's access declarations: `user`, `role`, `policy` and `grant`. */
29
+ export declare const CH_ACCESS_TYPES: ReadonlySet<string>;
30
+ /** The entity types of access declarations. */
31
+ export declare const ACCESS_TYPES: ReadonlySet<string>;
32
+ /** A plan change that is about access control. */
33
+ export declare const isAccessChange: (c: Pick<ClassifiedChange, "rule" | "field" | "object">) => boolean;
34
+ /** A migration step that is about access control. */
35
+ export declare function isAccessStep(s: MigrationStep): boolean;
36
+ /** Why a statement takes access away, or `undefined`. */
37
+ export declare function takesAccessAway(s: {
38
+ rule: string;
39
+ sql?: string;
40
+ }): string | undefined;
41
+ /** One access change in a declarative plan. */
42
+ export interface AccessItem {
43
+ object: string;
44
+ rule: string;
45
+ class: string;
46
+ field: string;
47
+ before?: unknown;
48
+ after?: unknown;
49
+ /** A revoke no declaration asks for: the server holds a privilege nothing declares (granted by hand, or its declaration deleted), and the apply takes it away. */
50
+ byHand?: true;
51
+ }
52
+ /** What `yodel plan --json` adds on Postgres: whether access is managed here, and the access changes. */
53
+ export interface AccessReview {
54
+ managed: boolean;
55
+ /** Where {@link managed} came from: `yodel.config.ts`, `sql.profiles.<env>.access` or `default`. */
56
+ source: string;
57
+ changes: AccessItem[];
58
+ }
59
+ /** The access changes of a plan, a revoke of what nothing declares marked {@link AccessItem.byHand}. */
60
+ export declare function accessItems(changes: readonly ClassifiedChange[], build: BuildOutput): Promise<AccessItem[]>;
61
+ /** A plan's access section: whether access is managed in environment `name` (and where that is set), and the plan's access changes. */
62
+ export declare function accessReview(project: Pick<Project, "yodel" | "profileAccess">, name: string, changes: readonly ClassifiedChange[], build: BuildOutput): Promise<AccessReview>;
63
+ /** The privileges a revoke takes away: what the role held before and does not after. */
64
+ export declare const removed: (c: Pick<AccessItem, "before" | "after">) => string[];
65
+ /** What a revoke takes away, from whom: `relation app.t FROM reader: insert` (Postgres), `INSERT ON db.t FROM reader` (ClickHouse, whose plan names the grantee as `grants <grantee>`). */
66
+ export declare function revokeLine(c: Pick<AccessItem, "object" | "before" | "after">): string;
67
+ /** The plan's access section, as text: every change with its rule and class, then the revokes of privileges granted by hand. */
68
+ export declare function renderAccessReview(review: AccessReview, environment: string): string[];
69
+ /** The access steps of a migration, each with why it takes access away when it does. */
70
+ export declare function accessSteps(steps: readonly MigrationStep[]): Array<{
71
+ index: number;
72
+ step: MigrationStep;
73
+ away?: string;
74
+ undeclared?: true;
75
+ }>;
76
+ /** A migration's access steps as lines: how many, and each one that takes access away. */
77
+ export declare function describeAccessSteps(steps: readonly MigrationStep[], md?: boolean): string[];
78
+ /**
79
+ * ClickHouse: the declarative path's refusal where environment `name` does
80
+ * not manage access and the build declares users, roles, row policies or
81
+ * grants. chant's ClickHouse apply (the project's ApplyOp) and `chant
82
+ * lifecycle diff` plan every access declaration they find, with no switch of
83
+ * their own, so yodel cannot leave them out there: rather than plan without
84
+ * them and apply with them, it refuses. `undefined` when access is managed,
85
+ * or nothing is declared.
86
+ */
87
+ export declare function clickhouseAccessRefusal(project: Pick<Project, "yodel" | "profileAccess">, name: string, build: Pick<BuildOutput, "objects">, what?: string): YodelError | undefined;