@intentius/sql-yodeler 0.3.0 → 0.5.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 (220) hide show
  1. package/README.md +2 -2
  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 +84 -0
  9. package/dist/chant/access.js +155 -0
  10. package/dist/chant/access.js.map +1 -0
  11. package/dist/chant/drift.d.ts +17 -1
  12. package/dist/chant/drift.js +55 -2
  13. package/dist/chant/drift.js.map +1 -1
  14. package/dist/chant/plan.d.ts +18 -2
  15. package/dist/chant/plan.js +21 -3
  16. package/dist/chant/plan.js.map +1 -1
  17. package/dist/ci/login.d.ts +78 -0
  18. package/dist/ci/login.js +199 -0
  19. package/dist/ci/login.js.map +1 -0
  20. package/dist/ci/render.d.ts +64 -10
  21. package/dist/ci/render.js +385 -88
  22. package/dist/ci/render.js.map +1 -1
  23. package/dist/cleanup/index.d.ts +92 -0
  24. package/dist/cleanup/index.js +213 -0
  25. package/dist/cleanup/index.js.map +1 -0
  26. package/dist/commands/apply.js +178 -20
  27. package/dist/commands/apply.js.map +1 -1
  28. package/dist/commands/approve.d.ts +13 -0
  29. package/dist/commands/approve.js +206 -24
  30. package/dist/commands/approve.js.map +1 -1
  31. package/dist/commands/ci.js +12 -0
  32. package/dist/commands/ci.js.map +1 -1
  33. package/dist/commands/cleanup.d.ts +13 -0
  34. package/dist/commands/cleanup.js +158 -0
  35. package/dist/commands/cleanup.js.map +1 -0
  36. package/dist/commands/config.d.ts +3 -0
  37. package/dist/commands/config.js +136 -0
  38. package/dist/commands/config.js.map +1 -0
  39. package/dist/commands/drift.js +33 -3
  40. package/dist/commands/drift.js.map +1 -1
  41. package/dist/commands/index.js +6 -0
  42. package/dist/commands/index.js.map +1 -1
  43. package/dist/commands/init.d.ts +11 -0
  44. package/dist/commands/init.js +98 -10
  45. package/dist/commands/init.js.map +1 -1
  46. package/dist/commands/lint.js +67 -10
  47. package/dist/commands/lint.js.map +1 -1
  48. package/dist/commands/new.js +135 -54
  49. package/dist/commands/new.js.map +1 -1
  50. package/dist/commands/plan.js +39 -5
  51. package/dist/commands/plan.js.map +1 -1
  52. package/dist/commands/test.d.ts +3 -0
  53. package/dist/commands/test.js +164 -0
  54. package/dist/commands/test.js.map +1 -0
  55. package/dist/config/define.d.ts +175 -2
  56. package/dist/config/define.js +2 -0
  57. package/dist/config/define.js.map +1 -1
  58. package/dist/config/load.js +78 -10
  59. package/dist/config/load.js.map +1 -1
  60. package/dist/config/settings.d.ts +20 -0
  61. package/dist/config/settings.js +28 -0
  62. package/dist/config/settings.js.map +1 -1
  63. package/dist/credentials.d.ts +119 -0
  64. package/dist/credentials.js +119 -0
  65. package/dist/credentials.js.map +1 -0
  66. package/dist/exit.js +1 -1
  67. package/dist/exit.js.map +1 -1
  68. package/dist/index.d.ts +5 -1
  69. package/dist/index.js +2 -0
  70. package/dist/index.js.map +1 -1
  71. package/dist/init/adopt.js +1 -1
  72. package/dist/init/adopt.js.map +1 -1
  73. package/dist/init/clickhouse-access.d.ts +64 -0
  74. package/dist/init/clickhouse-access.js +211 -0
  75. package/dist/init/clickhouse-access.js.map +1 -0
  76. package/dist/init/index.d.ts +2 -0
  77. package/dist/init/index.js +2 -0
  78. package/dist/init/index.js.map +1 -1
  79. package/dist/init/live.d.ts +4 -0
  80. package/dist/init/live.js +16 -0
  81. package/dist/init/live.js.map +1 -1
  82. package/dist/init/second.d.ts +70 -0
  83. package/dist/init/second.js +149 -0
  84. package/dist/init/second.js.map +1 -0
  85. package/dist/lint/access.d.ts +18 -0
  86. package/dist/lint/access.js +34 -0
  87. package/dist/lint/access.js.map +1 -0
  88. package/dist/lint/annotations.d.ts +49 -0
  89. package/dist/lint/annotations.js +82 -0
  90. package/dist/lint/annotations.js.map +1 -0
  91. package/dist/lint/check.js +25 -7
  92. package/dist/lint/check.js.map +1 -1
  93. package/dist/lint/config.d.ts +7 -1
  94. package/dist/lint/config.js +11 -5
  95. package/dist/lint/config.js.map +1 -1
  96. package/dist/lint/index.d.ts +4 -0
  97. package/dist/lint/index.js +4 -0
  98. package/dist/lint/index.js.map +1 -1
  99. package/dist/lint/naming.d.ts +62 -0
  100. package/dist/lint/naming.js +138 -0
  101. package/dist/lint/naming.js.map +1 -0
  102. package/dist/lint/rules.d.ts +37 -4
  103. package/dist/lint/rules.js +16 -4
  104. package/dist/lint/rules.js.map +1 -1
  105. package/dist/lint/schema-checks.d.ts +63 -0
  106. package/dist/lint/schema-checks.js +128 -0
  107. package/dist/lint/schema-checks.js.map +1 -0
  108. package/dist/lint/statements.d.ts +22 -1
  109. package/dist/lint/statements.js +40 -1
  110. package/dist/lint/statements.js.map +1 -1
  111. package/dist/mcp/tools.d.ts +1 -1
  112. package/dist/mcp/tools.js +1 -1
  113. package/dist/mcp/tools.js.map +1 -1
  114. package/dist/migrations/checkpoint.js +0 -2
  115. package/dist/migrations/checkpoint.js.map +1 -1
  116. package/dist/migrations/checks.d.ts +19 -1
  117. package/dist/migrations/checks.js +15 -0
  118. package/dist/migrations/checks.js.map +1 -1
  119. package/dist/migrations/create.d.ts +13 -4
  120. package/dist/migrations/create.js +24 -9
  121. package/dist/migrations/create.js.map +1 -1
  122. package/dist/migrations/routines.d.ts +5 -15
  123. package/dist/migrations/routines.js +5 -30
  124. package/dist/migrations/routines.js.map +1 -1
  125. package/dist/pr/model.d.ts +4 -1
  126. package/dist/pr/model.js +13 -5
  127. package/dist/pr/model.js.map +1 -1
  128. package/dist/pr/render.d.ts +4 -1
  129. package/dist/pr/render.js +22 -8
  130. package/dist/pr/render.js.map +1 -1
  131. package/dist/pr/tenants.d.ts +37 -0
  132. package/dist/pr/tenants.js +138 -0
  133. package/dist/pr/tenants.js.map +1 -0
  134. package/dist/probe.d.ts +27 -0
  135. package/dist/probe.js +106 -0
  136. package/dist/probe.js.map +1 -0
  137. package/dist/project.d.ts +16 -0
  138. package/dist/project.js +29 -1
  139. package/dist/project.js.map +1 -1
  140. package/dist/replay/clickhouse.d.ts +31 -0
  141. package/dist/replay/clickhouse.js +103 -6
  142. package/dist/replay/clickhouse.js.map +1 -1
  143. package/dist/replay/replay.d.ts +5 -2
  144. package/dist/replay/replay.js.map +1 -1
  145. package/dist/report/audit.d.ts +1 -1
  146. package/dist/report/audit.js +20 -3
  147. package/dist/report/audit.js.map +1 -1
  148. package/dist/schemas.d.ts +3 -1
  149. package/dist/schemas.js +2 -0
  150. package/dist/schemas.js.map +1 -1
  151. package/dist/sources/ddl.d.ts +15 -20
  152. package/dist/sources/ddl.js +34 -328
  153. package/dist/sources/ddl.js.map +1 -1
  154. package/dist/sources/index.js +2 -2
  155. package/dist/sources/index.js.map +1 -1
  156. package/dist/steps/backfill.d.ts +40 -5
  157. package/dist/steps/backfill.js +128 -8
  158. package/dist/steps/backfill.js.map +1 -1
  159. package/dist/steps/index.d.ts +19 -1
  160. package/dist/steps/index.js +34 -7
  161. package/dist/steps/index.js.map +1 -1
  162. package/dist/steps/kinds.d.ts +42 -4
  163. package/dist/steps/kinds.js +88 -14
  164. package/dist/steps/kinds.js.map +1 -1
  165. package/dist/steps/postgres-migration.d.ts +3 -0
  166. package/dist/steps/postgres-migration.js +7 -3
  167. package/dist/steps/postgres-migration.js.map +1 -1
  168. package/dist/steps/rebuild.d.ts +15 -6
  169. package/dist/steps/rebuild.js +12 -9
  170. package/dist/steps/rebuild.js.map +1 -1
  171. package/dist/tenants/index.d.ts +2 -1
  172. package/dist/tenants/index.js +3 -4
  173. package/dist/tenants/index.js.map +1 -1
  174. package/dist/tenants/wave.d.ts +128 -0
  175. package/dist/tenants/wave.js +254 -0
  176. package/dist/tenants/wave.js.map +1 -0
  177. package/dist/testing/define.d.ts +62 -0
  178. package/dist/testing/define.js +32 -0
  179. package/dist/testing/define.js.map +1 -0
  180. package/dist/testing/files.d.ts +5 -0
  181. package/dist/testing/files.js +18 -0
  182. package/dist/testing/files.js.map +1 -0
  183. package/dist/testing/report.d.ts +14 -0
  184. package/dist/testing/report.js +83 -0
  185. package/dist/testing/report.js.map +1 -0
  186. package/dist/testing/run.d.ts +104 -0
  187. package/dist/testing/run.js +328 -0
  188. package/dist/testing/run.js.map +1 -0
  189. package/dist/versioned/approval.d.ts +6 -0
  190. package/dist/versioned/approval.js +11 -5
  191. package/dist/versioned/approval.js.map +1 -1
  192. package/dist/versioned/checks.d.ts +8 -2
  193. package/dist/versioned/checks.js +34 -13
  194. package/dist/versioned/checks.js.map +1 -1
  195. package/dist/versioned/driver.d.ts +2 -2
  196. package/dist/versioned/engine.d.ts +10 -0
  197. package/dist/versioned/engine.js +8 -4
  198. package/dist/versioned/engine.js.map +1 -1
  199. package/dist/versioned/explain.d.ts +2 -0
  200. package/dist/versioned/explain.js +3 -1
  201. package/dist/versioned/explain.js.map +1 -1
  202. package/dist/versioned/op.d.ts +4 -0
  203. package/dist/versioned/op.js +5 -2
  204. package/dist/versioned/op.js.map +1 -1
  205. package/dist/versioned/state.js +1 -1
  206. package/dist/versioned/state.js.map +1 -1
  207. package/dist/waves/index.d.ts +58 -13
  208. package/dist/waves/index.js +109 -24
  209. package/dist/waves/index.js.map +1 -1
  210. package/dist/waves/record.d.ts +10 -0
  211. package/dist/waves/record.js.map +1 -1
  212. package/package.json +8 -7
  213. package/schemas/apply.schema.json +1 -1
  214. package/schemas/cleanup.schema.json +40 -0
  215. package/schemas/init.schema.json +51 -22
  216. package/schemas/lint-rules.schema.json +4 -0
  217. package/schemas/new.schema.json +41 -3
  218. package/schemas/plan.schema.json +39 -0
  219. package/schemas/report.schema.json +1 -1
  220. package/schemas/test.schema.json +41 -0
package/README.md CHANGED
@@ -34,7 +34,7 @@ The documentation is at [intentius.io/sql-yodeler](https://intentius.io/sql-yode
34
34
 
35
35
  `yodel apply <env>` takes the versioned path when the project has a `migrations/` directory holding at least one migration (`yodel new` writes them); otherwise it is the declarative path, unchanged. `yodel status <env>` lists applied, pending, out-of-order and part-way migrations and checksum mismatches (`--json`; exit 0 up to date, 3 pending, 4 mismatch or out of order, 2 an Op step, 1 error).
36
36
 
37
- `yodel init --from <env>` adopts an existing database: it reads it with chant's import (`chant import --from`), writes the declarations to `src/`, checks they plan clean, writes the baseline migration (the first one, creating everything) and records it applied in the history without running it (a succeeded migration row whose `note` starts `baseline:`). It needs a `chant.config.ts` with `sql.profiles.<env>` already, and for ClickHouse the profile's `databases`; `yodel init --help` has the rest. On Postgres it does the same, recording the baseline in `<history schema>.history`.
37
+ `yodel init --from <env>` adopts an existing database: it reads it with chant's import (`chant import --from`), writes the declarations to `src/`, checks they plan clean, writes the baseline migration (the first one, creating everything) and records it applied in the history without running it (a succeeded migration row whose `note` starts `baseline:`). It needs a `chant.config.ts` with `sql.profiles.<env>` already, and for ClickHouse the profile's `databases`; `yodel init --help` has the rest. On Postgres it does the same, recording the baseline in `<history schema>.history`. `yodel init --baseline <env>` then records the same baseline in another environment that already holds the schema (staging, prod): it compares that environment's live schema with the schema the baseline records, refuses naming each difference, and otherwise records the baseline applied behind the environment's gate.
38
38
 
39
39
  The history is an append-only table, `<db>.history`, one row per migration and per statement event, never updated or deleted.
40
40
 
@@ -92,7 +92,7 @@ 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), `pg-signature` (a function or procedure dropped and created with another signature), `ch-mutation` (a mutation rewriting parts), `ch-rebuild` (a `ClickHouseRebuildOp` step).
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
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:
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAgB,MAAM,eAAe,CAAC;AAyBzD,6DAA6D;AAC7D,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAuC,EAAE,GAAW,EAAE,aAAgC,OAAO,CAAC,GAAG;IACjI,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC;IAC9D,IAAI,EAAE,CAAC;IACP,IAAI,CAAC;QACH,EAAE,GAAG,MAAM,qBAAqB,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IAC5D,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC9E,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,gEAAgE,GAAG,qDAAqD,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IACnL,CAAC;IACD,MAAM,KAAK,GAAG,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC;IACpD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;IAC/E,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,GAAG,wCAAwC,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,wBAAwB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAChK,CAAC;AAED,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;AAE/F;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAA0C,EAC1C,IAA0B,EAC1B,IAAc,EACd,GAA4F;IAE5F,MAAM,GAAG,GAAkB,EAAE,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC;QAC3C,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;gBACnK,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;YAC5G,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,qBAAqB,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;YAC/K,CAAC;YACD,SAAS;QACX,CAAC;QACD,IAAI,GAAG,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;YAC5B,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,gEAAgE,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YACxK,SAAS;QACX,CAAC;QACD,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE;YACtD,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,QAAQ,EAAE,MAAM;YAChB,GAAG,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE;YACjF,OAAO,EAAE,EAAE,GAAG,MAAM;SACrB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;QAC7B,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IAC5L,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,+BAA+B;AAC/B,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAc,EAAU,EAAE,CACrD,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,IAAI,6BAA6B,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;AAEvN,8FAA8F;AAC9F,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,QAAgC,EAAY,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAElL,iFAAiF;AACjF,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,QAAgC,EAAE,IAAY;IACtF,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC;IACnG,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;IACtD,OAAO,IAAI,UAAU,CACnB,KAAK;QACH,CAAC,CAAC,GAAG,GAAG,0DAA0D,IAAI,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAClI,CAAC,CAAC,6BAA6B,GAAG,WAAW,IAAI,2BAA2B,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YAChI,kIAAkI,EACxI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CACzB,CAAC;AACJ,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,EAAE,CAAC;IACzC,IAAI,CAAC;QACH,MAAM,KAAK,GAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAyB,CAAC,KAAK,CAAC;QAC9D,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAAuB,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAIvD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,yBAAyB,CAAC;AAE1D,kFAAkF;AAClF,MAAM,CAAC,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAErD,6FAA6F;AAC7F,MAAM,UAAU,YAAY,CAAC,MAA+B,EAAE,GAAW;IACvE,OAAO,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,eAAe,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,IAAI,QAAQ,CAAC;AAC1F,CAAC;AAED,wHAAwH;AACxH,MAAM,UAAU,kBAAkB,CAAC,MAA+B,EAAE,GAAW;IAC7E,MAAM,GAAG,GAAG,MAAM,EAAE,YAAY,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC;IACnE,IAAI,GAAG;QAAE,OAAO,GAAG,CAAC;IACpB,OAAO,YAAY,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;AACtE,CAAC;AAWD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,OAAqE,EAAE,GAAW,EAAE,aAAgC,OAAO,CAAC,GAAG;IAC5K,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5D,IAAI,EAAE,CAAC;IACP,IAAI,CAAC;QACH,EAAE,GAAG,MAAM,qBAAqB,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IAC5D,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC9E,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,mEAAmE,GAAG,SAAS,GAAG,2CAA2C,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IACrM,CAAC;IACD,MAAM,IAAI,GAAG,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,GAAG,yBAAyB,IAAI,kDAAkD,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,IAAI,wBAAwB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAC7N,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAoB,EAAE,GAAW,EAAE,IAAwB;IAC1F,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,iCAAiC,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,CAAC,IAAY,EAAoB,EAAE,CAChD,IAAI,KAAK,SAAS;QAChB,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,6EAA6E,YAAY,MAAM,EAAE;QACxJ,CAAC,CAAC,MAAM,CAAC,kBAAkB,CAAC,IAAI,EAAE,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;IACvE,OAAO;QACL,GAAG,IAAI;QACP,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,KAAK,CAAC,YAAY,CAAC,IAAY;YAC7B,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;YACtD,OAAO,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7C,CAAC;KACF,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAoB,EAAE,IAAkB,EAAE,GAAW,EAAE,IAAwB;IAC3G,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAChE,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAkB,EAAU,EAAE,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAE7F,qGAAqG;AACrG,MAAM,UAAU,oBAAoB,CAAC,IAAkB,EAAE,OAA8B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC7F,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,yCAAyC,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC,sBAAsB,CAAC,OAAO,IAAI,CAAC,sBAAsB,CAAC,gBAAgB,IAAI,CAAC,YAAY,CAAC,6CAA6C,CAAC;IAC1O,IAAI,IAAI,KAAK,WAAW;QAAE,OAAO,YAAY,IAAI,CAAC,WAAW,CAAC,8HAA8H,IAAI,CAAC,eAAe,CAAC,4BAA4B,CAAC;IAC9O,OAAO,YAAY,IAAI,CAAC,QAAQ,CAAC,SAAS,IAAI,CAAC,eAAe,CAAC,wBAAwB,CAAC;AAC1F,CAAC"}
@@ -0,0 +1,84 @@
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 and applies where `sql.profiles.<env>.access` is on, as
8
+ * on Postgres.
9
+ *
10
+ * The plan already lists each access change with its rule and class. This
11
+ * module picks them out so `yodel plan`, `yodel new`, lint and the pull
12
+ * request comment can show them together, and names the changes that take
13
+ * access away: a privilege revoked (SQLPG297), a policy dropped (SQLPG292),
14
+ * row-level security turned off or no longer forced (SQLPG293), and on
15
+ * ClickHouse a privilege or role revoked (SQLCH274). A revoke no `REVOKE`
16
+ * declaration asks for takes away a privilege granted by hand (or one whose
17
+ * declaration was deleted) from the role that holds it, so it is called out
18
+ * on its own. ClickHouse takes no `REVOKE` declaration: a grantee's grant
19
+ * declarations are all it holds, so every revoke there is one of these.
20
+ */
21
+ import type { ClassifiedChange } from "@intentius/chant-lexicon-sql/core/classifier";
22
+ import type { MigrationStep } from "@intentius/chant-lexicon-sql/migration-statements";
23
+ import type { BuildOutput } from "./build.js";
24
+ import type { Project } from "../project.js";
25
+ /** chant's access rules: policies, row-level security, roles, grants, revokes, default privileges; on ClickHouse role settings, user clauses, row policies, grants and revokes. */
26
+ export declare const ACCESS_RULES: ReadonlySet<string>;
27
+ /** The entity types of ClickHouse's access declarations: `user`, `role`, `policy` and `grant`. */
28
+ export declare const CH_ACCESS_TYPES: ReadonlySet<string>;
29
+ /** The entity types of access declarations. */
30
+ export declare const ACCESS_TYPES: ReadonlySet<string>;
31
+ /** A plan change that is about access control. */
32
+ export declare const isAccessChange: (c: Pick<ClassifiedChange, "rule" | "field" | "object">) => boolean;
33
+ /** A migration step that is about access control. */
34
+ export declare function isAccessStep(s: MigrationStep): boolean;
35
+ /** Why a statement takes access away, or `undefined`. */
36
+ export declare function takesAccessAway(s: {
37
+ rule: string;
38
+ sql?: string;
39
+ }): string | undefined;
40
+ /** One access change in a declarative plan. */
41
+ export interface AccessItem {
42
+ object: string;
43
+ rule: string;
44
+ class: string;
45
+ field: string;
46
+ before?: unknown;
47
+ after?: unknown;
48
+ /** 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. */
49
+ byHand?: true;
50
+ }
51
+ /** What `yodel plan --json` adds on Postgres: whether access is managed here, and the access changes. */
52
+ export interface AccessReview {
53
+ managed: boolean;
54
+ /** Where {@link managed} came from: `yodel.config.ts`, `sql.profiles.<env>.access` or `default`. */
55
+ source: string;
56
+ changes: AccessItem[];
57
+ }
58
+ /** The access changes of a plan, a revoke of what nothing declares marked {@link AccessItem.byHand}. */
59
+ export declare function accessItems(changes: readonly ClassifiedChange[], build: BuildOutput): Promise<AccessItem[]>;
60
+ /** A plan's access section: whether access is managed in environment `name` (and where that is set), and the plan's access changes. */
61
+ export declare function accessReview(project: Pick<Project, "yodel" | "profileAccess">, name: string, changes: readonly ClassifiedChange[], build: BuildOutput): Promise<AccessReview>;
62
+ /** The privileges a revoke takes away: what the role held before and does not after. */
63
+ export declare const removed: (c: Pick<AccessItem, "before" | "after">) => string[];
64
+ /** 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>`). */
65
+ export declare function revokeLine(c: Pick<AccessItem, "object" | "before" | "after">): string;
66
+ /** The plan's access section, as text: every change with its rule and class, then the revokes of privileges granted by hand. */
67
+ export declare function renderAccessReview(review: AccessReview, environment: string): string[];
68
+ /** The access steps of a migration, each with why it takes access away when it does. */
69
+ export declare function accessSteps(steps: readonly MigrationStep[]): Array<{
70
+ index: number;
71
+ step: MigrationStep;
72
+ away?: string;
73
+ undeclared?: true;
74
+ }>;
75
+ /** A migration's access steps as lines: how many, and each one that takes access away. */
76
+ export declare function describeAccessSteps(steps: readonly MigrationStep[], md?: boolean): string[];
77
+ /**
78
+ * ClickHouse: whether environment `name` manages access, from yodel.config.ts
79
+ * or `sql.profiles.<env>.access`. chant plans and applies the users, roles,
80
+ * row policies and grants only where the profile's `access` is on, and the
81
+ * declarative path refuses a yodel.config.ts that says otherwise
82
+ * (`declarativeAccessRefusal`), so the two agree there.
83
+ */
84
+ export declare function clickhouseAccessManaged(project: Pick<Project, "yodel" | "profileAccess">, name: string): boolean;