@intentius/sql-yodeler 0.1.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.
- package/README.md +28 -26
- package/dist/bin.js +15 -0
- package/dist/bin.js.map +1 -1
- package/dist/chant/apply.d.ts +9 -1
- package/dist/chant/apply.js +13 -1
- package/dist/chant/apply.js.map +1 -1
- package/dist/chant/build.js +7 -0
- package/dist/chant/build.js.map +1 -1
- package/dist/chant/drift.d.ts +69 -1
- package/dist/chant/drift.js +152 -5
- package/dist/chant/drift.js.map +1 -1
- 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/ci/render.d.ts +289 -0
- package/dist/ci/render.js +688 -0
- package/dist/ci/render.js.map +1 -0
- package/dist/ci/replay.d.ts +17 -0
- package/dist/ci/replay.js +69 -0
- package/dist/ci/replay.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +40 -0
- package/dist/cli.js.map +1 -1
- package/dist/command.d.ts +6 -0
- package/dist/command.js.map +1 -1
- package/dist/commands/apply.d.ts +113 -0
- package/dist/commands/apply.js +376 -49
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/approve.d.ts +5 -0
- package/dist/commands/approve.js +155 -0
- package/dist/commands/approve.js.map +1 -0
- package/dist/commands/checkpoint.d.ts +3 -0
- package/dist/commands/checkpoint.js +78 -0
- package/dist/commands/checkpoint.js.map +1 -0
- package/dist/commands/ci.d.ts +3 -0
- package/dist/commands/ci.js +118 -0
- package/dist/commands/ci.js.map +1 -0
- package/dist/commands/docs.d.ts +11 -0
- package/dist/commands/docs.js +108 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/drift.d.ts +23 -0
- package/dist/commands/drift.js +263 -18
- package/dist/commands/drift.js.map +1 -1
- package/dist/commands/index.js +16 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +15 -11
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/lint.js +25 -8
- package/dist/commands/lint.js.map +1 -1
- 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 +2 -0
- package/dist/commands/new.js +134 -10
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/override.d.ts +3 -0
- package/dist/commands/override.js +150 -0
- package/dist/commands/override.js.map +1 -0
- package/dist/commands/plan.js +43 -27
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/repair.d.ts +11 -0
- package/dist/commands/repair.js +15 -7
- package/dist/commands/repair.js.map +1 -1
- package/dist/commands/report.d.ts +13 -0
- package/dist/commands/report.js +121 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/revert.d.ts +20 -0
- package/dist/commands/revert.js +246 -0
- package/dist/commands/revert.js.map +1 -0
- package/dist/commands/status.js +44 -17
- package/dist/commands/status.js.map +1 -1
- package/dist/config/base.d.ts +69 -0
- package/dist/config/base.js +121 -0
- package/dist/config/base.js.map +1 -0
- package/dist/config/define.d.ts +223 -3
- package/dist/config/define.js +3 -1
- package/dist/config/define.js.map +1 -1
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/load.d.ts +1 -1
- package/dist/config/load.js +150 -5
- package/dist/config/load.js.map +1 -1
- package/dist/config/settings.d.ts +18 -2
- package/dist/config/settings.js +40 -6
- package/dist/config/settings.js.map +1 -1
- package/dist/env-args.d.ts +6 -0
- package/dist/env-args.js +31 -1
- package/dist/env-args.js.map +1 -1
- 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 +8 -0
- package/dist/history/client.js +24 -1
- package/dist/history/client.js.map +1 -1
- package/dist/history/lock.d.ts +5 -1
- package/dist/history/lock.js +8 -2
- package/dist/history/lock.js.map +1 -1
- package/dist/history/postgres/ddl.d.ts +18 -0
- package/dist/history/postgres/ddl.js +27 -2
- package/dist/history/postgres/ddl.js.map +1 -1
- package/dist/history/postgres/store.d.ts +2 -0
- package/dist/history/postgres/store.js +7 -1
- package/dist/history/postgres/store.js.map +1 -1
- package/dist/history/rows.d.ts +16 -3
- package/dist/history/rows.js +30 -1
- package/dist/history/rows.js.map +1 -1
- package/dist/history/store.d.ts +2 -0
- package/dist/history/store.js +2 -2
- package/dist/history/store.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init/adopt.js +8 -3
- package/dist/init/adopt.js.map +1 -1
- package/dist/init/live.d.ts +11 -0
- package/dist/init/live.js +25 -2
- package/dist/init/live.js.map +1 -1
- package/dist/lint/check.d.ts +8 -1
- package/dist/lint/check.js +38 -4
- package/dist/lint/check.js.map +1 -1
- package/dist/lint/report.js +1 -1
- package/dist/lint/report.js.map +1 -1
- package/dist/lint/routines.d.ts +26 -0
- package/dist/lint/routines.js +65 -0
- package/dist/lint/routines.js.map +1 -0
- package/dist/lint/rules.d.ts +16 -4
- package/dist/lint/rules.js +9 -4
- package/dist/lint/rules.js.map +1 -1
- 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/checkpoint.d.ts +45 -0
- package/dist/migrations/checkpoint.js +110 -0
- package/dist/migrations/checkpoint.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/create.d.ts +3 -1
- package/dist/migrations/create.js +3 -0
- package/dist/migrations/create.js.map +1 -1
- package/dist/migrations/directory.js +4 -0
- package/dist/migrations/directory.js.map +1 -1
- package/dist/migrations/document.d.ts +33 -0
- package/dist/migrations/document.js +2 -0
- package/dist/migrations/document.js.map +1 -1
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +3 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/routines.d.ts +31 -0
- package/dist/migrations/routines.js +55 -0
- package/dist/migrations/routines.js.map +1 -0
- package/dist/notify/index.d.ts +124 -0
- package/dist/notify/index.js +210 -0
- package/dist/notify/index.js.map +1 -0
- package/dist/notify/verify.d.ts +53 -0
- package/dist/notify/verify.js +71 -0
- package/dist/notify/verify.js.map +1 -0
- package/dist/pr/forge.d.ts +4 -0
- package/dist/pr/forge.js +21 -7
- package/dist/pr/forge.js.map +1 -1
- 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 +12 -1
- package/dist/pr/model.js +44 -6
- package/dist/pr/model.js.map +1 -1
- package/dist/pr/render.d.ts +3 -1
- package/dist/pr/render.js +63 -4
- package/dist/pr/render.js.map +1 -1
- package/dist/project.d.ts +19 -2
- package/dist/project.js +33 -1
- package/dist/project.js.map +1 -1
- package/dist/replay/index.d.ts +3 -0
- package/dist/replay/index.js +3 -0
- package/dist/replay/index.js.map +1 -1
- package/dist/replay/replay.d.ts +9 -1
- package/dist/replay/replay.js +63 -32
- package/dist/replay/replay.js.map +1 -1
- package/dist/replay/report.js +1 -1
- package/dist/replay/report.js.map +1 -1
- 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/report/audit.d.ts +138 -0
- package/dist/report/audit.js +202 -0
- package/dist/report/audit.js.map +1 -0
- package/dist/report/jsonl.d.ts +21 -0
- package/dist/report/jsonl.js +52 -0
- package/dist/report/jsonl.js.map +1 -0
- package/dist/report/model.d.ts +68 -0
- package/dist/report/model.js +127 -0
- package/dist/report/model.js.map +1 -0
- package/dist/report/render.d.ts +18 -0
- package/dist/report/render.js +127 -0
- package/dist/report/render.js.map +1 -0
- package/dist/schema-docs/render.d.ts +49 -0
- package/dist/schema-docs/render.js +359 -0
- package/dist/schema-docs/render.js.map +1 -0
- package/dist/schemas.d.ts +37 -0
- package/dist/schemas.js +41 -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/sources/ddl.d.ts +53 -0
- package/dist/sources/ddl.js +347 -0
- package/dist/sources/ddl.js.map +1 -0
- package/dist/sources/index.d.ts +69 -0
- package/dist/sources/index.js +170 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/steps/backfill.d.ts +21 -17
- package/dist/steps/backfill.js +20 -20
- package/dist/steps/backfill.js.map +1 -1
- package/dist/steps/index.d.ts +2 -2
- package/dist/steps/index.js +3 -3
- package/dist/steps/index.js.map +1 -1
- package/dist/steps/postgres-migration.d.ts +35 -25
- package/dist/steps/postgres-migration.js +42 -29
- package/dist/steps/postgres-migration.js.map +1 -1
- package/dist/steps/rebuild.d.ts +15 -15
- package/dist/steps/rebuild.js +17 -24
- package/dist/steps/rebuild.js.map +1 -1
- package/dist/tenants/index.d.ts +40 -0
- package/dist/tenants/index.js +122 -0
- package/dist/tenants/index.js.map +1 -0
- package/dist/tenants/target.d.ts +23 -0
- package/dist/tenants/target.js +29 -0
- package/dist/tenants/target.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.js +8 -0
- package/dist/versioned/clickhouse.js.map +1 -1
- package/dist/versioned/driver.d.ts +10 -0
- package/dist/versioned/engine.d.ts +61 -1
- package/dist/versioned/engine.js +109 -6
- package/dist/versioned/engine.js.map +1 -1
- package/dist/versioned/explain.js +7 -2
- package/dist/versioned/explain.js.map +1 -1
- package/dist/versioned/index.d.ts +10 -0
- package/dist/versioned/index.js +25 -1
- package/dist/versioned/index.js.map +1 -1
- package/dist/versioned/op.d.ts +16 -1
- package/dist/versioned/op.js +43 -6
- package/dist/versioned/op.js.map +1 -1
- package/dist/versioned/policy.d.ts +98 -0
- package/dist/versioned/policy.js +212 -0
- package/dist/versioned/policy.js.map +1 -0
- package/dist/versioned/postgres/driver.d.ts +3 -1
- package/dist/versioned/postgres/driver.js +30 -3
- package/dist/versioned/postgres/driver.js.map +1 -1
- 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 +3 -0
- package/dist/versioned/report.js +13 -1
- package/dist/versioned/report.js.map +1 -1
- package/dist/versioned/revert.d.ts +128 -0
- package/dist/versioned/revert.js +251 -0
- package/dist/versioned/revert.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 +25 -0
- package/dist/versioned/state.js +36 -3
- package/dist/versioned/state.js.map +1 -1
- package/dist/waves/index.d.ts +179 -0
- package/dist/waves/index.js +268 -0
- package/dist/waves/index.js.map +1 -0
- package/dist/waves/promotion.d.ts +56 -0
- package/dist/waves/promotion.js +108 -0
- package/dist/waves/promotion.js.map +1 -0
- package/dist/waves/record.d.ts +24 -0
- package/dist/waves/record.js +20 -0
- package/dist/waves/record.js.map +1 -0
- package/package.json +22 -8
- package/schemas/apply.schema.json +428 -0
- package/schemas/checkpoint.schema.json +19 -0
- package/schemas/common.schema.json +213 -0
- package/schemas/drift.schema.json +61 -0
- package/schemas/event.schema.json +36 -0
- package/schemas/init.schema.json +31 -0
- package/schemas/lint-rules.schema.json +54 -0
- package/schemas/lint.schema.json +294 -0
- package/schemas/mcp.schema.json +70 -0
- package/schemas/new.schema.json +71 -0
- package/schemas/plan.schema.json +489 -0
- package/schemas/rebase.schema.json +54 -0
- package/schemas/repair.schema.json +20 -0
- package/schemas/report.schema.json +114 -0
- package/schemas/revert.schema.json +69 -0
- package/schemas/status.schema.json +371 -0
package/dist/commands/apply.js
CHANGED
|
@@ -4,11 +4,17 @@ import { loadProject, resolveEnvironment, YodelError } from "../project.js";
|
|
|
4
4
|
import { buildSchema } from "../chant/build.js";
|
|
5
5
|
import { declaredChanges, planEnvironment, planExitCode, planJson, renderPlan, PLAN_NEEDS_OP } from "../chant/plan.js";
|
|
6
6
|
import { findApplyOp, GATED, runApplyOp } from "../chant/apply.js";
|
|
7
|
-
import {
|
|
7
|
+
import { runnable } from "../chant/runnable.js";
|
|
8
8
|
import { migrationSilences } from "../lint/index.js";
|
|
9
9
|
import { askedAgainLine, describeMove, explainAgainst, latestApproval } from "../versioned/explain.js";
|
|
10
|
-
import { declarativeTopologyRefusal } from "../config/settings.js";
|
|
11
|
-
import {
|
|
10
|
+
import { declarativeTopologyRefusal, LOCK_WAIT_ENV, parseDuration } from "../config/settings.js";
|
|
11
|
+
import { policyAtBase } from "../config/base.js";
|
|
12
|
+
import { newerCommit, STAND_DOWN_ENV, standDownFromEnv, standDownLine } from "../versioned/stand-down.js";
|
|
13
|
+
import { describeServer, probeServer } from "../server.js";
|
|
14
|
+
import { eventFromRun, sendEvent } from "../notify/index.js";
|
|
15
|
+
import { EXIT_OUTSTANDING, EXIT_REFUSED } from "../exit.js";
|
|
16
|
+
import { enforcePromotion, forecastWave, planWave, readWavePlan, verifyWaveDecision, WAVES_SPEC_FILE, writeWavePlan } from "../waves/index.js";
|
|
17
|
+
import { ALLOW_OUT_OF_ORDER_ENV, APPROVED_PLAN_ENV, checksDue, checksRefusal, describeResults, judgePlan, judgeRevert, policyRefusal, runChecks, currentDigest, currentRevert, executeApply, executeRevert, revertFromEnv, findMigrateOp, gitLedger, verifyApproval, hasMigrations, inspect, readChain, recordStepExit, closeVersioned, openVersioned, renderStatus, runMigrateOp, selectToApply, statusJson, statusReport, } from "../versioned/index.js";
|
|
12
18
|
/** When the gate's newest approval does not hold: what moved since (or that approval was asked for again), in one line. */
|
|
13
19
|
async function movedSinceApproval(project, venv, ordered, op, digest) {
|
|
14
20
|
try {
|
|
@@ -31,7 +37,18 @@ async function movedSinceApproval(project, venv, ordered, op, digest) {
|
|
|
31
37
|
return undefined;
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
|
-
|
|
40
|
+
/** The document `yodel apply --json` prints (schemas/apply.schema.json). */
|
|
41
|
+
export function applyJson(doc) {
|
|
42
|
+
if (doc.mode === "versioned")
|
|
43
|
+
return { ...statusJson(doc.report), mode: "versioned", ...doc.outcome };
|
|
44
|
+
if (doc.mode === "execute")
|
|
45
|
+
return { environment: doc.environment, status: doc.result.stoodDown ? "stood-down" : "applied", planDigest: doc.planDigest, ...(doc.wavePlanDigest ? { wavePlanDigest: doc.wavePlanDigest } : {}), ...doc.result };
|
|
46
|
+
if (doc.mode === "wave-plan")
|
|
47
|
+
return { ...doc.plan };
|
|
48
|
+
return { environment: doc.environment, dialect: doc.dialect, ...(doc.server ? { server: doc.server } : {}), op: doc.op, ...doc.outcome };
|
|
49
|
+
}
|
|
50
|
+
const usage = "usage: yodel apply <env> [--json] [--dir <project>] [--op <name>] [--allow-out-of-order] [--plan] [--lock-wait <duration>] [--stand-down]";
|
|
51
|
+
const waveModes = ["wave-plan", "wave-apply"];
|
|
35
52
|
const help = `${usage}
|
|
36
53
|
|
|
37
54
|
Two paths, chosen by the project:
|
|
@@ -46,7 +63,9 @@ Two paths, chosen by the project:
|
|
|
46
63
|
environment (chant run <op>), whose gate is bound to the plan digest: the
|
|
47
64
|
pending migrations' checksums, the topology, the history and the live
|
|
48
65
|
schema. A run that stops at the gate prints the chant approve command;
|
|
49
|
-
approve, then run yodel apply again.
|
|
66
|
+
approve, then run yodel apply again. At a terminal, yodel approve <env>
|
|
67
|
+
approves the same plan without the digest to copy: yodel approve <env>,
|
|
68
|
+
then yodel apply <env>. If anything the digest covers changes after
|
|
50
69
|
approval, the gate asks again.
|
|
51
70
|
|
|
52
71
|
A migration that failed part way resumes at the statement that failed; a
|
|
@@ -59,6 +78,24 @@ Two paths, chosen by the project:
|
|
|
59
78
|
pending migration the chain puts before an applied one is out of order
|
|
60
79
|
and is refused unless --allow-out-of-order; it is never skipped.
|
|
61
80
|
|
|
81
|
+
The policy in yodel.config.ts (policy.rules) is judged over the plan
|
|
82
|
+
before the gate and again under the lock: a rule that denies it refuses
|
|
83
|
+
(exit 4) naming the rule, unless someone policy.overriders lists recorded
|
|
84
|
+
an override for exactly this plan digest (yodel override <env> --rule <id>
|
|
85
|
+
--reason <text>). The history records the overrides a run relied on.
|
|
86
|
+
|
|
87
|
+
A migration's pre-migration checks (checks in its migration.json, read-only
|
|
88
|
+
queries) run against the environment: the first pending migration's before
|
|
89
|
+
the gate, and every migration's under the lock just before its first
|
|
90
|
+
statement. One that fails refuses (exit 4), naming it, and nothing of that
|
|
91
|
+
migration is applied.
|
|
92
|
+
|
|
93
|
+
<env> can be one tenant of a tenant set, <env>/<tenant> (yodel.config.ts
|
|
94
|
+
environments.<env>.tenants): the environment's profile on the tenant's
|
|
95
|
+
database or schema, each statement with the template's name replaced by
|
|
96
|
+
the tenant's, and a history of its own. A tenant set is applied tenant by
|
|
97
|
+
tenant, or by its wave, which gates once for every tenant.
|
|
98
|
+
|
|
62
99
|
- Declarative: no migrations. Plans the declared schema against the live
|
|
63
100
|
database, then runs the project's ApplyOp for the environment behind its
|
|
64
101
|
plan-bound gate.
|
|
@@ -69,16 +106,37 @@ Options:
|
|
|
69
106
|
--allow-out-of-order versioned: apply out-of-order migrations too
|
|
70
107
|
--plan versioned: print the pending migrations and the plan
|
|
71
108
|
digest, apply nothing
|
|
109
|
+
--lock-wait <d> versioned: when another apply holds the lock, wait up
|
|
110
|
+
to <d> (90, 30s, 10m, 1h) for it, naming the holder,
|
|
111
|
+
before giving up with exit 5. Default: yodel.config.ts
|
|
112
|
+
lockWait (seconds), else YODEL_LOCK_WAIT, else 0
|
|
113
|
+
--stand-down versioned: when the branch this run is for has a newer
|
|
114
|
+
commit on origin (GITHUB_REF_NAME, CI_COMMIT_BRANCH,
|
|
115
|
+
else the checked-out branch), apply nothing and exit 0:
|
|
116
|
+
that commit's own apply applies this environment.
|
|
117
|
+
Asked before the lock and while waiting for it. Also
|
|
118
|
+
YODEL_STAND_DOWN=1
|
|
72
119
|
-C, --dir <dir> the project directory (default: the working directory)
|
|
73
120
|
|
|
74
121
|
The migrations Op runs two steps of its own, not for direct use:
|
|
75
122
|
--digest print the plan digest alone (the Op's Plan step)
|
|
76
|
-
--execute apply under the lock, if the digest is still the one in
|
|
77
|
-
and chant's gate ledger holds an approval
|
|
78
|
-
(the Op's Apply step)
|
|
123
|
+
--execute apply under the lock, if the digest is still the one in
|
|
124
|
+
${APPROVED_PLAN_ENV} and chant's gate ledger holds an approval
|
|
125
|
+
of the Op's gate for it (the Op's Apply step)
|
|
126
|
+
|
|
127
|
+
The apply pipeline's wave jobs (chant run wave, ${WAVES_SPEC_FILE}) run two more:
|
|
128
|
+
--wave-plan <file> write the environment's wave plan to <file>: the
|
|
129
|
+
plan digest, the gate policy at the base commit, and
|
|
130
|
+
whether a pending step is destructive
|
|
131
|
+
--wave-apply <file> apply the plan in <file> under the lock, if it is
|
|
132
|
+
still the plan now and chant's decision for the
|
|
133
|
+
wave lets it through (an approval of the wave's
|
|
134
|
+
digest, or a policy at the base commit that needs none)
|
|
135
|
+
--spec <file> the waves spec (default ${WAVES_SPEC_FILE})
|
|
79
136
|
|
|
80
137
|
Exit codes:
|
|
81
|
-
0 applied, or nothing to apply
|
|
138
|
+
0 applied, or nothing to apply, or (--stand-down) stood down for a newer
|
|
139
|
+
commit, applying nothing
|
|
82
140
|
2 a change needs its own Op (declarative), or a pending migration has a
|
|
83
141
|
manual step or an Op step apply does not run (versioned: it runs
|
|
84
142
|
ClickHouseRebuildOp in materialized-view mode and backfill steps on
|
|
@@ -86,9 +144,13 @@ Exit codes:
|
|
|
86
144
|
nothing was applied
|
|
87
145
|
3 stopped at the gate, waiting for approval of this plan
|
|
88
146
|
4 versioned: refused, nothing applied: a checksum mismatch, an applied
|
|
89
|
-
migration gone from the directory,
|
|
90
|
-
without --allow-out-of-order
|
|
91
|
-
|
|
147
|
+
migration gone from the directory, an out-of-order migration
|
|
148
|
+
without --allow-out-of-order, a pre-migration check that failed, a
|
|
149
|
+
policy rule that denies the plan with no override, or a pending
|
|
150
|
+
migration the environment this one requires (yodel.config.ts
|
|
151
|
+
waves[].requires, default the wave before) does not record as applied
|
|
152
|
+
with the same checksum
|
|
153
|
+
5 versioned: another apply holds the lock (after --lock-wait, if given)
|
|
92
154
|
1 anything else failed: a statement (the history records where; rerun to
|
|
93
155
|
resume), a plan that moved since approval, a bad config
|
|
94
156
|
`;
|
|
@@ -107,6 +169,11 @@ function parseApplyArgs(args) {
|
|
|
107
169
|
plan: { type: "boolean" },
|
|
108
170
|
digest: { type: "boolean" },
|
|
109
171
|
execute: { type: "boolean" },
|
|
172
|
+
"lock-wait": { type: "string" },
|
|
173
|
+
"stand-down": { type: "boolean" },
|
|
174
|
+
"wave-plan": { type: "string" },
|
|
175
|
+
"wave-apply": { type: "string" },
|
|
176
|
+
spec: { type: "string" },
|
|
110
177
|
},
|
|
111
178
|
});
|
|
112
179
|
}
|
|
@@ -118,7 +185,8 @@ function parseApplyArgs(args) {
|
|
|
118
185
|
if (env === undefined || rest.length > 0)
|
|
119
186
|
throw new YodelError(usage);
|
|
120
187
|
const flag = (k) => values[k] === true;
|
|
121
|
-
const
|
|
188
|
+
const str = (k) => (typeof values[k] === "string" ? values[k] : undefined);
|
|
189
|
+
const modes = [...["plan", "digest", "execute"].filter(flag), ...waveModes.filter((k) => str(k) !== undefined)];
|
|
122
190
|
if (modes.length > 1)
|
|
123
191
|
throw new YodelError(`--${modes.join(" and --")} cannot be combined\n${usage}`);
|
|
124
192
|
return {
|
|
@@ -130,47 +198,196 @@ function parseApplyArgs(args) {
|
|
|
130
198
|
plan: flag("plan"),
|
|
131
199
|
digest: flag("digest"),
|
|
132
200
|
execute: flag("execute"),
|
|
201
|
+
lockWaitMs: typeof values["lock-wait"] === "string" ? parseDuration(values["lock-wait"], "--lock-wait") : undefined,
|
|
202
|
+
standDown: flag("stand-down") || standDownFromEnv(process.env),
|
|
203
|
+
wavePlan: str("wave-plan"),
|
|
204
|
+
waveApply: str("wave-apply"),
|
|
205
|
+
spec: str("spec") ?? WAVES_SPEC_FILE,
|
|
133
206
|
};
|
|
134
207
|
}
|
|
135
208
|
/** `yodel apply <env>`; the exit codes are in `help`. */
|
|
136
209
|
export const apply = {
|
|
137
210
|
name: "apply",
|
|
138
|
-
summary: "bring an environment to
|
|
211
|
+
summary: "bring an environment to its declared schema or pending migrations",
|
|
139
212
|
implemented: true,
|
|
140
|
-
run: (args, io) =>
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
213
|
+
run: async (args, io) => {
|
|
214
|
+
const code = await guarded("apply", io, args, help, async () => {
|
|
215
|
+
const a = parseApplyArgs(args);
|
|
216
|
+
const project = await loadProject(a.dir);
|
|
217
|
+
const env = resolveEnvironment(project, a.env);
|
|
218
|
+
if (!env.tenant && project.yodel?.config.environments?.[env.name]?.tenants) {
|
|
219
|
+
throw new YodelError(`${env.name} is a tenant set (yodel.config.ts environments.${env.name}.tenants): apply a tenant, yodel apply ${env.name}/<tenant>, or every tenant through ${env.name}'s wave in the apply pipeline`);
|
|
220
|
+
}
|
|
221
|
+
if (hasMigrations(project))
|
|
222
|
+
return versionedApply(project, env, a, io);
|
|
223
|
+
if (a.plan || a.digest || a.execute || a.allowOutOfOrder || a.lockWaitMs !== undefined || args.includes("--stand-down") || a.wavePlan !== undefined || a.waveApply !== undefined) {
|
|
224
|
+
throw new YodelError(`--plan, --digest, --execute, --wave-plan, --wave-apply, --allow-out-of-order, --lock-wait and --stand-down are for versioned migrations, and ${project.dir} has no migrations/ directory with a migration`);
|
|
225
|
+
}
|
|
226
|
+
const refusal = project.dialect === "clickhouse" ? declarativeTopologyRefusal(project, env.name) : undefined;
|
|
227
|
+
if (refusal)
|
|
228
|
+
throw refusal;
|
|
229
|
+
return declarativeApply(project, env, a, io);
|
|
230
|
+
});
|
|
231
|
+
if (args.includes("--execute"))
|
|
232
|
+
recordStepExit(code);
|
|
233
|
+
return code;
|
|
234
|
+
},
|
|
154
235
|
};
|
|
155
236
|
async function versionedApply(project, env, a, io) {
|
|
156
|
-
const ordered =
|
|
237
|
+
const ordered = readChain(project);
|
|
157
238
|
const venv = await openVersioned(project, env);
|
|
239
|
+
const outcome = {};
|
|
240
|
+
let code;
|
|
158
241
|
try {
|
|
159
|
-
|
|
242
|
+
code = await versionedApplyWith(project, env, a, io, ordered, venv, outcome);
|
|
160
243
|
}
|
|
161
|
-
|
|
244
|
+
catch (e) {
|
|
245
|
+
// The wave modes run inside chant run wave, which keeps no outcome of its own: a refusal or failure there is told here (#87).
|
|
246
|
+
if (a.wavePlan !== undefined || a.waveApply !== undefined)
|
|
247
|
+
outcome.doc = waveFailure(env.name, e);
|
|
162
248
|
await closeVersioned(venv);
|
|
249
|
+
if (outcome.doc)
|
|
250
|
+
await sendEvent(project.yodel?.config.notify, eventFromRun("apply", outcome.doc), { io, verb: "apply" });
|
|
251
|
+
throw e;
|
|
163
252
|
}
|
|
253
|
+
await closeVersioned(venv);
|
|
254
|
+
// Waiting, refused or failed: the webhook in yodel.config.ts notify hears of it (#61).
|
|
255
|
+
if (outcome.doc)
|
|
256
|
+
await sendEvent(project.yodel?.config.notify, eventFromRun("apply", outcome.doc), { io, verb: "apply" });
|
|
257
|
+
return code;
|
|
258
|
+
}
|
|
259
|
+
/** The outcome a wave mode's error is: refused (exit 4, or 3 for a wave with no decision that lets it through; nothing applied) or failed. */
|
|
260
|
+
export function waveFailure(environment, e) {
|
|
261
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
262
|
+
const exitCode = e instanceof YodelError ? e.exitCode : 1;
|
|
263
|
+
return exitCode === EXIT_REFUSED || exitCode === EXIT_OUTSTANDING ? { environment, status: "refused", refusal: { message } } : { environment, status: "failed", exitCode, reason: message };
|
|
164
264
|
}
|
|
165
|
-
async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
265
|
+
async function versionedApplyWith(project, env, a, io, ordered, venv, outcome = {}) {
|
|
266
|
+
// yodel revert (#55) runs this Op with the migration to revert in the environment: its Plan and Apply steps plan and run that instead.
|
|
267
|
+
const revert = a.digest || a.execute ? revertFromEnv(process.env, project.dir) : undefined;
|
|
268
|
+
if (revert && a.digest) {
|
|
269
|
+
io.out(`${(await currentRevert(venv, ordered, revert.id, revert.step)).digest}\n`);
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
if (revert && a.execute) {
|
|
273
|
+
const approved = process.env[APPROVED_PLAN_ENV];
|
|
274
|
+
if (!approved)
|
|
275
|
+
throw new YodelError(`--execute reverts only the plan its Op's gate approved, and ${APPROVED_PLAN_ENV} is not set; run yodel revert ${env.name} ${revert.id}`);
|
|
276
|
+
const op = await findMigrateOp(project, env, a.op);
|
|
277
|
+
const ledger = await gitLedger(project.dir);
|
|
278
|
+
// The policy over the revert (#97), as the base commit has it (#73), as yodel revert judged it before the gate.
|
|
279
|
+
const { policy: basePolicy, note: policyNote } = await policyAtBase(project);
|
|
280
|
+
if (policyNote)
|
|
281
|
+
io.err(`${policyNote}\n`);
|
|
282
|
+
const plan = await executeRevert(venv, ordered, {
|
|
283
|
+
id: revert.id,
|
|
284
|
+
step: revert.step,
|
|
285
|
+
approvedDigest: approved,
|
|
286
|
+
...(a.lockWaitMs !== undefined ? { lockWaitMs: a.lockWaitMs } : {}),
|
|
287
|
+
approve: async (digest) => {
|
|
288
|
+
const by = await verifyApproval(ledger, op, digest);
|
|
289
|
+
io.err(`approval: ${op.name} / ${op.gate} for ${digest}, by ${by.resolvedBy} at ${by.timestamp}\n`);
|
|
290
|
+
},
|
|
291
|
+
...(basePolicy
|
|
292
|
+
? {
|
|
293
|
+
policy: async (digest, planned) => {
|
|
294
|
+
const results = await judgeRevert({ policy: basePolicy, environment: env.name, dialect: project.dialect, plan: planned, digest, ledger: () => Promise.resolve({ port: ledger, op }) });
|
|
295
|
+
const refusal = policyRefusal(results, env.name, digest, "under the lock", { id: revert.id, step: revert.step?.file });
|
|
296
|
+
if (refusal)
|
|
297
|
+
throw refusal;
|
|
298
|
+
for (const line of describeResults(results))
|
|
299
|
+
io.err(`${line}\n`);
|
|
300
|
+
return results.flatMap((r) => (r.override ? [r.override] : []));
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
: {}),
|
|
304
|
+
log: (l) => { io.err(`${l}\n`); },
|
|
305
|
+
});
|
|
306
|
+
io.out(`Reverted: ${plan.migration.id}, back to ${plan.to ?? "an empty schema"}\n`);
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
166
309
|
if (a.digest) {
|
|
167
310
|
const { status } = await inspect(venv, ordered);
|
|
168
311
|
const toApply = selectToApply(status, a.allowOutOfOrder);
|
|
169
312
|
io.out(`${await currentDigest(venv, status, toApply)}\n`);
|
|
170
313
|
return 0;
|
|
171
314
|
}
|
|
315
|
+
if (a.wavePlan !== undefined) {
|
|
316
|
+
const plan = await writeWavePlan(project, venv, ordered, a.wavePlan, { allowOutOfOrder: a.allowOutOfOrder });
|
|
317
|
+
io.err(`${env.name}: ${plan.empty ? "nothing to apply" : `${String(plan.pending.length)} pending (${plan.pending.join(", ")})`}; gate ${plan.gate} at ${plan.base.slice(0, 12)}` +
|
|
318
|
+
`${plan.destructive ? `; destructive: ${plan.destructiveSteps.map((d) => `${d.migration} step ${String(d.step)} (${d.why})`).join(", ")}` : ""}; plan ${plan.planDigest}\n`);
|
|
319
|
+
// chant decides the wave right after this; tell the webhook now when it will wait (#87), since chant sends nothing.
|
|
320
|
+
const forecast = await forecastWave(process.cwd(), a.spec, plan);
|
|
321
|
+
if (forecast?.waits) {
|
|
322
|
+
// A tenant set's wave (#56) is approved once for every tenant, with chant approve.
|
|
323
|
+
const how = env.tenant ? `for every tenant of ${env.tenant.env}. Approve it with: ` : `. Approve it with yodel approve ${env.name}, or: `;
|
|
324
|
+
io.err(`${env.name}: wave ${String(forecast.wave)} waits for an approval of ${forecast.digest} (gate ${forecast.policy} at ${forecast.base.slice(0, 12)})${how}${forecast.approve}\n`);
|
|
325
|
+
outcome.doc = { environment: env.name, status: "gated", pending: plan.pending.map((id) => ({ id })), gatePlanDigest: forecast.digest, approve: forecast.approve };
|
|
326
|
+
}
|
|
327
|
+
if (a.json)
|
|
328
|
+
io.out(`${JSON.stringify(applyJson({ mode: "wave-plan", plan }), null, 2)}\n`);
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
if (a.waveApply !== undefined) {
|
|
332
|
+
const planned = readWavePlan(a.waveApply);
|
|
333
|
+
const { policy: basePolicy, note: policyNote } = await policyAtBase(project);
|
|
334
|
+
if (policyNote)
|
|
335
|
+
io.err(`${policyNote}\n`);
|
|
336
|
+
if (planned.environment !== env.name)
|
|
337
|
+
throw new YodelError(`refused: ${a.waveApply} is ${planned.environment}'s plan, not ${env.name}'s; nothing was applied`, EXIT_REFUSED);
|
|
338
|
+
const cwd = process.cwd();
|
|
339
|
+
let wave;
|
|
340
|
+
const result = await executeApply(venv, ordered, {
|
|
341
|
+
runNote: () => (wave ? { wave } : undefined),
|
|
342
|
+
approvedDigest: planned.migrations,
|
|
343
|
+
allowOutOfOrder: a.allowOutOfOrder,
|
|
344
|
+
...(a.lockWaitMs !== undefined ? { lockWaitMs: a.lockWaitMs } : {}),
|
|
345
|
+
...(a.standDown ? { standDown: standingDown(project, env) } : {}),
|
|
346
|
+
...(basePolicy
|
|
347
|
+
? {
|
|
348
|
+
policy: async (digest, toApply) => {
|
|
349
|
+
const ledger = async () => {
|
|
350
|
+
try {
|
|
351
|
+
return { port: await gitLedger(project.dir), op: await findMigrateOp(project, env, a.op) };
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const results = await judgePlan({ policy: basePolicy, environment: env.name, dialect: project.dialect, toApply, digest, ledger });
|
|
358
|
+
const refusal = policyRefusal(results, env.name, digest, "under the lock");
|
|
359
|
+
if (refusal)
|
|
360
|
+
throw refusal;
|
|
361
|
+
for (const line of describeResults(results))
|
|
362
|
+
io.err(`${line}\n`);
|
|
363
|
+
return results.flatMap((r) => (r.override ? [r.override] : []));
|
|
364
|
+
},
|
|
365
|
+
}
|
|
366
|
+
: {}),
|
|
367
|
+
approve: async () => {
|
|
368
|
+
// Under the lock: the wave plan now, and chant's decision for it.
|
|
369
|
+
const now = await planWave(project, venv, ordered, { allowOutOfOrder: a.allowOutOfOrder });
|
|
370
|
+
if (now.planDigest !== planned.planDigest) {
|
|
371
|
+
throw new YodelError(`refused: ${env.name}'s wave plan moved since the wave was decided (decided ${planned.planDigest}, now ${now.planDigest}); nothing was applied. Run the wave again.`, 1);
|
|
372
|
+
}
|
|
373
|
+
const verified = await verifyWaveDecision(cwd, a.spec, env.name, planned.planDigest);
|
|
374
|
+
wave = verified.wave;
|
|
375
|
+
io.err(`${verified.line}\n`);
|
|
376
|
+
},
|
|
377
|
+
log: (l) => { io.err(`${l}\n`); },
|
|
378
|
+
silences: migrationSilences,
|
|
379
|
+
});
|
|
380
|
+
if (a.json)
|
|
381
|
+
io.out(`${JSON.stringify(applyJson({ mode: "execute", environment: env.name, planDigest: planned.migrations, wavePlanDigest: planned.planDigest, result }), null, 2)}\n`);
|
|
382
|
+
else
|
|
383
|
+
io.out(result.stoodDown ? `${result.stoodDown}\n` : result.applied.length === 0 ? "Nothing to apply.\n" : `Applied: ${result.applied.join(", ")}\n`);
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
172
386
|
if (a.execute) {
|
|
173
387
|
const approved = process.env[APPROVED_PLAN_ENV];
|
|
388
|
+
const { policy: basePolicy, note: policyNote } = await policyAtBase(project);
|
|
389
|
+
if (policyNote)
|
|
390
|
+
io.err(`${policyNote}\n`);
|
|
174
391
|
if (!approved)
|
|
175
392
|
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}`);
|
|
176
393
|
// The env var is only the Op's hand-off; the approval itself is checked in chant's gate ledger, under the lock.
|
|
@@ -179,23 +396,41 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
|
179
396
|
const result = await executeApply(venv, ordered, {
|
|
180
397
|
approvedDigest: approved,
|
|
181
398
|
allowOutOfOrder: a.allowOutOfOrder,
|
|
399
|
+
...(a.lockWaitMs !== undefined ? { lockWaitMs: a.lockWaitMs } : {}),
|
|
400
|
+
...(a.standDown ? { standDown: standingDown(project, env) } : {}),
|
|
182
401
|
approve: async (digest) => {
|
|
402
|
+
// Under the lock: every pending migration ran first where this environment requires (exit 4 otherwise).
|
|
403
|
+
await enforcePromotion(project, env.name, ordered, (await inspect(venv, ordered)).status.pending);
|
|
183
404
|
const by = await verifyApproval(ledger, op, digest);
|
|
184
405
|
io.err(`approval: ${op.name} / ${op.gate} for ${digest}, by ${by.resolvedBy} at ${by.timestamp}\n`);
|
|
185
406
|
},
|
|
186
407
|
log: (l) => { io.err(`${l}\n`); },
|
|
187
408
|
silences: migrationSilences,
|
|
409
|
+
...(basePolicy
|
|
410
|
+
? {
|
|
411
|
+
policy: async (digest, toApply) => {
|
|
412
|
+
const results = await judgePlan({ policy: basePolicy, environment: env.name, dialect: project.dialect, toApply, digest, ledger: () => Promise.resolve({ port: ledger, op }) });
|
|
413
|
+
const refusal = policyRefusal(results, env.name, digest, "under the lock");
|
|
414
|
+
if (refusal)
|
|
415
|
+
throw refusal;
|
|
416
|
+
for (const line of describeResults(results))
|
|
417
|
+
io.err(`${line}\n`);
|
|
418
|
+
return results.flatMap((r) => (r.override ? [r.override] : []));
|
|
419
|
+
},
|
|
420
|
+
}
|
|
421
|
+
: {}),
|
|
188
422
|
});
|
|
189
423
|
if (a.json)
|
|
190
|
-
io.out(`${JSON.stringify({ environment: env.name,
|
|
424
|
+
io.out(`${JSON.stringify(applyJson({ mode: "execute", environment: env.name, planDigest: approved, result }), null, 2)}\n`);
|
|
191
425
|
else
|
|
192
|
-
io.out(result.applied.length === 0 ? "Nothing to apply.\n" : `Applied: ${result.applied.join(", ")}\n`);
|
|
426
|
+
io.out(result.stoodDown ? `${result.stoodDown}\n` : result.applied.length === 0 ? "Nothing to apply.\n" : `Applied: ${result.applied.join(", ")}\n`);
|
|
193
427
|
return 0;
|
|
194
428
|
}
|
|
195
429
|
const report = await statusReport(venv, ordered, { allowOutOfOrder: a.allowOutOfOrder });
|
|
196
430
|
const emit = (doc, code) => {
|
|
431
|
+
outcome.doc = applyJson({ mode: "versioned", report, outcome: doc });
|
|
197
432
|
if (a.json)
|
|
198
|
-
io.out(`${JSON.stringify(
|
|
433
|
+
io.out(`${JSON.stringify(outcome.doc, null, 2)}\n`);
|
|
199
434
|
return code;
|
|
200
435
|
};
|
|
201
436
|
if (a.plan) {
|
|
@@ -210,38 +445,112 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
|
210
445
|
}
|
|
211
446
|
if (report.status.pending.length === 0) {
|
|
212
447
|
if (!a.json)
|
|
213
|
-
io.out(`${env.name} is up to date: every migration is applied.\n`);
|
|
448
|
+
io.out(`${env.name}${report.server ? ` (${describeServer(report.server)})` : ""} is up to date: every migration is applied.\n`);
|
|
214
449
|
return emit({ status: "unchanged" }, 0);
|
|
215
450
|
}
|
|
451
|
+
const standDown = a.standDown ? standingDown(project, env) : undefined;
|
|
452
|
+
const stood = await standDown?.();
|
|
453
|
+
if (stood) {
|
|
454
|
+
if (!a.json)
|
|
455
|
+
io.out(`${stood}\n`);
|
|
456
|
+
return emit({ status: "stood-down", stoodDown: stood }, 0);
|
|
457
|
+
}
|
|
458
|
+
// Before the gate: a pending migration the required environment has not applied refuses, and nothing is asked.
|
|
459
|
+
try {
|
|
460
|
+
await enforcePromotion(project, env.name, ordered, report.status.pending);
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
if (!(e instanceof YodelError) || e.exitCode !== EXIT_REFUSED)
|
|
464
|
+
throw e;
|
|
465
|
+
if (!a.json)
|
|
466
|
+
io.err(`yodel apply: ${e.message}\n`);
|
|
467
|
+
return emit({ status: "refused", refusal: { exitCode: EXIT_REFUSED, message: e.message } }, EXIT_REFUSED);
|
|
468
|
+
}
|
|
469
|
+
// 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.
|
|
470
|
+
const first = report.status.pending[0];
|
|
471
|
+
let checked;
|
|
472
|
+
if (first && checksDue(first)) {
|
|
473
|
+
const outcomes = await runChecks(venv.driver, first.migration);
|
|
474
|
+
checked = { migration: first.id, checks: outcomes };
|
|
475
|
+
const refusal = checksRefusal(first.migration, outcomes, "before the gate");
|
|
476
|
+
if (refusal) {
|
|
477
|
+
if (!a.json)
|
|
478
|
+
io.err(`yodel apply: ${refusal.message}\n`);
|
|
479
|
+
return emit({ status: "refused", preChecks: checked }, refusal.exitCode);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const op = await findMigrateOp(project, env, a.op);
|
|
483
|
+
// The policy (#53), as the base commit has it (#73): a rule that denies this plan, with no override recorded for its digest, refuses before the gate.
|
|
484
|
+
const basePolicy = await policyAtBase(project);
|
|
485
|
+
if (basePolicy.note && !a.json)
|
|
486
|
+
io.err(`${basePolicy.note}\n`);
|
|
487
|
+
const policy = await judgePlan({
|
|
488
|
+
policy: basePolicy.policy,
|
|
489
|
+
environment: env.name,
|
|
490
|
+
dialect: project.dialect,
|
|
491
|
+
toApply: report.status.pending,
|
|
492
|
+
digest: report.digest,
|
|
493
|
+
ledger: async () => ({ port: await gitLedger(project.dir), op }),
|
|
494
|
+
});
|
|
495
|
+
const denied = policyRefusal(policy, env.name, report.digest, "before the gate");
|
|
496
|
+
if (denied) {
|
|
497
|
+
if (!a.json)
|
|
498
|
+
io.err(`yodel apply: ${denied.message}\n`);
|
|
499
|
+
return emit({ status: "denied", policy }, denied.exitCode);
|
|
500
|
+
}
|
|
216
501
|
if (!a.json)
|
|
217
502
|
io.out(renderStatus(report));
|
|
218
|
-
|
|
503
|
+
if (!a.json && checked)
|
|
504
|
+
for (const o of checked.checks)
|
|
505
|
+
io.out(`check ${o.name} of ${checked.migration} passed (${o.detail}); it runs again under the lock before the first statement\n`);
|
|
506
|
+
if (!a.json)
|
|
507
|
+
for (const line of describeResults(policy))
|
|
508
|
+
io.out(`${line}\n`);
|
|
219
509
|
if (!a.json)
|
|
220
510
|
io.out(`\nRunning ${op.name} (${op.filePath})\n`);
|
|
221
|
-
|
|
511
|
+
// The Op's Apply step (yodel apply --execute) takes these from its environment.
|
|
512
|
+
const childEnv = {
|
|
513
|
+
...process.env,
|
|
514
|
+
...(a.lockWaitMs !== undefined ? { [LOCK_WAIT_ENV]: `${String(a.lockWaitMs)}ms` } : {}),
|
|
515
|
+
...(a.standDown ? { [STAND_DOWN_ENV]: "1" } : {}),
|
|
516
|
+
};
|
|
517
|
+
const run = await runMigrateOp(project, op, { allowOutOfOrder: a.allowOutOfOrder, env: childEnv, onOutput: (t) => { io.err(t); } });
|
|
222
518
|
if (run.status === "gated") {
|
|
223
519
|
const stale = await movedSinceApproval(project, venv, ordered, op, run.planDigest ?? report.digest);
|
|
224
520
|
if (stale && !a.json)
|
|
225
521
|
io.out(`\n${stale.line}\n`);
|
|
226
522
|
if (!a.json) {
|
|
227
523
|
io.out(`\n${op.name} is waiting at gate "${op.gate}" for approval of this plan${run.planDigest ? ` (${run.planDigest})` : ""}.\n`);
|
|
228
|
-
if (run.approve)
|
|
229
|
-
io.out(`Approve it with:\n ${run.approve
|
|
524
|
+
if (run.approve) {
|
|
525
|
+
io.out(`Approve it with:\n ${runnable(run.approve, project.dir)}\n` +
|
|
526
|
+
`or, at a terminal, with ${runnable(`yodel approve ${env.name}`, project.dir)}, which shows the plan and asks you first;\n` +
|
|
527
|
+
`then run ${runnable(`yodel apply ${env.name}`, project.dir)} again.\n`);
|
|
528
|
+
}
|
|
230
529
|
}
|
|
231
530
|
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);
|
|
232
531
|
}
|
|
233
532
|
const after = await statusReport(venv, ordered, { allowOutOfOrder: a.allowOutOfOrder });
|
|
234
533
|
if (run.status === "failed") {
|
|
534
|
+
// 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.
|
|
535
|
+
const code = run.stepExit !== undefined && run.stepExit !== 0 ? run.stepExit : 1;
|
|
235
536
|
if (!a.json) {
|
|
236
|
-
io.err(`yodel apply: chant run ${op.name} failed (exit ${String(run.code)})\n`);
|
|
537
|
+
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`);
|
|
237
538
|
io.out(`\n${renderStatus(after)}`);
|
|
238
539
|
}
|
|
540
|
+
outcome.doc = applyJson({ mode: "versioned", report: after, outcome: { status: "failed", op: op.name, exitCode: code, chantExitCode: run.code } });
|
|
239
541
|
if (a.json)
|
|
240
|
-
io.out(`${JSON.stringify(
|
|
241
|
-
return
|
|
542
|
+
io.out(`${JSON.stringify(outcome.doc, null, 2)}\n`);
|
|
543
|
+
return code;
|
|
544
|
+
}
|
|
545
|
+
// 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.
|
|
546
|
+
const stoodLate = after.status.pending.length > 0 ? await standDown?.() : undefined;
|
|
547
|
+
if (stoodLate) {
|
|
548
|
+
if (!a.json)
|
|
549
|
+
io.out(`\n${stoodLate}\n`);
|
|
550
|
+
return emit({ status: "stood-down", stoodDown: stoodLate, op: op.name }, 0);
|
|
242
551
|
}
|
|
243
552
|
if (a.json) {
|
|
244
|
-
io.out(`${JSON.stringify({
|
|
553
|
+
io.out(`${JSON.stringify(applyJson({ mode: "versioned", report: after, outcome: { status: "applied", op: op.name, appliedNow: report.status.pending.map((p) => p.id) } }), null, 2)}\n`);
|
|
245
554
|
}
|
|
246
555
|
else {
|
|
247
556
|
io.out(`\nApplied: ${report.status.pending.map((p) => p.id).join(", ")}.\n`);
|
|
@@ -249,18 +558,29 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
|
249
558
|
return after.status.pending.length === 0 ? 0 : 1;
|
|
250
559
|
}
|
|
251
560
|
async function declarativeApply(project, env, a, io) {
|
|
252
|
-
const json = a.json;
|
|
253
561
|
const op = await findApplyOp(project, env, a.op);
|
|
562
|
+
let server;
|
|
563
|
+
let printed;
|
|
254
564
|
const report = (doc, code) => {
|
|
255
|
-
|
|
256
|
-
|
|
565
|
+
printed = applyJson({ mode: "declarative", environment: env.name, dialect: project.dialect, server, op: op.name, outcome: doc });
|
|
566
|
+
if (a.json)
|
|
567
|
+
io.out(`${JSON.stringify(printed, null, 2)}\n`);
|
|
257
568
|
return code;
|
|
258
569
|
};
|
|
570
|
+
const code = await declarativeRun(project, env, a, io, op, report, (s) => (server = s));
|
|
571
|
+
if (printed)
|
|
572
|
+
await sendEvent(project.yodel?.config.notify, eventFromRun("apply", printed), { io, verb: "apply" });
|
|
573
|
+
return code;
|
|
574
|
+
}
|
|
575
|
+
async function declarativeRun(project, env, a, io, op, report, setServer) {
|
|
576
|
+
const json = a.json;
|
|
259
577
|
const build = await buildSchema(project);
|
|
260
578
|
try {
|
|
261
579
|
const before = await planEnvironment(project, env, build);
|
|
580
|
+
const server = await probeServer(project, env);
|
|
581
|
+
setServer(server);
|
|
262
582
|
if (!json)
|
|
263
|
-
io.out(`${await renderPlan(before, `Plan for ${env.name} (${project.dialect}, sql.profiles.${env.name})`)}\n`);
|
|
583
|
+
io.out(`${await renderPlan(before, `Plan for ${env.name} (${server ? describeServer(server) : project.dialect}, sql.profiles.${env.name})`)}\n`);
|
|
264
584
|
if (planExitCode(before) === PLAN_NEEDS_OP) {
|
|
265
585
|
if (!json)
|
|
266
586
|
io.err(`yodel apply: refused: ${String(before.refused.length)} change(s) need their own Op; nothing was applied\n`);
|
|
@@ -278,7 +598,7 @@ async function declarativeApply(project, env, a, io) {
|
|
|
278
598
|
if (!json) {
|
|
279
599
|
io.out(`\n${op.name} is waiting at gate "${op.gate}" for approval of this plan.\n`);
|
|
280
600
|
if (run.approve)
|
|
281
|
-
io.out(`Approve it with:\n ${run.approve}\nthen run yodel apply ${env.name} again.\n`);
|
|
601
|
+
io.out(`Approve it with:\n ${runnable(run.approve, project.dir)}\nthen run ${runnable(`yodel apply ${env.name}`, project.dir)} again.\n`);
|
|
282
602
|
}
|
|
283
603
|
return report({ status: "gated", gate: op.gate, ...(run.approve ? { approve: run.approve } : {}), ...(run.planDigest ? { planDigest: run.planDigest } : {}), plan: planJson(before) }, GATED);
|
|
284
604
|
}
|
|
@@ -303,4 +623,11 @@ async function declarativeApply(project, env, a, io) {
|
|
|
303
623
|
build.dispose();
|
|
304
624
|
}
|
|
305
625
|
}
|
|
626
|
+
/** `--stand-down`'s question: the line to stand down with, when a newer commit on the run's branch will apply `env`. */
|
|
627
|
+
function standingDown(project, env) {
|
|
628
|
+
return async () => {
|
|
629
|
+
const n = await newerCommit(project.dir);
|
|
630
|
+
return n ? standDownLine(env.name, n) : undefined;
|
|
631
|
+
};
|
|
632
|
+
}
|
|
306
633
|
//# sourceMappingURL=apply.js.map
|