@intentius/sql-yodeler 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -26
- package/dist/bin.js +15 -0
- package/dist/bin.js.map +1 -1
- package/dist/chant/apply.d.ts +9 -1
- package/dist/chant/apply.js +13 -1
- package/dist/chant/apply.js.map +1 -1
- package/dist/chant/build.js +7 -0
- package/dist/chant/build.js.map +1 -1
- package/dist/chant/drift.d.ts +69 -1
- package/dist/chant/drift.js +152 -5
- package/dist/chant/drift.js.map +1 -1
- package/dist/chant/runnable.d.ts +19 -0
- package/dist/chant/runnable.js +47 -0
- package/dist/chant/runnable.js.map +1 -0
- package/dist/ci/render.d.ts +289 -0
- package/dist/ci/render.js +688 -0
- package/dist/ci/render.js.map +1 -0
- package/dist/ci/replay.d.ts +17 -0
- package/dist/ci/replay.js +69 -0
- package/dist/ci/replay.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +40 -0
- package/dist/cli.js.map +1 -1
- package/dist/command.d.ts +6 -0
- package/dist/command.js.map +1 -1
- package/dist/commands/apply.d.ts +113 -0
- package/dist/commands/apply.js +376 -49
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/approve.d.ts +5 -0
- package/dist/commands/approve.js +155 -0
- package/dist/commands/approve.js.map +1 -0
- package/dist/commands/checkpoint.d.ts +3 -0
- package/dist/commands/checkpoint.js +78 -0
- package/dist/commands/checkpoint.js.map +1 -0
- package/dist/commands/ci.d.ts +3 -0
- package/dist/commands/ci.js +118 -0
- package/dist/commands/ci.js.map +1 -0
- package/dist/commands/docs.d.ts +11 -0
- package/dist/commands/docs.js +108 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/drift.d.ts +23 -0
- package/dist/commands/drift.js +263 -18
- package/dist/commands/drift.js.map +1 -1
- package/dist/commands/index.js +16 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +15 -11
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/lint.js +25 -8
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.js +50 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.js +134 -10
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/override.d.ts +3 -0
- package/dist/commands/override.js +150 -0
- package/dist/commands/override.js.map +1 -0
- package/dist/commands/plan.js +43 -27
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/repair.d.ts +11 -0
- package/dist/commands/repair.js +15 -7
- package/dist/commands/repair.js.map +1 -1
- package/dist/commands/report.d.ts +13 -0
- package/dist/commands/report.js +121 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/revert.d.ts +20 -0
- package/dist/commands/revert.js +246 -0
- package/dist/commands/revert.js.map +1 -0
- package/dist/commands/status.js +44 -17
- package/dist/commands/status.js.map +1 -1
- package/dist/config/base.d.ts +69 -0
- package/dist/config/base.js +121 -0
- package/dist/config/base.js.map +1 -0
- package/dist/config/define.d.ts +223 -3
- package/dist/config/define.js +3 -1
- package/dist/config/define.js.map +1 -1
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/load.d.ts +1 -1
- package/dist/config/load.js +150 -5
- package/dist/config/load.js.map +1 -1
- package/dist/config/settings.d.ts +18 -2
- package/dist/config/settings.js +40 -6
- package/dist/config/settings.js.map +1 -1
- package/dist/env-args.d.ts +6 -0
- package/dist/env-args.js +31 -1
- package/dist/env-args.js.map +1 -1
- package/dist/exit.d.ts +20 -0
- package/dist/exit.js +28 -0
- package/dist/exit.js.map +1 -0
- package/dist/history/client.d.ts +8 -0
- package/dist/history/client.js +24 -1
- package/dist/history/client.js.map +1 -1
- package/dist/history/lock.d.ts +5 -1
- package/dist/history/lock.js +8 -2
- package/dist/history/lock.js.map +1 -1
- package/dist/history/postgres/ddl.d.ts +18 -0
- package/dist/history/postgres/ddl.js +27 -2
- package/dist/history/postgres/ddl.js.map +1 -1
- package/dist/history/postgres/store.d.ts +2 -0
- package/dist/history/postgres/store.js +7 -1
- package/dist/history/postgres/store.js.map +1 -1
- package/dist/history/rows.d.ts +16 -3
- package/dist/history/rows.js +30 -1
- package/dist/history/rows.js.map +1 -1
- package/dist/history/store.d.ts +2 -0
- package/dist/history/store.js +2 -2
- package/dist/history/store.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init/adopt.js +8 -3
- package/dist/init/adopt.js.map +1 -1
- package/dist/init/live.d.ts +11 -0
- package/dist/init/live.js +25 -2
- package/dist/init/live.js.map +1 -1
- package/dist/lint/check.d.ts +8 -1
- package/dist/lint/check.js +38 -4
- package/dist/lint/check.js.map +1 -1
- package/dist/lint/report.js +1 -1
- package/dist/lint/report.js.map +1 -1
- package/dist/lint/routines.d.ts +26 -0
- package/dist/lint/routines.js +65 -0
- package/dist/lint/routines.js.map +1 -0
- package/dist/lint/rules.d.ts +16 -4
- package/dist/lint/rules.js +9 -4
- package/dist/lint/rules.js.map +1 -1
- package/dist/mcp/server.d.ts +40 -0
- package/dist/mcp/server.js +106 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +72 -0
- package/dist/mcp/tools.js +178 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/migrations/checkpoint.d.ts +45 -0
- package/dist/migrations/checkpoint.js +110 -0
- package/dist/migrations/checkpoint.js.map +1 -0
- package/dist/migrations/checks.d.ts +28 -0
- package/dist/migrations/checks.js +97 -0
- package/dist/migrations/checks.js.map +1 -0
- package/dist/migrations/create.d.ts +3 -1
- package/dist/migrations/create.js +3 -0
- package/dist/migrations/create.js.map +1 -1
- package/dist/migrations/directory.js +4 -0
- package/dist/migrations/directory.js.map +1 -1
- package/dist/migrations/document.d.ts +33 -0
- package/dist/migrations/document.js +2 -0
- package/dist/migrations/document.js.map +1 -1
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +3 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/routines.d.ts +31 -0
- package/dist/migrations/routines.js +55 -0
- package/dist/migrations/routines.js.map +1 -0
- package/dist/notify/index.d.ts +124 -0
- package/dist/notify/index.js +210 -0
- package/dist/notify/index.js.map +1 -0
- package/dist/notify/verify.d.ts +53 -0
- package/dist/notify/verify.js +71 -0
- package/dist/notify/verify.js.map +1 -0
- package/dist/pr/forge.d.ts +4 -0
- package/dist/pr/forge.js +21 -7
- package/dist/pr/forge.js.map +1 -1
- package/dist/pr/issue.d.ts +68 -0
- package/dist/pr/issue.js +107 -0
- package/dist/pr/issue.js.map +1 -0
- package/dist/pr/model.d.ts +12 -1
- package/dist/pr/model.js +44 -6
- package/dist/pr/model.js.map +1 -1
- package/dist/pr/render.d.ts +3 -1
- package/dist/pr/render.js +63 -4
- package/dist/pr/render.js.map +1 -1
- package/dist/project.d.ts +19 -2
- package/dist/project.js +33 -1
- package/dist/project.js.map +1 -1
- package/dist/replay/index.d.ts +3 -0
- package/dist/replay/index.js +3 -0
- package/dist/replay/index.js.map +1 -1
- package/dist/replay/replay.d.ts +9 -1
- package/dist/replay/replay.js +63 -32
- package/dist/replay/replay.js.map +1 -1
- package/dist/replay/report.js +1 -1
- package/dist/replay/report.js.map +1 -1
- package/dist/replay/shared.d.ts +28 -0
- package/dist/replay/shared.js +77 -0
- package/dist/replay/shared.js.map +1 -0
- package/dist/report/audit.d.ts +138 -0
- package/dist/report/audit.js +202 -0
- package/dist/report/audit.js.map +1 -0
- package/dist/report/jsonl.d.ts +21 -0
- package/dist/report/jsonl.js +52 -0
- package/dist/report/jsonl.js.map +1 -0
- package/dist/report/model.d.ts +68 -0
- package/dist/report/model.js +127 -0
- package/dist/report/model.js.map +1 -0
- package/dist/report/render.d.ts +18 -0
- package/dist/report/render.js +127 -0
- package/dist/report/render.js.map +1 -0
- package/dist/schema-docs/render.d.ts +49 -0
- package/dist/schema-docs/render.js +359 -0
- package/dist/schema-docs/render.js.map +1 -0
- package/dist/schemas.d.ts +37 -0
- package/dist/schemas.js +41 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts +40 -0
- package/dist/server.js +87 -0
- package/dist/server.js.map +1 -0
- package/dist/sources/ddl.d.ts +53 -0
- package/dist/sources/ddl.js +347 -0
- package/dist/sources/ddl.js.map +1 -0
- package/dist/sources/index.d.ts +69 -0
- package/dist/sources/index.js +170 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/steps/backfill.d.ts +21 -17
- package/dist/steps/backfill.js +20 -20
- package/dist/steps/backfill.js.map +1 -1
- package/dist/steps/index.d.ts +2 -2
- package/dist/steps/index.js +3 -3
- package/dist/steps/index.js.map +1 -1
- package/dist/steps/postgres-migration.d.ts +35 -25
- package/dist/steps/postgres-migration.js +42 -29
- package/dist/steps/postgres-migration.js.map +1 -1
- package/dist/steps/rebuild.d.ts +15 -15
- package/dist/steps/rebuild.js +17 -24
- package/dist/steps/rebuild.js.map +1 -1
- package/dist/tenants/index.d.ts +40 -0
- package/dist/tenants/index.js +122 -0
- package/dist/tenants/index.js.map +1 -0
- package/dist/tenants/target.d.ts +23 -0
- package/dist/tenants/target.js +29 -0
- package/dist/tenants/target.js.map +1 -0
- package/dist/versioned/checks.d.ts +29 -0
- package/dist/versioned/checks.js +54 -0
- package/dist/versioned/checks.js.map +1 -0
- package/dist/versioned/clickhouse.js +8 -0
- package/dist/versioned/clickhouse.js.map +1 -1
- package/dist/versioned/driver.d.ts +10 -0
- package/dist/versioned/engine.d.ts +61 -1
- package/dist/versioned/engine.js +109 -6
- package/dist/versioned/engine.js.map +1 -1
- package/dist/versioned/explain.js +7 -2
- package/dist/versioned/explain.js.map +1 -1
- package/dist/versioned/index.d.ts +10 -0
- package/dist/versioned/index.js +25 -1
- package/dist/versioned/index.js.map +1 -1
- package/dist/versioned/op.d.ts +16 -1
- package/dist/versioned/op.js +43 -6
- package/dist/versioned/op.js.map +1 -1
- package/dist/versioned/policy.d.ts +98 -0
- package/dist/versioned/policy.js +212 -0
- package/dist/versioned/policy.js.map +1 -0
- package/dist/versioned/postgres/driver.d.ts +3 -1
- package/dist/versioned/postgres/driver.js +30 -3
- package/dist/versioned/postgres/driver.js.map +1 -1
- package/dist/versioned/postgres/waits.d.ts +46 -0
- package/dist/versioned/postgres/waits.js +78 -0
- package/dist/versioned/postgres/waits.js.map +1 -0
- package/dist/versioned/report.d.ts +3 -0
- package/dist/versioned/report.js +13 -1
- package/dist/versioned/report.js.map +1 -1
- package/dist/versioned/revert.d.ts +128 -0
- package/dist/versioned/revert.js +251 -0
- package/dist/versioned/revert.js.map +1 -0
- package/dist/versioned/stand-down.d.ts +30 -0
- package/dist/versioned/stand-down.js +56 -0
- package/dist/versioned/stand-down.js.map +1 -0
- package/dist/versioned/state.d.ts +25 -0
- package/dist/versioned/state.js +36 -3
- package/dist/versioned/state.js.map +1 -1
- package/dist/waves/index.d.ts +179 -0
- package/dist/waves/index.js +268 -0
- package/dist/waves/index.js.map +1 -0
- package/dist/waves/promotion.d.ts +56 -0
- package/dist/waves/promotion.js +108 -0
- package/dist/waves/promotion.js.map +1 -0
- package/dist/waves/record.d.ts +24 -0
- package/dist/waves/record.js +20 -0
- package/dist/waves/record.js.map +1 -0
- package/package.json +22 -8
- package/schemas/apply.schema.json +428 -0
- package/schemas/checkpoint.schema.json +19 -0
- package/schemas/common.schema.json +213 -0
- package/schemas/drift.schema.json +61 -0
- package/schemas/event.schema.json +36 -0
- package/schemas/init.schema.json +31 -0
- package/schemas/lint-rules.schema.json +54 -0
- package/schemas/lint.schema.json +294 -0
- package/schemas/mcp.schema.json +70 -0
- package/schemas/new.schema.json +71 -0
- package/schemas/plan.schema.json +489 -0
- package/schemas/rebase.schema.json +54 -0
- package/schemas/repair.schema.json +20 -0
- package/schemas/report.schema.json +114 -0
- package/schemas/revert.schema.json +69 -0
- package/schemas/status.schema.json +371 -0
|
@@ -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/forge.d.ts
CHANGED
|
@@ -46,5 +46,9 @@ export interface ForgeFlags {
|
|
|
46
46
|
export declare function detectForge(env: NodeJS.ProcessEnv): ForgeKind | undefined;
|
|
47
47
|
/** The PR number from a GitHub-style event payload, or from the ref. */
|
|
48
48
|
export declare function prFromEvent(eventPath: string | undefined, ref: string | undefined): number | undefined;
|
|
49
|
+
/** Where a job writes on its forge, without a pull request: the repository, the API and the token. */
|
|
50
|
+
export type ForgeRepo = Omit<ForgeTarget, "pr">;
|
|
49
51
|
/** Resolves the target from flags and the environment, or says what is missing. */
|
|
50
52
|
export declare function resolveForgeTarget(flags: ForgeFlags, env: NodeJS.ProcessEnv): ForgeTarget;
|
|
53
|
+
/** The repository a job writes to (`yodel drift --issue`), from flags and the environment, or says what is missing. */
|
|
54
|
+
export declare function resolveForgeRepo(flags: Omit<ForgeFlags, "pr">, env: NodeJS.ProcessEnv, what?: string): ForgeRepo;
|
package/dist/pr/forge.js
CHANGED
|
@@ -56,13 +56,17 @@ const DEFAULT_TOKEN_ENV = {
|
|
|
56
56
|
gitlab: ["GITLAB_TOKEN"],
|
|
57
57
|
forgejo: ["FORGEJO_TOKEN", "GITHUB_TOKEN"],
|
|
58
58
|
};
|
|
59
|
-
/**
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Resolves the repository, API and token from flags and the environment;
|
|
61
|
+
* `pr` also the pull (merge) request. `what` names the option that needs
|
|
62
|
+
* them, for messages (`--comment`, `--issue`).
|
|
63
|
+
*/
|
|
64
|
+
function resolve(flags, env, what, withPr) {
|
|
61
65
|
if (flags.forge !== undefined && !FORGES.includes(flags.forge))
|
|
62
66
|
throw new YodelError(`--forge must be one of ${FORGES.join(", ")}, got ${JSON.stringify(flags.forge)}`);
|
|
63
67
|
const forge = flags.forge ?? detectForge(env);
|
|
64
68
|
if (!forge)
|
|
65
|
-
throw new YodelError(
|
|
69
|
+
throw new YodelError(`${what} cannot tell the forge: not a GitHub Actions, GitLab CI or Forgejo Actions job; pass --forge github|gitlab|forgejo with --repo${withPr ? ", --pr" : ""} and --api-url`);
|
|
66
70
|
const pick = (...names) => names.map((n) => env[n]).find((v) => v !== undefined && v !== "");
|
|
67
71
|
let repo = flags.repo;
|
|
68
72
|
let pr = flags.pr !== undefined ? Number(flags.pr) : undefined;
|
|
@@ -75,7 +79,8 @@ export function resolveForgeTarget(flags, env) {
|
|
|
75
79
|
else {
|
|
76
80
|
const p = forge === "forgejo" ? "FORGEJO_" : "GITHUB_";
|
|
77
81
|
repo ??= pick(`${p}REPOSITORY`, "GITHUB_REPOSITORY");
|
|
78
|
-
|
|
82
|
+
if (withPr)
|
|
83
|
+
pr ??= prFromEvent(pick(`${p}EVENT_PATH`, "GITHUB_EVENT_PATH"), pick(`${p}REF`, "GITHUB_REF"));
|
|
79
84
|
apiUrl ??= pick(`${p}API_URL`, "GITHUB_API_URL");
|
|
80
85
|
if (!apiUrl && forge === "forgejo") {
|
|
81
86
|
const server = pick("FORGEJO_SERVER_URL", "GITHUB_SERVER_URL");
|
|
@@ -88,7 +93,7 @@ export function resolveForgeTarget(flags, env) {
|
|
|
88
93
|
const missing = [];
|
|
89
94
|
if (!repo)
|
|
90
95
|
missing.push(forge === "gitlab" ? "the project (--repo, or CI_PROJECT_ID)" : "the repository (--repo owner/name, or GITHUB_REPOSITORY)");
|
|
91
|
-
if (pr === undefined || !Number.isInteger(pr) || pr <= 0) {
|
|
96
|
+
if (withPr && (pr === undefined || !Number.isInteger(pr) || pr <= 0)) {
|
|
92
97
|
missing.push(forge === "gitlab" ? "the merge request (--pr, or CI_MERGE_REQUEST_IID in a merge request pipeline)" : "the pull request (--pr, or a pull_request event)");
|
|
93
98
|
}
|
|
94
99
|
if (!apiUrl)
|
|
@@ -99,7 +104,16 @@ export function resolveForgeTarget(flags, env) {
|
|
|
99
104
|
if (!token)
|
|
100
105
|
missing.push(`a token (${names.join(" or ")} is not set${flags.tokenEnv ? "" : "; name another variable with --token-env"})`);
|
|
101
106
|
if (missing.length > 0)
|
|
102
|
-
throw new YodelError(
|
|
103
|
-
|
|
107
|
+
throw new YodelError(`${what} on ${forge} needs ${missing.join(", ")}`);
|
|
108
|
+
const base = { forge, repo: repo, apiUrl: apiUrl.replace(/\/+$/, ""), token, tokenEnv };
|
|
109
|
+
return withPr ? { ...base, pr: pr } : base;
|
|
110
|
+
}
|
|
111
|
+
/** Resolves the target from flags and the environment, or says what is missing. */
|
|
112
|
+
export function resolveForgeTarget(flags, env) {
|
|
113
|
+
return resolve(flags, env, "--comment", true);
|
|
114
|
+
}
|
|
115
|
+
/** The repository a job writes to (`yodel drift --issue`), from flags and the environment, or says what is missing. */
|
|
116
|
+
export function resolveForgeRepo(flags, env, what = "--issue") {
|
|
117
|
+
return resolve(flags, env, what, false);
|
|
104
118
|
}
|
|
105
119
|
//# sourceMappingURL=forge.js.map
|
package/dist/pr/forge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forge.js","sourceRoot":"","sources":["../../src/pr/forge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG3C,MAAM,CAAC,MAAM,MAAM,GAAyB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;AAuB5E,MAAM,MAAM,GAAG,CAAC,CAAqB,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,GAAG,CAAC;AAEpE,yFAAyF;AACzF,MAAM,UAAU,WAAW,CAAC,GAAsB;IAChD,IAAI,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC3C,IAAI,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/E,IAAI,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC;QAAE,OAAO,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC7G,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,WAAW,CAAC,SAA6B,EAAE,GAAuB;IAChF,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAoH,CAAC;YAC7K,MAAM,CAAC,GAAG,KAAK,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;YACxJ,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;gBAAE,OAAO,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,4CAA4C;QAC9C,CAAC;IACH,CAAC;IACD,MAAM,CAAC,GAAG,qCAAqC,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IAChE,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACtC,CAAC;AAED,MAAM,iBAAiB,GAAgC;IACrD,MAAM,EAAE,CAAC,cAAc,CAAC;IACxB,MAAM,EAAE,CAAC,cAAc,CAAC;IACxB,OAAO,EAAE,CAAC,eAAe,EAAE,cAAc,CAAC;CAC3C,CAAC;
|
|
1
|
+
{"version":3,"file":"forge.js","sourceRoot":"","sources":["../../src/pr/forge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drift watch's tracking issue (#49): one open issue per project and
|
|
3
|
+
* environment, found by its marker, opened when drift is found, updated
|
|
4
|
+
* while it lasts, and closed, with a comment, when it is gone. The three
|
|
5
|
+
* forges' REST APIs:
|
|
6
|
+
*
|
|
7
|
+
* GitHub GET/POST {api}/repos/{owner}/{repo}/issues (open ones; pull requests left out)
|
|
8
|
+
* PATCH {api}/repos/{owner}/{repo}/issues/{n} body, or state closed
|
|
9
|
+
* POST {api}/repos/{owner}/{repo}/issues/{n}/comments
|
|
10
|
+
* GitLab GET/POST {api}/projects/{id}/issues (state opened)
|
|
11
|
+
* PUT {api}/projects/{id}/issues/{iid} description, or state_event close
|
|
12
|
+
* POST {api}/projects/{id}/issues/{iid}/notes
|
|
13
|
+
* Forgejo as GitHub, under /api/v1, with type=issues and limit
|
|
14
|
+
*
|
|
15
|
+
* The client lists only open issues, so a closed one is never reopened:
|
|
16
|
+
* drift found again after a close opens a new issue.
|
|
17
|
+
*/
|
|
18
|
+
import type { ForgeRepo } from "./forge.js";
|
|
19
|
+
export interface ForgeIssue {
|
|
20
|
+
number: number;
|
|
21
|
+
title: string;
|
|
22
|
+
body: string;
|
|
23
|
+
url?: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
export interface IssueClient {
|
|
26
|
+
/** The open issues (not pull requests). */
|
|
27
|
+
open(): Promise<ForgeIssue[]>;
|
|
28
|
+
create(title: string, body: string): Promise<ForgeIssue>;
|
|
29
|
+
update(n: number, title: string, body: string): Promise<ForgeIssue>;
|
|
30
|
+
/** Comments `comment` on the issue, then closes it. */
|
|
31
|
+
close(n: number, comment: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
type Fetch = typeof fetch;
|
|
34
|
+
export declare function issueClient(t: ForgeRepo, f?: Fetch): IssueClient;
|
|
35
|
+
/**
|
|
36
|
+
* The marker an environment's tracking issue carries, so the next run finds
|
|
37
|
+
* it. Keyed by the project's directory in its repository (`.` at the root),
|
|
38
|
+
* so two projects in one repository keep their own issues.
|
|
39
|
+
*/
|
|
40
|
+
export declare function driftIssueMarker(project: string, environment: string): string;
|
|
41
|
+
/** What the run found, as the tracking issue needs it. */
|
|
42
|
+
export type DriftOutcome = {
|
|
43
|
+
kind: "drift";
|
|
44
|
+
title: string;
|
|
45
|
+
body: string;
|
|
46
|
+
fingerprint: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "clean";
|
|
49
|
+
comment: string;
|
|
50
|
+
}
|
|
51
|
+
/** The drift could not be read, or part of the environment could not: the issue is left as it is. */
|
|
52
|
+
| {
|
|
53
|
+
kind: "unknown";
|
|
54
|
+
why: string;
|
|
55
|
+
};
|
|
56
|
+
export type IssueAction = {
|
|
57
|
+
action: "opened" | "updated" | "unchanged" | "closed";
|
|
58
|
+
issue: ForgeIssue;
|
|
59
|
+
} | {
|
|
60
|
+
action: "none";
|
|
61
|
+
} | {
|
|
62
|
+
action: "left";
|
|
63
|
+
why: string;
|
|
64
|
+
issue?: ForgeIssue;
|
|
65
|
+
};
|
|
66
|
+
/** Brings the environment's tracking issue in line with the outcome; see the module comment. */
|
|
67
|
+
export declare function trackDrift(client: IssueClient, marker: string, outcome: DriftOutcome): Promise<IssueAction>;
|
|
68
|
+
export {};
|
package/dist/pr/issue.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drift watch's tracking issue (#49): one open issue per project and
|
|
3
|
+
* environment, found by its marker, opened when drift is found, updated
|
|
4
|
+
* while it lasts, and closed, with a comment, when it is gone. The three
|
|
5
|
+
* forges' REST APIs:
|
|
6
|
+
*
|
|
7
|
+
* GitHub GET/POST {api}/repos/{owner}/{repo}/issues (open ones; pull requests left out)
|
|
8
|
+
* PATCH {api}/repos/{owner}/{repo}/issues/{n} body, or state closed
|
|
9
|
+
* POST {api}/repos/{owner}/{repo}/issues/{n}/comments
|
|
10
|
+
* GitLab GET/POST {api}/projects/{id}/issues (state opened)
|
|
11
|
+
* PUT {api}/projects/{id}/issues/{iid} description, or state_event close
|
|
12
|
+
* POST {api}/projects/{id}/issues/{iid}/notes
|
|
13
|
+
* Forgejo as GitHub, under /api/v1, with type=issues and limit
|
|
14
|
+
*
|
|
15
|
+
* The client lists only open issues, so a closed one is never reopened:
|
|
16
|
+
* drift found again after a close opens a new issue.
|
|
17
|
+
*/
|
|
18
|
+
import { YodelError } from "../project.js";
|
|
19
|
+
const PAGE = 100;
|
|
20
|
+
const MAX_PAGES = 20;
|
|
21
|
+
const repoPath = (repo) => repo.split("/").map(encodeURIComponent).join("/");
|
|
22
|
+
export function issueClient(t, f = fetch) {
|
|
23
|
+
const gitlab = t.forge === "gitlab";
|
|
24
|
+
const headers = gitlab
|
|
25
|
+
? { "PRIVATE-TOKEN": t.token, Accept: "application/json" }
|
|
26
|
+
: t.forge === "forgejo"
|
|
27
|
+
? { Authorization: `token ${t.token}`, Accept: "application/json" }
|
|
28
|
+
: { Authorization: `Bearer ${t.token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" };
|
|
29
|
+
const base = gitlab ? `${t.apiUrl}/projects/${encodeURIComponent(t.repo)}/issues` : `${t.apiUrl}/repos/${repoPath(t.repo)}/issues`;
|
|
30
|
+
const call = async (method, url, body) => {
|
|
31
|
+
let res;
|
|
32
|
+
try {
|
|
33
|
+
res = await f(url, { method, headers: { ...headers, ...(body ? { "Content-Type": "application/json" } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
throw new YodelError(`${t.forge}: ${method} ${url} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
37
|
+
}
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
if (!res.ok)
|
|
40
|
+
throw new YodelError(`${t.forge}: ${method} ${url} answered ${String(res.status)}: ${text.slice(0, 300).replaceAll(t.token, "[token]")}`);
|
|
41
|
+
return text ? JSON.parse(text) : {};
|
|
42
|
+
};
|
|
43
|
+
const toIssue = (r) => ({
|
|
44
|
+
number: Number(gitlab ? r.iid : r.number),
|
|
45
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
46
|
+
body: typeof (gitlab ? r.description : r.body) === "string" ? String(gitlab ? r.description : r.body) : "",
|
|
47
|
+
url: typeof r.html_url === "string" ? r.html_url : typeof r.web_url === "string" ? r.web_url : undefined,
|
|
48
|
+
});
|
|
49
|
+
const list = (page) => gitlab
|
|
50
|
+
? `${base}?state=opened&per_page=${String(PAGE)}&page=${String(page)}`
|
|
51
|
+
: t.forge === "forgejo"
|
|
52
|
+
? `${base}?state=open&type=issues&limit=${String(PAGE)}&page=${String(page)}`
|
|
53
|
+
: `${base}?state=open&per_page=${String(PAGE)}&page=${String(page)}`;
|
|
54
|
+
return {
|
|
55
|
+
async open() {
|
|
56
|
+
const all = [];
|
|
57
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
58
|
+
const got = (await call("GET", list(page)));
|
|
59
|
+
if (!Array.isArray(got))
|
|
60
|
+
throw new YodelError(`${t.forge}: listing issues did not return a list`);
|
|
61
|
+
all.push(...got.filter((r) => r.pull_request === undefined || r.pull_request === null).map(toIssue));
|
|
62
|
+
if (got.length < PAGE)
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
return all;
|
|
66
|
+
},
|
|
67
|
+
async create(title, body) {
|
|
68
|
+
return toIssue((await call("POST", base, gitlab ? { title, description: body } : { title, body })));
|
|
69
|
+
},
|
|
70
|
+
async update(n, title, body) {
|
|
71
|
+
return toIssue((await call(gitlab ? "PUT" : "PATCH", `${base}/${String(n)}`, gitlab ? { title, description: body } : { title, body })));
|
|
72
|
+
},
|
|
73
|
+
async close(n, comment) {
|
|
74
|
+
await call("POST", `${base}/${String(n)}/${gitlab ? "notes" : "comments"}`, { body: comment });
|
|
75
|
+
await call(gitlab ? "PUT" : "PATCH", `${base}/${String(n)}`, gitlab ? { state_event: "close" } : { state: "closed" });
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The marker an environment's tracking issue carries, so the next run finds
|
|
81
|
+
* it. Keyed by the project's directory in its repository (`.` at the root),
|
|
82
|
+
* so two projects in one repository keep their own issues.
|
|
83
|
+
*/
|
|
84
|
+
export function driftIssueMarker(project, environment) {
|
|
85
|
+
const clean = (s) => s.replace(/\s+/g, "_").replace(/-->/g, "");
|
|
86
|
+
return `<!-- yodel:drift project=${clean(project || ".")} env=${clean(environment)} -->`;
|
|
87
|
+
}
|
|
88
|
+
/** Brings the environment's tracking issue in line with the outcome; see the module comment. */
|
|
89
|
+
export async function trackDrift(client, marker, outcome) {
|
|
90
|
+
const found = (await client.open()).filter((i) => i.body.includes(marker)).sort((a, b) => a.number - b.number);
|
|
91
|
+
const issue = found[0];
|
|
92
|
+
if (outcome.kind === "unknown")
|
|
93
|
+
return { action: "left", why: outcome.why, ...(issue ? { issue } : {}) };
|
|
94
|
+
if (outcome.kind === "clean") {
|
|
95
|
+
if (!issue)
|
|
96
|
+
return { action: "none" };
|
|
97
|
+
await client.close(issue.number, outcome.comment);
|
|
98
|
+
return { action: "closed", issue };
|
|
99
|
+
}
|
|
100
|
+
const body = `${marker}\n${outcome.body}`;
|
|
101
|
+
if (!issue)
|
|
102
|
+
return { action: "opened", issue: await client.create(outcome.title, body) };
|
|
103
|
+
if (issue.body.includes(outcome.fingerprint) && issue.title === outcome.title)
|
|
104
|
+
return { action: "unchanged", issue };
|
|
105
|
+
return { action: "updated", issue: await client.update(issue.number, outcome.title, body) };
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=issue.js.map
|