@intentius/sql-yodeler 0.0.0-stage → 0.2.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.
- package/LICENSE +202 -0
- package/README.md +112 -2
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +29 -0
- package/dist/bin.js.map +1 -0
- package/dist/chant/apply.d.ts +59 -0
- package/dist/chant/apply.js +116 -0
- package/dist/chant/apply.js.map +1 -0
- package/dist/chant/build.d.ts +34 -0
- package/dist/chant/build.js +48 -0
- package/dist/chant/build.js.map +1 -0
- package/dist/chant/drift.d.ts +112 -0
- package/dist/chant/drift.js +238 -0
- package/dist/chant/drift.js.map +1 -0
- package/dist/chant/plan.d.ts +45 -0
- package/dist/chant/plan.js +61 -0
- package/dist/chant/plan.js.map +1 -0
- package/dist/chant/process.d.ts +41 -0
- package/dist/chant/process.js +87 -0
- package/dist/chant/process.js.map +1 -0
- package/dist/chant/runnable.d.ts +19 -0
- package/dist/chant/runnable.js +47 -0
- package/dist/chant/runnable.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +99 -0
- package/dist/cli.js.map +1 -0
- package/dist/command.d.ts +23 -0
- package/dist/command.js +13 -0
- package/dist/command.js.map +1 -0
- package/dist/commands/apply.d.ts +3 -0
- package/dist/commands/apply.js +426 -0
- package/dist/commands/apply.js.map +1 -0
- package/dist/commands/approve.d.ts +5 -0
- package/dist/commands/approve.js +111 -0
- package/dist/commands/approve.js.map +1 -0
- package/dist/commands/drift.d.ts +14 -0
- package/dist/commands/drift.js +267 -0
- package/dist/commands/drift.js.map +1 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.js +28 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +139 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +3 -0
- package/dist/commands/lint.js +202 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.js +50 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/new.d.ts +5 -0
- package/dist/commands/new.js +292 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/override.d.ts +3 -0
- package/dist/commands/override.js +118 -0
- package/dist/commands/override.js.map +1 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.js +183 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/rebase.d.ts +3 -0
- package/dist/commands/rebase.js +167 -0
- package/dist/commands/rebase.js.map +1 -0
- package/dist/commands/repair.d.ts +14 -0
- package/dist/commands/repair.js +125 -0
- package/dist/commands/repair.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +54 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/config/define.d.ts +148 -0
- package/dist/config/define.js +20 -0
- package/dist/config/define.js.map +1 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +12 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +162 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/settings.d.ts +53 -0
- package/dist/config/settings.js +116 -0
- package/dist/config/settings.js.map +1 -0
- package/dist/env-args.d.ts +24 -0
- package/dist/env-args.js +92 -0
- package/dist/env-args.js.map +1 -0
- package/dist/exit.d.ts +20 -0
- package/dist/exit.js +28 -0
- package/dist/exit.js.map +1 -0
- package/dist/history/client.d.ts +25 -0
- package/dist/history/client.js +42 -0
- package/dist/history/client.js.map +1 -0
- package/dist/history/ddl.d.ts +58 -0
- package/dist/history/ddl.js +100 -0
- package/dist/history/ddl.js.map +1 -0
- package/dist/history/index.d.ts +25 -0
- package/dist/history/index.js +26 -0
- package/dist/history/index.js.map +1 -0
- package/dist/history/lock.d.ts +104 -0
- package/dist/history/lock.js +230 -0
- package/dist/history/lock.js.map +1 -0
- package/dist/history/postgres/ddl.d.ts +22 -0
- package/dist/history/postgres/ddl.js +47 -0
- package/dist/history/postgres/ddl.js.map +1 -0
- package/dist/history/postgres/index.d.ts +12 -0
- package/dist/history/postgres/index.js +13 -0
- package/dist/history/postgres/index.js.map +1 -0
- package/dist/history/postgres/lock.d.ts +48 -0
- package/dist/history/postgres/lock.js +95 -0
- package/dist/history/postgres/lock.js.map +1 -0
- package/dist/history/postgres/store.d.ts +39 -0
- package/dist/history/postgres/store.js +68 -0
- package/dist/history/postgres/store.js.map +1 -0
- package/dist/history/rows.d.ts +96 -0
- package/dist/history/rows.js +73 -0
- package/dist/history/rows.js.map +1 -0
- package/dist/history/store.d.ts +72 -0
- package/dist/history/store.js +134 -0
- package/dist/history/store.js.map +1 -0
- package/dist/history/topology.d.ts +41 -0
- package/dist/history/topology.js +54 -0
- package/dist/history/topology.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/init/adopt.d.ts +70 -0
- package/dist/init/adopt.js +143 -0
- package/dist/init/adopt.js.map +1 -0
- package/dist/init/baseline.d.ts +61 -0
- package/dist/init/baseline.js +88 -0
- package/dist/init/baseline.js.map +1 -0
- package/dist/init/files.d.ts +23 -0
- package/dist/init/files.js +77 -0
- package/dist/init/files.js.map +1 -0
- package/dist/init/index.d.ts +16 -0
- package/dist/init/index.js +17 -0
- package/dist/init/index.js.map +1 -0
- package/dist/init/live.d.ts +39 -0
- package/dist/init/live.js +68 -0
- package/dist/init/live.js.map +1 -0
- package/dist/lint/check.d.ts +88 -0
- package/dist/lint/check.js +227 -0
- package/dist/lint/check.js.map +1 -0
- package/dist/lint/config.d.ts +21 -0
- package/dist/lint/config.js +43 -0
- package/dist/lint/config.js.map +1 -0
- package/dist/lint/index.d.ts +31 -0
- package/dist/lint/index.js +43 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/lint/project-rules.d.ts +19 -0
- package/dist/lint/project-rules.js +32 -0
- package/dist/lint/project-rules.js.map +1 -0
- package/dist/lint/report.d.ts +7 -0
- package/dist/lint/report.js +48 -0
- package/dist/lint/report.js.map +1 -0
- package/dist/lint/rules.d.ts +53 -0
- package/dist/lint/rules.js +42 -0
- package/dist/lint/rules.js.map +1 -0
- package/dist/lint/silences.d.ts +49 -0
- package/dist/lint/silences.js +91 -0
- package/dist/lint/silences.js.map +1 -0
- package/dist/lint/statements.d.ts +38 -0
- package/dist/lint/statements.js +120 -0
- package/dist/lint/statements.js.map +1 -0
- package/dist/mcp/server.d.ts +40 -0
- package/dist/mcp/server.js +106 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +72 -0
- package/dist/mcp/tools.js +178 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/migrations/checks.d.ts +28 -0
- package/dist/migrations/checks.js +97 -0
- package/dist/migrations/checks.js.map +1 -0
- package/dist/migrations/checksum.d.ts +56 -0
- package/dist/migrations/checksum.js +72 -0
- package/dist/migrations/checksum.js.map +1 -0
- package/dist/migrations/create.d.ts +75 -0
- package/dist/migrations/create.js +120 -0
- package/dist/migrations/create.js.map +1 -0
- package/dist/migrations/directory.d.ts +119 -0
- package/dist/migrations/directory.js +229 -0
- package/dist/migrations/directory.js.map +1 -0
- package/dist/migrations/document.d.ts +106 -0
- package/dist/migrations/document.js +83 -0
- package/dist/migrations/document.js.map +1 -0
- package/dist/migrations/index.d.ts +21 -0
- package/dist/migrations/index.js +22 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/pr/comment.d.ts +32 -0
- package/dist/pr/comment.js +99 -0
- package/dist/pr/comment.js.map +1 -0
- package/dist/pr/forge.d.ts +54 -0
- package/dist/pr/forge.js +119 -0
- package/dist/pr/forge.js.map +1 -0
- package/dist/pr/issue.d.ts +68 -0
- package/dist/pr/issue.js +107 -0
- package/dist/pr/issue.js.map +1 -0
- package/dist/pr/model.d.ts +99 -0
- package/dist/pr/model.js +137 -0
- package/dist/pr/model.js.map +1 -0
- package/dist/pr/render.d.ts +29 -0
- package/dist/pr/render.js +279 -0
- package/dist/pr/render.js.map +1 -0
- package/dist/project.d.ts +62 -0
- package/dist/project.js +92 -0
- package/dist/project.js.map +1 -0
- package/dist/rebase/index.d.ts +121 -0
- package/dist/rebase/index.js +390 -0
- package/dist/rebase/index.js.map +1 -0
- package/dist/rebase/repair.d.ts +41 -0
- package/dist/rebase/repair.js +68 -0
- package/dist/rebase/repair.js.map +1 -0
- package/dist/replay/clickhouse.d.ts +33 -0
- package/dist/replay/clickhouse.js +106 -0
- package/dist/replay/clickhouse.js.map +1 -0
- package/dist/replay/index.d.ts +39 -0
- package/dist/replay/index.js +57 -0
- package/dist/replay/index.js.map +1 -0
- package/dist/replay/postgres.d.ts +31 -0
- package/dist/replay/postgres.js +140 -0
- package/dist/replay/postgres.js.map +1 -0
- package/dist/replay/replay.d.ts +114 -0
- package/dist/replay/replay.js +98 -0
- package/dist/replay/replay.js.map +1 -0
- package/dist/replay/report.d.ts +18 -0
- package/dist/replay/report.js +72 -0
- package/dist/replay/report.js.map +1 -0
- package/dist/replay/shared.d.ts +28 -0
- package/dist/replay/shared.js +77 -0
- package/dist/replay/shared.js.map +1 -0
- package/dist/schemas.d.ts +34 -0
- package/dist/schemas.js +38 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts +40 -0
- package/dist/server.js +87 -0
- package/dist/server.js.map +1 -0
- package/dist/steps/backfill.d.ts +132 -0
- package/dist/steps/backfill.js +307 -0
- package/dist/steps/backfill.js.map +1 -0
- package/dist/steps/index.d.ts +52 -0
- package/dist/steps/index.js +52 -0
- package/dist/steps/index.js.map +1 -0
- package/dist/steps/kinds.d.ts +101 -0
- package/dist/steps/kinds.js +143 -0
- package/dist/steps/kinds.js.map +1 -0
- package/dist/steps/postgres-migration.d.ts +95 -0
- package/dist/steps/postgres-migration.js +149 -0
- package/dist/steps/postgres-migration.js.map +1 -0
- package/dist/steps/rebuild.d.ts +97 -0
- package/dist/steps/rebuild.js +155 -0
- package/dist/steps/rebuild.js.map +1 -0
- package/dist/steps/run.d.ts +45 -0
- package/dist/steps/run.js +72 -0
- package/dist/steps/run.js.map +1 -0
- package/dist/topology/render.d.ts +17 -0
- package/dist/topology/render.js +17 -0
- package/dist/topology/render.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/dist/versioned/approval.d.ts +35 -0
- package/dist/versioned/approval.js +53 -0
- package/dist/versioned/approval.js.map +1 -0
- package/dist/versioned/checks.d.ts +29 -0
- package/dist/versioned/checks.js +54 -0
- package/dist/versioned/checks.js.map +1 -0
- package/dist/versioned/clickhouse.d.ts +24 -0
- package/dist/versioned/clickhouse.js +42 -0
- package/dist/versioned/clickhouse.js.map +1 -0
- package/dist/versioned/digest.d.ts +77 -0
- package/dist/versioned/digest.js +69 -0
- package/dist/versioned/digest.js.map +1 -0
- package/dist/versioned/driver.d.ts +51 -0
- package/dist/versioned/driver.js +13 -0
- package/dist/versioned/driver.js.map +1 -0
- package/dist/versioned/engine.d.ts +206 -0
- package/dist/versioned/engine.js +370 -0
- package/dist/versioned/engine.js.map +1 -0
- package/dist/versioned/explain.d.ts +123 -0
- package/dist/versioned/explain.js +294 -0
- package/dist/versioned/explain.js.map +1 -0
- package/dist/versioned/index.d.ts +46 -0
- package/dist/versioned/index.js +77 -0
- package/dist/versioned/index.js.map +1 -0
- package/dist/versioned/op.d.ts +72 -0
- package/dist/versioned/op.js +185 -0
- package/dist/versioned/op.js.map +1 -0
- package/dist/versioned/policy.d.ts +67 -0
- package/dist/versioned/policy.js +161 -0
- package/dist/versioned/policy.js.map +1 -0
- package/dist/versioned/postgres/catalog.d.ts +33 -0
- package/dist/versioned/postgres/catalog.js +34 -0
- package/dist/versioned/postgres/catalog.js.map +1 -0
- package/dist/versioned/postgres/driver.d.ts +71 -0
- package/dist/versioned/postgres/driver.js +140 -0
- package/dist/versioned/postgres/driver.js.map +1 -0
- package/dist/versioned/postgres/executor.d.ts +104 -0
- package/dist/versioned/postgres/executor.js +172 -0
- package/dist/versioned/postgres/executor.js.map +1 -0
- package/dist/versioned/postgres/index.d.ts +11 -0
- package/dist/versioned/postgres/index.js +12 -0
- package/dist/versioned/postgres/index.js.map +1 -0
- package/dist/versioned/postgres/waits.d.ts +46 -0
- package/dist/versioned/postgres/waits.js +78 -0
- package/dist/versioned/postgres/waits.js.map +1 -0
- package/dist/versioned/report.d.ts +25 -0
- package/dist/versioned/report.js +84 -0
- package/dist/versioned/report.js.map +1 -0
- package/dist/versioned/stand-down.d.ts +30 -0
- package/dist/versioned/stand-down.js +56 -0
- package/dist/versioned/stand-down.js.map +1 -0
- package/dist/versioned/state.d.ts +108 -0
- package/dist/versioned/state.js +153 -0
- package/dist/versioned/state.js.map +1 -0
- package/package.json +61 -4
- package/schemas/apply.schema.json +269 -0
- package/schemas/common.schema.json +213 -0
- package/schemas/drift.schema.json +36 -0
- package/schemas/init.schema.json +31 -0
- package/schemas/lint-rules.schema.json +54 -0
- package/schemas/lint.schema.json +290 -0
- package/schemas/mcp.schema.json +70 -0
- package/schemas/new.schema.json +71 -0
- package/schemas/plan.schema.json +371 -0
- package/schemas/rebase.schema.json +54 -0
- package/schemas/repair.schema.json +20 -0
- package/schemas/status.schema.json +310 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands yodel prints for a person to run (#14): `chant approve ...`,
|
|
3
|
+
* `yodel apply ...`. In a project that installs chant and yodel locally,
|
|
4
|
+
* neither is on PATH, only in node_modules/.bin, so a printed `chant approve`
|
|
5
|
+
* fails with "command not found". Such a command is printed as `npx chant
|
|
6
|
+
* ...` instead, which runs the project's own copy. When the tool is on PATH,
|
|
7
|
+
* or the project does not install it (where `npx` would fetch an unrelated
|
|
8
|
+
* package of that name), the command is printed as it is.
|
|
9
|
+
*/
|
|
10
|
+
import { accessSync, constants } from "node:fs";
|
|
11
|
+
import { delimiter, dirname, join } from "node:path";
|
|
12
|
+
const executable = (path) => {
|
|
13
|
+
try {
|
|
14
|
+
accessSync(path, constants.X_OK);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Whether `name` is an executable on `PATH`, not counting `node_modules/.bin`
|
|
23
|
+
* directories: `npx yodel` puts the project's on yodel's PATH, but the
|
|
24
|
+
* person's shell does not have it.
|
|
25
|
+
*/
|
|
26
|
+
export function onPath(name, env = process.env) {
|
|
27
|
+
return (env.PATH ?? "")
|
|
28
|
+
.split(delimiter)
|
|
29
|
+
.some((d) => d !== "" && !/[\\/]node_modules[\\/]\.bin[\\/]?$/.test(d) && executable(join(d, name)));
|
|
30
|
+
}
|
|
31
|
+
/** Whether `dir` or a directory above it has `node_modules/.bin/<name>`. */
|
|
32
|
+
export function installedLocally(name, dir) {
|
|
33
|
+
for (let d = dir;; d = dirname(d)) {
|
|
34
|
+
if (executable(join(d, "node_modules", ".bin", name)))
|
|
35
|
+
return true;
|
|
36
|
+
if (dirname(d) === d)
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** `command` as a person can run it in `projectDir`: prefixed with `npx ` when its tool is installed there but not on PATH. */
|
|
41
|
+
export function runnable(command, projectDir, env = process.env) {
|
|
42
|
+
const tool = command.split(" ", 1)[0] ?? "";
|
|
43
|
+
if (tool === "" || tool === "npx" || onPath(tool, env) || !installedLocally(tool, projectDir))
|
|
44
|
+
return command;
|
|
45
|
+
return `npx ${command}`;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=runnable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runnable.js","sourceRoot":"","sources":["../../src/chant/runnable.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAErD,MAAM,UAAU,GAAG,CAAC,IAAY,EAAW,EAAE;IAC3C,IAAI,CAAC;QACH,UAAU,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QACjC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,MAAyB,OAAO,CAAC,GAAG;IACvE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;SACpB,KAAK,CAAC,SAAS,CAAC;SAChB,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,oCAAoC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AACzG,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,GAAW;IACxD,KAAK,IAAI,CAAC,GAAG,GAAG,GAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QACnC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QACnE,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;IACrC,CAAC;AACH,CAAC;AAED,+HAA+H;AAC/H,MAAM,UAAU,QAAQ,CAAC,OAAe,EAAE,UAAkB,EAAE,MAAyB,OAAO,CAAC,GAAG;IAChG,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5C,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,KAAK,IAAI,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC;QAAE,OAAO,OAAO,CAAC;IAC9G,OAAO,OAAO,OAAO,EAAE,CAAC;AAC1B,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Command, Io } from "./command.js";
|
|
2
|
+
/** Where the docs are published from docs/. */
|
|
3
|
+
export declare const DOCS_URL = "https://intentius.io/sql-yodeler/";
|
|
4
|
+
export declare function help(list?: readonly Command[]): string;
|
|
5
|
+
/** Wraps `line` at 80 columns, continuing with `indent`. */
|
|
6
|
+
export declare function wrap(line: string, indent: string, width?: number): string;
|
|
7
|
+
/** Parses argv (without the node and script paths) and runs a verb; resolves to the exit code. */
|
|
8
|
+
export declare function main(argv: string[], io: Io, list?: readonly Command[]): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { commands } from "./commands/index.js";
|
|
3
|
+
import { version } from "./version.js";
|
|
4
|
+
import { EXIT_CODES, PLAN_PENDING_NOTE } from "./exit.js";
|
|
5
|
+
/** Where the docs are published from docs/. */
|
|
6
|
+
export const DOCS_URL = "https://intentius.io/sql-yodeler/";
|
|
7
|
+
export function help(list = commands) {
|
|
8
|
+
const width = Math.max(...list.map((c) => c.name.length));
|
|
9
|
+
const lines = list.map((c) => {
|
|
10
|
+
const mark = c.implemented ? "" : " (not implemented yet)";
|
|
11
|
+
return ` ${c.name.padEnd(width)} ${c.summary}${mark}`;
|
|
12
|
+
});
|
|
13
|
+
return [
|
|
14
|
+
"Usage: yodel <command> [options]",
|
|
15
|
+
"",
|
|
16
|
+
"Schema management and migrations for ClickHouse and Postgres.",
|
|
17
|
+
"",
|
|
18
|
+
"Commands:",
|
|
19
|
+
...lines,
|
|
20
|
+
"",
|
|
21
|
+
"Options:",
|
|
22
|
+
" -h, --help show this help",
|
|
23
|
+
" -v, --version print the version",
|
|
24
|
+
"",
|
|
25
|
+
"yodel <command> --help (or yodel help <command>) shows a command's options.",
|
|
26
|
+
`Docs: ${DOCS_URL}`,
|
|
27
|
+
"",
|
|
28
|
+
"Exit codes (yodel <command> --help lists a command's own):",
|
|
29
|
+
...EXIT_CODES.map(([code, meaning]) => wrap(` ${String(code)} ${meaning}`, " ")),
|
|
30
|
+
wrap(` ${PLAN_PENDING_NOTE}`, " "),
|
|
31
|
+
"",
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
/** Wraps `line` at 80 columns, continuing with `indent`. */
|
|
35
|
+
export function wrap(line, indent, width = 80) {
|
|
36
|
+
const out = [];
|
|
37
|
+
let current = "";
|
|
38
|
+
for (const word of line.split(/(?<=\S) (?=\S)/)) {
|
|
39
|
+
if (current && current.length + 1 + word.length > width) {
|
|
40
|
+
out.push(current);
|
|
41
|
+
current = indent + word.trimStart();
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
current = current ? `${current} ${word}` : word;
|
|
45
|
+
}
|
|
46
|
+
if (current)
|
|
47
|
+
out.push(current);
|
|
48
|
+
return out.join("\n");
|
|
49
|
+
}
|
|
50
|
+
/** Parses argv (without the node and script paths) and runs a verb; resolves to the exit code. */
|
|
51
|
+
export async function main(argv, io, list = commands) {
|
|
52
|
+
const verbAt = argv.findIndex((a) => !a.startsWith("-"));
|
|
53
|
+
const globals = verbAt === -1 ? argv : argv.slice(0, verbAt);
|
|
54
|
+
let values;
|
|
55
|
+
try {
|
|
56
|
+
({ values } = parseArgs({
|
|
57
|
+
args: globals,
|
|
58
|
+
options: {
|
|
59
|
+
help: { type: "boolean", short: "h" },
|
|
60
|
+
version: { type: "boolean", short: "v" },
|
|
61
|
+
},
|
|
62
|
+
strict: true,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
io.err(`yodel: ${e instanceof Error ? e.message : String(e)}\n\n${help(list)}`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
if (values.version) {
|
|
70
|
+
io.out(`${version()}\n`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
if (values.help || verbAt === -1) {
|
|
74
|
+
(values.help ? io.out : io.err)(help(list));
|
|
75
|
+
return values.help ? 0 : 1;
|
|
76
|
+
}
|
|
77
|
+
const name = argv[verbAt];
|
|
78
|
+
if (name === "help") {
|
|
79
|
+
// `yodel help` is `yodel --help`; `yodel help <command>` is `yodel <command> --help`.
|
|
80
|
+
const topic = argv.slice(verbAt + 1).find((a) => !a.startsWith("-"));
|
|
81
|
+
if (topic === undefined) {
|
|
82
|
+
io.out(help(list));
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
const target = list.find((c) => c.name === topic);
|
|
86
|
+
if (!target) {
|
|
87
|
+
io.err(`yodel: unknown command "${topic}"\n\n${help(list)}`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
return target.run(["--help"], io);
|
|
91
|
+
}
|
|
92
|
+
const command = list.find((c) => c.name === name);
|
|
93
|
+
if (!command) {
|
|
94
|
+
io.err(`yodel: unknown command "${String(name)}"\n\n${help(list)}`);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
return command.run(argv.slice(verbAt + 1), io);
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Where a command writes. Tests pass their own; the bin passes process.stdout and stderr. */
|
|
2
|
+
export interface Io {
|
|
3
|
+
out: (text: string) => void;
|
|
4
|
+
err: (text: string) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Asks the person at the terminal and resolves to their answer. Present
|
|
7
|
+
* only when stdin and stderr are a terminal: a script, CI or an agent has
|
|
8
|
+
* no one to ask, and a command that needs a person's consent refuses.
|
|
9
|
+
*/
|
|
10
|
+
ask?: (question: string) => Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
/** One `yodel` verb. Each lives in its own module under src/commands/. */
|
|
13
|
+
export interface Command {
|
|
14
|
+
name: string;
|
|
15
|
+
/** One line for `yodel --help`. */
|
|
16
|
+
summary: string;
|
|
17
|
+
/** False until a later issue builds the verb; `yodel --help` marks it. */
|
|
18
|
+
implemented: boolean;
|
|
19
|
+
/** Runs the verb with the arguments after its name; resolves to the exit code. */
|
|
20
|
+
run: (args: string[], io: Io) => Promise<number>;
|
|
21
|
+
}
|
|
22
|
+
/** A verb that is listed but not built yet. Later issues replace these one by one. */
|
|
23
|
+
export declare function notImplemented(name: string, summary: string): Command;
|
package/dist/command.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A verb that is listed but not built yet. Later issues replace these one by one. */
|
|
2
|
+
export function notImplemented(name, summary) {
|
|
3
|
+
return {
|
|
4
|
+
name,
|
|
5
|
+
summary,
|
|
6
|
+
implemented: false,
|
|
7
|
+
run: (_args, io) => {
|
|
8
|
+
io.err(`yodel ${name}: not implemented yet\n`);
|
|
9
|
+
return Promise.resolve(2);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command.js","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAuBA,sFAAsF;AACtF,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,OAAe;IAC1D,OAAO;QACL,IAAI;QACJ,OAAO;QACP,WAAW,EAAE,KAAK;QAClB,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACjB,EAAE,CAAC,GAAG,CAAC,SAAS,IAAI,yBAAyB,CAAC,CAAC;YAC/C,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC5B,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { guarded } from "../env-args.js";
|
|
3
|
+
import { loadProject, resolveEnvironment, YodelError } from "../project.js";
|
|
4
|
+
import { buildSchema } from "../chant/build.js";
|
|
5
|
+
import { declaredChanges, planEnvironment, planExitCode, planJson, renderPlan, PLAN_NEEDS_OP } from "../chant/plan.js";
|
|
6
|
+
import { findApplyOp, GATED, runApplyOp } from "../chant/apply.js";
|
|
7
|
+
import { runnable } from "../chant/runnable.js";
|
|
8
|
+
import { migrationSilences } from "../lint/index.js";
|
|
9
|
+
import { askedAgainLine, describeMove, explainAgainst, latestApproval } from "../versioned/explain.js";
|
|
10
|
+
import { declarativeTopologyRefusal, LOCK_WAIT_ENV, parseDuration } from "../config/settings.js";
|
|
11
|
+
import { newerCommit, STAND_DOWN_ENV, standDownFromEnv, standDownLine } from "../versioned/stand-down.js";
|
|
12
|
+
import { describeServer, probeServer } from "../server.js";
|
|
13
|
+
import { ALLOW_OUT_OF_ORDER_ENV, APPROVED_PLAN_ENV, checksDue, checksRefusal, describeResults, judgePlan, policyRefusal, runChecks, currentDigest, executeApply, findMigrateOp, gitLedger, verifyApproval, hasMigrations, inspect, readChain, recordStepExit, closeVersioned, openVersioned, renderStatus, runMigrateOp, selectToApply, statusJson, statusReport, } from "../versioned/index.js";
|
|
14
|
+
/** When the gate's newest approval does not hold: what moved since (or that approval was asked for again), in one line. */
|
|
15
|
+
async function movedSinceApproval(project, venv, ordered, op, digest) {
|
|
16
|
+
try {
|
|
17
|
+
const latest = await latestApproval(await gitLedger(project.dir), op, digest);
|
|
18
|
+
if (!latest?.digest || latest.holds)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (latest.sameDigest) {
|
|
21
|
+
return {
|
|
22
|
+
moved: { approved: latest.digest, by: latest.by, at: latest.at, confirmed: true, changes: [], ...(latest.askedAgain ? { askedAgain: latest.askedAgain } : {}) },
|
|
23
|
+
line: `Nothing was applied. The approval of ${latest.digest} (by ${latest.by} at ${latest.at}) ${askedAgainLine(latest)}.`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const moved = await explainAgainst(venv, ordered, { approvedDigest: latest.digest, approvedAt: latest.at });
|
|
27
|
+
return {
|
|
28
|
+
moved: { approved: latest.digest, by: latest.by, at: latest.at, confirmed: moved.confirmed, changes: moved.changes },
|
|
29
|
+
line: `Nothing was applied. The approval of ${latest.digest} (by ${latest.by} at ${latest.at}) does not hold: the plan moved since. ${moved.confirmed ? "What moved" : "What moved, as far as yodel can tell"}: ${describeMove(moved)}.`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const usage = "usage: yodel apply <env> [--json] [--dir <project>] [--op <name>] [--allow-out-of-order] [--plan] [--lock-wait <duration>] [--stand-down]";
|
|
37
|
+
const help = `${usage}
|
|
38
|
+
|
|
39
|
+
Two paths, chosen by the project:
|
|
40
|
+
|
|
41
|
+
- Versioned: the project has a migrations/ directory holding a migration.
|
|
42
|
+
Applies the pending migrations in chain order, statement by statement,
|
|
43
|
+
recording each in the history table (yodel.config.ts history.database,
|
|
44
|
+
else YODEL_HISTORY_DATABASE, default yodeler) under the apply lock. Each
|
|
45
|
+
statement is rendered for the environment's topology (yodel.config.ts
|
|
46
|
+
topology, else sql.profiles.<env>.topology, else YODEL_TOPOLOGY, default
|
|
47
|
+
single) as it is sent. Runs the project's migrations Op for the
|
|
48
|
+
environment (chant run <op>), whose gate is bound to the plan digest: the
|
|
49
|
+
pending migrations' checksums, the topology, the history and the live
|
|
50
|
+
schema. A run that stops at the gate prints the chant approve command;
|
|
51
|
+
approve, then run yodel apply again. At a terminal, yodel approve <env>
|
|
52
|
+
approves the same plan without the digest to copy: yodel approve <env>,
|
|
53
|
+
then yodel apply <env>. If anything the digest covers changes after
|
|
54
|
+
approval, the gate asks again.
|
|
55
|
+
|
|
56
|
+
A migration that failed part way resumes at the statement that failed; a
|
|
57
|
+
statement that succeeded is never sent again. An Op step (a ClickHouse
|
|
58
|
+
rebuild, a backfill) runs in its place among the statements, under the
|
|
59
|
+
same lock and approval; one that failed or was stopped runs again and
|
|
60
|
+
resumes from its receipts, skipping the partitions or batches that
|
|
61
|
+
finished. Every applied migration's
|
|
62
|
+
checksum is checked against its files first, and a mismatch refuses. A
|
|
63
|
+
pending migration the chain puts before an applied one is out of order
|
|
64
|
+
and is refused unless --allow-out-of-order; it is never skipped.
|
|
65
|
+
|
|
66
|
+
The policy in yodel.config.ts (policy.rules) is judged over the plan
|
|
67
|
+
before the gate and again under the lock: a rule that denies it refuses
|
|
68
|
+
(exit 4) naming the rule, unless someone policy.overriders lists recorded
|
|
69
|
+
an override for exactly this plan digest (yodel override <env> --rule <id>
|
|
70
|
+
--reason <text>). The history records the overrides a run relied on.
|
|
71
|
+
|
|
72
|
+
A migration's pre-migration checks (checks in its migration.json, read-only
|
|
73
|
+
queries) run against the environment: the first pending migration's before
|
|
74
|
+
the gate, and every migration's under the lock just before its first
|
|
75
|
+
statement. One that fails refuses (exit 4), naming it, and nothing of that
|
|
76
|
+
migration is applied.
|
|
77
|
+
|
|
78
|
+
- Declarative: no migrations. Plans the declared schema against the live
|
|
79
|
+
database, then runs the project's ApplyOp for the environment behind its
|
|
80
|
+
plan-bound gate.
|
|
81
|
+
|
|
82
|
+
Options:
|
|
83
|
+
--json print the outcome as JSON
|
|
84
|
+
--op <name> the Op to run, when more than one applies <env>
|
|
85
|
+
--allow-out-of-order versioned: apply out-of-order migrations too
|
|
86
|
+
--plan versioned: print the pending migrations and the plan
|
|
87
|
+
digest, apply nothing
|
|
88
|
+
--lock-wait <d> versioned: when another apply holds the lock, wait up
|
|
89
|
+
to <d> (90, 30s, 10m, 1h) for it, naming the holder,
|
|
90
|
+
before giving up with exit 5. Default: yodel.config.ts
|
|
91
|
+
lockWait (seconds), else YODEL_LOCK_WAIT, else 0
|
|
92
|
+
--stand-down versioned: when the branch this run is for has a newer
|
|
93
|
+
commit on origin (GITHUB_REF_NAME, CI_COMMIT_BRANCH,
|
|
94
|
+
else the checked-out branch), apply nothing and exit 0:
|
|
95
|
+
that commit's own apply applies this environment.
|
|
96
|
+
Asked before the lock and while waiting for it. Also
|
|
97
|
+
YODEL_STAND_DOWN=1
|
|
98
|
+
-C, --dir <dir> the project directory (default: the working directory)
|
|
99
|
+
|
|
100
|
+
The migrations Op runs two steps of its own, not for direct use:
|
|
101
|
+
--digest print the plan digest alone (the Op's Plan step)
|
|
102
|
+
--execute apply under the lock, if the digest is still the one in
|
|
103
|
+
${APPROVED_PLAN_ENV} and chant's gate ledger holds an approval
|
|
104
|
+
of the Op's gate for it (the Op's Apply step)
|
|
105
|
+
|
|
106
|
+
Exit codes:
|
|
107
|
+
0 applied, or nothing to apply, or (--stand-down) stood down for a newer
|
|
108
|
+
commit, applying nothing
|
|
109
|
+
2 a change needs its own Op (declarative), or a pending migration has a
|
|
110
|
+
manual step or an Op step apply does not run (versioned: it runs
|
|
111
|
+
ClickHouseRebuildOp in materialized-view mode and backfill steps on
|
|
112
|
+
ClickHouse, PostgresMigrationOp on Postgres);
|
|
113
|
+
nothing was applied
|
|
114
|
+
3 stopped at the gate, waiting for approval of this plan
|
|
115
|
+
4 versioned: refused, nothing applied: a checksum mismatch, an applied
|
|
116
|
+
migration gone from the directory, an out-of-order migration
|
|
117
|
+
without --allow-out-of-order, a pre-migration check that failed, or a
|
|
118
|
+
policy rule that denies the plan with no override
|
|
119
|
+
5 versioned: another apply holds the lock (after --lock-wait, if given)
|
|
120
|
+
1 anything else failed: a statement (the history records where; rerun to
|
|
121
|
+
resume), a plan that moved since approval, a bad config
|
|
122
|
+
`;
|
|
123
|
+
function parseApplyArgs(args) {
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = parseArgs({
|
|
127
|
+
args,
|
|
128
|
+
allowPositionals: true,
|
|
129
|
+
strict: true,
|
|
130
|
+
options: {
|
|
131
|
+
json: { type: "boolean" },
|
|
132
|
+
dir: { type: "string", short: "C" },
|
|
133
|
+
op: { type: "string" },
|
|
134
|
+
"allow-out-of-order": { type: "boolean" },
|
|
135
|
+
plan: { type: "boolean" },
|
|
136
|
+
digest: { type: "boolean" },
|
|
137
|
+
execute: { type: "boolean" },
|
|
138
|
+
"lock-wait": { type: "string" },
|
|
139
|
+
"stand-down": { type: "boolean" },
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
throw new YodelError(`${e instanceof Error ? e.message : String(e)}\n${usage}`);
|
|
145
|
+
}
|
|
146
|
+
const { values, positionals } = parsed;
|
|
147
|
+
const [env, ...rest] = positionals;
|
|
148
|
+
if (env === undefined || rest.length > 0)
|
|
149
|
+
throw new YodelError(usage);
|
|
150
|
+
const flag = (k) => values[k] === true;
|
|
151
|
+
const modes = ["plan", "digest", "execute"].filter(flag);
|
|
152
|
+
if (modes.length > 1)
|
|
153
|
+
throw new YodelError(`--${modes.join(" and --")} cannot be combined\n${usage}`);
|
|
154
|
+
return {
|
|
155
|
+
env,
|
|
156
|
+
json: flag("json"),
|
|
157
|
+
dir: typeof values.dir === "string" ? values.dir : process.cwd(),
|
|
158
|
+
op: typeof values.op === "string" ? values.op : undefined,
|
|
159
|
+
allowOutOfOrder: flag("allow-out-of-order") || process.env[ALLOW_OUT_OF_ORDER_ENV] === "1",
|
|
160
|
+
plan: flag("plan"),
|
|
161
|
+
digest: flag("digest"),
|
|
162
|
+
execute: flag("execute"),
|
|
163
|
+
lockWaitMs: typeof values["lock-wait"] === "string" ? parseDuration(values["lock-wait"], "--lock-wait") : undefined,
|
|
164
|
+
standDown: flag("stand-down") || standDownFromEnv(process.env),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** `yodel apply <env>`; the exit codes are in `help`. */
|
|
168
|
+
export const apply = {
|
|
169
|
+
name: "apply",
|
|
170
|
+
summary: "bring an environment to its declared schema or pending migrations",
|
|
171
|
+
implemented: true,
|
|
172
|
+
run: async (args, io) => {
|
|
173
|
+
const code = await guarded("apply", io, args, help, async () => {
|
|
174
|
+
const a = parseApplyArgs(args);
|
|
175
|
+
const project = await loadProject(a.dir);
|
|
176
|
+
const env = resolveEnvironment(project, a.env);
|
|
177
|
+
if (hasMigrations(project))
|
|
178
|
+
return versionedApply(project, env, a, io);
|
|
179
|
+
if (a.plan || a.digest || a.execute || a.allowOutOfOrder || a.lockWaitMs !== undefined || args.includes("--stand-down")) {
|
|
180
|
+
throw new YodelError(`--plan, --digest, --execute, --allow-out-of-order, --lock-wait and --stand-down are for versioned migrations, and ${project.dir} has no migrations/ directory with a migration`);
|
|
181
|
+
}
|
|
182
|
+
const refusal = project.dialect === "clickhouse" ? declarativeTopologyRefusal(project, env.name) : undefined;
|
|
183
|
+
if (refusal)
|
|
184
|
+
throw refusal;
|
|
185
|
+
return declarativeApply(project, env, a, io);
|
|
186
|
+
});
|
|
187
|
+
if (args.includes("--execute"))
|
|
188
|
+
recordStepExit(code);
|
|
189
|
+
return code;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
async function versionedApply(project, env, a, io) {
|
|
193
|
+
const ordered = readChain(project);
|
|
194
|
+
const venv = await openVersioned(project, env);
|
|
195
|
+
try {
|
|
196
|
+
return await versionedApplyWith(project, env, a, io, ordered, venv);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await closeVersioned(venv);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
203
|
+
if (a.digest) {
|
|
204
|
+
const { status } = await inspect(venv, ordered);
|
|
205
|
+
const toApply = selectToApply(status, a.allowOutOfOrder);
|
|
206
|
+
io.out(`${await currentDigest(venv, status, toApply)}\n`);
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
if (a.execute) {
|
|
210
|
+
const approved = process.env[APPROVED_PLAN_ENV];
|
|
211
|
+
if (!approved)
|
|
212
|
+
throw new YodelError(`--execute applies only the plan its Op's gate approved, and ${APPROVED_PLAN_ENV} is not set; run yodel apply ${env.name}`);
|
|
213
|
+
// The env var is only the Op's hand-off; the approval itself is checked in chant's gate ledger, under the lock.
|
|
214
|
+
const op = await findMigrateOp(project, env, a.op);
|
|
215
|
+
const ledger = await gitLedger(project.dir);
|
|
216
|
+
const result = await executeApply(venv, ordered, {
|
|
217
|
+
approvedDigest: approved,
|
|
218
|
+
allowOutOfOrder: a.allowOutOfOrder,
|
|
219
|
+
...(a.lockWaitMs !== undefined ? { lockWaitMs: a.lockWaitMs } : {}),
|
|
220
|
+
...(a.standDown ? { standDown: standingDown(project, env) } : {}),
|
|
221
|
+
approve: async (digest) => {
|
|
222
|
+
const by = await verifyApproval(ledger, op, digest);
|
|
223
|
+
io.err(`approval: ${op.name} / ${op.gate} for ${digest}, by ${by.resolvedBy} at ${by.timestamp}\n`);
|
|
224
|
+
},
|
|
225
|
+
log: (l) => { io.err(`${l}\n`); },
|
|
226
|
+
silences: migrationSilences,
|
|
227
|
+
...(project.yodel?.config.policy
|
|
228
|
+
? {
|
|
229
|
+
policy: async (digest, toApply) => {
|
|
230
|
+
const results = await judgePlan({ policy: project.yodel?.config.policy, environment: env.name, dialect: project.dialect, toApply, digest, ledger: () => Promise.resolve({ port: ledger, op }) });
|
|
231
|
+
const refusal = policyRefusal(results, env.name, digest, "under the lock");
|
|
232
|
+
if (refusal)
|
|
233
|
+
throw refusal;
|
|
234
|
+
for (const line of describeResults(results))
|
|
235
|
+
io.err(`${line}\n`);
|
|
236
|
+
return results.flatMap((r) => (r.override ? [r.override] : []));
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
: {}),
|
|
240
|
+
});
|
|
241
|
+
if (a.json)
|
|
242
|
+
io.out(`${JSON.stringify({ environment: env.name, status: result.stoodDown ? "stood-down" : "applied", planDigest: approved, ...result }, null, 2)}\n`);
|
|
243
|
+
else
|
|
244
|
+
io.out(result.stoodDown ? `${result.stoodDown}\n` : result.applied.length === 0 ? "Nothing to apply.\n" : `Applied: ${result.applied.join(", ")}\n`);
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
const report = await statusReport(venv, ordered, { allowOutOfOrder: a.allowOutOfOrder });
|
|
248
|
+
const emit = (doc, code) => {
|
|
249
|
+
if (a.json)
|
|
250
|
+
io.out(`${JSON.stringify({ ...statusJson(report), mode: "versioned", ...doc }, null, 2)}\n`);
|
|
251
|
+
return code;
|
|
252
|
+
};
|
|
253
|
+
if (a.plan) {
|
|
254
|
+
if (!a.json)
|
|
255
|
+
io.out(renderStatus(report));
|
|
256
|
+
return emit({ status: report.refusal ? "refused" : report.status.pending.length === 0 ? "unchanged" : "planned" }, report.refusal?.exitCode ?? 0);
|
|
257
|
+
}
|
|
258
|
+
if (report.refusal) {
|
|
259
|
+
if (!a.json)
|
|
260
|
+
io.err(`yodel apply: ${report.refusal.message}\n`);
|
|
261
|
+
return emit({ status: "refused" }, report.refusal.exitCode);
|
|
262
|
+
}
|
|
263
|
+
if (report.status.pending.length === 0) {
|
|
264
|
+
if (!a.json)
|
|
265
|
+
io.out(`${env.name}${report.server ? ` (${describeServer(report.server)})` : ""} is up to date: every migration is applied.\n`);
|
|
266
|
+
return emit({ status: "unchanged" }, 0);
|
|
267
|
+
}
|
|
268
|
+
const standDown = a.standDown ? standingDown(project, env) : undefined;
|
|
269
|
+
const stood = await standDown?.();
|
|
270
|
+
if (stood) {
|
|
271
|
+
if (!a.json)
|
|
272
|
+
io.out(`${stood}\n`);
|
|
273
|
+
return emit({ status: "stood-down", stoodDown: stood }, 0);
|
|
274
|
+
}
|
|
275
|
+
// The first pending migration's checks (#52) read the data as it is now: one that fails refuses before anyone is asked to approve. The Apply step runs every migration's again under the lock.
|
|
276
|
+
const first = report.status.pending[0];
|
|
277
|
+
let checked;
|
|
278
|
+
if (first && checksDue(first)) {
|
|
279
|
+
const outcomes = await runChecks(venv.driver, first.migration);
|
|
280
|
+
checked = { migration: first.id, checks: outcomes };
|
|
281
|
+
const refusal = checksRefusal(first.migration, outcomes, "before the gate");
|
|
282
|
+
if (refusal) {
|
|
283
|
+
if (!a.json)
|
|
284
|
+
io.err(`yodel apply: ${refusal.message}\n`);
|
|
285
|
+
return emit({ status: "refused", preChecks: checked }, refusal.exitCode);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const op = await findMigrateOp(project, env, a.op);
|
|
289
|
+
// The policy (#53): a rule that denies this plan, with no override recorded for its digest, refuses before the gate.
|
|
290
|
+
const policy = await judgePlan({
|
|
291
|
+
policy: project.yodel?.config.policy,
|
|
292
|
+
environment: env.name,
|
|
293
|
+
dialect: project.dialect,
|
|
294
|
+
toApply: report.status.pending,
|
|
295
|
+
digest: report.digest,
|
|
296
|
+
ledger: async () => ({ port: await gitLedger(project.dir), op }),
|
|
297
|
+
});
|
|
298
|
+
const denied = policyRefusal(policy, env.name, report.digest, "before the gate");
|
|
299
|
+
if (denied) {
|
|
300
|
+
if (!a.json)
|
|
301
|
+
io.err(`yodel apply: ${denied.message}\n`);
|
|
302
|
+
return emit({ status: "denied", policy }, denied.exitCode);
|
|
303
|
+
}
|
|
304
|
+
if (!a.json)
|
|
305
|
+
io.out(renderStatus(report));
|
|
306
|
+
if (!a.json && checked)
|
|
307
|
+
for (const o of checked.checks)
|
|
308
|
+
io.out(`check ${o.name} of ${checked.migration} passed (${o.detail}); it runs again under the lock before the first statement\n`);
|
|
309
|
+
if (!a.json)
|
|
310
|
+
for (const line of describeResults(policy))
|
|
311
|
+
io.out(`${line}\n`);
|
|
312
|
+
if (!a.json)
|
|
313
|
+
io.out(`\nRunning ${op.name} (${op.filePath})\n`);
|
|
314
|
+
// The Op's Apply step (yodel apply --execute) takes these from its environment.
|
|
315
|
+
const childEnv = {
|
|
316
|
+
...process.env,
|
|
317
|
+
...(a.lockWaitMs !== undefined ? { [LOCK_WAIT_ENV]: `${String(a.lockWaitMs)}ms` } : {}),
|
|
318
|
+
...(a.standDown ? { [STAND_DOWN_ENV]: "1" } : {}),
|
|
319
|
+
};
|
|
320
|
+
const run = await runMigrateOp(project, op, { allowOutOfOrder: a.allowOutOfOrder, env: childEnv, onOutput: (t) => { io.err(t); } });
|
|
321
|
+
if (run.status === "gated") {
|
|
322
|
+
const stale = await movedSinceApproval(project, venv, ordered, op, run.planDigest ?? report.digest);
|
|
323
|
+
if (stale && !a.json)
|
|
324
|
+
io.out(`\n${stale.line}\n`);
|
|
325
|
+
if (!a.json) {
|
|
326
|
+
io.out(`\n${op.name} is waiting at gate "${op.gate}" for approval of this plan${run.planDigest ? ` (${run.planDigest})` : ""}.\n`);
|
|
327
|
+
if (run.approve) {
|
|
328
|
+
io.out(`Approve it with:\n ${runnable(run.approve, project.dir)}\n` +
|
|
329
|
+
`or, at a terminal, with ${runnable(`yodel approve ${env.name}`, project.dir)}, which shows the plan and asks you first;\n` +
|
|
330
|
+
`then run ${runnable(`yodel apply ${env.name}`, project.dir)} again.\n`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return emit({ status: "gated", op: op.name, gate: op.gate, ...(stale ? { moved: stale.moved } : {}), ...(run.approve ? { approve: run.approve } : {}), ...(run.planDigest ? { gatePlanDigest: run.planDigest } : {}) }, GATED);
|
|
334
|
+
}
|
|
335
|
+
const after = await statusReport(venv, ordered, { allowOutOfOrder: a.allowOutOfOrder });
|
|
336
|
+
if (run.status === "failed") {
|
|
337
|
+
// The Apply step's own code (5 for the lock, 4 for a refusal) is what a script retries or stops on; chant run reports any failed step as 1.
|
|
338
|
+
const code = run.stepExit !== undefined && run.stepExit !== 0 ? run.stepExit : 1;
|
|
339
|
+
if (!a.json) {
|
|
340
|
+
io.err(`yodel apply: chant run ${op.name} failed (exit ${String(run.code)})${run.stepExit ? `: its Apply step, yodel apply ${env.name} --execute, exited ${String(run.stepExit)}` : ""}\n`);
|
|
341
|
+
io.out(`\n${renderStatus(after)}`);
|
|
342
|
+
}
|
|
343
|
+
if (a.json)
|
|
344
|
+
io.out(`${JSON.stringify({ ...statusJson(after), mode: "versioned", status: "failed", op: op.name, exitCode: code, chantExitCode: run.code }, null, 2)}\n`);
|
|
345
|
+
return code;
|
|
346
|
+
}
|
|
347
|
+
// The Apply step stood down (a newer commit came while it waited for the lock): what it applied is nothing, and that is not a failure.
|
|
348
|
+
const stoodLate = after.status.pending.length > 0 ? await standDown?.() : undefined;
|
|
349
|
+
if (stoodLate) {
|
|
350
|
+
if (!a.json)
|
|
351
|
+
io.out(`\n${stoodLate}\n`);
|
|
352
|
+
return emit({ status: "stood-down", stoodDown: stoodLate, op: op.name }, 0);
|
|
353
|
+
}
|
|
354
|
+
if (a.json) {
|
|
355
|
+
io.out(`${JSON.stringify({ ...statusJson(after), mode: "versioned", status: "applied", op: op.name, appliedNow: report.status.pending.map((p) => p.id) }, null, 2)}\n`);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
io.out(`\nApplied: ${report.status.pending.map((p) => p.id).join(", ")}.\n`);
|
|
359
|
+
}
|
|
360
|
+
return after.status.pending.length === 0 ? 0 : 1;
|
|
361
|
+
}
|
|
362
|
+
async function declarativeApply(project, env, a, io) {
|
|
363
|
+
const json = a.json;
|
|
364
|
+
const op = await findApplyOp(project, env, a.op);
|
|
365
|
+
let server;
|
|
366
|
+
const report = (doc, code) => {
|
|
367
|
+
if (json)
|
|
368
|
+
io.out(`${JSON.stringify({ environment: env.name, dialect: project.dialect, ...(server ? { server } : {}), op: op.name, ...doc }, null, 2)}\n`);
|
|
369
|
+
return code;
|
|
370
|
+
};
|
|
371
|
+
const build = await buildSchema(project);
|
|
372
|
+
try {
|
|
373
|
+
const before = await planEnvironment(project, env, build);
|
|
374
|
+
server = await probeServer(project, env);
|
|
375
|
+
if (!json)
|
|
376
|
+
io.out(`${await renderPlan(before, `Plan for ${env.name} (${server ? describeServer(server) : project.dialect}, sql.profiles.${env.name})`)}\n`);
|
|
377
|
+
if (planExitCode(before) === PLAN_NEEDS_OP) {
|
|
378
|
+
if (!json)
|
|
379
|
+
io.err(`yodel apply: refused: ${String(before.refused.length)} change(s) need their own Op; nothing was applied\n`);
|
|
380
|
+
return report({ status: "refused", plan: planJson(before) }, PLAN_NEEDS_OP);
|
|
381
|
+
}
|
|
382
|
+
if (declaredChanges(before, build).length === 0) {
|
|
383
|
+
if (!json)
|
|
384
|
+
io.out(`${env.name} already matches the declared schema; nothing to apply.\n`);
|
|
385
|
+
return report({ status: "unchanged", plan: planJson(before) }, 0);
|
|
386
|
+
}
|
|
387
|
+
if (!json)
|
|
388
|
+
io.out(`\nRunning ${op.name} (${op.filePath})\n`);
|
|
389
|
+
const run = await runApplyOp(project, op, { onOutput: (t) => { io.err(t); } });
|
|
390
|
+
if (run.status === "gated") {
|
|
391
|
+
if (!json) {
|
|
392
|
+
io.out(`\n${op.name} is waiting at gate "${op.gate}" for approval of this plan.\n`);
|
|
393
|
+
if (run.approve)
|
|
394
|
+
io.out(`Approve it with:\n ${runnable(run.approve, project.dir)}\nthen run ${runnable(`yodel apply ${env.name}`, project.dir)} again.\n`);
|
|
395
|
+
}
|
|
396
|
+
return report({ status: "gated", gate: op.gate, ...(run.approve ? { approve: run.approve } : {}), ...(run.planDigest ? { planDigest: run.planDigest } : {}), plan: planJson(before) }, GATED);
|
|
397
|
+
}
|
|
398
|
+
if (run.status === "failed") {
|
|
399
|
+
if (!json)
|
|
400
|
+
io.err(`yodel apply: chant run ${op.name} failed (exit ${String(run.code)})\n`);
|
|
401
|
+
return report({ status: "failed", exitCode: run.code, plan: planJson(before) }, 1);
|
|
402
|
+
}
|
|
403
|
+
// The Op completed: check the server now matches, so a change the applier did not attempt is not reported as applied.
|
|
404
|
+
const after = await planEnvironment(project, env, build);
|
|
405
|
+
const remaining = declaredChanges(after, build);
|
|
406
|
+
if (remaining.length > 0) {
|
|
407
|
+
if (!json)
|
|
408
|
+
io.err(`yodel apply: ${op.name} completed but ${String(remaining.length)} change(s) to declared objects are still planned (a drop the Op's delete mode withholds, or a change it did not attempt):\n${await renderPlan(after, `Plan for ${env.name} after apply`)}\n`);
|
|
409
|
+
return report({ status: "incomplete", plan: planJson(before), remaining: planJson(after) }, 1);
|
|
410
|
+
}
|
|
411
|
+
if (!json)
|
|
412
|
+
io.out(`\nApplied: ${env.name} matches the declared schema.\n`);
|
|
413
|
+
return report({ status: "applied", plan: planJson(before) }, 0);
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
build.dispose();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/** `--stand-down`'s question: the line to stand down with, when a newer commit on the run's branch will apply `env`. */
|
|
420
|
+
function standingDown(project, env) {
|
|
421
|
+
return async () => {
|
|
422
|
+
const n = await newerCommit(project.dir);
|
|
423
|
+
return n ? standDownLine(env.name, n) : undefined;
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
//# sourceMappingURL=apply.js.map
|