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