@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
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Webhook events (#61). `notify` in yodel.config.ts names the environment
3
+ * variables that hold a webhook's URL and its signing key (never the values
4
+ * themselves); `yodel apply`, `yodel plan` and `yodel drift` post an event
5
+ * when an environment is waiting for approval, when a run was refused or
6
+ * failed, and when drift is found:
7
+ *
8
+ * ```ts
9
+ * notify: { webhook: "YODEL_WEBHOOK_URL", key: "YODEL_WEBHOOK_KEY", slack: "SLACK_WEBHOOK_URL" }
10
+ * ```
11
+ *
12
+ * The webhook gets the event as JSON, signed (`./verify.ts`); a Slack
13
+ * incoming webhook, when `slack` names one, gets a line of text. A delivery
14
+ * that fails is reported on stderr and never changes the command's exit
15
+ * code: the event is news about the run, not part of it.
16
+ */
17
+ import type { Io } from "../command.js";
18
+ export * from "./verify.js";
19
+ export declare const NOTIFY_EVENTS: readonly ["waiting", "refused", "failed", "drift"];
20
+ export type NotifyEventKind = (typeof NOTIFY_EVENTS)[number];
21
+ /** `notify` in yodel.config.ts. Each value is the NAME of an environment variable; the config never holds a URL or a key. */
22
+ export interface NotifyConfig {
23
+ /** The variable holding the webhook's URL. */
24
+ webhook?: string;
25
+ /** The variable holding the key the webhook's events are signed with (HMAC-SHA256). Required with `webhook`. */
26
+ key?: string;
27
+ /** The variable holding a Slack incoming webhook's URL. Its messages are text and unsigned (Slack does not check a signature). */
28
+ slack?: string;
29
+ /** The events to send. Default: all of them. */
30
+ events?: NotifyEventKind[];
31
+ }
32
+ /** What an event says. The JSON Schema is schemas/event.schema.json. */
33
+ export interface NotifyEvent {
34
+ version: 1;
35
+ /** Unique per event; the `x-yodel-delivery` header carries it too, so a receiver can drop a repeat. */
36
+ id: string;
37
+ event: NotifyEventKind;
38
+ /** When yodel sent it, ISO 8601 UTC. */
39
+ at: string;
40
+ environment: string;
41
+ /** The command that sent it. */
42
+ command: "apply" | "plan" | "drift";
43
+ /** The command's own word for the outcome (`gated`, `refused`, `denied`, `failed`, `drifted`). */
44
+ status: string;
45
+ /** One line a person can read. */
46
+ summary: string;
47
+ /** The migrations concerned: the pending ones for apply and plan. */
48
+ migrations: string[];
49
+ /** The plan digest the run computed, when it computed one. */
50
+ digest?: string;
51
+ /** The command that approves the plan, on `waiting`. */
52
+ approve?: string;
53
+ /** Why it was refused or failed: the refusal's message, the failing exit code. */
54
+ reason?: string;
55
+ /** drift: the objects changed and missing, and the drift's fingerprint (the same drift, the same fingerprint, run after run). */
56
+ drift?: {
57
+ changed: string[];
58
+ missing: string[];
59
+ fingerprint?: string;
60
+ };
61
+ /** The CI run, when yodel runs in one (GitHub Actions, Forgejo Actions, GitLab CI). */
62
+ runUrl?: string;
63
+ /** The repository and commit, when CI says. */
64
+ repository?: string;
65
+ commit?: string;
66
+ }
67
+ /** Checks `notify` in yodel.config.ts; returns a message for the first problem, or undefined. */
68
+ export declare function notifyConfigProblem(value: unknown): string | undefined;
69
+ /** Where the CI run is, when yodel runs in one. */
70
+ export declare function ciContext(env: NodeJS.ProcessEnv): Pick<NotifyEvent, "runUrl" | "repository" | "commit">;
71
+ type Fields = Omit<NotifyEvent, "version" | "id" | "at" | "runUrl" | "repository" | "commit">;
72
+ /**
73
+ * The event for what `yodel apply --json` (or `yodel plan --json`) printed,
74
+ * or undefined when the outcome is not one an event is sent for.
75
+ */
76
+ export declare function eventFromRun(command: "apply" | "plan", doc: Record<string, unknown>): Fields | undefined;
77
+ /** The event for what `yodel drift --json` printed, or undefined when nothing drifted. */
78
+ export declare function eventFromDrift(doc: {
79
+ environment?: string;
80
+ drifted?: boolean;
81
+ changed?: Array<{
82
+ name: string;
83
+ }>;
84
+ missing?: string[];
85
+ fingerprint?: string;
86
+ }): Fields | undefined;
87
+ /** A whole event: the fields, with its id, time and the CI run. */
88
+ export declare function makeEvent(fields: Fields, env?: NodeJS.ProcessEnv, now?: Date): NotifyEvent;
89
+ /** The Slack message for an event: one line of text, with the approve command and the run on `waiting`. */
90
+ export declare function slackMessage(e: NotifyEvent): {
91
+ text: string;
92
+ };
93
+ export type Fetch = (url: string, init: {
94
+ method: string;
95
+ headers: Record<string, string>;
96
+ body: string;
97
+ signal?: AbortSignal;
98
+ }) => Promise<{
99
+ ok: boolean;
100
+ status: number;
101
+ }>;
102
+ export interface Delivery {
103
+ target: "webhook" | "slack";
104
+ ok: boolean;
105
+ /** The HTTP status, or why there was none. */
106
+ detail: string;
107
+ }
108
+ /** The request that delivers `event` to the webhook, signed with `key`. */
109
+ export declare function webhookRequest(event: NotifyEvent, key: string, now?: number): {
110
+ headers: Record<string, string>;
111
+ body: string;
112
+ };
113
+ /**
114
+ * Sends the event to what `config` names. Reads the URLs and the key from
115
+ * `env` at send time. Never throws: each delivery's outcome is returned and
116
+ * a failed one is written to `io.err`.
117
+ */
118
+ export declare function sendEvent(config: NotifyConfig | undefined, fields: Fields | undefined, options: {
119
+ io: Io;
120
+ verb: string;
121
+ env?: NodeJS.ProcessEnv;
122
+ fetch?: Fetch;
123
+ attempts?: number;
124
+ }): Promise<Delivery[]>;
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Webhook events (#61). `notify` in yodel.config.ts names the environment
3
+ * variables that hold a webhook's URL and its signing key (never the values
4
+ * themselves); `yodel apply`, `yodel plan` and `yodel drift` post an event
5
+ * when an environment is waiting for approval, when a run was refused or
6
+ * failed, and when drift is found:
7
+ *
8
+ * ```ts
9
+ * notify: { webhook: "YODEL_WEBHOOK_URL", key: "YODEL_WEBHOOK_KEY", slack: "SLACK_WEBHOOK_URL" }
10
+ * ```
11
+ *
12
+ * The webhook gets the event as JSON, signed (`./verify.ts`); a Slack
13
+ * incoming webhook, when `slack` names one, gets a line of text. A delivery
14
+ * that fails is reported on stderr and never changes the command's exit
15
+ * code: the event is news about the run, not part of it.
16
+ */
17
+ import { randomUUID } from "node:crypto";
18
+ import { DELIVERY_HEADER, EVENT_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER, signWebhook } from "./verify.js";
19
+ export * from "./verify.js";
20
+ export const NOTIFY_EVENTS = ["waiting", "refused", "failed", "drift"];
21
+ const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
22
+ /** Checks `notify` in yodel.config.ts; returns a message for the first problem, or undefined. */
23
+ export function notifyConfigProblem(value) {
24
+ if (typeof value !== "object" || value === null || Array.isArray(value))
25
+ return "notify: expected { webhook, key, slack?, events? }";
26
+ const v = value;
27
+ for (const k of Object.keys(v))
28
+ if (!["webhook", "key", "slack", "events"].includes(k))
29
+ return `notify.${k}: unknown setting (expected webhook, key, slack, events)`;
30
+ for (const k of ["webhook", "key", "slack"]) {
31
+ const n = v[k];
32
+ if (n === undefined)
33
+ continue;
34
+ if (typeof n !== "string" || n.includes("://") || !NAME.test(n)) {
35
+ return `notify.${k}: the name of the environment variable that holds the ${k === "key" ? "signing key" : "URL"} (such as ${k === "key" ? "YODEL_WEBHOOK_KEY" : k === "slack" ? "SLACK_WEBHOOK_URL" : "YODEL_WEBHOOK_URL"}), never the ${k === "key" ? "key" : "URL"} itself`;
36
+ }
37
+ }
38
+ if (v.webhook !== undefined && v.key === undefined)
39
+ return "notify.key: a webhook needs a signing key; name the variable that holds it";
40
+ if (v.key !== undefined && v.webhook === undefined)
41
+ return "notify.webhook: a key is for a webhook; name the variable that holds its URL";
42
+ if (v.webhook === undefined && v.slack === undefined)
43
+ return "notify: name a webhook (with its key) or a slack URL variable";
44
+ if (v.events !== undefined && !(Array.isArray(v.events) && v.events.every((e) => NOTIFY_EVENTS.includes(e)))) {
45
+ return `notify.events: a list of ${NOTIFY_EVENTS.join(", ")}`;
46
+ }
47
+ return undefined;
48
+ }
49
+ /** Where the CI run is, when yodel runs in one. */
50
+ export function ciContext(env) {
51
+ const out = {};
52
+ if (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID)
53
+ out.runUrl = `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`;
54
+ else if (env.CI_JOB_URL)
55
+ out.runUrl = env.CI_JOB_URL;
56
+ else if (env.CI_PIPELINE_URL)
57
+ out.runUrl = env.CI_PIPELINE_URL;
58
+ const repository = env.GITHUB_REPOSITORY ?? env.CI_PROJECT_PATH;
59
+ if (repository)
60
+ out.repository = repository;
61
+ const commit = env.GITHUB_SHA ?? env.CI_COMMIT_SHA;
62
+ if (commit)
63
+ out.commit = commit;
64
+ return out;
65
+ }
66
+ const ids = (list) => (Array.isArray(list) ? list.flatMap((p) => (typeof p === "object" && p !== null && typeof p.id === "string" ? [p.id] : [])) : []);
67
+ const str = (v) => (typeof v === "string" && v !== "" ? v : undefined);
68
+ const firstLine = (s) => s.split("\n")[0] ?? s;
69
+ /**
70
+ * The event for what `yodel apply --json` (or `yodel plan --json`) printed,
71
+ * or undefined when the outcome is not one an event is sent for.
72
+ */
73
+ export function eventFromRun(command, doc) {
74
+ const status = str(doc.status) ?? (doc.refusal ? "refused" : undefined);
75
+ const environment = str(doc.environment) ?? "";
76
+ const migrations = ids(doc.pending);
77
+ const digest = str(doc.gatePlanDigest) ?? str(doc.planDigest);
78
+ const base = { environment, command, migrations, ...(digest ? { digest } : {}) };
79
+ const refusal = doc.refusal?.message;
80
+ const deniedRules = Array.isArray(doc.policy) ? doc.policy.filter((r) => r.outcome === "denied").map((r) => r.id ?? "") : [];
81
+ if (status === "gated") {
82
+ const approve = str(doc.approve);
83
+ return { ...base, event: "waiting", status, summary: `${environment} is waiting for approval of ${digest ?? "its plan"} (${String(migrations.length)} migration${migrations.length === 1 ? "" : "s"})`, ...(approve ? { approve } : {}) };
84
+ }
85
+ if (status === "refused" || status === "denied" || (command === "plan" && (refusal !== undefined || deniedRules.length > 0))) {
86
+ const reason = typeof refusal === "string" ? refusal : deniedRules.length > 0 ? `policy rule ${deniedRules.join(", ")} denies the plan` : status === "refused" ? "refused" : "denied by policy";
87
+ return { ...base, event: "refused", status: status ?? (refusal !== undefined ? "refused" : "denied"), summary: `${command} in ${environment} was refused: ${firstLine(reason)}`, reason };
88
+ }
89
+ if (status === "failed" || status === "incomplete") {
90
+ const code = typeof doc.exitCode === "number" ? doc.exitCode : undefined;
91
+ const reason = `${command} ${status}${code !== undefined ? ` (exit ${String(code)})` : ""}`;
92
+ return { ...base, event: "failed", status, summary: `${command} in ${environment} ${status}${code !== undefined ? ` with exit ${String(code)}` : ""}`, reason };
93
+ }
94
+ return undefined;
95
+ }
96
+ /** The event for what `yodel drift --json` printed, or undefined when nothing drifted. */
97
+ export function eventFromDrift(doc) {
98
+ if (!doc.drifted)
99
+ return undefined;
100
+ const changed = (doc.changed ?? []).map((c) => c.name);
101
+ const missing = doc.missing ?? [];
102
+ const environment = doc.environment ?? "";
103
+ return {
104
+ event: "drift",
105
+ environment,
106
+ command: "drift",
107
+ status: "drifted",
108
+ migrations: [],
109
+ summary: `${environment} drifted: ${[changed.length > 0 ? `${changed.join(", ")} changed` : "", missing.length > 0 ? `${missing.join(", ")} missing` : ""].filter(Boolean).join("; ")}`,
110
+ drift: { changed, missing, ...(doc.fingerprint ? { fingerprint: doc.fingerprint } : {}) },
111
+ };
112
+ }
113
+ /** A whole event: the fields, with its id, time and the CI run. */
114
+ export function makeEvent(fields, env = process.env, now = new Date()) {
115
+ return { version: 1, id: randomUUID(), at: now.toISOString(), ...fields, ...ciContext(env) };
116
+ }
117
+ /** The Slack message for an event: one line of text, with the approve command and the run on `waiting`. */
118
+ export function slackMessage(e) {
119
+ const lines = [`yodel ${e.event}: ${e.summary}`];
120
+ if (e.approve)
121
+ lines.push(`Approve with: \`${e.approve}\``);
122
+ if (e.runUrl)
123
+ lines.push(`Run: ${e.runUrl}`);
124
+ return { text: lines.join("\n") };
125
+ }
126
+ /** The request that delivers `event` to the webhook, signed with `key`. */
127
+ export function webhookRequest(event, key, now = Math.floor(Date.now() / 1000)) {
128
+ const body = JSON.stringify(event);
129
+ return {
130
+ body,
131
+ headers: {
132
+ "content-type": "application/json",
133
+ "user-agent": "sql-yodeler",
134
+ [EVENT_HEADER]: event.event,
135
+ [DELIVERY_HEADER]: event.id,
136
+ [TIMESTAMP_HEADER]: String(now),
137
+ [SIGNATURE_HEADER]: signWebhook(key, now, body),
138
+ },
139
+ };
140
+ }
141
+ async function post(fetchFn, url, request, attempts) {
142
+ let last = "";
143
+ for (let i = 0; i < attempts; i++) {
144
+ if (i > 0)
145
+ await new Promise((r) => setTimeout(r, 500 * i));
146
+ try {
147
+ const res = await fetchFn(url, { method: "POST", ...request, signal: AbortSignal.timeout(10_000) });
148
+ if (res.ok)
149
+ return undefined;
150
+ last = `HTTP ${String(res.status)}`;
151
+ // A 4xx will not change on a retry: the receiver refused the event (a signature it does not accept, say).
152
+ if (res.status < 500)
153
+ return last;
154
+ }
155
+ catch (e) {
156
+ last = e instanceof Error ? e.message : String(e);
157
+ }
158
+ }
159
+ return last;
160
+ }
161
+ /**
162
+ * Sends the event to what `config` names. Reads the URLs and the key from
163
+ * `env` at send time. Never throws: each delivery's outcome is returned and
164
+ * a failed one is written to `io.err`.
165
+ */
166
+ export async function sendEvent(config, fields, options) {
167
+ if (!config || !fields)
168
+ return [];
169
+ if (config.events && !config.events.includes(fields.event))
170
+ return [];
171
+ const env = options.env ?? process.env;
172
+ const fetchFn = options.fetch ?? globalThis.fetch;
173
+ const attempts = options.attempts ?? 3;
174
+ const event = makeEvent(fields, env);
175
+ const out = [];
176
+ const warn = (d) => {
177
+ options.io.err(`yodel ${options.verb}: notify: the ${fields.event} event was not delivered to the ${d.target}: ${d.detail}\n`);
178
+ };
179
+ if (config.webhook) {
180
+ const url = env[config.webhook];
181
+ const key = config.key ? env[config.key] : undefined;
182
+ let d;
183
+ if (!url)
184
+ d = { target: "webhook", ok: false, detail: `${config.webhook} is not set` };
185
+ else if (!key)
186
+ d = { target: "webhook", ok: false, detail: `${config.key ?? "the key variable"} is not set, and yodel does not send an unsigned event` };
187
+ else {
188
+ const failed = await post(fetchFn, url, webhookRequest(event, key), attempts);
189
+ d = { target: "webhook", ok: failed === undefined, detail: failed ?? "delivered" };
190
+ }
191
+ if (!d.ok)
192
+ warn(d);
193
+ out.push(d);
194
+ }
195
+ if (config.slack) {
196
+ const url = env[config.slack];
197
+ let d;
198
+ if (!url)
199
+ d = { target: "slack", ok: false, detail: `${config.slack} is not set` };
200
+ else {
201
+ const failed = await post(fetchFn, url, { headers: { "content-type": "application/json" }, body: JSON.stringify(slackMessage(event)) }, attempts);
202
+ d = { target: "slack", ok: failed === undefined, detail: failed ?? "delivered" };
203
+ }
204
+ if (!d.ok)
205
+ warn(d);
206
+ out.push(d);
207
+ }
208
+ return out;
209
+ }
210
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notify/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Signing and checking a yodel webhook event (#61). This module needs only
3
+ * node:crypto, so a receiver can import it alone:
4
+ *
5
+ * ```ts
6
+ * import { verifyWebhook } from "@intentius/sql-yodeler/webhook";
7
+ *
8
+ * const checked = verifyWebhook({ body, headers: req.headers, key: process.env.YODEL_WEBHOOK_KEY });
9
+ * if (!checked.ok) return reply(401, checked.reason);
10
+ * handle(checked.event);
11
+ * ```
12
+ *
13
+ * The signature is HMAC-SHA256, keyed with the secret, over
14
+ * `<timestamp>.<body>`: the `x-yodel-timestamp` header (seconds since the
15
+ * epoch), a dot, and the request body exactly as sent. It travels as
16
+ * `x-yodel-signature: sha256=<hex>`. Signing the timestamp with the body
17
+ * lets a receiver refuse a delivery replayed later; {@link verifyWebhook}
18
+ * refuses one older than five minutes by default.
19
+ */
20
+ export declare const SIGNATURE_HEADER = "x-yodel-signature";
21
+ export declare const TIMESTAMP_HEADER = "x-yodel-timestamp";
22
+ export declare const EVENT_HEADER = "x-yodel-event";
23
+ export declare const DELIVERY_HEADER = "x-yodel-delivery";
24
+ /** How old a delivery {@link verifyWebhook} still accepts, in seconds. */
25
+ export declare const DEFAULT_TOLERANCE_SECONDS = 300;
26
+ /** `sha256=<hex>` of `<timestamp>.<body>`, keyed with `key`. */
27
+ export declare function signWebhook(key: string, timestamp: number | string, body: string): string;
28
+ export type WebhookHeaders = Record<string, string | readonly string[] | undefined> | Headers;
29
+ export interface VerifyInput {
30
+ /** The request body exactly as received, before any JSON parsing. */
31
+ body: string | Uint8Array;
32
+ /** The request's headers; names are matched without regard to case. */
33
+ headers: WebhookHeaders;
34
+ /** The shared secret. An empty or missing key never verifies. */
35
+ key: string | undefined;
36
+ /** Now, in seconds since the epoch. Default: the clock. */
37
+ now?: number;
38
+ /** How old a delivery may be, in seconds. Default {@link DEFAULT_TOLERANCE_SECONDS}. */
39
+ toleranceSeconds?: number;
40
+ }
41
+ export type VerifyResult = {
42
+ ok: true;
43
+ event: unknown;
44
+ } | {
45
+ ok: false;
46
+ reason: string;
47
+ };
48
+ /**
49
+ * Whether a delivery was signed with `key` and is recent: the signature
50
+ * header matches (compared in constant time) and the timestamp is within the
51
+ * tolerance either side of now. On success, the parsed event.
52
+ */
53
+ export declare function verifyWebhook(input: VerifyInput): VerifyResult;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Signing and checking a yodel webhook event (#61). This module needs only
3
+ * node:crypto, so a receiver can import it alone:
4
+ *
5
+ * ```ts
6
+ * import { verifyWebhook } from "@intentius/sql-yodeler/webhook";
7
+ *
8
+ * const checked = verifyWebhook({ body, headers: req.headers, key: process.env.YODEL_WEBHOOK_KEY });
9
+ * if (!checked.ok) return reply(401, checked.reason);
10
+ * handle(checked.event);
11
+ * ```
12
+ *
13
+ * The signature is HMAC-SHA256, keyed with the secret, over
14
+ * `<timestamp>.<body>`: the `x-yodel-timestamp` header (seconds since the
15
+ * epoch), a dot, and the request body exactly as sent. It travels as
16
+ * `x-yodel-signature: sha256=<hex>`. Signing the timestamp with the body
17
+ * lets a receiver refuse a delivery replayed later; {@link verifyWebhook}
18
+ * refuses one older than five minutes by default.
19
+ */
20
+ import { createHmac, timingSafeEqual } from "node:crypto";
21
+ export const SIGNATURE_HEADER = "x-yodel-signature";
22
+ export const TIMESTAMP_HEADER = "x-yodel-timestamp";
23
+ export const EVENT_HEADER = "x-yodel-event";
24
+ export const DELIVERY_HEADER = "x-yodel-delivery";
25
+ /** How old a delivery {@link verifyWebhook} still accepts, in seconds. */
26
+ export const DEFAULT_TOLERANCE_SECONDS = 300;
27
+ /** `sha256=<hex>` of `<timestamp>.<body>`, keyed with `key`. */
28
+ export function signWebhook(key, timestamp, body) {
29
+ return `sha256=${createHmac("sha256", key).update(`${String(timestamp)}.${body}`, "utf8").digest("hex")}`;
30
+ }
31
+ function header(headers, name) {
32
+ if (typeof headers.get === "function")
33
+ return headers.get(name) ?? undefined;
34
+ for (const [k, v] of Object.entries(headers)) {
35
+ if (k.toLowerCase() !== name)
36
+ continue;
37
+ return Array.isArray(v) ? v[0] : v;
38
+ }
39
+ return undefined;
40
+ }
41
+ /**
42
+ * Whether a delivery was signed with `key` and is recent: the signature
43
+ * header matches (compared in constant time) and the timestamp is within the
44
+ * tolerance either side of now. On success, the parsed event.
45
+ */
46
+ export function verifyWebhook(input) {
47
+ if (!input.key)
48
+ return { ok: false, reason: "no key to verify with" };
49
+ const body = typeof input.body === "string" ? input.body : Buffer.from(input.body).toString("utf8");
50
+ const signature = header(input.headers, SIGNATURE_HEADER);
51
+ const timestamp = header(input.headers, TIMESTAMP_HEADER);
52
+ if (!signature)
53
+ return { ok: false, reason: `no ${SIGNATURE_HEADER} header` };
54
+ if (!timestamp || !/^\d+$/.test(timestamp))
55
+ return { ok: false, reason: `no ${TIMESTAMP_HEADER} header, or not a number of seconds` };
56
+ const expected = Buffer.from(signWebhook(input.key, timestamp, body));
57
+ const given = Buffer.from(signature);
58
+ if (given.length !== expected.length || !timingSafeEqual(given, expected))
59
+ return { ok: false, reason: "the signature does not match" };
60
+ const now = input.now ?? Math.floor(Date.now() / 1000);
61
+ const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
62
+ if (Math.abs(now - Number(timestamp)) > tolerance)
63
+ return { ok: false, reason: `the timestamp is more than ${String(tolerance)}s from now` };
64
+ try {
65
+ return { ok: true, event: JSON.parse(body) };
66
+ }
67
+ catch {
68
+ return { ok: false, reason: "the body is not JSON" };
69
+ }
70
+ }
71
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/notify/verify.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@
10
10
  * no ledger entry.
11
11
  */
12
12
  import type { MigrationStep } from "@intentius/chant-lexicon-sql/migration-statements";
13
+ import { type EnvironmentApprovalMode } from "../approval-mode/index.js";
13
14
  import type { GateLedgerPort } from "@intentius/chant/op/gate";
14
15
  import { type Environment, type Project, type SqlDialect } from "../project.js";
15
16
  import { type MigrationCheck } from "../migrations/index.js";
@@ -17,6 +18,7 @@ import { type MigrateOpRef, type RuleResult, type VersionedEnv } from "../versio
17
18
  import { type LatestApproval, type MoveExplanation } from "../versioned/explain.js";
18
19
  import type { DigestSubject } from "../versioned/digest.js";
19
20
  import type { ServerInfo } from "../server.js";
21
+ import { type WaveView } from "../waves/index.js";
20
22
  export interface StepView {
21
23
  index: number;
22
24
  step: MigrationStep;
@@ -35,7 +37,7 @@ export interface PendingView {
35
37
  outOfOrder: boolean;
36
38
  /** Where a migration that ran part way resumes. */
37
39
  resumeAt?: number | undefined;
38
- /** Its pre-migration checks (#52), run before its first statement. */
40
+ /** Its pre-migration checks (#52) and its statements' pre-checks (#51), run before its first statement. */
39
41
  checks?: MigrationCheck[] | undefined;
40
42
  steps: StepView[];
41
43
  }
@@ -54,6 +56,8 @@ export type ApprovalView = {
54
56
  }) | undefined;
55
57
  /** Why the ledger was not read. */
56
58
  ledgerNote?: string | undefined;
59
+ /** Which approvals of the gate count (#48), as yodel.config.ts at the base commit has it; absent, `ledger`. */
60
+ mode?: EnvironmentApprovalMode | undefined;
57
61
  };
58
62
  export interface VersionedPlan {
59
63
  environment: string;
@@ -80,6 +84,8 @@ export interface VersionedPlan {
80
84
  policy?: RuleResult[] | undefined;
81
85
  /** The comment this one replaces showed another digest: what moved since. */
82
86
  sinceLast?: MoveExplanation | undefined;
87
+ /** The environment's wave in the apply pipeline, when yodel.config.ts declares waves. */
88
+ wave?: WaveView | undefined;
83
89
  notes: string[];
84
90
  }
85
91
  export interface CollectOptions {
package/dist/pr/model.js CHANGED
@@ -9,20 +9,25 @@
9
9
  * the post-merge apply needs. Nothing here writes: no lock, no history row,
10
10
  * no ledger entry.
11
11
  */
12
+ import { migrationsApprovalAtBase, portFor, signFlag } from "../approval-mode/index.js";
12
13
  import { YodelError } from "../project.js";
13
- import { scanMigrations } from "../migrations/index.js";
14
+ import { migrationChecks, scanMigrations } from "../migrations/index.js";
14
15
  import { describeTopology } from "../history/index.js";
15
16
  import { asBackfill } from "../steps/kinds.js";
17
+ import { retainedNote } from "../cleanup/index.js";
16
18
  import { EXIT_REFUSED } from "../exit.js";
17
19
  import { BROKEN_CHAIN, findMigrateOp, inspect, gitLedger, judgePlan, migrateOpExample, migrationsDir, closeVersioned, openVersioned, outOfOrder, planDigest, sameDigest, statusReport, } from "../versioned/index.js";
18
20
  import { currentSubject, explainAgainst, explainMove, latestApproval } from "../versioned/explain.js";
21
+ import { waveView } from "../waves/index.js";
22
+ import { policyAtBase } from "../config/base.js";
19
23
  const viewOf = (m, done = [], outOfOrder = false, resumeAt, refused = []) => ({
20
24
  id: m.id,
21
25
  outOfOrder,
22
26
  resumeAt,
23
- checks: m.document.checks ?? [],
27
+ checks: migrationChecks(m.document).map((c) => ({ name: c.name, sql: c.sql, ...(c.expect !== undefined ? { expect: c.expect } : {}) })),
24
28
  steps: m.document.steps.map((step, index) => {
25
- const file = asBackfill(step)?.options.file;
29
+ const options = asBackfill(step)?.options;
30
+ const file = options && "file" in options ? options.file : undefined;
26
31
  const text = file === undefined ? undefined : m.files.extra?.[file];
27
32
  return { index, step, done: done.includes(index), refused: refused.find((r) => r.index === index)?.reason, ...(file !== undefined && text !== undefined ? { source: { file, text } } : {}) };
28
33
  }),
@@ -78,11 +83,18 @@ async function collectWith(project, env, options, venv, ordered, base, problems)
78
83
  if (!(await sameDigest(prev, report.digest)))
79
84
  plan.sinceLast = await explainMove({ approvedDigest: prev, now: subject, previous: options.previous, ordered, applied: status.applied });
80
85
  }
86
+ const kept = await retainedNote(venv);
87
+ if (kept)
88
+ plan.notes.push(kept);
81
89
  plan.approval = await approvalView(project, env, venv, ordered, plan, options);
82
- if (project.yodel?.config.policy && !report.refusal) {
90
+ // The policy as the base commit has it (#73): a pull request that loosens a rule is judged by the old one.
91
+ const basePolicy = await policyAtBase(project, options.processEnv);
92
+ if (basePolicy.note)
93
+ plan.notes.push(basePolicy.note);
94
+ if (basePolicy.policy && !report.refusal) {
83
95
  const approval = plan.approval;
84
96
  plan.policy = await judgePlan({
85
- policy: project.yodel.config.policy,
97
+ policy: basePolicy.policy,
86
98
  environment: env.name,
87
99
  dialect: project.dialect,
88
100
  toApply: status.pending,
@@ -99,6 +111,14 @@ async function collectWith(project, env, options, venv, ordered, base, problems)
99
111
  },
100
112
  });
101
113
  }
114
+ if (report.digest && !report.refusal) {
115
+ try {
116
+ plan.wave = await waveView(project, env.name, ordered, status.pending, report.digest);
117
+ }
118
+ catch (e) {
119
+ plan.notes.push(`The wave's gate could not be read at the base commit: ${e instanceof Error ? (e.message.split("\n")[0] ?? "") : String(e)}`);
120
+ }
121
+ }
102
122
  return plan;
103
123
  }
104
124
  async function approvalView(project, env, venv, ordered, plan, options) {
@@ -111,16 +131,18 @@ async function approvalView(project, env, venv, ordered, plan, options) {
111
131
  throw e;
112
132
  return { kind: "no-op", message: e.message.includes("no Op applies") ? `no Op applies the migrations to ${env.name}; declare one, for example:\n\n${migrateOpExample(env.name)}` : e.message };
113
133
  }
134
+ const mode = await migrationsApprovalAtBase(project, env.name);
114
135
  const view = {
115
136
  kind: "op",
116
137
  op: op.name,
117
138
  gate: op.gate,
118
- command: plan.digest ? `chant approve ${op.name} ${op.gate} --plan ${plan.digest}` : undefined,
139
+ command: plan.digest ? `chant approve ${op.name} ${op.gate} --plan ${plan.digest}${signFlag(mode.mode)}` : undefined,
140
+ ...(mode.mode !== "ledger" ? { mode: mode.mode } : {}),
119
141
  };
120
142
  if (options.ledger === null)
121
143
  return view;
122
144
  try {
123
- const ledger = options.ledger ?? (await gitLedger(project.dir));
145
+ const ledger = await portFor(options.ledger ?? (await gitLedger(project.dir)), mode.mode, project.dir, mode.base);
124
146
  const latest = await latestApproval(ledger, op, plan.digest);
125
147
  if (!latest)
126
148
  return view;