@intentius/sql-yodeler 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/apply-steps/index.d.ts +65 -0
- package/dist/apply-steps/index.js +106 -0
- package/dist/apply-steps/index.js.map +1 -0
- package/dist/approval-mode/index.d.ts +78 -0
- package/dist/approval-mode/index.js +105 -0
- package/dist/approval-mode/index.js.map +1 -0
- package/dist/chant/access.d.ts +87 -0
- package/dist/chant/access.js +169 -0
- package/dist/chant/access.js.map +1 -0
- package/dist/chant/build.js +7 -0
- package/dist/chant/build.js.map +1 -1
- package/dist/chant/drift.d.ts +17 -1
- package/dist/chant/drift.js +55 -2
- package/dist/chant/drift.js.map +1 -1
- package/dist/chant/plan.d.ts +18 -2
- package/dist/chant/plan.js +21 -3
- package/dist/chant/plan.js.map +1 -1
- package/dist/ci/render.d.ts +333 -0
- package/dist/ci/render.js +885 -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/cleanup/index.d.ts +86 -0
- package/dist/cleanup/index.js +207 -0
- package/dist/cleanup/index.js.map +1 -0
- package/dist/commands/apply.d.ts +113 -0
- package/dist/commands/apply.js +370 -30
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/approve.d.ts +13 -0
- package/dist/commands/approve.js +239 -13
- package/dist/commands/approve.js.map +1 -1
- 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 +130 -0
- package/dist/commands/ci.js.map +1 -0
- package/dist/commands/cleanup.d.ts +13 -0
- package/dist/commands/cleanup.js +138 -0
- package/dist/commands/cleanup.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.js +136 -0
- package/dist/commands/config.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 +13 -1
- package/dist/commands/drift.js +52 -5
- 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.js +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/lint.js +39 -7
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/new.js +133 -34
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/override.js +48 -16
- package/dist/commands/override.js.map +1 -1
- package/dist/commands/plan.js +47 -6
- package/dist/commands/plan.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 +32 -5
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/test.d.ts +3 -0
- package/dist/commands/test.js +164 -0
- package/dist/commands/test.js.map +1 -0
- 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 +240 -3
- package/dist/config/define.js +4 -0
- 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 +156 -7
- package/dist/config/load.js.map +1 -1
- package/dist/config/settings.d.ts +22 -1
- package/dist/config/settings.js +34 -2
- package/dist/config/settings.js.map +1 -1
- package/dist/credentials.d.ts +119 -0
- package/dist/credentials.js +119 -0
- package/dist/credentials.js.map +1 -0
- package/dist/exit.js +1 -1
- package/dist/exit.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 +8 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/init/adopt.js +1 -1
- package/dist/init/adopt.js.map +1 -1
- package/dist/init/clickhouse-access.d.ts +64 -0
- package/dist/init/clickhouse-access.js +211 -0
- package/dist/init/clickhouse-access.js.map +1 -0
- package/dist/init/live.d.ts +4 -0
- package/dist/init/live.js +16 -0
- package/dist/init/live.js.map +1 -1
- package/dist/lint/access.d.ts +18 -0
- package/dist/lint/access.js +34 -0
- package/dist/lint/access.js.map +1 -0
- package/dist/lint/check.js +38 -6
- package/dist/lint/check.js.map +1 -1
- package/dist/lint/config.d.ts +7 -1
- package/dist/lint/config.js +11 -5
- package/dist/lint/config.js.map +1 -1
- package/dist/lint/index.d.ts +2 -0
- package/dist/lint/index.js +2 -0
- package/dist/lint/index.js.map +1 -1
- package/dist/lint/naming.d.ts +62 -0
- package/dist/lint/naming.js +138 -0
- package/dist/lint/naming.js.map +1 -0
- 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 +51 -6
- package/dist/lint/rules.js +23 -6
- package/dist/lint/rules.js.map +1 -1
- package/dist/lint/schema-checks.d.ts +63 -0
- package/dist/lint/schema-checks.js +128 -0
- package/dist/lint/schema-checks.js.map +1 -0
- package/dist/lint/statements.d.ts +22 -1
- package/dist/lint/statements.js +40 -1
- package/dist/lint/statements.js.map +1 -1
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.js +1 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/migrations/checkpoint.d.ts +45 -0
- package/dist/migrations/checkpoint.js +108 -0
- package/dist/migrations/checkpoint.js.map +1 -0
- package/dist/migrations/checks.d.ts +19 -1
- package/dist/migrations/checks.js +15 -0
- package/dist/migrations/checks.js.map +1 -1
- package/dist/migrations/create.d.ts +13 -4
- package/dist/migrations/create.js +24 -7
- package/dist/migrations/create.js.map +1 -1
- package/dist/migrations/document.d.ts +10 -0
- package/dist/migrations/document.js +1 -0
- package/dist/migrations/document.js.map +1 -1
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.js +2 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/routines.d.ts +21 -0
- package/dist/migrations/routines.js +30 -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/model.d.ts +7 -1
- package/dist/pr/model.js +29 -7
- package/dist/pr/model.js.map +1 -1
- package/dist/pr/render.d.ts +4 -1
- package/dist/pr/render.js +49 -9
- package/dist/pr/render.js.map +1 -1
- package/dist/pr/tenants.d.ts +37 -0
- package/dist/pr/tenants.js +138 -0
- package/dist/pr/tenants.js.map +1 -0
- package/dist/probe.d.ts +27 -0
- package/dist/probe.js +106 -0
- package/dist/probe.js.map +1 -0
- package/dist/project.d.ts +35 -2
- package/dist/project.js +62 -2
- package/dist/project.js.map +1 -1
- package/dist/replay/clickhouse.d.ts +31 -0
- package/dist/replay/clickhouse.js +103 -6
- package/dist/replay/clickhouse.js.map +1 -1
- package/dist/replay/replay.d.ts +14 -3
- package/dist/replay/replay.js +62 -31
- 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/report/audit.d.ts +138 -0
- package/dist/report/audit.js +219 -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 +7 -2
- package/dist/schemas.js +7 -2
- package/dist/schemas.js.map +1 -1
- package/dist/sources/ddl.d.ts +48 -0
- package/dist/sources/ddl.js +53 -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 +40 -5
- package/dist/steps/backfill.js +128 -8
- package/dist/steps/backfill.js.map +1 -1
- package/dist/steps/index.d.ts +19 -1
- package/dist/steps/index.js +34 -7
- package/dist/steps/index.js.map +1 -1
- package/dist/steps/kinds.d.ts +42 -4
- package/dist/steps/kinds.js +88 -14
- package/dist/steps/kinds.js.map +1 -1
- package/dist/steps/postgres-migration.d.ts +38 -25
- package/dist/steps/postgres-migration.js +49 -32
- package/dist/steps/postgres-migration.js.map +1 -1
- package/dist/steps/rebuild.d.ts +15 -6
- package/dist/steps/rebuild.js +12 -9
- package/dist/steps/rebuild.js.map +1 -1
- package/dist/tenants/index.d.ts +41 -0
- package/dist/tenants/index.js +121 -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/tenants/wave.d.ts +128 -0
- package/dist/tenants/wave.js +254 -0
- package/dist/tenants/wave.js.map +1 -0
- package/dist/testing/define.d.ts +62 -0
- package/dist/testing/define.js +32 -0
- package/dist/testing/define.js.map +1 -0
- package/dist/testing/files.d.ts +5 -0
- package/dist/testing/files.js +18 -0
- package/dist/testing/files.js.map +1 -0
- package/dist/testing/report.d.ts +14 -0
- package/dist/testing/report.js +83 -0
- package/dist/testing/report.js.map +1 -0
- package/dist/testing/run.d.ts +104 -0
- package/dist/testing/run.js +328 -0
- package/dist/testing/run.js.map +1 -0
- package/dist/versioned/approval.d.ts +6 -0
- package/dist/versioned/approval.js +11 -5
- package/dist/versioned/approval.js.map +1 -1
- package/dist/versioned/checks.d.ts +8 -2
- package/dist/versioned/checks.js +34 -13
- package/dist/versioned/checks.js.map +1 -1
- package/dist/versioned/driver.d.ts +2 -2
- package/dist/versioned/engine.d.ts +16 -0
- package/dist/versioned/engine.js +20 -6
- package/dist/versioned/engine.js.map +1 -1
- package/dist/versioned/explain.d.ts +2 -0
- package/dist/versioned/explain.js +3 -1
- package/dist/versioned/explain.js.map +1 -1
- package/dist/versioned/index.d.ts +1 -0
- package/dist/versioned/index.js +1 -0
- package/dist/versioned/index.js.map +1 -1
- package/dist/versioned/op.d.ts +6 -1
- package/dist/versioned/op.js +7 -2
- package/dist/versioned/op.js.map +1 -1
- package/dist/versioned/policy.d.ts +33 -2
- package/dist/versioned/policy.js +59 -8
- package/dist/versioned/policy.js.map +1 -1
- package/dist/versioned/report.js +9 -0
- 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/state.d.ts +25 -0
- package/dist/versioned/state.js +37 -4
- package/dist/versioned/state.js.map +1 -1
- package/dist/waves/index.d.ts +224 -0
- package/dist/waves/index.js +353 -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 +34 -0
- package/dist/waves/record.js +20 -0
- package/dist/waves/record.js.map +1 -0
- package/package.json +11 -7
- package/schemas/apply.schema.json +162 -3
- package/schemas/checkpoint.schema.json +19 -0
- package/schemas/cleanup.schema.json +40 -0
- package/schemas/drift.schema.json +26 -1
- package/schemas/event.schema.json +36 -0
- package/schemas/lint-rules.schema.json +4 -0
- package/schemas/lint.schema.json +4 -0
- package/schemas/new.schema.json +41 -3
- package/schemas/plan.schema.json +157 -0
- package/schemas/report.schema.json +114 -0
- package/schemas/revert.schema.json +69 -0
- package/schemas/status.schema.json +64 -3
- package/schemas/test.schema.json +41 -0
package/dist/commands/apply.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
+
import { accessReview, clickhouseAccessRefusal, renderAccessReview } from "../chant/access.js";
|
|
2
3
|
import { guarded } from "../env-args.js";
|
|
3
4
|
import { loadProject, resolveEnvironment, YodelError } from "../project.js";
|
|
4
5
|
import { buildSchema } from "../chant/build.js";
|
|
5
|
-
import { declaredChanges, planEnvironment, planExitCode, planJson, renderPlan, PLAN_NEEDS_OP } from "../chant/plan.js";
|
|
6
|
+
import { declaredChanges, planEnvironment, planExitCode, planJson, renderPlan, unreadableOf, PLAN_NEEDS_OP } from "../chant/plan.js";
|
|
6
7
|
import { findApplyOp, GATED, runApplyOp } from "../chant/apply.js";
|
|
7
8
|
import { runnable } from "../chant/runnable.js";
|
|
8
9
|
import { migrationSilences } from "../lint/index.js";
|
|
9
10
|
import { askedAgainLine, describeMove, explainAgainst, latestApproval } from "../versioned/explain.js";
|
|
10
|
-
import { declarativeTopologyRefusal, LOCK_WAIT_ENV, parseDuration } from "../config/settings.js";
|
|
11
|
+
import { declarativeAccessRefusal, declarativeTopologyRefusal, LOCK_WAIT_ENV, parseDuration } from "../config/settings.js";
|
|
12
|
+
import { policyAtBase } from "../config/base.js";
|
|
11
13
|
import { newerCommit, STAND_DOWN_ENV, standDownFromEnv, standDownLine } from "../versioned/stand-down.js";
|
|
12
14
|
import { describeServer, probeServer } from "../server.js";
|
|
13
|
-
import {
|
|
15
|
+
import { eventFromRun, sendEvent } from "../notify/index.js";
|
|
16
|
+
import { EXIT_OUTSTANDING, EXIT_REFUSED } from "../exit.js";
|
|
17
|
+
import { describeStep, heldSteps, runApplySteps, stepsAtBase, stepsRefusal } from "../apply-steps/index.js";
|
|
18
|
+
import { migrationsApprovalAtBase, portFor } from "../approval-mode/index.js";
|
|
19
|
+
import { enforcePromotion, forecastWave, planWave, RECORD_PLANS_ENV, readWavePlan, verifyWaveDecision, WavePlanMoved, WAVES_SPEC_FILE, writeWavePlan } from "../waves/index.js";
|
|
20
|
+
import { applySlot, splitSlot, writeSlotPlan } from "../tenants/wave.js";
|
|
21
|
+
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";
|
|
14
22
|
/** When the gate's newest approval does not hold: what moved since (or that approval was asked for again), in one line. */
|
|
15
23
|
async function movedSinceApproval(project, venv, ordered, op, digest) {
|
|
16
24
|
try {
|
|
@@ -33,7 +41,18 @@ async function movedSinceApproval(project, venv, ordered, op, digest) {
|
|
|
33
41
|
return undefined;
|
|
34
42
|
}
|
|
35
43
|
}
|
|
44
|
+
/** The document `yodel apply --json` prints (schemas/apply.schema.json). */
|
|
45
|
+
export function applyJson(doc) {
|
|
46
|
+
if (doc.mode === "versioned")
|
|
47
|
+
return { ...statusJson(doc.report), mode: "versioned", ...doc.outcome };
|
|
48
|
+
if (doc.mode === "execute")
|
|
49
|
+
return { environment: doc.environment, status: doc.result.stoodDown ? "stood-down" : "applied", planDigest: doc.planDigest, ...(doc.wavePlanDigest ? { wavePlanDigest: doc.wavePlanDigest } : {}), ...doc.result };
|
|
50
|
+
if (doc.mode === "wave-plan")
|
|
51
|
+
return { ...doc.plan };
|
|
52
|
+
return { environment: doc.environment, dialect: doc.dialect, ...(doc.server ? { server: doc.server } : {}), op: doc.op, ...doc.outcome };
|
|
53
|
+
}
|
|
36
54
|
const usage = "usage: yodel apply <env> [--json] [--dir <project>] [--op <name>] [--allow-out-of-order] [--plan] [--lock-wait <duration>] [--stand-down]";
|
|
55
|
+
const waveModes = ["wave-plan", "wave-apply"];
|
|
37
56
|
const help = `${usage}
|
|
38
57
|
|
|
39
58
|
Two paths, chosen by the project:
|
|
@@ -75,6 +94,14 @@ Two paths, chosen by the project:
|
|
|
75
94
|
statement. One that fails refuses (exit 4), naming it, and nothing of that
|
|
76
95
|
migration is applied.
|
|
77
96
|
|
|
97
|
+
<env> can be one tenant of a tenant set, <env>/<tenant> (yodel.config.ts
|
|
98
|
+
environments.<env>.tenants): the environment's profile on the tenant's
|
|
99
|
+
database or schema, each statement with the template's name replaced by
|
|
100
|
+
the tenant's, and a history of its own. A tenant set is applied tenant by
|
|
101
|
+
tenant, or by its wave, which gates once for every tenant. The wave of a
|
|
102
|
+
set given by a query runs slots, <env>/@<i>of<n>, each every tenant the
|
|
103
|
+
query lists when it runs whose name hashes to it.
|
|
104
|
+
|
|
78
105
|
- Declarative: no migrations. Plans the declared schema against the live
|
|
79
106
|
database, then runs the project's ApplyOp for the environment behind its
|
|
80
107
|
plan-bound gate.
|
|
@@ -103,6 +130,16 @@ The migrations Op runs two steps of its own, not for direct use:
|
|
|
103
130
|
${APPROVED_PLAN_ENV} and chant's gate ledger holds an approval
|
|
104
131
|
of the Op's gate for it (the Op's Apply step)
|
|
105
132
|
|
|
133
|
+
The apply pipeline's wave jobs (chant run wave, ${WAVES_SPEC_FILE}) run two more:
|
|
134
|
+
--wave-plan <file> write the environment's wave plan to <file>: the
|
|
135
|
+
plan digest, the gate policy at the base commit, and
|
|
136
|
+
whether a pending step is destructive
|
|
137
|
+
--wave-apply <file> apply the plan in <file> under the lock, if it is
|
|
138
|
+
still the plan now and chant's decision for the
|
|
139
|
+
wave lets it through (an approval of the wave's
|
|
140
|
+
digest, or a policy at the base commit that needs none)
|
|
141
|
+
--spec <file> the waves spec (default ${WAVES_SPEC_FILE})
|
|
142
|
+
|
|
106
143
|
Exit codes:
|
|
107
144
|
0 applied, or nothing to apply, or (--stand-down) stood down for a newer
|
|
108
145
|
commit, applying nothing
|
|
@@ -114,8 +151,11 @@ Exit codes:
|
|
|
114
151
|
3 stopped at the gate, waiting for approval of this plan
|
|
115
152
|
4 versioned: refused, nothing applied: a checksum mismatch, an applied
|
|
116
153
|
migration gone from the directory, an out-of-order migration
|
|
117
|
-
without --allow-out-of-order, a pre-migration check that failed,
|
|
118
|
-
policy rule that denies the plan with no override
|
|
154
|
+
without --allow-out-of-order, a pre-migration check that failed, a
|
|
155
|
+
policy rule that denies the plan with no override, or a pending
|
|
156
|
+
migration the environment this one requires (yodel.config.ts
|
|
157
|
+
waves[].requires, default the wave before) does not record as applied
|
|
158
|
+
with the same checksum
|
|
119
159
|
5 versioned: another apply holds the lock (after --lock-wait, if given)
|
|
120
160
|
1 anything else failed: a statement (the history records where; rerun to
|
|
121
161
|
resume), a plan that moved since approval, a bad config
|
|
@@ -137,6 +177,9 @@ function parseApplyArgs(args) {
|
|
|
137
177
|
execute: { type: "boolean" },
|
|
138
178
|
"lock-wait": { type: "string" },
|
|
139
179
|
"stand-down": { type: "boolean" },
|
|
180
|
+
"wave-plan": { type: "string" },
|
|
181
|
+
"wave-apply": { type: "string" },
|
|
182
|
+
spec: { type: "string" },
|
|
140
183
|
},
|
|
141
184
|
});
|
|
142
185
|
}
|
|
@@ -148,7 +191,8 @@ function parseApplyArgs(args) {
|
|
|
148
191
|
if (env === undefined || rest.length > 0)
|
|
149
192
|
throw new YodelError(usage);
|
|
150
193
|
const flag = (k) => values[k] === true;
|
|
151
|
-
const
|
|
194
|
+
const str = (k) => (typeof values[k] === "string" ? values[k] : undefined);
|
|
195
|
+
const modes = [...["plan", "digest", "execute"].filter(flag), ...waveModes.filter((k) => str(k) !== undefined)];
|
|
152
196
|
if (modes.length > 1)
|
|
153
197
|
throw new YodelError(`--${modes.join(" and --")} cannot be combined\n${usage}`);
|
|
154
198
|
return {
|
|
@@ -162,6 +206,9 @@ function parseApplyArgs(args) {
|
|
|
162
206
|
execute: flag("execute"),
|
|
163
207
|
lockWaitMs: typeof values["lock-wait"] === "string" ? parseDuration(values["lock-wait"], "--lock-wait") : undefined,
|
|
164
208
|
standDown: flag("stand-down") || standDownFromEnv(process.env),
|
|
209
|
+
wavePlan: str("wave-plan"),
|
|
210
|
+
waveApply: str("wave-apply"),
|
|
211
|
+
spec: str("spec") ?? WAVES_SPEC_FILE,
|
|
165
212
|
};
|
|
166
213
|
}
|
|
167
214
|
/** `yodel apply <env>`; the exit codes are in `help`. */
|
|
@@ -173,13 +220,18 @@ export const apply = {
|
|
|
173
220
|
const code = await guarded("apply", io, args, help, async () => {
|
|
174
221
|
const a = parseApplyArgs(args);
|
|
175
222
|
const project = await loadProject(a.dir);
|
|
223
|
+
if (splitSlot(a.env))
|
|
224
|
+
return slotWave(project, a, io);
|
|
176
225
|
const env = resolveEnvironment(project, a.env);
|
|
226
|
+
if (!env.tenant && project.yodel?.config.environments?.[env.name]?.tenants) {
|
|
227
|
+
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`);
|
|
228
|
+
}
|
|
177
229
|
if (hasMigrations(project))
|
|
178
230
|
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`);
|
|
231
|
+
if (a.plan || a.digest || a.execute || a.allowOutOfOrder || a.lockWaitMs !== undefined || args.includes("--stand-down") || a.wavePlan !== undefined || a.waveApply !== undefined) {
|
|
232
|
+
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`);
|
|
181
233
|
}
|
|
182
|
-
const refusal = project.dialect === "clickhouse" ? declarativeTopologyRefusal(project, env.name) :
|
|
234
|
+
const refusal = project.dialect === "clickhouse" ? declarativeTopologyRefusal(project, env.name) : declarativeAccessRefusal(project, env.name);
|
|
183
235
|
if (refusal)
|
|
184
236
|
throw refusal;
|
|
185
237
|
return declarativeApply(project, env, a, io);
|
|
@@ -189,45 +241,242 @@ export const apply = {
|
|
|
189
241
|
return code;
|
|
190
242
|
},
|
|
191
243
|
};
|
|
244
|
+
/**
|
|
245
|
+
* A slot of a tenant set's wave read at run time (#56), `<env>/@<i>of<n>`:
|
|
246
|
+
* every tenant the set's query lists now that hashes to the slot, planned
|
|
247
|
+
* or applied each against its own history and lock.
|
|
248
|
+
*/
|
|
249
|
+
async function slotWave(project, a, io) {
|
|
250
|
+
if (!hasMigrations(project))
|
|
251
|
+
throw new YodelError(`${a.env} is a slot of a tenant set's wave, for versioned migrations, and ${project.dir} has no migrations/ directory with a migration`);
|
|
252
|
+
const ordered = readChain(project);
|
|
253
|
+
const log = (l) => { io.err(`${l}\n`); };
|
|
254
|
+
if (a.wavePlan !== undefined) {
|
|
255
|
+
const forReview = process.env[RECORD_PLANS_ENV] === "1";
|
|
256
|
+
const plan = await writeSlotPlan(project, a.env, ordered, a.wavePlan, { allowOutOfOrder: a.allowOutOfOrder, forReview }, log);
|
|
257
|
+
const forecast = forReview ? undefined : await forecastWave(process.cwd(), a.spec, plan);
|
|
258
|
+
if (forecast?.waits) {
|
|
259
|
+
const set = splitSlot(a.env)?.env ?? a.env;
|
|
260
|
+
log(`${set}: wave ${String(forecast.wave)} waits for an approval of ${forecast.digest} (gate ${forecast.policy} at ${forecast.base.slice(0, 12)}${forecast.approval === "ledger" ? "" : `, approval ${forecast.approval}`}) for every tenant. Approve it with yodel approve ${set}${forecast.approval === "sealed" ? " --sign" : ""}, or: ${forecast.approve}`);
|
|
261
|
+
}
|
|
262
|
+
if (a.json)
|
|
263
|
+
io.out(`${JSON.stringify(plan, null, 2)}\n`);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
if (a.waveApply !== undefined) {
|
|
267
|
+
return applySlot(a.env, a.waveApply, (tenant, file, decidedAs) => versionedApply(project, resolveEnvironment(project, tenant), { ...a, env: tenant, waveApply: file, decidedAs }, io), log);
|
|
268
|
+
}
|
|
269
|
+
throw new YodelError(`${a.env} is a slot of ${splitSlot(a.env)?.env ?? ""}'s wave, which chant run wave plans and applies with --wave-plan and --wave-apply; apply a tenant with yodel apply <env>/<tenant>`);
|
|
270
|
+
}
|
|
192
271
|
async function versionedApply(project, env, a, io) {
|
|
193
272
|
const ordered = readChain(project);
|
|
194
273
|
const venv = await openVersioned(project, env);
|
|
274
|
+
const outcome = {};
|
|
275
|
+
let code;
|
|
195
276
|
try {
|
|
196
|
-
|
|
277
|
+
code = await versionedApplyWith(project, env, a, io, ordered, venv, outcome);
|
|
197
278
|
}
|
|
198
|
-
|
|
279
|
+
catch (e) {
|
|
280
|
+
// The wave modes run inside chant run wave, which keeps no outcome of its own: a refusal or failure there is told here (#87).
|
|
281
|
+
if (a.wavePlan !== undefined || a.waveApply !== undefined)
|
|
282
|
+
outcome.doc = waveFailure(env.name, e);
|
|
199
283
|
await closeVersioned(venv);
|
|
284
|
+
if (outcome.doc)
|
|
285
|
+
await sendEvent(project.yodel?.config.notify, eventFromRun("apply", outcome.doc), { io, verb: "apply" });
|
|
286
|
+
throw e;
|
|
200
287
|
}
|
|
288
|
+
await closeVersioned(venv);
|
|
289
|
+
// Waiting, refused or failed: the webhook in yodel.config.ts notify hears of it (#61).
|
|
290
|
+
if (outcome.doc)
|
|
291
|
+
await sendEvent(project.yodel?.config.notify, eventFromRun("apply", outcome.doc), { io, verb: "apply" });
|
|
292
|
+
return code;
|
|
293
|
+
}
|
|
294
|
+
/** 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. */
|
|
295
|
+
export function waveFailure(environment, e) {
|
|
296
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
297
|
+
const exitCode = e instanceof YodelError ? e.exitCode : 1;
|
|
298
|
+
return exitCode === EXIT_REFUSED || exitCode === EXIT_OUTSTANDING ? { environment, status: "refused", refusal: { message } } : { environment, status: "failed", exitCode, reason: message };
|
|
201
299
|
}
|
|
202
|
-
async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
300
|
+
async function versionedApplyWith(project, env, a, io, ordered, venv, outcome = {}) {
|
|
301
|
+
// yodel revert (#55) runs this Op with the migration to revert in the environment: its Plan and Apply steps plan and run that instead.
|
|
302
|
+
const revert = a.digest || a.execute ? revertFromEnv(process.env, project.dir) : undefined;
|
|
303
|
+
if (revert && a.digest) {
|
|
304
|
+
io.out(`${(await currentRevert(venv, ordered, revert.id, revert.step)).digest}\n`);
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
if (revert && a.execute) {
|
|
308
|
+
const approved = process.env[APPROVED_PLAN_ENV];
|
|
309
|
+
if (!approved)
|
|
310
|
+
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}`);
|
|
311
|
+
const op = await findMigrateOp(project, env, a.op);
|
|
312
|
+
const ledger = await migrationsLedger(project, env.name, io);
|
|
313
|
+
// The policy over the revert (#97), as the base commit has it (#73), as yodel revert judged it before the gate.
|
|
314
|
+
const { policy: basePolicy, note: policyNote } = await policyAtBase(project);
|
|
315
|
+
if (policyNote)
|
|
316
|
+
io.err(`${policyNote}\n`);
|
|
317
|
+
const plan = await executeRevert(venv, ordered, {
|
|
318
|
+
id: revert.id,
|
|
319
|
+
step: revert.step,
|
|
320
|
+
approvedDigest: approved,
|
|
321
|
+
...(a.lockWaitMs !== undefined ? { lockWaitMs: a.lockWaitMs } : {}),
|
|
322
|
+
approve: async (digest) => {
|
|
323
|
+
const by = await verifyApproval(ledger, op, digest);
|
|
324
|
+
io.err(`approval: ${op.name} / ${op.gate} for ${digest}, by ${by.resolvedBy} at ${by.timestamp}\n`);
|
|
325
|
+
},
|
|
326
|
+
...(basePolicy
|
|
327
|
+
? {
|
|
328
|
+
policy: async (digest, planned) => {
|
|
329
|
+
const results = await judgeRevert({ policy: basePolicy, environment: env.name, dialect: project.dialect, plan: planned, digest, ledger: () => Promise.resolve({ port: ledger, op }) });
|
|
330
|
+
const refusal = policyRefusal(results, env.name, digest, "under the lock", { id: revert.id, step: revert.step?.file });
|
|
331
|
+
if (refusal)
|
|
332
|
+
throw refusal;
|
|
333
|
+
for (const line of describeResults(results))
|
|
334
|
+
io.err(`${line}\n`);
|
|
335
|
+
return results.flatMap((r) => (r.override ? [r.override] : []));
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
: {}),
|
|
339
|
+
log: (l) => { io.err(`${l}\n`); },
|
|
340
|
+
});
|
|
341
|
+
io.out(`Reverted: ${plan.migration.id}, back to ${plan.to ?? "an empty schema"}\n`);
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
203
344
|
if (a.digest) {
|
|
204
345
|
const { status } = await inspect(venv, ordered);
|
|
205
346
|
const toApply = selectToApply(status, a.allowOutOfOrder);
|
|
206
347
|
io.out(`${await currentDigest(venv, status, toApply)}\n`);
|
|
207
348
|
return 0;
|
|
208
349
|
}
|
|
350
|
+
if (a.wavePlan !== undefined) {
|
|
351
|
+
// In the pull request job that records the plans a review approves (#48), the wave as it will be after the merge.
|
|
352
|
+
const forReview = process.env[RECORD_PLANS_ENV] === "1";
|
|
353
|
+
const plan = await writeWavePlan(project, venv, ordered, a.wavePlan, { allowOutOfOrder: a.allowOutOfOrder, forReview });
|
|
354
|
+
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)}` +
|
|
355
|
+
`${plan.destructive ? `; destructive: ${plan.destructiveSteps.map((d) => `${d.migration} step ${String(d.step)} (${d.why})`).join(", ")}` : ""}; plan ${plan.planDigest}\n`);
|
|
356
|
+
// The before steps (#64): the SQL checks as they ran, and each command as one that will run under the lock (#107).
|
|
357
|
+
for (const o of plan.steps ?? [])
|
|
358
|
+
io.err(`${describeStep(o)}\n`);
|
|
359
|
+
// chant decides the wave right after this; tell the webhook now when it will wait (#87), since chant sends nothing.
|
|
360
|
+
// Recorded for a review (#48), nothing waits now: the wave is decided after the merge.
|
|
361
|
+
const forecast = forReview ? undefined : await forecastWave(process.cwd(), a.spec, plan);
|
|
362
|
+
if (forecast?.waits) {
|
|
363
|
+
// A tenant set's wave (#56) is approved once for every tenant.
|
|
364
|
+
const sign = forecast.approval === "sealed" ? " --sign" : "";
|
|
365
|
+
const how = env.tenant ? `for every tenant of ${env.tenant.env}. Approve it with yodel approve ${env.tenant.env}${sign}, or: ` : `. Approve it with yodel approve ${env.name}${sign}, or: `;
|
|
366
|
+
const mode = forecast.approval === "ledger" ? "" : `, approval ${forecast.approval}`;
|
|
367
|
+
io.err(`${env.name}: wave ${String(forecast.wave)} waits for an approval of ${forecast.digest} (gate ${forecast.policy} at ${forecast.base.slice(0, 12)}${mode})${how}${forecast.approve}\n`);
|
|
368
|
+
if (forecast.approval === "pr-review")
|
|
369
|
+
io.err(`${env.name}: under pr-review, an approving review of the merged pull request's head by a writer other than its author approves it too, when that head's pipeline recorded this plan (chant run wave --record-plans).\n`);
|
|
370
|
+
outcome.doc = { environment: env.name, status: "gated", pending: plan.pending.map((id) => ({ id })), gatePlanDigest: forecast.digest, approve: forecast.approve };
|
|
371
|
+
}
|
|
372
|
+
if (a.json)
|
|
373
|
+
io.out(`${JSON.stringify(applyJson({ mode: "wave-plan", plan }), null, 2)}\n`);
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
if (a.waveApply !== undefined) {
|
|
377
|
+
const planned = readWavePlan(a.waveApply);
|
|
378
|
+
const { policy: basePolicy, note: policyNote } = await policyAtBase(project);
|
|
379
|
+
if (policyNote)
|
|
380
|
+
io.err(`${policyNote}\n`);
|
|
381
|
+
if (planned.environment !== env.name)
|
|
382
|
+
throw new YodelError(`refused: ${a.waveApply} is ${planned.environment}'s plan, not ${env.name}'s; nothing was applied`, EXIT_REFUSED);
|
|
383
|
+
const cwd = process.cwd();
|
|
384
|
+
let wave;
|
|
385
|
+
let before = [];
|
|
386
|
+
const steps = await stepsAtBase(project, env.name);
|
|
387
|
+
if (steps.note)
|
|
388
|
+
io.err(`${steps.note}\n`);
|
|
389
|
+
const result = await executeApply(venv, ordered, {
|
|
390
|
+
runNote: () => (wave || before.length > 0 ? { ...(wave ? { wave } : {}), ...(before.length > 0 ? { steps: before } : {}) } : undefined),
|
|
391
|
+
...afterSteps(project, venv, env, steps.steps?.after, io),
|
|
392
|
+
approvedDigest: planned.migrations,
|
|
393
|
+
allowOutOfOrder: a.allowOutOfOrder,
|
|
394
|
+
...(a.lockWaitMs !== undefined ? { lockWaitMs: a.lockWaitMs } : {}),
|
|
395
|
+
...(a.standDown ? { standDown: standingDown(project, env) } : {}),
|
|
396
|
+
...(basePolicy
|
|
397
|
+
? {
|
|
398
|
+
policy: async (digest, toApply) => {
|
|
399
|
+
const ledger = async () => {
|
|
400
|
+
try {
|
|
401
|
+
return { port: await gitLedger(project.dir), op: await findMigrateOp(project, env, a.op) };
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
const results = await judgePlan({ policy: basePolicy, environment: env.name, dialect: project.dialect, toApply, digest, ledger });
|
|
408
|
+
const refusal = policyRefusal(results, env.name, digest, "under the lock");
|
|
409
|
+
if (refusal)
|
|
410
|
+
throw refusal;
|
|
411
|
+
for (const line of describeResults(results))
|
|
412
|
+
io.err(`${line}\n`);
|
|
413
|
+
return results.flatMap((r) => (r.override ? [r.override] : []));
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
: {}),
|
|
417
|
+
approve: async () => {
|
|
418
|
+
// Under the lock: the wave plan now, and chant's decision for it.
|
|
419
|
+
const now = await planWave(project, venv, ordered, { allowOutOfOrder: a.allowOutOfOrder });
|
|
420
|
+
if (now.planDigest !== planned.planDigest) {
|
|
421
|
+
throw new WavePlanMoved(`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.`);
|
|
422
|
+
}
|
|
423
|
+
// The before steps' SQL checks (#64) ran again in that plan, under the lock; a held one needs a person's approval whatever the gate.
|
|
424
|
+
const checked = (now.steps ?? []).filter((o) => !o.deferred);
|
|
425
|
+
for (const o of checked)
|
|
426
|
+
io.err(`${describeStep(o)}\n`);
|
|
427
|
+
let verified = await verifyWaveDecision(cwd, a.spec, a.decidedAs?.target ?? env.name, a.decidedAs?.planDigest ?? planned.planDigest, now.held ?? []);
|
|
428
|
+
// The commands (#107) run once, here, once the wave may apply: one that fails refuses; a held one needs the approval too.
|
|
429
|
+
const commands = await runBeforeSteps(project, venv, env, steps.steps?.before?.filter((s) => s.sql === undefined), "under the lock", io);
|
|
430
|
+
before = [...checked, ...commands];
|
|
431
|
+
if (heldSteps(commands).length > 0 && verified.wave.status !== "approved")
|
|
432
|
+
verified = await verifyWaveDecision(cwd, a.spec, a.decidedAs?.target ?? env.name, a.decidedAs?.planDigest ?? planned.planDigest, [...(now.held ?? []), ...heldSteps(commands)]);
|
|
433
|
+
wave = verified.wave;
|
|
434
|
+
io.err(`${verified.line}\n`);
|
|
435
|
+
},
|
|
436
|
+
log: (l) => { io.err(`${l}\n`); },
|
|
437
|
+
silences: migrationSilences,
|
|
438
|
+
});
|
|
439
|
+
if (a.json)
|
|
440
|
+
io.out(`${JSON.stringify(applyJson({ mode: "execute", environment: env.name, planDigest: planned.migrations, wavePlanDigest: planned.planDigest, result }), null, 2)}\n`);
|
|
441
|
+
else
|
|
442
|
+
io.out(result.stoodDown ? `${result.stoodDown}\n` : result.applied.length === 0 ? "Nothing to apply.\n" : `Applied: ${result.applied.join(", ")}\n`);
|
|
443
|
+
return 0;
|
|
444
|
+
}
|
|
209
445
|
if (a.execute) {
|
|
210
446
|
const approved = process.env[APPROVED_PLAN_ENV];
|
|
447
|
+
const { policy: basePolicy, note: policyNote } = await policyAtBase(project);
|
|
448
|
+
if (policyNote)
|
|
449
|
+
io.err(`${policyNote}\n`);
|
|
211
450
|
if (!approved)
|
|
212
451
|
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
452
|
// The env var is only the Op's hand-off; the approval itself is checked in chant's gate ledger, under the lock.
|
|
214
453
|
const op = await findMigrateOp(project, env, a.op);
|
|
215
|
-
const ledger = await
|
|
454
|
+
const ledger = await migrationsLedger(project, env.name, io);
|
|
455
|
+
const steps = await stepsAtBase(project, env.name);
|
|
456
|
+
if (steps.note)
|
|
457
|
+
io.err(`${steps.note}\n`);
|
|
458
|
+
let before = [];
|
|
216
459
|
const result = await executeApply(venv, ordered, {
|
|
460
|
+
runNote: () => (before.length > 0 ? { steps: before } : undefined),
|
|
461
|
+
...afterSteps(project, venv, env, steps.steps?.after, io),
|
|
217
462
|
approvedDigest: approved,
|
|
218
463
|
allowOutOfOrder: a.allowOutOfOrder,
|
|
219
464
|
...(a.lockWaitMs !== undefined ? { lockWaitMs: a.lockWaitMs } : {}),
|
|
220
465
|
...(a.standDown ? { standDown: standingDown(project, env) } : {}),
|
|
221
466
|
approve: async (digest) => {
|
|
467
|
+
// Under the lock: every pending migration ran first where this environment requires (exit 4 otherwise).
|
|
468
|
+
await enforcePromotion(project, env.name, ordered, (await inspect(venv, ordered)).status.pending);
|
|
222
469
|
const by = await verifyApproval(ledger, op, digest);
|
|
223
|
-
io.err(`approval: ${op.name} / ${op.gate} for ${digest}, by ${by.resolvedBy} at ${by.timestamp}\n`);
|
|
470
|
+
io.err(`approval: ${op.name} / ${op.gate} for ${digest}, by ${by.resolvedBy} at ${by.timestamp}${by.signer !== undefined ? `, sealed by ${by.signer}` : ""}\n`);
|
|
471
|
+
// The before steps (#64), under the lock: one that fails refuses; a held one is let through by the Op's approval, which every run of it asks for.
|
|
472
|
+
before = await runBeforeSteps(project, venv, env, steps.steps?.before, "under the lock", io);
|
|
224
473
|
},
|
|
225
474
|
log: (l) => { io.err(`${l}\n`); },
|
|
226
475
|
silences: migrationSilences,
|
|
227
|
-
...(
|
|
476
|
+
...(basePolicy
|
|
228
477
|
? {
|
|
229
478
|
policy: async (digest, toApply) => {
|
|
230
|
-
const results = await judgePlan({ policy:
|
|
479
|
+
const results = await judgePlan({ policy: basePolicy, environment: env.name, dialect: project.dialect, toApply, digest, ledger: () => Promise.resolve({ port: ledger, op }) });
|
|
231
480
|
const refusal = policyRefusal(results, env.name, digest, "under the lock");
|
|
232
481
|
if (refusal)
|
|
233
482
|
throw refusal;
|
|
@@ -239,15 +488,16 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
|
239
488
|
: {}),
|
|
240
489
|
});
|
|
241
490
|
if (a.json)
|
|
242
|
-
io.out(`${JSON.stringify({
|
|
491
|
+
io.out(`${JSON.stringify(applyJson({ mode: "execute", environment: env.name, planDigest: approved, result }), null, 2)}\n`);
|
|
243
492
|
else
|
|
244
493
|
io.out(result.stoodDown ? `${result.stoodDown}\n` : result.applied.length === 0 ? "Nothing to apply.\n" : `Applied: ${result.applied.join(", ")}\n`);
|
|
245
494
|
return 0;
|
|
246
495
|
}
|
|
247
496
|
const report = await statusReport(venv, ordered, { allowOutOfOrder: a.allowOutOfOrder });
|
|
248
497
|
const emit = (doc, code) => {
|
|
498
|
+
outcome.doc = applyJson({ mode: "versioned", report, outcome: doc });
|
|
249
499
|
if (a.json)
|
|
250
|
-
io.out(`${JSON.stringify(
|
|
500
|
+
io.out(`${JSON.stringify(outcome.doc, null, 2)}\n`);
|
|
251
501
|
return code;
|
|
252
502
|
};
|
|
253
503
|
if (a.plan) {
|
|
@@ -272,6 +522,17 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
|
272
522
|
io.out(`${stood}\n`);
|
|
273
523
|
return emit({ status: "stood-down", stoodDown: stood }, 0);
|
|
274
524
|
}
|
|
525
|
+
// Before the gate: a pending migration the required environment has not applied refuses, and nothing is asked.
|
|
526
|
+
try {
|
|
527
|
+
await enforcePromotion(project, env.name, ordered, report.status.pending);
|
|
528
|
+
}
|
|
529
|
+
catch (e) {
|
|
530
|
+
if (!(e instanceof YodelError) || e.exitCode !== EXIT_REFUSED)
|
|
531
|
+
throw e;
|
|
532
|
+
if (!a.json)
|
|
533
|
+
io.err(`yodel apply: ${e.message}\n`);
|
|
534
|
+
return emit({ status: "refused", refusal: { exitCode: EXIT_REFUSED, message: e.message } }, EXIT_REFUSED);
|
|
535
|
+
}
|
|
275
536
|
// 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
537
|
const first = report.status.pending[0];
|
|
277
538
|
let checked;
|
|
@@ -285,10 +546,30 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
|
285
546
|
return emit({ status: "refused", preChecks: checked }, refusal.exitCode);
|
|
286
547
|
}
|
|
287
548
|
}
|
|
549
|
+
// The before steps (#64), as the base commit has them: a SQL check that fails refuses before anyone is asked; the Apply step runs them again under the lock.
|
|
550
|
+
// A command runs there alone (#107), once per apply: here the plan only says it will.
|
|
551
|
+
const steps = await stepsAtBase(project, env.name);
|
|
552
|
+
if (steps.note && !a.json)
|
|
553
|
+
io.err(`${steps.note}\n`);
|
|
554
|
+
try {
|
|
555
|
+
const outcomes = await runBeforeSteps(project, venv, env, steps.steps?.before, "before the gate", a.json ? undefined : io, "plan");
|
|
556
|
+
if (!a.json && heldSteps(outcomes).length > 0)
|
|
557
|
+
io.out(`before step ${heldSteps(outcomes).join(", ")} failed with onFailure approve: approving this plan lets it through\n`);
|
|
558
|
+
}
|
|
559
|
+
catch (e) {
|
|
560
|
+
if (!(e instanceof YodelError) || e.exitCode !== EXIT_REFUSED)
|
|
561
|
+
throw e;
|
|
562
|
+
if (!a.json)
|
|
563
|
+
io.err(`yodel apply: ${e.message}\n`);
|
|
564
|
+
return emit({ status: "refused", refusal: { exitCode: EXIT_REFUSED, message: e.message } }, EXIT_REFUSED);
|
|
565
|
+
}
|
|
288
566
|
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.
|
|
567
|
+
// 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.
|
|
568
|
+
const basePolicy = await policyAtBase(project);
|
|
569
|
+
if (basePolicy.note && !a.json)
|
|
570
|
+
io.err(`${basePolicy.note}\n`);
|
|
290
571
|
const policy = await judgePlan({
|
|
291
|
-
policy:
|
|
572
|
+
policy: basePolicy.policy,
|
|
292
573
|
environment: env.name,
|
|
293
574
|
dialect: project.dialect,
|
|
294
575
|
toApply: report.status.pending,
|
|
@@ -325,8 +606,11 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
|
325
606
|
if (!a.json) {
|
|
326
607
|
io.out(`\n${op.name} is waiting at gate "${op.gate}" for approval of this plan${run.planDigest ? ` (${run.planDigest})` : ""}.\n`);
|
|
327
608
|
if (run.approve) {
|
|
328
|
-
|
|
329
|
-
|
|
609
|
+
// A sealed gate (#48) counts only a sealed approval, which --execute checks.
|
|
610
|
+
const sealed = (await migrationsApprovalAtBase(project, env.name)).mode === "sealed";
|
|
611
|
+
const approve = sealed && !/ --sign\b/.test(run.approve) ? `${run.approve} --sign` : run.approve;
|
|
612
|
+
io.out(`Approve it with:\n ${runnable(approve, project.dir)}\n` +
|
|
613
|
+
`or, at a terminal, with ${runnable(`yodel approve ${env.name}${sealed ? " --sign" : ""}`, project.dir)}, which shows the plan and asks you first;\n` +
|
|
330
614
|
`then run ${runnable(`yodel apply ${env.name}`, project.dir)} again.\n`);
|
|
331
615
|
}
|
|
332
616
|
}
|
|
@@ -340,8 +624,9 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
|
340
624
|
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
625
|
io.out(`\n${renderStatus(after)}`);
|
|
342
626
|
}
|
|
627
|
+
outcome.doc = applyJson({ mode: "versioned", report: after, outcome: { status: "failed", op: op.name, exitCode: code, chantExitCode: run.code } });
|
|
343
628
|
if (a.json)
|
|
344
|
-
io.out(`${JSON.stringify(
|
|
629
|
+
io.out(`${JSON.stringify(outcome.doc, null, 2)}\n`);
|
|
345
630
|
return code;
|
|
346
631
|
}
|
|
347
632
|
// 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.
|
|
@@ -352,7 +637,7 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
|
352
637
|
return emit({ status: "stood-down", stoodDown: stoodLate, op: op.name }, 0);
|
|
353
638
|
}
|
|
354
639
|
if (a.json) {
|
|
355
|
-
io.out(`${JSON.stringify({
|
|
640
|
+
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`);
|
|
356
641
|
}
|
|
357
642
|
else {
|
|
358
643
|
io.out(`\nApplied: ${report.status.pending.map((p) => p.id).join(", ")}.\n`);
|
|
@@ -360,23 +645,40 @@ async function versionedApplyWith(project, env, a, io, ordered, venv) {
|
|
|
360
645
|
return after.status.pending.length === 0 ? 0 : 1;
|
|
361
646
|
}
|
|
362
647
|
async function declarativeApply(project, env, a, io) {
|
|
363
|
-
const json = a.json;
|
|
364
648
|
const op = await findApplyOp(project, env, a.op);
|
|
365
649
|
let server;
|
|
650
|
+
let printed;
|
|
366
651
|
const report = (doc, code) => {
|
|
367
|
-
|
|
368
|
-
|
|
652
|
+
printed = applyJson({ mode: "declarative", environment: env.name, dialect: project.dialect, server, op: op.name, outcome: doc });
|
|
653
|
+
if (a.json)
|
|
654
|
+
io.out(`${JSON.stringify(printed, null, 2)}\n`);
|
|
369
655
|
return code;
|
|
370
656
|
};
|
|
657
|
+
const code = await declarativeRun(project, env, a, io, op, report, (s) => (server = s));
|
|
658
|
+
if (printed)
|
|
659
|
+
await sendEvent(project.yodel?.config.notify, eventFromRun("apply", printed), { io, verb: "apply" });
|
|
660
|
+
return code;
|
|
661
|
+
}
|
|
662
|
+
async function declarativeRun(project, env, a, io, op, report, setServer) {
|
|
663
|
+
const json = a.json;
|
|
371
664
|
const build = await buildSchema(project);
|
|
372
665
|
try {
|
|
666
|
+
const chRefusal = project.dialect === "clickhouse" ? clickhouseAccessRefusal(project, env.name, build.output) : undefined;
|
|
667
|
+
if (chRefusal)
|
|
668
|
+
throw chRefusal;
|
|
373
669
|
const before = await planEnvironment(project, env, build);
|
|
374
|
-
server = await probeServer(project, env);
|
|
375
|
-
|
|
670
|
+
const server = await probeServer(project, env);
|
|
671
|
+
setServer(server);
|
|
672
|
+
if (!json) {
|
|
376
673
|
io.out(`${await renderPlan(before, `Plan for ${env.name} (${server ? describeServer(server) : project.dialect}, sql.profiles.${env.name})`)}\n`);
|
|
674
|
+
const review = renderAccessReview(await accessReview(project, env.name, before.diff.changes, build.output), env.name);
|
|
675
|
+
if (review.length > 0)
|
|
676
|
+
io.out(`\n${review.join("\n")}\n`);
|
|
677
|
+
}
|
|
377
678
|
if (planExitCode(before) === PLAN_NEEDS_OP) {
|
|
679
|
+
const unreadable = unreadableOf(before);
|
|
378
680
|
if (!json)
|
|
379
|
-
io.err(`yodel apply: refused: ${String(before.refused.length)} change(s) need their own Op; nothing was applied\n`);
|
|
681
|
+
io.err(`yodel apply: refused: ${unreadable.length > 0 ? `${unreadable.map((u) => u.object).join(", ")} could not be read` : `${String(before.refused.length)} change(s) need their own Op`}; nothing was applied\n`);
|
|
380
682
|
return report({ status: "refused", plan: planJson(before) }, PLAN_NEEDS_OP);
|
|
381
683
|
}
|
|
382
684
|
if (declaredChanges(before, build).length === 0) {
|
|
@@ -423,4 +725,42 @@ function standingDown(project, env) {
|
|
|
423
725
|
return n ? standDownLine(env.name, n) : undefined;
|
|
424
726
|
};
|
|
425
727
|
}
|
|
728
|
+
/** Runs `env`'s before steps (#64); throws the refusal for one that fails with onFailure fail. */
|
|
729
|
+
async function runBeforeSteps(project, venv, env, list, when, io, commands = "run") {
|
|
730
|
+
if (!list || list.length === 0)
|
|
731
|
+
return [];
|
|
732
|
+
const outcomes = await runApplySteps(venv.driver, list, "before", { dir: project.dir, env: env.name, commands });
|
|
733
|
+
for (const o of outcomes)
|
|
734
|
+
io?.err(`${describeStep(o)}\n`);
|
|
735
|
+
const refusal = stepsRefusal(env.name, outcomes, when);
|
|
736
|
+
if (refusal)
|
|
737
|
+
throw refusal;
|
|
738
|
+
return outcomes;
|
|
739
|
+
}
|
|
740
|
+
/** executeApply's `after` (#64), when `env` has after steps. */
|
|
741
|
+
function afterSteps(project, venv, env, list, io) {
|
|
742
|
+
if (!list || list.length === 0)
|
|
743
|
+
return {};
|
|
744
|
+
return {
|
|
745
|
+
after: async () => {
|
|
746
|
+
const outcomes = await runApplySteps(venv.driver, list, "after", { dir: project.dir, env: env.name });
|
|
747
|
+
for (const o of outcomes)
|
|
748
|
+
io.err(`${describeStep(o)}\n`);
|
|
749
|
+
const error = stepsRefusal(env.name, outcomes, "under the lock");
|
|
750
|
+
return { note: { steps: outcomes }, ...(error ? { error } : {}) };
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* chant's gate ledger as the migrations Op's gate counts approvals for
|
|
756
|
+
* `env` (#48): sealed when yodel.config.ts at the base commit says so, so
|
|
757
|
+
* `--execute` applies nothing on an approval without a seal a signer at
|
|
758
|
+
* base made.
|
|
759
|
+
*/
|
|
760
|
+
async function migrationsLedger(project, env, io) {
|
|
761
|
+
const mode = await migrationsApprovalAtBase(project, env);
|
|
762
|
+
if (mode.note)
|
|
763
|
+
io.err(`${mode.note}\n`);
|
|
764
|
+
return portFor(await gitLedger(project.dir), mode.mode, project.dir, mode.base);
|
|
765
|
+
}
|
|
426
766
|
//# sourceMappingURL=apply.js.map
|