@intentius/chant 0.123.0 → 0.124.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/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/config-import.d.ts +1 -1
- package/dist/lint/policy-import.d.ts +1 -1
- package/dist/notify-verify.js +76 -0
- package/dist/notify.js +307 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts +1 -1
- package/dist/op/index.d.ts +4 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/op-waves-run.d.ts +128 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves.d.ts +14 -0
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/pr-forge.d.ts +30 -3
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +1 -1
- package/dist/workspace/conformance/broker.d.ts +1 -1
- package/package.json +14 -2
- package/src/cli/commands/check-lexicon.ts +3 -1
- package/src/cli/commands/lint-post-synth.e2e.test.ts +117 -0
- package/src/cli/commands/lint.test.ts +4 -109
- package/src/cli/discovery-skip.e2e.test.ts +18 -3
- package/src/config-import.ts +1 -1
- package/src/discovery/sandbox/run.test.ts +1 -1
- package/src/lint/policy-import.ts +1 -1
- package/src/notify-plain-node.e2e.test.ts +68 -0
- package/src/op/__fixtures__/decide-stub-backend.ts +1 -1
- package/src/op/index.ts +18 -2
- package/src/op/op-waves-run.ts +444 -82
- package/src/op/op-waves.test.ts +161 -1
- package/src/op/op-waves.ts +29 -1
- package/src/pr-forge.test.ts +76 -1
- package/src/pr-forge.ts +52 -8
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +1 -1
- package/src/workspace/conformance/__fixtures__/reference-broker.ts +1 -1
- package/src/workspace/conformance/broker.ts +1 -1
- package/src/workspace/records-write.e2e.test.ts +1 -1
- /package/src/audit/{catalog.test.ts → catalog.e2e.test.ts} +0 -0
- /package/src/audit/{discover.test.ts → discover.e2e.test.ts} +0 -0
- /package/src/audit/{remote-discovery.test.ts → remote-discovery.e2e.test.ts} +0 -0
- /package/src/audit/{rules-doc.test.ts → rules-doc.e2e.test.ts} +0 -0
- /package/src/{bench.test.ts → bench.e2e.test.ts} +0 -0
- /package/src/cli/commands/{check-lexicon-doc-drift.test.ts → check-lexicon-doc-drift.e2e.test.ts} +0 -0
- /package/src/cli/commands/{import.test.ts → import.e2e.test.ts} +0 -0
- /package/src/cli/{component-security-boundary.test.ts → component-security-boundary.e2e.test.ts} +0 -0
- /package/src/cli/mcp/{workspace-tools.test.ts → workspace-tools.e2e.test.ts} +0 -0
- /package/src/cli/{security-boundary.test.ts → security-boundary.e2e.test.ts} +0 -0
- /package/src/cli/{serve-mcp-workspace.test.ts → serve-mcp-workspace.e2e.test.ts} +0 -0
- /package/src/cli/{static-config-read.test.ts → static-config-read.e2e.test.ts} +0 -0
- /package/src/codegen/{json-schema-to-ts.test.ts → json-schema-to-ts.e2e.test.ts} +0 -0
- /package/src/{lexicon-seams.test.ts → lexicon-seams.e2e.test.ts} +0 -0
- /package/src/meta/{declared-imports.test.ts → declared-imports.e2e.test.ts} +0 -0
- /package/src/op/{command-gate.test.ts → command-gate.e2e.test.ts} +0 -0
- /package/src/op/{steward-beside.test.ts → steward-beside.e2e.test.ts} +0 -0
- /package/src/op/{steward-points.test.ts → steward-points.e2e.test.ts} +0 -0
- /package/src/workspace/{checks.test.ts → checks.e2e.test.ts} +0 -0
- /package/src/workspace/{intent-reviews.test.ts → intent-reviews.e2e.test.ts} +0 -0
- /package/src/workspace/{profiles.test.ts → profiles.e2e.test.ts} +0 -0
- /package/src/workspace/{read-contract.test.ts → read-contract.e2e.test.ts} +0 -0
- /package/src/workspace/{records-contract.test.ts → records-contract.e2e.test.ts} +0 -0
- /package/src/workspace/{records-since.test.ts → records-since.e2e.test.ts} +0 -0
- /package/src/workspace/{records-write.test.ts → records-write-commands.e2e.test.ts} +0 -0
- /package/src/workspace/{work-size.test.ts → work-size.e2e.test.ts} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check-lexicon.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/check-lexicon.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAInD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB;AAyBD,gEAAgE;AAChE;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAIlE;AAuBD;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAyB/F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAanC;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAazE;AAID;;GAEG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"check-lexicon.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/check-lexicon.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAInD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB;AAyBD,gEAAgE;AAChE;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAIlE;AAuBD;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAyB/F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAanC;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAazE;AAID;;GAEG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAgmBpE;AAqBD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAmDzE"}
|
package/dist/config-import.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* — one narrow module whose only job is "execute project-authored code here",
|
|
7
7
|
* so that the question "did any project code run in this process?" has one
|
|
8
8
|
* place to look and one place to instrument (see
|
|
9
|
-
* `examples/sandbox-execution-boundary.test.ts`, which spies on both).
|
|
9
|
+
* `examples/sandbox-execution-boundary.e2e.test.ts`, which spies on both).
|
|
10
10
|
*
|
|
11
11
|
* Callers must not `import()` a config file directly. `./config-sandbox.ts`
|
|
12
12
|
* decides whether a given load is allowed to come through here at all: under
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `importConfigModule` (`chant.config.ts`): one narrow module whose only job is
|
|
8
8
|
* "execute project-authored code here", so that "did any project code run in
|
|
9
9
|
* this process?" has one place to look and one place to instrument — see
|
|
10
|
-
* `examples/sandbox-execution-boundary.test.ts`, which spies on all three.
|
|
10
|
+
* `examples/sandbox-execution-boundary.e2e.test.ts`, which spies on all three.
|
|
11
11
|
*
|
|
12
12
|
* Before chant #1131 `./policy.ts` called `await import(resolved)` inline,
|
|
13
13
|
* which is why the corpus boundary gate could not see it even after #1113 put
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signing and checking a notify event (#3761). This module needs only
|
|
3
|
+
* `node:crypto`, so a receiver can import it alone and bundle nothing else:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { verifyWebhook } from "@intentius/chant/notify-verify";
|
|
7
|
+
*
|
|
8
|
+
* const checked = verifyWebhook({ body, headers: req.headers, key: process.env.CHANT_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 shared secret, over
|
|
14
|
+
* `<timestamp>.<body>`: the timestamp header (seconds since the epoch), a
|
|
15
|
+
* dot, and the request body exactly as sent. It travels as
|
|
16
|
+
* `x-chant-signature: sha256=<hex>`. Signing the timestamp with the body lets
|
|
17
|
+
* a receiver refuse a delivery replayed later; {@link verifyWebhook} refuses
|
|
18
|
+
* one more than five minutes from now by default. This is SQL Yodeler's
|
|
19
|
+
* scheme (#3756), with the header prefix a product can keep as its own.
|
|
20
|
+
*/
|
|
21
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
22
|
+
/** The header names' default prefix. A product passes its own (`x-yodel`, `x-terragucci`) to keep its receivers working. */
|
|
23
|
+
export const DEFAULT_HEADER_PREFIX = "x-chant";
|
|
24
|
+
/** The four headers a delivery carries, under `prefix`. */
|
|
25
|
+
export function webhookHeaderNames(prefix = DEFAULT_HEADER_PREFIX) {
|
|
26
|
+
const p = prefix.toLowerCase();
|
|
27
|
+
return { signature: `${p}-signature`, timestamp: `${p}-timestamp`, event: `${p}-event`, delivery: `${p}-delivery` };
|
|
28
|
+
}
|
|
29
|
+
/** How far from now a delivery's timestamp {@link verifyWebhook} still accepts, in seconds. */
|
|
30
|
+
export const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
31
|
+
/** `sha256=<hex>` of `<timestamp>.<body>`, keyed with `key`. */
|
|
32
|
+
export function signWebhook(key, timestamp, body) {
|
|
33
|
+
return `sha256=${createHmac("sha256", key).update(`${String(timestamp)}.${body}`, "utf8").digest("hex")}`;
|
|
34
|
+
}
|
|
35
|
+
function header(headers, name) {
|
|
36
|
+
if (typeof headers.get === "function")
|
|
37
|
+
return headers.get(name) ?? undefined;
|
|
38
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
39
|
+
if (k.toLowerCase() !== name)
|
|
40
|
+
continue;
|
|
41
|
+
return Array.isArray(v) ? v[0] : v;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a delivery was signed with `key` and is recent: the signature
|
|
47
|
+
* header matches (compared in constant time) and the timestamp is within the
|
|
48
|
+
* tolerance either side of now. On success, the parsed event.
|
|
49
|
+
*/
|
|
50
|
+
export function verifyWebhook(input) {
|
|
51
|
+
if (!input.key)
|
|
52
|
+
return { ok: false, reason: "no key to verify with" };
|
|
53
|
+
const names = webhookHeaderNames(input.prefix);
|
|
54
|
+
const body = typeof input.body === "string" ? input.body : Buffer.from(input.body).toString("utf8");
|
|
55
|
+
const signature = header(input.headers, names.signature);
|
|
56
|
+
const timestamp = header(input.headers, names.timestamp);
|
|
57
|
+
if (!signature)
|
|
58
|
+
return { ok: false, reason: `no ${names.signature} header` };
|
|
59
|
+
if (!timestamp || !/^\d+$/.test(timestamp))
|
|
60
|
+
return { ok: false, reason: `no ${names.timestamp} header, or not a number of seconds` };
|
|
61
|
+
const expected = Buffer.from(signWebhook(input.key, timestamp, body));
|
|
62
|
+
const given = Buffer.from(signature);
|
|
63
|
+
if (given.length !== expected.length || !timingSafeEqual(given, expected))
|
|
64
|
+
return { ok: false, reason: "the signature does not match" };
|
|
65
|
+
const now = input.now ?? Math.floor(Date.now() / 1000);
|
|
66
|
+
const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
67
|
+
if (Math.abs(now - Number(timestamp)) > tolerance)
|
|
68
|
+
return { ok: false, reason: `the timestamp is more than ${String(tolerance)}s from now` };
|
|
69
|
+
try {
|
|
70
|
+
return { ok: true, event: JSON.parse(body) };
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { ok: false, reason: "the body is not JSON" };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=notify-verify.js.map
|
package/dist/notify.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notify (#3761): tell people that a wave waits for an approval, that a run
|
|
3
|
+
* was refused or failed, or that drift was found.
|
|
4
|
+
*
|
|
5
|
+
* terragucci and SQL Yodeler each carried a copy of this (#3756). chant owns
|
|
6
|
+
* the event, its signing and the Slack and Teams posting; a product decides
|
|
7
|
+
* when to send and what its summary says. One event shape,
|
|
8
|
+
* `chant.notify/v1` (`./notify.schema.json`), carries what both send: the
|
|
9
|
+
* wave or environment, the targets concerned, the plan digest, the command
|
|
10
|
+
* that approves it and the run's link.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { makeNotifyEvent, sendNotify } from "@intentius/chant/notify";
|
|
14
|
+
*
|
|
15
|
+
* const event = makeNotifyEvent({ source: "sql-yodeler", event: "waiting", summary: "prod waits for an approval", targets: ["0007_add_index"], digest, approve: { command } });
|
|
16
|
+
* await sendNotify({ webhook: "YODEL_WEBHOOK_URL", key: "YODEL_WEBHOOK_KEY", slack: "SLACK_WEBHOOK_URL" }, event);
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The configuration names environment variables, never URLs or keys, and
|
|
20
|
+
* nothing here prints a URL or a key. The generic webhook gets the event as
|
|
21
|
+
* JSON, signed over `<timestamp>.<body>` (`./notify-verify.ts`); it is never
|
|
22
|
+
* posted unsigned. Slack and Teams incoming webhooks get a message rendered
|
|
23
|
+
* from the event, unsigned, since neither checks a signature. A delivery that
|
|
24
|
+
* fails is logged and returned; {@link sendNotify} never throws, so a
|
|
25
|
+
* caller's exit code never depends on it.
|
|
26
|
+
*
|
|
27
|
+
* Bundle-safe: this module reaches only `node:crypto` and
|
|
28
|
+
* `./notify-verify.ts` (`./notify-bundle.test.ts`).
|
|
29
|
+
*/
|
|
30
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
31
|
+
import { DEFAULT_HEADER_PREFIX, signWebhook, webhookHeaderNames } from "./notify-verify.js";
|
|
32
|
+
export { DEFAULT_HEADER_PREFIX, DEFAULT_TOLERANCE_SECONDS, signWebhook, verifyWebhook, webhookHeaderNames } from "./notify-verify.js";
|
|
33
|
+
/** The event's `schema` value. A field is only ever added within a version. */
|
|
34
|
+
export const NOTIFY_SCHEMA = "chant.notify/v1";
|
|
35
|
+
/** The JSON Schema's `$id`. */
|
|
36
|
+
export const NOTIFY_SCHEMA_ID = "https://intentius.io/chant/schemas/notify/v1/notify.schema.json";
|
|
37
|
+
/** What happened. */
|
|
38
|
+
export const NOTIFY_EVENTS = ["waiting", "refused", "failed", "drift"];
|
|
39
|
+
/** The CI run, from its environment: GitHub Actions, Forgejo Actions or GitLab CI. */
|
|
40
|
+
export function ciRun(env) {
|
|
41
|
+
const out = {};
|
|
42
|
+
if (env.CI_JOB_URL)
|
|
43
|
+
out.url = env.CI_JOB_URL;
|
|
44
|
+
else if (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID)
|
|
45
|
+
out.url = `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`;
|
|
46
|
+
else if (env.CI_PIPELINE_URL)
|
|
47
|
+
out.url = env.CI_PIPELINE_URL;
|
|
48
|
+
if (env.GITLAB_CI === "true")
|
|
49
|
+
out.forge = "gitlab";
|
|
50
|
+
else if (env.GITEA_ACTIONS === "true" || env.FORGEJO_ACTIONS === "true")
|
|
51
|
+
out.forge = "forgejo";
|
|
52
|
+
else if (env.GITHUB_ACTIONS === "true")
|
|
53
|
+
out.forge = "github";
|
|
54
|
+
const repository = env.GITHUB_REPOSITORY || env.CI_PROJECT_PATH;
|
|
55
|
+
if (repository)
|
|
56
|
+
out.repository = repository;
|
|
57
|
+
const commit = env.GITHUB_SHA || env.CI_COMMIT_SHA;
|
|
58
|
+
if (commit)
|
|
59
|
+
out.commit = commit;
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A whole event: the fields, with the schema, the time, the CI run (the
|
|
64
|
+
* product's own `run` fields win) and an id. The id is a hash over the
|
|
65
|
+
* source, project, event, wave, environment and digest (else the drift
|
|
66
|
+
* fingerprint, else the commit), so a re-run sends the same id; with none of
|
|
67
|
+
* the three it is random.
|
|
68
|
+
*/
|
|
69
|
+
export function makeNotifyEvent(fields, options = {}) {
|
|
70
|
+
const env = options.env ?? process.env;
|
|
71
|
+
const { id: given, project, targets, run: own, ...rest } = fields;
|
|
72
|
+
const run = { ...ciRun(env), ...(own ?? {}) };
|
|
73
|
+
const resolvedProject = project ?? run.repository ?? "this project";
|
|
74
|
+
const key = fields.digest ?? fields.drift?.fingerprint ?? run.commit;
|
|
75
|
+
const id = given ??
|
|
76
|
+
(key
|
|
77
|
+
? createHash("sha256").update(JSON.stringify([fields.source, resolvedProject, fields.event, fields.wave?.number ?? null, fields.environment ?? null, key])).digest("hex")
|
|
78
|
+
: randomUUID());
|
|
79
|
+
return {
|
|
80
|
+
schema: NOTIFY_SCHEMA,
|
|
81
|
+
id,
|
|
82
|
+
at: (options.now ?? new Date()).toISOString(),
|
|
83
|
+
...rest,
|
|
84
|
+
project: resolvedProject,
|
|
85
|
+
targets: targets ?? [],
|
|
86
|
+
...(Object.keys(run).length > 0 ? { run } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const HEADLINE = {
|
|
90
|
+
waiting: "waits for an approval",
|
|
91
|
+
refused: "was refused",
|
|
92
|
+
failed: "failed",
|
|
93
|
+
drift: "drifted",
|
|
94
|
+
};
|
|
95
|
+
/** The first line of every chat message: `<source>: <summary>`, or what happened to what when there is no summary. */
|
|
96
|
+
export function notifyHeadline(e) {
|
|
97
|
+
if (e.summary)
|
|
98
|
+
return `${e.source}: ${e.summary}`;
|
|
99
|
+
const what = e.wave ? `wave ${e.wave.number}${e.wave.name ? ` (${e.wave.name})` : ""} of ${e.project}` : e.environment ? `${e.environment} in ${e.project}` : e.project;
|
|
100
|
+
return `${e.source}: ${what} ${HEADLINE[e.event]}`;
|
|
101
|
+
}
|
|
102
|
+
/** The label and value lines both chat messages show, in order. */
|
|
103
|
+
function facts(e) {
|
|
104
|
+
const out = [];
|
|
105
|
+
if (e.approve?.review)
|
|
106
|
+
out.push({ title: "Review and approve", value: `pull request ${e.approve.review.pr}, then run it again`, link: e.approve.review.url });
|
|
107
|
+
if (e.drift) {
|
|
108
|
+
if (e.drift.changed.length > 0)
|
|
109
|
+
out.push({ title: "Drifted", value: e.drift.changed.join(", ") });
|
|
110
|
+
if (e.drift.missing?.length)
|
|
111
|
+
out.push({ title: "Missing", value: e.drift.missing.join(", ") });
|
|
112
|
+
if (e.drift.failed?.length)
|
|
113
|
+
out.push({ title: "Not refreshed", value: e.drift.failed.join(", ") });
|
|
114
|
+
}
|
|
115
|
+
else if (e.targets.length > 0)
|
|
116
|
+
out.push({ title: "Targets", value: e.targets.join(", ") });
|
|
117
|
+
if (e.digest)
|
|
118
|
+
out.push({ title: "Digest", value: e.digest });
|
|
119
|
+
if (e.approve)
|
|
120
|
+
out.push({ title: e.approve.review ? "Or approve" : "Approve", value: e.approve.command });
|
|
121
|
+
if (e.reason)
|
|
122
|
+
out.push({ title: "Reason", value: e.reason });
|
|
123
|
+
if (e.run?.url)
|
|
124
|
+
out.push({ title: "Run", value: e.run.url, link: e.run.url });
|
|
125
|
+
if (e.reportUrl)
|
|
126
|
+
out.push({ title: "Report", value: e.reportUrl, link: e.reportUrl });
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/** A Slack incoming webhook's body: one mrkdwn text, with the approve command as code and the links as links. */
|
|
130
|
+
export function slackMessage(e) {
|
|
131
|
+
const lines = [`*${notifyHeadline(e)}*`];
|
|
132
|
+
for (const f of facts(e)) {
|
|
133
|
+
if (f.title === "Review and approve")
|
|
134
|
+
lines.push(`${f.title}: <${f.link}|pull request ${e.approve.review.pr}>, then run it again`);
|
|
135
|
+
else if (f.title === "Approve" || f.title === "Or approve" || f.title === "Digest")
|
|
136
|
+
lines.push(`${f.title}: \`${f.value}\``);
|
|
137
|
+
else if (f.link)
|
|
138
|
+
lines.push(`${f.title}: <${f.link}>`);
|
|
139
|
+
else
|
|
140
|
+
lines.push(`${f.title}: ${f.value}`);
|
|
141
|
+
}
|
|
142
|
+
return { text: lines.join("\n") };
|
|
143
|
+
}
|
|
144
|
+
/** A Teams incoming webhook's body (a Workflows webhook): one Adaptive Card, with buttons that open the review page and the run. */
|
|
145
|
+
export function teamsMessage(e) {
|
|
146
|
+
const all = facts(e);
|
|
147
|
+
const actions = [
|
|
148
|
+
...(e.approve?.review ? [{ type: "Action.OpenUrl", title: "Review and approve", url: e.approve.review.url }] : []),
|
|
149
|
+
...(e.run?.url ? [{ type: "Action.OpenUrl", title: "Open the run", url: e.run.url }] : []),
|
|
150
|
+
];
|
|
151
|
+
return {
|
|
152
|
+
type: "message",
|
|
153
|
+
attachments: [
|
|
154
|
+
{
|
|
155
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
156
|
+
contentUrl: null,
|
|
157
|
+
content: {
|
|
158
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
159
|
+
type: "AdaptiveCard",
|
|
160
|
+
version: "1.4",
|
|
161
|
+
body: [
|
|
162
|
+
{ type: "TextBlock", text: notifyHeadline(e), weight: "Bolder", wrap: true },
|
|
163
|
+
{ type: "FactSet", facts: all.map((f) => ({ title: f.title, value: f.link && f.title === "Review and approve" ? `${f.value}: ${f.link}` : f.value })) },
|
|
164
|
+
],
|
|
165
|
+
...(actions.length > 0 ? { actions } : {}),
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const VARIABLE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
172
|
+
const SETTINGS = ["webhook", "key", "slack", "teams", "events"];
|
|
173
|
+
/** The first problem with a {@link NotifyConfig}, or undefined. `where` prefixes the message. */
|
|
174
|
+
export function notifyConfigProblem(value, where = "notify") {
|
|
175
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
176
|
+
return `${where}: expected { webhook, key, slack, teams, events }`;
|
|
177
|
+
const v = value;
|
|
178
|
+
for (const k of Object.keys(v))
|
|
179
|
+
if (!SETTINGS.includes(k))
|
|
180
|
+
return `${where}.${k}: unknown setting (expected ${SETTINGS.join(", ")})`;
|
|
181
|
+
for (const k of ["webhook", "key", "slack", "teams"]) {
|
|
182
|
+
const n = v[k];
|
|
183
|
+
if (n === undefined)
|
|
184
|
+
continue;
|
|
185
|
+
if (typeof n !== "string" || !VARIABLE.test(n)) {
|
|
186
|
+
const what = k === "key" ? "signing key" : "URL";
|
|
187
|
+
return `${where}.${k}: the name of the environment variable that holds the ${what}, never the ${k === "key" ? "key" : "URL"} itself`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (v.webhook !== undefined && v.key === undefined)
|
|
191
|
+
return `${where}.key: a webhook needs a signing key; name the variable that holds it`;
|
|
192
|
+
if (v.key !== undefined && v.webhook === undefined)
|
|
193
|
+
return `${where}.webhook: a key is for a webhook; name the variable that holds its URL`;
|
|
194
|
+
if (v.webhook === undefined && v.slack === undefined && v.teams === undefined)
|
|
195
|
+
return `${where}: name a webhook (with its key), a slack or a teams URL variable`;
|
|
196
|
+
if (v.events !== undefined && !(Array.isArray(v.events) && v.events.every((e) => NOTIFY_EVENTS.includes(e)))) {
|
|
197
|
+
return `${where}.events: a list of ${NOTIFY_EVENTS.join(", ")}`;
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
/** The environment variables a configuration names, for a pipeline to map its secrets into. */
|
|
202
|
+
export function notifyVariables(config) {
|
|
203
|
+
if (!config)
|
|
204
|
+
return [];
|
|
205
|
+
return [config.webhook, config.key, config.slack, config.teams].filter((n) => typeof n === "string" && n !== "");
|
|
206
|
+
}
|
|
207
|
+
/** The request {@link sendNotify} posts to the webhook: the event as JSON, signed with `key` over `<timestamp>.<body>`. */
|
|
208
|
+
export function webhookRequest(event, key, options = {}) {
|
|
209
|
+
const body = JSON.stringify(event);
|
|
210
|
+
const now = options.now ?? Math.floor(Date.now() / 1000);
|
|
211
|
+
const names = webhookHeaderNames(options.prefix ?? DEFAULT_HEADER_PREFIX);
|
|
212
|
+
return {
|
|
213
|
+
body,
|
|
214
|
+
headers: {
|
|
215
|
+
"content-type": "application/json",
|
|
216
|
+
"user-agent": options.userAgent ?? event.source,
|
|
217
|
+
[names.event]: event.event,
|
|
218
|
+
[names.delivery]: event.id,
|
|
219
|
+
[names.timestamp]: String(now),
|
|
220
|
+
[names.signature]: signWebhook(key, now, body),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** A failure's message, with every URL it names cut out. */
|
|
225
|
+
function redact(message, urls) {
|
|
226
|
+
let out = message;
|
|
227
|
+
for (const u of urls)
|
|
228
|
+
if (u)
|
|
229
|
+
out = out.split(u).join("<url>");
|
|
230
|
+
return out.replace(/https?:\/\/\S+/g, "<url>");
|
|
231
|
+
}
|
|
232
|
+
async function post(fetchFn, url, request, attempts, sleep) {
|
|
233
|
+
let last = "not attempted";
|
|
234
|
+
for (let i = 0; i < attempts; i++) {
|
|
235
|
+
if (i > 0)
|
|
236
|
+
await sleep(500 * i);
|
|
237
|
+
try {
|
|
238
|
+
const res = await fetchFn(url, { method: "POST", ...request, signal: AbortSignal.timeout(10000) });
|
|
239
|
+
if (res.ok)
|
|
240
|
+
return undefined;
|
|
241
|
+
last = `HTTP ${String(res.status)}`;
|
|
242
|
+
// A 4xx will not change on a retry: the receiver refused the event.
|
|
243
|
+
if (res.status < 500)
|
|
244
|
+
return last;
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
last = redact(e instanceof Error ? e.message : String(e), [url]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return last;
|
|
251
|
+
}
|
|
252
|
+
const LABEL = { webhook: "the webhook", slack: "Slack", teams: "Teams" };
|
|
253
|
+
/**
|
|
254
|
+
* Sends `event` to what `config` names, reading the URLs and the key from
|
|
255
|
+
* the environment at send time. Never throws: each delivery's outcome is
|
|
256
|
+
* returned, and a failed one is logged without its URL.
|
|
257
|
+
*/
|
|
258
|
+
export async function sendNotify(config, event, options = {}) {
|
|
259
|
+
if (!config || !event)
|
|
260
|
+
return [];
|
|
261
|
+
if (config.events && !config.events.includes(event.event))
|
|
262
|
+
return [];
|
|
263
|
+
const env = options.env ?? process.env;
|
|
264
|
+
const fetchFn = options.fetch ?? ((url, init) => globalThis.fetch(url, init));
|
|
265
|
+
const attempts = Math.max(1, options.attempts ?? 3);
|
|
266
|
+
const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
267
|
+
const log = options.log ?? ((line) => process.stderr.write(line + "\n"));
|
|
268
|
+
const out = [];
|
|
269
|
+
const deliver = async (target, variable, request) => {
|
|
270
|
+
let d;
|
|
271
|
+
try {
|
|
272
|
+
const url = env[variable];
|
|
273
|
+
if (!url)
|
|
274
|
+
d = { target, ok: false, detail: `${variable} is not set` };
|
|
275
|
+
else {
|
|
276
|
+
const r = request();
|
|
277
|
+
if (typeof r === "string")
|
|
278
|
+
d = { target, ok: false, detail: r };
|
|
279
|
+
else {
|
|
280
|
+
const failed = await post(fetchFn, url, r, attempts, sleep);
|
|
281
|
+
d = { target, ok: failed === undefined, detail: failed ?? "delivered" };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
d = { target, ok: false, detail: redact(e instanceof Error ? e.message : String(e), [env[variable] ?? ""]) };
|
|
287
|
+
}
|
|
288
|
+
if (!d.ok)
|
|
289
|
+
log(`notify: the ${event.event} event was not delivered to ${LABEL[target]}: ${d.detail}`);
|
|
290
|
+
out.push(d);
|
|
291
|
+
};
|
|
292
|
+
const json = (body) => ({ headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
293
|
+
if (config.webhook) {
|
|
294
|
+
await deliver("webhook", config.webhook, () => {
|
|
295
|
+
const key = config.key ? env[config.key] : undefined;
|
|
296
|
+
if (!key)
|
|
297
|
+
return `${config.key ?? "the key variable"} is not set, and an event is never sent unsigned`;
|
|
298
|
+
return webhookRequest(event, key, { ...(options.now !== undefined ? { now: options.now } : {}), ...(options.prefix ? { prefix: options.prefix } : {}), ...(options.userAgent ? { userAgent: options.userAgent } : {}) });
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (config.slack)
|
|
302
|
+
await deliver("slack", config.slack, () => json((options.render?.slack ?? slackMessage)(event)));
|
|
303
|
+
if (config.teams)
|
|
304
|
+
await deliver("teams", config.teams, () => json((options.render?.teams ?? teamsMessage)(event)));
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
//# sourceMappingURL=notify.js.map
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* forces one (429 or 529, say). Every request it read is kept in `requests`.
|
|
16
16
|
*
|
|
17
17
|
* It lives under `__fixtures__` because it listens on a port, and no module
|
|
18
|
-
* chant runs does (#2657, `test/no-listener.test.ts`): only tests start it.
|
|
18
|
+
* chant runs does (#2657, `test/no-listener.e2e.test.ts`): only tests start it.
|
|
19
19
|
*/
|
|
20
20
|
import type { WireAnswer } from "../../workspace/points.js";
|
|
21
21
|
import { type BackendRequest } from "../decide-backend.js";
|
package/dist/op/index.d.ts
CHANGED
|
@@ -20,10 +20,10 @@ export { discoverOps } from "./discover.js";
|
|
|
20
20
|
export type { DiscoveredOp, OpDiscoveryResult } from "./discover.js";
|
|
21
21
|
export { generateOpsPipeline, generateOpWavesPipeline, withOpSchedules } from "./generate-pipeline.js";
|
|
22
22
|
export type { GenerateOpsPipelineResult, GenerateOpWavesPipelineResult } from "./generate-pipeline.js";
|
|
23
|
-
export { OP_WAVE_DEFAULT_APPLY, OP_WAVE_GATE_POLICIES, OP_WAVE_HOOK_MOMENTS, assertOpWaveHooks, assertOpWavesSpec, opWaveHooksAt, opWaveJobs, opWaveRecordPath, opWaveShare, } from "./op-waves.js";
|
|
24
|
-
export type { OpWave, OpWaveGatePolicy, OpWaveHook, OpWaveHookFailure, OpWaveHookMoment, OpWaveHooks, OpWaveJob, OpWaveRun, OpWavesSpec } from "./op-waves.js";
|
|
25
|
-
export { opWaveNeedsApproval, opWavePlansPath, opWaveRunsPath, parseOpWavesSpec, readOpWavePolicy, runOpWave } from "./op-waves-run.js";
|
|
26
|
-
export type { OpWaveBaseRules, OpWaveDecision, OpWaveHookOutcome, OpWaveMember, OpWavePlanFile, OpWavePlansFile, OpWaveRunsFile, RunOpWaveOptions, RunOpWaveResult, } from "./op-waves-run.js";
|
|
23
|
+
export { OP_WAVE_APPLY_HOOKS_RUNNERS, OP_WAVE_DEFAULT_APPLY, OP_WAVE_GATE_POLICIES, OP_WAVE_HOOK_MOMENTS, assertOpWaveHooks, assertOpWavesSpec, opWaveApplyHooksRunBy, opWaveHooksAt, opWaveJobs, opWaveRecordPath, opWaveShare, } from "./op-waves.js";
|
|
24
|
+
export type { OpWave, OpWaveApplyHooksRunner, OpWaveGatePolicy, OpWaveHook, OpWaveHookFailure, OpWaveHookMoment, OpWaveHooks, OpWaveJob, OpWaveRun, OpWavesSpec } from "./op-waves.js";
|
|
25
|
+
export { opWaveHooksAtBase, opWaveNeedsApproval, opWavePlansPath, opWaveRunsPath, parseOpWavesSpec, readOpWaveHooksFile, readOpWavePolicy, runOpWave, runOpWaveApplyHooks, } from "./op-waves-run.js";
|
|
26
|
+
export type { OpWaveBaseRules, OpWaveDecision, OpWaveApplyHooksRun, OpWaveHookOutcome, OpWaveHookRun, OpWaveHooksFile, OpWaveHooksResult, OpWaveMember, OpWavePlanFile, OpWavePlansFile, OpWaveRunsFile, RunOpWaveOptions, RunOpWaveResult, } from "./op-waves-run.js";
|
|
27
27
|
export { notifyOpWave, opWaveNotifyFields } from "./op-waves-notify.js";
|
|
28
28
|
export type { OpWaveOutcome } from "./op-waves-notify.js";
|
|
29
29
|
export { loadActivities, loadProfiles, resolveActivity } from "./activity-registry.js";
|
package/dist/op/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/op/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/op/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,12 +29,20 @@
|
|
|
29
29
|
* approved set (#3762). A share job (`--share i`) re-plans its
|
|
30
30
|
* slice first and refuses, without applying it, a run whose plan digest is
|
|
31
31
|
* not the one the deciding job recorded (exit 4).
|
|
32
|
+
* - Hooks file (#3804): every apply call gets `CHANT_HOOKS`, a JSON file
|
|
33
|
+
* ({@link OpWaveHooksFile}) with the outcomes of the hooks that already ran
|
|
34
|
+
* and whether an approval stands. Under `hooks.apply.runBy: "apply"` (read
|
|
35
|
+
* at base) chant does not run the apply-stage hooks itself: the file hands
|
|
36
|
+
* them to the apply, which runs them under its own lock with
|
|
37
|
+
* {@link runOpWaveApplyHooks}, and their outcomes come back through
|
|
38
|
+
* `CHANT_HOOKS_RESULT`. A non-wave apply reads the same hooks at base with
|
|
39
|
+
* {@link opWaveHooksAtBase}.
|
|
32
40
|
*/
|
|
33
41
|
import { type WaveMember } from "../gated-waves.js";
|
|
34
42
|
import { type GateLedgerPort } from "./gate.js";
|
|
35
43
|
import type { HeadPlans, ReviewSource } from "./gate-review.js";
|
|
36
44
|
import type { TrustPolicy } from "../workspace/trust/policy.js";
|
|
37
|
-
import { type GateApprovalSource, type OpWaveGatePolicy, type OpWaveHook, type OpWaveHookMoment, type OpWavesSpec } from "./op-waves.js";
|
|
45
|
+
import { type GateApprovalSource, type OpWaveApplyHooksRunner, type OpWaveGatePolicy, type OpWaveHook, type OpWaveHookMoment, type OpWavesSpec } from "./op-waves.js";
|
|
38
46
|
/** What a run's plan command writes to `{plan}`. */
|
|
39
47
|
export interface OpWavePlanFile {
|
|
40
48
|
planDigest: string;
|
|
@@ -119,6 +127,10 @@ export interface OpWaveDecision {
|
|
|
119
127
|
approvedAt?: string;
|
|
120
128
|
/** When the wave recorded that it applies under the approval, which spent it. */
|
|
121
129
|
spentAt?: string;
|
|
130
|
+
/** `apply`: the apply command runs the apply-stage hooks under its own lock (#3804), as read at base. Absent: chant runs them. */
|
|
131
|
+
applyHooksRunBy?: "apply";
|
|
132
|
+
/** Every hook's outcome so far in the job that wrote the record (#3804). */
|
|
133
|
+
outcomes?: OpWaveHookOutcome[];
|
|
122
134
|
}
|
|
123
135
|
/** One hook's run (#3762). */
|
|
124
136
|
export interface OpWaveHookOutcome {
|
|
@@ -129,6 +141,73 @@ export interface OpWaveHookOutcome {
|
|
|
129
141
|
exit: number;
|
|
130
142
|
outcome: "passed" | "failed" | "held";
|
|
131
143
|
}
|
|
144
|
+
/** One run as a hook sees it (#3804). */
|
|
145
|
+
export interface OpWaveHookRun {
|
|
146
|
+
target: string;
|
|
147
|
+
op: string;
|
|
148
|
+
/** The run's plan file, once planned. */
|
|
149
|
+
plan?: string;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* What chant hands an apply command in `CHANT_HOOKS` (#3804), and what
|
|
153
|
+
* {@link opWaveHooksAtBase} builds for an apply outside the waves. Paths are
|
|
154
|
+
* relative to the job's working directory.
|
|
155
|
+
*/
|
|
156
|
+
export interface OpWaveHooksFile {
|
|
157
|
+
version: 1;
|
|
158
|
+
/** The Op waves spec's name. */
|
|
159
|
+
opWaves: string;
|
|
160
|
+
/** The spec's `op`: a hook's `{op}` when it runs once for the wave. */
|
|
161
|
+
op: string;
|
|
162
|
+
wave: string;
|
|
163
|
+
/** 1-based; 0 when the working tree's spec has no such wave. */
|
|
164
|
+
number: number;
|
|
165
|
+
/** The commit the hooks were read at. */
|
|
166
|
+
base?: string;
|
|
167
|
+
/** The wave's set digest, once decided. */
|
|
168
|
+
digest?: string;
|
|
169
|
+
/** An approval of the digest stands, so a held hook does not stop the apply. */
|
|
170
|
+
approved: boolean;
|
|
171
|
+
/** The labels of the hooks that held the wave, approved or not. */
|
|
172
|
+
heldBy?: string[];
|
|
173
|
+
/** Who runs the apply-stage hooks. With `chant`, `hooks` is empty: chant runs them around the apply. */
|
|
174
|
+
runBy: OpWaveApplyHooksRunner;
|
|
175
|
+
/** The runs this apply call applies. */
|
|
176
|
+
runs: OpWaveHookRun[];
|
|
177
|
+
/** The runs file, a hook's `{runs}` and `CHANT_RUNS`. */
|
|
178
|
+
runsFile: string;
|
|
179
|
+
/** The hooks the apply runs, by moment, as read at base. */
|
|
180
|
+
hooks: Partial<Record<OpWaveHookMoment, OpWaveHook[]>>;
|
|
181
|
+
/** The hooks that already ran for this wave in this job, and how each came out. */
|
|
182
|
+
outcomes: OpWaveHookOutcome[];
|
|
183
|
+
/** Where {@link runOpWaveApplyHooks} writes its outcomes (`CHANT_HOOKS_RESULT`), which chant reads back. */
|
|
184
|
+
result?: string;
|
|
185
|
+
/** Set when the hooks at base could not be read: none run, and the runner refuses to proceed. */
|
|
186
|
+
refused?: string;
|
|
187
|
+
}
|
|
188
|
+
/** What {@link runOpWaveApplyHooks} writes to `CHANT_HOOKS_RESULT` (#3804). */
|
|
189
|
+
export interface OpWaveHooksResult {
|
|
190
|
+
version: 1;
|
|
191
|
+
/** The moments the apply ran, empty ones included. */
|
|
192
|
+
moments: OpWaveHookMoment[];
|
|
193
|
+
outcomes: OpWaveHookOutcome[];
|
|
194
|
+
}
|
|
195
|
+
/** One call of {@link runOpWaveApplyHooks}. */
|
|
196
|
+
export interface OpWaveApplyHooksRun {
|
|
197
|
+
moment: OpWaveHookMoment;
|
|
198
|
+
outcomes: OpWaveHookOutcome[];
|
|
199
|
+
/** Set when a hook with `onFailure: fail` failed; the hooks after it did not run. */
|
|
200
|
+
failed?: string;
|
|
201
|
+
/** The `onFailure: approve` hooks that failed, or why the hooks could not be read at base. */
|
|
202
|
+
held: string[];
|
|
203
|
+
/**
|
|
204
|
+
* Whether the apply may go on: no hook failed, and nothing held it or an
|
|
205
|
+
* approval of the digest stands. When false after a hold, the apply applies
|
|
206
|
+
* nothing and exits non-zero; chant then waits for, or finds, an approval
|
|
207
|
+
* and calls it again.
|
|
208
|
+
*/
|
|
209
|
+
proceed: boolean;
|
|
210
|
+
}
|
|
132
211
|
/** Runs one argv; returns its exit code. `env` is added to the job's environment. Injected so tests run no processes. */
|
|
133
212
|
export type OpWaveExec = (argv: string[], cwd: string, env?: Record<string, string>) => number;
|
|
134
213
|
/** Reads one file at one commit, or null when the commit has no such file. Injected for tests. */
|
|
@@ -150,6 +229,8 @@ export interface OpWaveBaseRules {
|
|
|
150
229
|
hooksRefused?: string;
|
|
151
230
|
/** The spec at base sets `approvals.spend` (#3784). */
|
|
152
231
|
spend: boolean;
|
|
232
|
+
/** Who runs the apply-stage hooks, as read at base (#3804). `chant` when the file, the wave or the hooks are missing there. */
|
|
233
|
+
applyHooksRunBy: OpWaveApplyHooksRunner;
|
|
153
234
|
}
|
|
154
235
|
/**
|
|
155
236
|
* The policy of wave `name`, as the spec file at `base` declares it. The
|
|
@@ -211,6 +292,52 @@ export interface RunOpWaveResult {
|
|
|
211
292
|
/** The hooks that ran (#3762). */
|
|
212
293
|
hooks: OpWaveHookOutcome[];
|
|
213
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Read the hooks file chant hands an apply command (#3804): the one at
|
|
297
|
+
* `path`, by default `CHANT_HOOKS`. Null when there is none, as for an apply
|
|
298
|
+
* that a wave job did not start.
|
|
299
|
+
*/
|
|
300
|
+
export declare function readOpWaveHooksFile(path?: string | undefined, cwd?: string): OpWaveHooksFile | null;
|
|
301
|
+
/**
|
|
302
|
+
* Run one moment's hooks from a hooks file (#3804): what an apply command
|
|
303
|
+
* calls under its own lock, `before-apply` right before it changes anything
|
|
304
|
+
* and `after-apply` once it has, when the spec's `hooks.apply.runBy` at base
|
|
305
|
+
* is `apply`. Also what a product's apply outside the waves calls, with a
|
|
306
|
+
* file from {@link opWaveHooksAtBase}. A moment the file has no hooks for
|
|
307
|
+
* runs nothing and proceeds, so an apply can call it whoever runs the hooks.
|
|
308
|
+
*
|
|
309
|
+
* Each call adds its outcomes to the file's `result` (`CHANT_HOOKS_RESULT`),
|
|
310
|
+
* which chant reads back after the apply exits: the outcomes join the wave's
|
|
311
|
+
* result and decision record, and a hold makes the wave wait for, or find,
|
|
312
|
+
* an approval of its digest. `exec` runs one hook; inject it to run a hook
|
|
313
|
+
* in process or to keep its output.
|
|
314
|
+
*/
|
|
315
|
+
export declare function runOpWaveApplyHooks(file: OpWaveHooksFile, moment: OpWaveHookMoment, options?: {
|
|
316
|
+
cwd?: string;
|
|
317
|
+
exec?: OpWaveExec;
|
|
318
|
+
log?: (line: string) => void;
|
|
319
|
+
}): OpWaveApplyHooksRun;
|
|
320
|
+
/**
|
|
321
|
+
* The hooks of wave `wave` read at base, for an apply outside the waves
|
|
322
|
+
* (#3804): a product's own `apply <env>` that runs the same Op as the wave
|
|
323
|
+
* does, and the same hooks. All four moments are in the file; the apply runs
|
|
324
|
+
* the ones it has a stage for with {@link runOpWaveApplyHooks}. Nothing is
|
|
325
|
+
* approved: a held hook stops the apply. The wave or the file missing at
|
|
326
|
+
* base has no hooks, as in a wave job.
|
|
327
|
+
*/
|
|
328
|
+
export declare function opWaveHooksAtBase(options: {
|
|
329
|
+
specFile: string;
|
|
330
|
+
wave: string;
|
|
331
|
+
/** The runs this apply applies. Default: the wave's runs in the working tree's spec. */
|
|
332
|
+
runs?: ReadonlyArray<{
|
|
333
|
+
target: string;
|
|
334
|
+
op?: string;
|
|
335
|
+
}>;
|
|
336
|
+
/** Default: the spec's `base`, else `HEAD^1`. */
|
|
337
|
+
base?: string;
|
|
338
|
+
cwd?: string;
|
|
339
|
+
show?: OpWaveShowAtBase;
|
|
340
|
+
}): OpWaveHooksFile;
|
|
214
341
|
/** Whether `policy` lets a wave with these members through without an approval. */
|
|
215
342
|
export declare function opWaveNeedsApproval(policy: OpWaveGatePolicy, members: readonly OpWaveMember[]): boolean;
|
|
216
343
|
/** Plan, decide and apply one wave, or one part of a wide wave. */
|