@intentius/sql-yodeler 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +3 -3
  2. package/dist/chant/build.js +7 -0
  3. package/dist/chant/build.js.map +1 -1
  4. package/dist/ci/render.d.ts +289 -0
  5. package/dist/ci/render.js +688 -0
  6. package/dist/ci/render.js.map +1 -0
  7. package/dist/ci/replay.d.ts +17 -0
  8. package/dist/ci/replay.js +69 -0
  9. package/dist/ci/replay.js.map +1 -0
  10. package/dist/commands/apply.d.ts +113 -0
  11. package/dist/commands/apply.js +228 -21
  12. package/dist/commands/apply.js.map +1 -1
  13. package/dist/commands/approve.js +44 -0
  14. package/dist/commands/approve.js.map +1 -1
  15. package/dist/commands/checkpoint.d.ts +3 -0
  16. package/dist/commands/checkpoint.js +78 -0
  17. package/dist/commands/checkpoint.js.map +1 -0
  18. package/dist/commands/ci.d.ts +3 -0
  19. package/dist/commands/ci.js +118 -0
  20. package/dist/commands/ci.js.map +1 -0
  21. package/dist/commands/docs.d.ts +11 -0
  22. package/dist/commands/docs.js +108 -0
  23. package/dist/commands/docs.js.map +1 -0
  24. package/dist/commands/drift.d.ts +13 -1
  25. package/dist/commands/drift.js +18 -2
  26. package/dist/commands/drift.js.map +1 -1
  27. package/dist/commands/index.js +10 -0
  28. package/dist/commands/index.js.map +1 -1
  29. package/dist/commands/init.js +1 -1
  30. package/dist/commands/init.js.map +1 -1
  31. package/dist/commands/lint.js +11 -3
  32. package/dist/commands/lint.js.map +1 -1
  33. package/dist/commands/override.js +48 -16
  34. package/dist/commands/override.js.map +1 -1
  35. package/dist/commands/plan.js +5 -1
  36. package/dist/commands/plan.js.map +1 -1
  37. package/dist/commands/report.d.ts +13 -0
  38. package/dist/commands/report.js +121 -0
  39. package/dist/commands/report.js.map +1 -0
  40. package/dist/commands/revert.d.ts +20 -0
  41. package/dist/commands/revert.js +246 -0
  42. package/dist/commands/revert.js.map +1 -0
  43. package/dist/commands/status.js +32 -5
  44. package/dist/commands/status.js.map +1 -1
  45. package/dist/config/base.d.ts +69 -0
  46. package/dist/config/base.js +121 -0
  47. package/dist/config/base.js.map +1 -0
  48. package/dist/config/define.d.ts +123 -1
  49. package/dist/config/define.js +2 -0
  50. package/dist/config/define.js.map +1 -1
  51. package/dist/config/index.d.ts +2 -0
  52. package/dist/config/index.js +2 -0
  53. package/dist/config/index.js.map +1 -1
  54. package/dist/config/load.d.ts +1 -1
  55. package/dist/config/load.js +90 -5
  56. package/dist/config/load.js.map +1 -1
  57. package/dist/config/settings.d.ts +4 -1
  58. package/dist/config/settings.js +8 -2
  59. package/dist/config/settings.js.map +1 -1
  60. package/dist/exit.js +1 -1
  61. package/dist/exit.js.map +1 -1
  62. package/dist/history/postgres/ddl.d.ts +18 -0
  63. package/dist/history/postgres/ddl.js +27 -2
  64. package/dist/history/postgres/ddl.js.map +1 -1
  65. package/dist/history/postgres/store.d.ts +2 -0
  66. package/dist/history/postgres/store.js +7 -1
  67. package/dist/history/postgres/store.js.map +1 -1
  68. package/dist/history/rows.d.ts +16 -3
  69. package/dist/history/rows.js +30 -1
  70. package/dist/history/rows.js.map +1 -1
  71. package/dist/history/store.d.ts +2 -0
  72. package/dist/history/store.js +2 -2
  73. package/dist/history/store.js.map +1 -1
  74. package/dist/index.d.ts +4 -1
  75. package/dist/index.js +1 -0
  76. package/dist/index.js.map +1 -1
  77. package/dist/lint/check.js +17 -2
  78. package/dist/lint/check.js.map +1 -1
  79. package/dist/lint/report.js +1 -1
  80. package/dist/lint/report.js.map +1 -1
  81. package/dist/lint/routines.d.ts +26 -0
  82. package/dist/lint/routines.js +65 -0
  83. package/dist/lint/routines.js.map +1 -0
  84. package/dist/lint/rules.d.ts +16 -4
  85. package/dist/lint/rules.js +9 -4
  86. package/dist/lint/rules.js.map +1 -1
  87. package/dist/mcp/tools.d.ts +1 -1
  88. package/dist/mcp/tools.js +1 -1
  89. package/dist/mcp/tools.js.map +1 -1
  90. package/dist/migrations/checkpoint.d.ts +45 -0
  91. package/dist/migrations/checkpoint.js +110 -0
  92. package/dist/migrations/checkpoint.js.map +1 -0
  93. package/dist/migrations/create.js +2 -0
  94. package/dist/migrations/create.js.map +1 -1
  95. package/dist/migrations/document.d.ts +10 -0
  96. package/dist/migrations/document.js +1 -0
  97. package/dist/migrations/document.js.map +1 -1
  98. package/dist/migrations/index.d.ts +2 -0
  99. package/dist/migrations/index.js +2 -0
  100. package/dist/migrations/index.js.map +1 -1
  101. package/dist/migrations/routines.d.ts +31 -0
  102. package/dist/migrations/routines.js +55 -0
  103. package/dist/migrations/routines.js.map +1 -0
  104. package/dist/notify/index.d.ts +124 -0
  105. package/dist/notify/index.js +210 -0
  106. package/dist/notify/index.js.map +1 -0
  107. package/dist/notify/verify.d.ts +53 -0
  108. package/dist/notify/verify.js +71 -0
  109. package/dist/notify/verify.js.map +1 -0
  110. package/dist/pr/model.d.ts +3 -0
  111. package/dist/pr/model.js +16 -2
  112. package/dist/pr/model.js.map +1 -1
  113. package/dist/pr/render.js +27 -1
  114. package/dist/pr/render.js.map +1 -1
  115. package/dist/project.d.ts +19 -2
  116. package/dist/project.js +33 -1
  117. package/dist/project.js.map +1 -1
  118. package/dist/replay/replay.d.ts +9 -1
  119. package/dist/replay/replay.js +62 -31
  120. package/dist/replay/replay.js.map +1 -1
  121. package/dist/replay/report.js +1 -1
  122. package/dist/replay/report.js.map +1 -1
  123. package/dist/report/audit.d.ts +138 -0
  124. package/dist/report/audit.js +202 -0
  125. package/dist/report/audit.js.map +1 -0
  126. package/dist/report/jsonl.d.ts +21 -0
  127. package/dist/report/jsonl.js +52 -0
  128. package/dist/report/jsonl.js.map +1 -0
  129. package/dist/report/model.d.ts +68 -0
  130. package/dist/report/model.js +127 -0
  131. package/dist/report/model.js.map +1 -0
  132. package/dist/report/render.d.ts +18 -0
  133. package/dist/report/render.js +127 -0
  134. package/dist/report/render.js.map +1 -0
  135. package/dist/schema-docs/render.d.ts +49 -0
  136. package/dist/schema-docs/render.js +359 -0
  137. package/dist/schema-docs/render.js.map +1 -0
  138. package/dist/schemas.d.ts +5 -2
  139. package/dist/schemas.js +5 -2
  140. package/dist/schemas.js.map +1 -1
  141. package/dist/sources/ddl.d.ts +53 -0
  142. package/dist/sources/ddl.js +347 -0
  143. package/dist/sources/ddl.js.map +1 -0
  144. package/dist/sources/index.d.ts +69 -0
  145. package/dist/sources/index.js +170 -0
  146. package/dist/sources/index.js.map +1 -0
  147. package/dist/steps/postgres-migration.d.ts +35 -25
  148. package/dist/steps/postgres-migration.js +42 -29
  149. package/dist/steps/postgres-migration.js.map +1 -1
  150. package/dist/tenants/index.d.ts +40 -0
  151. package/dist/tenants/index.js +122 -0
  152. package/dist/tenants/index.js.map +1 -0
  153. package/dist/tenants/target.d.ts +23 -0
  154. package/dist/tenants/target.js +29 -0
  155. package/dist/tenants/target.js.map +1 -0
  156. package/dist/versioned/engine.d.ts +6 -0
  157. package/dist/versioned/engine.js +12 -2
  158. package/dist/versioned/engine.js.map +1 -1
  159. package/dist/versioned/index.d.ts +1 -0
  160. package/dist/versioned/index.js +1 -0
  161. package/dist/versioned/index.js.map +1 -1
  162. package/dist/versioned/op.d.ts +6 -1
  163. package/dist/versioned/op.js +7 -2
  164. package/dist/versioned/op.js.map +1 -1
  165. package/dist/versioned/policy.d.ts +33 -2
  166. package/dist/versioned/policy.js +59 -8
  167. package/dist/versioned/policy.js.map +1 -1
  168. package/dist/versioned/report.js +9 -0
  169. package/dist/versioned/report.js.map +1 -1
  170. package/dist/versioned/revert.d.ts +128 -0
  171. package/dist/versioned/revert.js +251 -0
  172. package/dist/versioned/revert.js.map +1 -0
  173. package/dist/versioned/state.d.ts +25 -0
  174. package/dist/versioned/state.js +36 -3
  175. package/dist/versioned/state.js.map +1 -1
  176. package/dist/waves/index.d.ts +179 -0
  177. package/dist/waves/index.js +268 -0
  178. package/dist/waves/index.js.map +1 -0
  179. package/dist/waves/promotion.d.ts +56 -0
  180. package/dist/waves/promotion.js +108 -0
  181. package/dist/waves/promotion.js.map +1 -0
  182. package/dist/waves/record.d.ts +24 -0
  183. package/dist/waves/record.js +20 -0
  184. package/dist/waves/record.js.map +1 -0
  185. package/package.json +11 -7
  186. package/schemas/apply.schema.json +162 -3
  187. package/schemas/checkpoint.schema.json +19 -0
  188. package/schemas/drift.schema.json +26 -1
  189. package/schemas/event.schema.json +36 -0
  190. package/schemas/lint.schema.json +4 -0
  191. package/schemas/plan.schema.json +118 -0
  192. package/schemas/report.schema.json +114 -0
  193. package/schemas/revert.schema.json +69 -0
  194. package/schemas/status.schema.json +64 -3
package/README.md CHANGED
@@ -22,7 +22,7 @@ npx yodel apply dev
22
22
 
23
23
  For Postgres, use `templates/postgres` with `--param name=app-schema --param schema=app`.
24
24
 
25
- Each template gives `chant.config.ts` with a profile per environment (`dev`, `prod`), `yodel.config.ts`, a migrate Op per environment (`yodel apply` behind a gate bound to the plan digest), a `WatchOp` per environment for drift on a schedule, and pipelines for GitHub Actions, GitLab CI and Forgejo Actions: `yodel lint` and the plan comment on pull requests with read-only credentials, `yodel apply` on a push to main with the writer, and the drift watch on its cron with the reader. `scripts/ci.ts` in the project renders the pipelines again after an environment is added (`npm run ci`). The parameters are in each template's `chant.template.json`; the template's README covers the credentials and each forge's setup.
25
+ Each template gives `chant.config.ts` with a profile per environment (`dev`, `prod`), `yodel.config.ts`, a migrate Op per environment (`yodel apply` behind a gate bound to the plan digest), a `WatchOp` per environment for drift on a schedule, and pipelines for GitHub Actions, GitLab CI and Forgejo Actions: `yodel lint` and the plan comment on pull requests with read-only credentials, `yodel apply` on a push to main with the writer, and the drift watch on its cron with the reader. `yodel ci` (`npm run ci`) renders the pipelines from the installed package, so upgrading yodel and rendering again brings its pipeline fixes, and the project's own jobs, declared in a module `yodel.config.ts` `ci.jobs` names, are kept. The parameters are in each template's `chant.template.json`; the template's README covers the credentials and each forge's setup.
26
26
 
27
27
  ## Documentation
28
28
 
@@ -92,8 +92,8 @@ export const op = Op({
92
92
  `yodel lint` checks the migrations directory offline, with no server, so it runs in CI on every pull request. Every rule is free. Exit 0 when there is no error (warnings and silenced findings do not fail), 3 when there is at least one, 1 when lint cannot run; `--json` prints the findings.
93
93
 
94
94
  - Integrity, always errors: `fork` (two migrations follow the same parent; `yodel rebase`), `directory` (a bad name or file, a missing parent, a cycle, another dialect), `checksum` (a migration's files changed since it was written), `unreadable-object` (an object in a recorded schema, or a step, the sql lexicon cannot read or classify; never skipped).
95
- - From the classifier's classes: `destructive` (a drop), `pg-rewrite` (a rewrite or full scan under ACCESS EXCLUSIVE), `pg-lock` (writes blocked for a whole index build or scan: no `CONCURRENTLY`, a constraint validated in place), `ch-mutation` (a mutation rewriting parts), `ch-rebuild` (a `ClickHouseRebuildOp` step).
96
- - `refused-step` (a step `yodel apply` will not run), `migration-sql` (migration.sql does not hold a statement migration.json records), `silence` (a bad or unused `yodel:allow` line).
95
+ - From the classifier's classes: `destructive` (a drop), `pg-rewrite` (a rewrite or full scan under ACCESS EXCLUSIVE), `pg-lock` (writes blocked for a whole index build or scan: no `CONCURRENTLY`, a constraint validated in place), `pg-signature` (a function or procedure dropped and created with another signature), `ch-mutation` (a mutation rewriting parts), `ch-rebuild` (a `ClickHouseRebuildOp` step).
96
+ - `refused-step` (a step `yodel apply` will not run), `pg-routine-in-use` (a function dropped while a declared trigger still executes it), `migration-sql` (migration.sql does not hold a statement migration.json records), `silence` (a bad or unused `yodel:allow` line).
97
97
 
98
98
  A line above a statement in its `migration.sql` silences one of the classifier rules for that statement:
99
99
 
@@ -7,6 +7,7 @@ import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import { YodelError } from "../project.js";
9
9
  import { runChant } from "./process.js";
10
+ import { checkOwnership, duplicateExport, syncSources } from "../sources/index.js";
10
11
  /**
11
12
  * Builds the project's declared schema. Writes to `out` when given, else to
12
13
  * a temporary directory that {@link Build.dispose} removes. A build that
@@ -20,12 +21,17 @@ export async function buildSchema(project, options = {}) {
20
21
  rmSync(tmp, { recursive: true, force: true });
21
22
  };
22
23
  try {
24
+ // The DDL sources (#67) first: their declarations are part of the build.
25
+ const synced = await syncSources(project, options.env ? { env: options.env } : {});
23
26
  const result = await runChant(["build", project.sourceDir, "--lexicon", "sql", "-o", path], {
24
27
  cwd: project.dir,
25
28
  ...(options.env ? { env: options.env } : {}),
26
29
  });
27
30
  if (result.code !== 0) {
28
31
  const detail = (result.stderr || result.stdout).trim();
32
+ const duplicate = duplicateExport(detail, synced);
33
+ if (duplicate)
34
+ throw duplicate;
29
35
  throw new YodelError(`chant build failed in ${project.dir}${detail ? `:\n${detail}` : ""}`);
30
36
  }
31
37
  let output;
@@ -38,6 +44,7 @@ export async function buildSchema(project, options = {}) {
38
44
  if (output.dialect !== project.dialect) {
39
45
  throw new YodelError(`the build holds ${output.dialect} objects, but the project's dialect is ${project.dialect}`);
40
46
  }
47
+ await checkOwnership(output, synced);
41
48
  return { path, output, dispose };
42
49
  }
43
50
  catch (e) {
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/chant/build.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAiC,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAyBxC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAgB,EAAE,UAAqD,EAAE;IACzG,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC;IAClF,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,IAAI,GAAG,EAAE,aAAa,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,GAAG;YAAE,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC;IACF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE;YAC1F,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC7C,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACtB,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YACvD,MAAM,IAAI,UAAU,CAAC,yBAAyB,OAAO,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9F,CAAC;QACD,IAAI,MAAmB,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAgB,CAAC;QACjE,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,IAAI,UAAU,CAAC,2CAA2C,IAAI,KAAK,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACzH,CAAC;QACD,IAAI,MAAM,CAAC,OAAO,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC;YACvC,MAAM,IAAI,UAAU,CAAC,mBAAmB,MAAM,CAAC,OAAO,0CAA0C,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QACrH,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IACnC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,CAAC;QACV,MAAM,CAAC,CAAC;IACV,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/chant/build.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,289 @@
1
+ /**
2
+ * `yodel ci` (#65): a project's CI for GitHub Actions, GitLab CI and Forgejo
3
+ * Actions, rendered by the installed package, so a fix to the pipelines
4
+ * reaches a project when it upgrades yodel and renders again. Until #65 the
5
+ * templates carried this as scripts/ci.ts.
6
+ *
7
+ * yodel ci write the files
8
+ * yodel ci --check exit 1 when a file differs from what this renders
9
+ * yodel ci --replay the lint job's replay check on this machine (./replay.ts)
10
+ *
11
+ * yodel.config.ts `ci` picks the forges (default all three) and names the
12
+ * module that declares the project's own jobs, which are merged after
13
+ * yodel's into the pull request pipeline and kept on every render
14
+ * ({@link ProjectJobs}).
15
+ *
16
+ * Environments are the keys of `sql.profiles` in chant.config.ts, in order,
17
+ * but `replay`, which is the lint job's throwaway server.
18
+ * Each one needs ops/migrate-<env>.op.ts and ops/watch-<env>.op.ts.
19
+ * The apply pipeline's waves are yodel.config.ts `waves`, in order; without
20
+ * it, one wave per environment in profile order, each with gate `always`.
21
+ *
22
+ * Three pipelines per forge, each declared with chant: yodel-pr with the
23
+ * github lexicon's Workflow, Job and Step (Forgejo renders the same
24
+ * declaration through the forgejo lexicon's dialect) and the gitlab
25
+ * lexicon's Workflow and Job; yodel-apply with chant's
26
+ * generateOpWavesPipeline, from the waves spec this writes
27
+ * (yodel-waves.json); the watch with generateOpsPipeline.
28
+ *
29
+ * - yodel-pr: on a pull (merge) request, `yodel ci --check` (these files are
30
+ * what yodel ci renders), `yodel lint`, and the replay check into a
31
+ * throwaway server the job starts (the `replay` profile), then for each
32
+ * environment `yodel plan <env> --comment`, which posts the pending
33
+ * migrations and the digest to approve. One job per environment, holding
34
+ * that environment's reader credentials and nothing else.
35
+ * - yodel-apply: on a push to main, one job per wave, each needing the one
36
+ * before, running `chant run wave`: it plans the environment (`yodel apply
37
+ * <env> --wave-plan`), reads the wave's gate policy from yodel-waves.json
38
+ * as the commit before the push has it, waits for an approval when that
39
+ * policy asks for one (exit 3, and the waves after it do not run), and
40
+ * applies (`yodel apply <env> --wave-apply`). The only jobs that hold a
41
+ * writer's credentials, one environment's each, with
42
+ * YODEL_CREDENTIALS=writer. Two merges in quick succession do not fail the
43
+ * second apply: it waits for the first's lock (YODEL_LOCK_WAIT), and an
44
+ * apply whose commit is no longer main's tip stands down
45
+ * (YODEL_STAND_DOWN), since the newer commit's waves apply the same
46
+ * environments.
47
+ * - watch-<env>: the environment's WatchOp on its own schedule, rendered by
48
+ * chant's generateOpsPipeline, with the reader's credentials. After the
49
+ * Op, `yodel drift <env> --issue` compares the server with the schema the
50
+ * newest applied migration records (so a migration waiting at its gate is
51
+ * not drift), keeps one tracking issue for the environment (opened on
52
+ * drift, updated while it changes, closed when it is gone), and fails the
53
+ * job on drift. The issue is written with the job's token on GitHub
54
+ * (issues: write) and Forgejo, and with GITLAB_TOKEN on GitLab.
55
+ *
56
+ * The scheduled watch never pushes. Its first phase (chant lifecycle
57
+ * snapshot) commits a snapshot to chant/lifecycle and tries to push it; the
58
+ * drift it reports comes from `chant lifecycle diff --live`, declared against
59
+ * live, which needs no snapshot on the remote. So the job sets a push URL
60
+ * that cannot be pushed to: the push fails, chant carries on, and the job
61
+ * works the same with a read-only token (GitHub `contents: read`, GitLab's
62
+ * CI_JOB_TOKEN) as with Forgejo's token, which can write. A push that could
63
+ * land would race the approvals people push to chant/lifecycle.
64
+ *
65
+ * Every job installs @intentius/sql-yodeler from npm with `npm ci`, which
66
+ * needs no token, in IMAGE. With yodel.config.ts `ci.image` (#66), the CI
67
+ * image ghcr.io/intentius/sql-yodeler pinned by its digest, every job runs
68
+ * in that image instead, which carries node, yodel, chant and the lexicons
69
+ * on PATH, and no job runs `npm ci` ({@link Toolchain}).
70
+ */
71
+ import type { Declarable } from "@intentius/chant/declarable";
72
+ export type Forge = "github" | "gitlab" | "forgejo";
73
+ export declare const FORGES: readonly Forge[];
74
+ export interface RenderedFile {
75
+ /** Relative to the project. */
76
+ path: string;
77
+ content: string;
78
+ }
79
+ /** The image every job runs in, where the forge runs jobs in a container. It needs git, so not -slim. */
80
+ export declare const IMAGE = "node:22-bookworm";
81
+ /** The CI image (#66): yodel, chant and the sql lexicon, published for each release. */
82
+ export declare const YODEL_IMAGE = "ghcr.io/intentius/sql-yodeler";
83
+ /**
84
+ * What the jobs run in. By default IMAGE, and each job installs the
85
+ * project's packages with `npm ci` and puts node_modules/.bin on PATH. With
86
+ * `ci.image` the yodel image (pinned by digest): its yodel, chant and tsx
87
+ * are on PATH, and a checkout resolves chant and the lexicons from the
88
+ * image's /node_modules, so nothing is installed.
89
+ */
90
+ export interface Toolchain {
91
+ image: string;
92
+ /** The yodel image: no `npm ci`, and every job in the container. */
93
+ yodelImage: boolean;
94
+ }
95
+ export declare const toolchainOf: (envs: Pick<Environments, "image">) => Toolchain;
96
+ export declare const MAIN = "main";
97
+ /** DEV for dev: the prefix of an environment's variables. */
98
+ export declare const prefixOf: (env: string) => string;
99
+ export type Dialect = "clickhouse" | "postgres";
100
+ /** The environments to render for: chant.config.ts's sql.dialect and its profiles, in order. */
101
+ export interface Environments {
102
+ dialect: Dialect;
103
+ names: readonly string[];
104
+ /** The forges to render for: yodel.config.ts `ci.forges`, default every one. */
105
+ forges?: readonly Forge[];
106
+ /** The project's own jobs (yodel.config.ts `ci.jobs`), merged after yodel's. */
107
+ jobs?: ProjectJobs;
108
+ /** yodel.config.ts `ci.image`: the yodel image every job runs in, by digest. */
109
+ image?: string;
110
+ /** The apply pipeline's waves, in order. */
111
+ waves: readonly Wave[];
112
+ /** chant.config.ts declares the `replay` profile, so the lint job runs the replay check. */
113
+ replay: boolean;
114
+ }
115
+ /** DEV_CLICKHOUSE for dev on ClickHouse: what an environment's variables start with. */
116
+ export declare const variablePrefix: (env: string, dialect: Dialect) => string;
117
+ /** The variables a job reading `env` holds: the URL and the reader. */
118
+ export declare const readerVariables: (env: string, dialect: Dialect) => string[];
119
+ /** The variables the apply job for `env` holds: the URL and the writer. */
120
+ export declare const writerVariables: (env: string, dialect: Dialect) => string[];
121
+ /**
122
+ * The approvals: `chant approve` records them on the chant/lifecycle branch,
123
+ * and approvers push it. A CI checkout fetches one ref, so fetch this one too.
124
+ * The remote-tracking ref is what chant's push lease reads.
125
+ */
126
+ export declare const FETCH_APPROVALS = "git fetch --no-tags origin '+refs/heads/chant/lifecycle:refs/remotes/origin/chant/lifecycle' && git branch -f chant/lifecycle origin/chant/lifecycle || echo 'no chant/lifecycle branch yet, so nothing is approved'";
127
+ /** The apply jobs' lock settings: wait for another apply's lock, and stand down for a newer commit on main. */
128
+ export declare const APPLY_LOCK: Record<string, string>;
129
+ /** The watch reads; it never pushes. See the comment at the top. */
130
+ export declare const NO_PUSH = "git remote set-url --push origin no-push://the-drift-watch-only-reads";
131
+ /** After the watch's `chant run`: drift against the history, the environment's tracking issue, and exit 2 on drift. */
132
+ export declare const watchDrift: (env: string) => string;
133
+ /**
134
+ * The replay check: `yodel lint --replay replay --reset` replays every
135
+ * migration, in order, into a fresh database on the `replay` profile's
136
+ * server and fails when one does not give the schema it recorded. That
137
+ * server is a throwaway container the lint job starts as a service, the
138
+ * image chant's emulator runs, which the job (in IMAGE, on every forge)
139
+ * reaches by the service's name; never a server whose data matters, since
140
+ * --reset drops what the history names there first.
141
+ */
142
+ export declare const REPLAY_PROFILE = "replay";
143
+ interface ReplayService {
144
+ /**
145
+ * The service's name, and its host where the job runs in a container.
146
+ * Prefixed, so it cannot meet another container's alias on a runner's
147
+ * shared network: a host that answers for two servers would split the replay.
148
+ */
149
+ name: string;
150
+ image: string;
151
+ port: number;
152
+ /** The service container's environment. */
153
+ env: Record<string, string>;
154
+ health: string;
155
+ /** The server's URL at `host`, on `port` (default: the service's own). */
156
+ url: (host: string, port?: number) => string;
157
+ /** What the lint job sets besides the URL: the throwaway server's credentials. */
158
+ jobEnv: Record<string, string>;
159
+ }
160
+ export declare const REPLAY_SERVICE: Record<Dialect, ReplayService>;
161
+ /** Waits up to 90 s for the replay server's port; a shell-free one-liner, so every forge runs it the same. */
162
+ export declare const waitFor: (host: string, port: number) => string;
163
+ export declare const LINT = "yodel lint";
164
+ export declare const REPLAY = "yodel lint --replay replay --reset";
165
+ /** Fails a pull request whose pipelines differ from what yodel ci renders. */
166
+ export declare const CI_CHECK = "yodel ci --check";
167
+ /**
168
+ * Webhook events (yodel.config.ts `notify`): the variables the apply, plan
169
+ * and watch jobs pass from the forge's secrets. yodel sends nothing while
170
+ * they are unset (empty), so an unconfigured repository needs neither.
171
+ * GitLab jobs have the project's variables already.
172
+ */
173
+ export declare const WEBHOOK_VARIABLES: string[];
174
+ /** Where the plan and apply jobs write `yodel report`, kept as the job's artifact. */
175
+ export declare const REPORT_DIR = "yodel-report";
176
+ /**
177
+ * `yodel report <env>` as JSON and as the HTML run view, into REPORT_DIR.
178
+ * It runs whatever the job's outcome, and a gap it reports (exit 4) does not
179
+ * fail the job: the report is for reading. `yodel` is the project's, named
180
+ * by path, since GitLab's after_script does not keep before_script's PATH;
181
+ * in the yodel image, the image's, which is on PATH.
182
+ */
183
+ export declare const reportCommand: (env: string, tc?: Toolchain) => string;
184
+ /** The artifact a job keeps its report in. */
185
+ export declare const reportArtifact: (job: string) => string;
186
+ type Actions = "github" | "forgejo";
187
+ export declare function actionsReview(forge: Actions, envs_: Environments): RenderedFile;
188
+ export declare const GITLAB_WATCH_FILE = ".gitlab/yodel-watch.gitlab-ci.yml";
189
+ export declare const GITLAB_APPLY_FILE = ".gitlab/yodel-apply.gitlab-ci.yml";
190
+ export declare function gitlabPipeline(envs_: Environments): RenderedFile;
191
+ /** When a wave waits for an approval; yodel.config.ts `waves[].gate`. */
192
+ export type WaveGate = "always" | "on-destructive" | "never";
193
+ export interface Wave {
194
+ env: string;
195
+ gate: WaveGate;
196
+ /**
197
+ * The environment whose history must record a migration applied before
198
+ * this one applies it (yodel.config.ts `waves[].requires`, default the
199
+ * wave before): this wave's jobs, and its plan job, read it with its reader.
200
+ */
201
+ requires?: string;
202
+ /**
203
+ * A tenant set's wave (#56): a run per tenant, `<env>/<tenant>`, in place
204
+ * of the environment's one, all behind the wave's one gate.
205
+ */
206
+ targets?: readonly string[];
207
+ /** Split the wave into a deciding job and this many share jobs (yodel.config.ts `waves[].shares`). */
208
+ shares?: number;
209
+ }
210
+ /** The waves spec: committed, and read by every wave job (its gate policy at the commit before the push). */
211
+ export declare const WAVES_SPEC = "yodel-waves.json";
212
+ export declare const APPLY_PIPELINE = "yodel-apply";
213
+ interface WavesSpec {
214
+ name: string;
215
+ op: string;
216
+ plan: string[];
217
+ apply: string[];
218
+ branches: string[];
219
+ waves: Array<{
220
+ name: string;
221
+ runs: Array<{
222
+ target: string;
223
+ op: string;
224
+ }>;
225
+ gate: WaveGate;
226
+ shares?: number;
227
+ environment?: {
228
+ name: string;
229
+ };
230
+ variables?: Record<string, string>;
231
+ }>;
232
+ }
233
+ /**
234
+ * The waves spec. What the file holds is the same for every forge; each
235
+ * forge's jobs get their own variables on top (`variables`).
236
+ */
237
+ export declare function wavesSpec(waves: readonly Wave[], variables?: (env: string) => Record<string, string>): WavesSpec;
238
+ /** The waves spec file and the apply pipeline on each forge. */
239
+ export declare function applyPipelines(dir: string, envs: Environments): Promise<RenderedFile[]>;
240
+ /** yodel.config.ts `waves`, checked against the environments; one wave per environment, gate `always`, without it. */
241
+ export declare function readWaves(dir: string, envs: readonly string[]): Promise<Wave[]>;
242
+ /**
243
+ * Each wave of an environment with a tenant set (#56), with a run per
244
+ * tenant. A set given by a query is read from the server now, with the
245
+ * environment's profile: run yodel ci again when the tenants change.
246
+ */
247
+ export declare function withTenants(dir: string, waves: readonly Wave[]): Promise<Wave[]>;
248
+ /**
249
+ * The watch's spec for one environment: the Op, and on GitHub and Forgejo the
250
+ * reader's secrets. A GitLab job already has the project's variables (a
251
+ * writer's only under its environment, which the watch does not declare), and
252
+ * `NAME: $NAME` there would refer to itself.
253
+ */
254
+ export declare function watchSpec(env: string, dialect: Dialect, forge: Forge): {
255
+ name: string;
256
+ variables?: Record<string, string>;
257
+ permissions?: Record<string, string>;
258
+ };
259
+ export declare function watchPipelines(dir: string, envs_: Environments): Promise<RenderedFile[]>;
260
+ /** The dialect and the environments chant.config.ts declares, in order. */
261
+ export declare function environments(dir: string): Promise<Environments>;
262
+ /**
263
+ * The project's own jobs (#65): the module yodel.config.ts `ci.jobs` names,
264
+ * relative to the project, declaring jobs with chant's lexicons. `actions`
265
+ * (the github lexicon's Job) goes into yodel-pr on GitHub and Forgejo, after
266
+ * yodel's jobs; `gitlab` (the gitlab lexicon's Job) into .gitlab-ci.yml,
267
+ * after yodel's, its stages after `review`. They are declared here, not
268
+ * edited into the rendered files, so every `yodel ci` keeps them:
269
+ *
270
+ * ```ts
271
+ * // ci/jobs.ts
272
+ * import { Job, Step } from "@intentius/chant-lexicon-github";
273
+ * import { Job as GitLabJob, Image } from "@intentius/chant-lexicon-gitlab";
274
+ * export const actions = { test: new Job({ "runs-on": "ubuntu-latest", steps: [new Step({ uses: "actions/checkout@v7" }), new Step({ run: "npm test" })] }) };
275
+ * export const gitlab = { test: new GitLabJob({ stage: "test", image: new Image({ name: "node:22" }), script: ["npm test"] }) };
276
+ * ```
277
+ */
278
+ export interface ProjectJobs {
279
+ actions?: Record<string, Declarable>;
280
+ gitlab?: Record<string, Declarable>;
281
+ }
282
+ /** Loads the module `ci.jobs` names. */
283
+ export declare function loadProjectJobs(dir: string, file: string): Promise<ProjectJobs>;
284
+ /** The pipelines that need no Op discovery: pull requests, and GitLab's main file. */
285
+ export declare function review(envs: Environments): RenderedFile[];
286
+ /** The forge a rendered file is for; undefined for the waves spec, which every forge's apply reads. */
287
+ export declare function forgeOf(path: string): Forge | undefined;
288
+ export declare function render(dir: string): Promise<RenderedFile[]>;
289
+ export {};