@intentius/chant 0.124.40 → 0.124.42
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/__fixtures__/oidc-emulator.d.ts +49 -0
- package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
- package/dist/__fixtures__/oidc-emulator.js +89 -0
- package/dist/ci-handover.d.ts +117 -0
- package/dist/ci-handover.d.ts.map +1 -0
- package/dist/ci-handover.js +217 -0
- package/dist/ci-identity.d.ts +190 -0
- package/dist/ci-identity.d.ts.map +1 -0
- package/dist/ci-identity.js +219 -0
- package/dist/cli/handlers/approve.d.ts +2 -1
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +20 -6
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +18 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +42 -5
- package/dist/cli/handlers/gate-resume.d.ts +3 -0
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
- package/dist/cli/handlers/gate-resume.js +15 -2
- package/dist/cli/handlers/lock.d.ts +8 -0
- package/dist/cli/handlers/lock.d.ts.map +1 -0
- package/dist/cli/handlers/lock.js +197 -0
- package/dist/cli/handlers/pr-env.d.ts +34 -0
- package/dist/cli/handlers/pr-env.d.ts.map +1 -0
- package/dist/cli/handlers/pr-env.js +436 -0
- package/dist/cli/handlers/pr.d.ts +76 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +615 -55
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +9 -0
- package/dist/cli/handlers/query.d.ts +6 -0
- package/dist/cli/handlers/query.d.ts.map +1 -0
- package/dist/cli/handlers/query.js +80 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +11 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +49 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +46 -2
- package/dist/cli/registry.d.ts +31 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability.d.ts +47 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +44 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +8 -1
- package/dist/components/driver.d.ts +10 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +52 -2
- package/dist/components/fan-out-run.d.ts +37 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +44 -4
- package/dist/components/gate-plan.d.ts +2 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +1 -0
- package/dist/components/pr-environment.d.ts +298 -0
- package/dist/components/pr-environment.d.ts.map +1 -0
- package/dist/components/pr-environment.js +406 -0
- package/dist/components/pr-hooks.d.ts +66 -0
- package/dist/components/pr-hooks.d.ts.map +1 -0
- package/dist/components/pr-hooks.js +119 -0
- package/dist/components/pr-pipeline.d.ts +92 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +150 -21
- package/dist/components/pr-policy.d.ts +109 -0
- package/dist/components/pr-policy.d.ts.map +1 -0
- package/dist/components/pr-policy.js +150 -0
- package/dist/components/pr-run.d.ts +6 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +8 -3
- package/dist/components/wave-plan.d.ts +14 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +34 -2
- package/dist/gate-decision.d.ts +15 -2
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +17 -2
- package/dist/gate-policy.d.ts +140 -0
- package/dist/gate-policy.d.ts.map +1 -0
- package/dist/gate-policy.js +242 -0
- package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
- package/dist/lifecycle/gate-checkpoint.js +1 -1
- package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
- package/dist/lifecycle/gate-seal.d.ts.map +1 -0
- package/dist/lifecycle/query.d.ts +68 -0
- package/dist/lifecycle/query.d.ts.map +1 -0
- package/dist/lifecycle/query.js +246 -0
- package/dist/lifecycle/run-ledger.d.ts +13 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +16 -0
- package/dist/lifecycle/target-lock.d.ts +97 -0
- package/dist/lifecycle/target-lock.d.ts.map +1 -0
- package/dist/lifecycle/target-lock.js +286 -0
- package/dist/op/activities/activity-contracts.d.ts +30 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +26 -0
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +4 -0
- package/dist/op/activities/pr-refs.d.ts +10 -0
- package/dist/op/activities/pr-refs.d.ts.map +1 -0
- package/dist/op/activities/pr-refs.js +26 -0
- package/dist/op/activities/predict-behaviour.d.ts +2 -4
- package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
- package/dist/op/activities/predict-behaviour.js +2 -21
- package/dist/op/activities/review-agent.d.ts +137 -0
- package/dist/op/activities/review-agent.d.ts.map +1 -0
- package/dist/op/activities/review-agent.js +266 -0
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +11 -0
- package/dist/op/cron.d.ts +8 -0
- package/dist/op/cron.d.ts.map +1 -1
- package/dist/op/cron.js +51 -0
- package/dist/op/gate-approval.d.ts +20 -13
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-approval.js +13 -18
- package/dist/op/gate-quorum.d.ts +131 -0
- package/dist/op/gate-quorum.d.ts.map +1 -0
- package/dist/op/gate-quorum.js +183 -0
- package/dist/op/gate-resume.d.ts +4 -2
- package/dist/op/gate-resume.d.ts.map +1 -1
- package/dist/op/gate-resume.js +5 -4
- package/dist/op/gate-review.js +1 -1
- package/dist/op/gate.d.ts +32 -4
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +40 -24
- package/dist/op/index.d.ts +6 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +4 -3
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/op-waves-run.d.ts +28 -43
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +170 -120
- package/dist/op/op-waves.d.ts +55 -3
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +63 -8
- package/dist/op/schedule-overdue.d.ts +57 -0
- package/dist/op/schedule-overdue.d.ts.map +1 -0
- package/dist/op/schedule-overdue.js +138 -0
- package/dist/op/stage-hooks.d.ts +132 -0
- package/dist/op/stage-hooks.d.ts.map +1 -0
- package/dist/op/stage-hooks.js +142 -0
- package/dist/override-gate.d.ts +2 -2
- package/dist/override-gate.d.ts.map +1 -1
- package/dist/override-gate.js +1 -1
- package/dist/pr-forge.d.ts +64 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +331 -44
- package/dist/pr-loop.d.ts +107 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +84 -3
- package/dist/pr-post.d.ts +72 -0
- package/dist/pr-post.d.ts.map +1 -0
- package/dist/pr-post.js +134 -0
- package/dist/pr-requires.d.ts +38 -0
- package/dist/pr-requires.d.ts.map +1 -0
- package/dist/pr-requires.js +48 -0
- package/dist/wave-blocked.d.ts +79 -0
- package/dist/wave-blocked.d.ts.map +1 -0
- package/dist/wave-blocked.js +245 -0
- package/dist/wave-engine.d.ts +6 -0
- package/dist/wave-engine.d.ts.map +1 -1
- package/dist/wave-engine.js +2 -0
- package/dist/wave-runtime.mjs +243 -243
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/checks/ci-workflow.js +19 -4
- package/dist/workspace/ci-branches.d.ts +51 -0
- package/dist/workspace/ci-branches.d.ts.map +1 -0
- package/dist/workspace/ci-branches.js +69 -0
- package/dist/workspace/ci-cloud-identity.d.ts +11 -0
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
- package/dist/workspace/ci-cloud-identity.js +24 -0
- package/dist/workspace/ci-init.d.ts.map +1 -1
- package/dist/workspace/ci-init.js +5 -4
- package/dist/workspace/ci-locks.d.ts +12 -0
- package/dist/workspace/ci-locks.d.ts.map +1 -0
- package/dist/workspace/ci-locks.js +6 -0
- package/dist/workspace/ci-run.d.ts +17 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +66 -4
- package/dist/workspace/ci-workflow-at.d.ts +14 -0
- package/dist/workspace/ci-workflow-at.d.ts.map +1 -0
- package/dist/workspace/ci-workflow-at.js +17 -0
- package/dist/workspace/ci-workflow.d.ts +11 -3
- package/dist/workspace/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-workflow.js +10 -2
- package/dist/workspace/declaration.d.ts +69 -3
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +72 -1
- package/dist/workspace/declaration.schema.json +149 -2
- package/dist/workspace/identity-gate-rule.d.ts +1 -1
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +1 -1
- package/dist/workspace/identity.d.ts +1 -1
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +8 -5
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +23 -0
- package/dist/workspace/pr-report.schema.json +94 -3
- package/dist/workspace/reason-codes.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +1 -1
- package/dist/workspace/security.d.ts.map +1 -1
- package/dist/workspace/security.js +4 -0
- package/dist/workspace/status-gates.d.ts +3 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +8 -7
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +4 -1
- package/dist/workspace/trust/seal.d.ts +2 -2
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +2 -2
- package/package.json +1 -1
- package/src/__fixtures__/oidc-emulator.ts +118 -0
- package/src/ci-handover.test.ts +209 -0
- package/src/ci-handover.ts +237 -0
- package/src/ci-identity.test.ts +201 -0
- package/src/ci-identity.ts +310 -0
- package/src/cli/handlers/approve.ts +18 -5
- package/src/cli/handlers/build.ts +18 -1
- package/src/cli/handlers/fan-out.ts +43 -6
- package/src/cli/handlers/gate-resume.ts +16 -2
- package/src/cli/handlers/lock.test.ts +98 -0
- package/src/cli/handlers/lock.ts +202 -0
- package/src/cli/handlers/operator.test.ts +3 -1
- package/src/cli/handlers/pr-before-merge.test.ts +276 -0
- package/src/cli/handlers/pr-env.ts +462 -0
- package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
- package/src/cli/handlers/pr.test.ts +184 -0
- package/src/cli/handlers/pr.ts +605 -61
- package/src/cli/handlers/promote.ts +9 -0
- package/src/cli/handlers/query.ts +78 -0
- package/src/cli/handlers/run.ts +10 -0
- package/src/cli/main.ts +49 -4
- package/src/cli/parse-args.ts +34 -2
- package/src/cli/registry.ts +31 -2
- package/src/components/capability-targets.test.ts +162 -0
- package/src/components/capability.ts +71 -0
- package/src/components/cli-support.ts +14 -1
- package/src/components/driver.ts +54 -3
- package/src/components/fan-out-run.ts +58 -3
- package/src/components/fan-out-waves.test.ts +45 -1
- package/src/components/gate-plan.ts +3 -0
- package/src/components/pr-environment.test.ts +249 -0
- package/src/components/pr-environment.ts +538 -0
- package/src/components/pr-hooks.test.ts +118 -0
- package/src/components/pr-hooks.ts +157 -0
- package/src/components/pr-pipeline.ts +194 -21
- package/src/components/pr-policy.test.ts +145 -0
- package/src/components/pr-policy.ts +211 -0
- package/src/components/pr-run.ts +14 -2
- package/src/components/wave-plan.ts +38 -3
- package/src/gate-decision-bundle.test.ts +4 -3
- package/src/gate-decision.ts +39 -2
- package/src/gate-policy-bundle.test.ts +28 -0
- package/src/gate-policy.test.ts +162 -0
- package/src/gate-policy.ts +298 -0
- package/src/lifecycle/gate-checkpoint.ts +1 -1
- package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
- package/src/lifecycle/query.test.ts +91 -0
- package/src/lifecycle/query.ts +315 -0
- package/src/lifecycle/run-ledger.ts +18 -0
- package/src/lifecycle/target-lock.test.ts +195 -0
- package/src/lifecycle/target-lock.ts +347 -0
- package/src/op/activities/activity-contracts.ts +31 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activities/pr-refs.ts +28 -0
- package/src/op/activities/predict-behaviour.ts +3 -23
- package/src/op/activities/review-agent.test.ts +240 -0
- package/src/op/activities/review-agent.ts +355 -0
- package/src/op/builders.ts +13 -0
- package/src/op/cron.ts +44 -0
- package/src/op/gate-alias.test.ts +2 -2
- package/src/op/gate-approval.test.ts +30 -9
- package/src/op/gate-approval.ts +23 -26
- package/src/op/gate-quorum.test.ts +139 -0
- package/src/op/gate-quorum.ts +248 -0
- package/src/op/gate-resume.ts +6 -3
- package/src/op/gate-review.ts +1 -1
- package/src/op/gate.ts +64 -23
- package/src/op/index.ts +13 -1
- package/src/op/local-executor.ts +9 -2
- package/src/op/op-waves-hook-targets.test.ts +126 -0
- package/src/op/op-waves-policy-hold.test.ts +66 -0
- package/src/op/op-waves-quorum.test.ts +101 -0
- package/src/op/op-waves-run.ts +197 -183
- package/src/op/op-waves.ts +83 -10
- package/src/op/schedule-overdue.test.ts +220 -0
- package/src/op/schedule-overdue.ts +161 -0
- package/src/op/stage-hooks.ts +251 -0
- package/src/override-gate-bundle.test.ts +1 -1
- package/src/override-gate.ts +2 -2
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +33 -0
- package/src/pr-forge.ts +347 -35
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.test.ts +58 -0
- package/src/pr-loop.ts +170 -4
- package/src/pr-post.test.ts +141 -0
- package/src/pr-post.ts +159 -0
- package/src/pr-requires.test.ts +184 -0
- package/src/pr-requires.ts +79 -0
- package/src/review-agent-bundle.test.ts +35 -0
- package/src/stage-hooks-bundle.test.ts +38 -0
- package/src/wave-blocked-bundle.test.ts +25 -0
- package/src/wave-blocked.test.ts +146 -0
- package/src/wave-blocked.ts +271 -0
- package/src/wave-engine.ts +8 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
- package/src/workspace/checks/ci-workflow.test.ts +9 -0
- package/src/workspace/checks/ci-workflow.ts +18 -4
- package/src/workspace/ci-branches.test.ts +62 -0
- package/src/workspace/ci-branches.ts +79 -0
- package/src/workspace/ci-cloud-identity.test.ts +53 -0
- package/src/workspace/ci-cloud-identity.ts +23 -0
- package/src/workspace/ci-init.ts +5 -4
- package/src/workspace/ci-locks.ts +16 -0
- package/src/workspace/ci-run-index.test.ts +2 -1
- package/src/workspace/ci-run.test.ts +107 -8
- package/src/workspace/ci-run.ts +74 -5
- package/src/workspace/ci-workflow-at.ts +23 -0
- package/src/workspace/ci-workflow.ts +13 -2
- package/src/workspace/declaration.schema.json +149 -2
- package/src/workspace/declaration.test.ts +76 -0
- package/src/workspace/declaration.ts +140 -5
- package/src/workspace/identity-gate-rule.ts +1 -1
- package/src/workspace/identity.ts +1 -1
- package/src/workspace/lineage-upgrade.ts +11 -8
- package/src/workspace/member-pipeline.test.ts +29 -0
- package/src/workspace/member-pipeline.ts +30 -0
- package/src/workspace/pr-report.schema.json +94 -3
- package/src/workspace/reason-codes.ts +1 -1
- package/src/workspace/security.ts +4 -0
- package/src/workspace/status-gates.ts +9 -6
- package/src/workspace/status.ts +4 -1
- package/src/workspace/trust/gate-seal.test.ts +1 -1
- package/src/workspace/trust/seal.ts +2 -2
- package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
- /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate-time policy (#4294): a policy engine run over a plan's change set,
|
|
3
|
+
* with the policy read at base, whose rules deny a plan or hold it for a
|
|
4
|
+
* person.
|
|
5
|
+
*
|
|
6
|
+
* The engine is any command that prints conftest's JSON result
|
|
7
|
+
* (`conftest test --output json`) or OPA's (`opa eval --format json`). The
|
|
8
|
+
* rules are counted as conftest counts them:
|
|
9
|
+
*
|
|
10
|
+
* - `deny`, `violation`, `deny_<name>` and `violation_<name>` deny the plan.
|
|
11
|
+
* A denied plan applies only under an override that counts
|
|
12
|
+
* (`./override-gate.ts`), on top of the gate's own approval.
|
|
13
|
+
* - `hold` and `hold_<name>` hold the plan: it waits for a person's approval
|
|
14
|
+
* of its digest, even where the gate would let it through without one.
|
|
15
|
+
* conftest runs only deny and warn rules, so a hold needs the opa shape.
|
|
16
|
+
* - `warn` and `warn_<name>` are advice. They fail nothing.
|
|
17
|
+
*
|
|
18
|
+
* There is no outcome that lets a plan apply with nobody approving it: a
|
|
19
|
+
* non-empty change set always waits for a person (ws-104, ws-080). A rule
|
|
20
|
+
* named `allow_unattended` is read as no rule at all.
|
|
21
|
+
*
|
|
22
|
+
* A policy that cannot run, or gives no answer this module can read, is an
|
|
23
|
+
* error, and the caller stops the apply: no path waives a policy.
|
|
24
|
+
*
|
|
25
|
+
* Like `./override-gate.ts` this module reads what it is handed and imports
|
|
26
|
+
* only node built-ins, so a product can bundle it into one file
|
|
27
|
+
* (`gate-policy-bundle.test.ts`). The pull-request jobs' use of it is in
|
|
28
|
+
* `./components/pr-policy.ts`.
|
|
29
|
+
*/
|
|
30
|
+
import { spawnSync } from "node:child_process";
|
|
31
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { tmpdir } from "node:os";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
/** The result shapes chant reads: conftest's and OPA's. */
|
|
35
|
+
export const GATE_POLICY_ENGINES = ["conftest", "opa"];
|
|
36
|
+
/** Where the policy is read when the settings name no `path`. */
|
|
37
|
+
export const DEFAULT_POLICY_PATH = "policy";
|
|
38
|
+
export const DENY_RULE = /^(deny|violation)(_[A-Za-z0-9]+)*$/;
|
|
39
|
+
export const WARN_RULE = /^warn(_[A-Za-z0-9]+)*$/;
|
|
40
|
+
export const HOLD_RULE = /^hold(_[A-Za-z0-9]+)*$/;
|
|
41
|
+
const ruleId = (query) => query.replace(/^data\./, "");
|
|
42
|
+
function messageOf(v) {
|
|
43
|
+
if (typeof v === "string")
|
|
44
|
+
return v;
|
|
45
|
+
if (v !== null && typeof v === "object" && typeof v.msg === "string")
|
|
46
|
+
return v.msg;
|
|
47
|
+
return JSON.stringify(v);
|
|
48
|
+
}
|
|
49
|
+
/** The findings in `conftest test --output json`. Undefined when it is not that shape. */
|
|
50
|
+
export function conftestFindings(stdout) {
|
|
51
|
+
let results;
|
|
52
|
+
try {
|
|
53
|
+
results = JSON.parse(stdout);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (!Array.isArray(results))
|
|
59
|
+
return undefined;
|
|
60
|
+
const out = { denials: [], holds: [], warnings: [] };
|
|
61
|
+
for (const r of results) {
|
|
62
|
+
if (r === null || typeof r !== "object")
|
|
63
|
+
return undefined;
|
|
64
|
+
for (const f of r.failures ?? []) {
|
|
65
|
+
// conftest names the query that denied in the failure's metadata; an older one gives only the namespace.
|
|
66
|
+
const rule = typeof f.metadata?.query === "string" ? ruleId(f.metadata.query) : `${r.namespace ?? "main"}.deny`;
|
|
67
|
+
out.denials.push({ rule, message: messageOf(f.msg) });
|
|
68
|
+
}
|
|
69
|
+
for (const w of r.warnings ?? [])
|
|
70
|
+
out.warnings.push(messageOf(w.msg));
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/** The rules of one package's value, and with `nested` its child packages one level down. */
|
|
75
|
+
function packageFindings(doc, out, pkg, nested) {
|
|
76
|
+
for (const [name, value] of Object.entries(doc).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
|
|
77
|
+
// A rule matches when its value is true, a message, or a non-empty set of messages.
|
|
78
|
+
const said = Array.isArray(value) ? value.map(messageOf) : value === true ? [name] : typeof value === "string" && value !== "" ? [value] : [];
|
|
79
|
+
if (DENY_RULE.test(name)) {
|
|
80
|
+
for (const message of said)
|
|
81
|
+
out.denials.push({ rule: `${pkg}.${name}`, message });
|
|
82
|
+
}
|
|
83
|
+
else if (HOLD_RULE.test(name)) {
|
|
84
|
+
for (const message of said)
|
|
85
|
+
out.holds.push({ rule: `${pkg}.${name}`, message });
|
|
86
|
+
}
|
|
87
|
+
else if (WARN_RULE.test(name)) {
|
|
88
|
+
out.warnings.push(...said);
|
|
89
|
+
}
|
|
90
|
+
else if (nested && value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
91
|
+
packageFindings(value, out, `${pkg}.${name}`, false);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The findings in `opa eval --format json data.<namespace>`. The query's
|
|
97
|
+
* value is the package: its rules are read by name. An array value is a
|
|
98
|
+
* deny set named after the query. A query that is undefined has no rule
|
|
99
|
+
* that matched. Undefined when the output is not that shape.
|
|
100
|
+
*/
|
|
101
|
+
export function opaFindings(stdout, namespace = "main") {
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(stdout);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || parsed.errors !== undefined)
|
|
110
|
+
return undefined;
|
|
111
|
+
const value = parsed.result?.[0]?.expressions?.[0]?.value;
|
|
112
|
+
const out = { denials: [], holds: [], warnings: [] };
|
|
113
|
+
if (value === undefined)
|
|
114
|
+
return out;
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
out.denials.push(...value.map((v) => ({ rule: namespace, message: messageOf(v) })));
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
if (value === null || typeof value !== "object")
|
|
120
|
+
return undefined;
|
|
121
|
+
packageFindings(value, out, namespace, true);
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** A verdict from what the engine found. */
|
|
125
|
+
export function gatePolicyVerdict(engine, found) {
|
|
126
|
+
const rules = [...new Set(found.denials.map((d) => d.rule))].sort();
|
|
127
|
+
return {
|
|
128
|
+
result: found.denials.length > 0 ? "denied" : found.holds.length > 0 ? "held" : "passed",
|
|
129
|
+
engine,
|
|
130
|
+
denials: found.denials,
|
|
131
|
+
rules,
|
|
132
|
+
holds: found.holds,
|
|
133
|
+
warnings: found.warnings,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const errorVerdict = (engine, error) => ({ result: "error", engine, denials: [], rules: [], holds: [], warnings: [], error });
|
|
137
|
+
/** The argv one check runs: the settings' `command` with its placeholders filled, or the engine's own. */
|
|
138
|
+
export function gatePolicyArgv(settings, at) {
|
|
139
|
+
if (settings.command)
|
|
140
|
+
return settings.command.map((a) => a.replaceAll("{policy}", at.policy).replaceAll("{input}", at.input));
|
|
141
|
+
if ((settings.engine ?? "conftest") === "opa") {
|
|
142
|
+
return ["opa", "eval", "--format", "json", "--data", at.policy, "--input", at.input, `data.${settings.namespace ?? "main"}`];
|
|
143
|
+
}
|
|
144
|
+
return ["conftest", "test", "--no-color", "--output", "json", "--policy", at.policy, ...(settings.namespace ? ["--namespace", settings.namespace] : ["--all-namespaces"]), at.input];
|
|
145
|
+
}
|
|
146
|
+
/** Runs the engine with the job's environment. */
|
|
147
|
+
export const defaultGatePolicyExec = (argv, cwd) => {
|
|
148
|
+
const r = spawnSync(argv[0], argv.slice(1), { cwd, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 });
|
|
149
|
+
return { status: r.error ? null : r.status, stdout: r.stdout ?? "", stderr: `${r.stderr ?? ""}${r.error ? r.error.message : ""}` };
|
|
150
|
+
};
|
|
151
|
+
const tail = (text) => text.trim().split("\n").slice(-3).join(" ");
|
|
152
|
+
/** Run the engine once over `input` and read its answer. */
|
|
153
|
+
export function checkGatePolicy(options) {
|
|
154
|
+
const { settings } = options;
|
|
155
|
+
const engine = settings.engine ?? "conftest";
|
|
156
|
+
const work = mkdtempSync(join(tmpdir(), "chant-policy-input-"));
|
|
157
|
+
try {
|
|
158
|
+
const file = join(work, "input.json");
|
|
159
|
+
writeFileSync(file, typeof options.input === "string" ? options.input : JSON.stringify(options.input));
|
|
160
|
+
const argv = gatePolicyArgv(settings, { policy: options.policyDir, input: file });
|
|
161
|
+
const r = (options.exec ?? defaultGatePolicyExec)(argv, options.cwd ?? options.policyDir);
|
|
162
|
+
if (r.status === null)
|
|
163
|
+
return errorVerdict(engine, `${argv[0]} did not run: ${tail(r.stderr) || "no such command"}`);
|
|
164
|
+
const found = engine === "opa" ? opaFindings(r.stdout, settings.namespace ?? "main") : conftestFindings(r.stdout);
|
|
165
|
+
if (found === undefined)
|
|
166
|
+
return errorVerdict(engine, `${argv[0]} gave no ${engine} result (exit ${r.status}): ${tail(r.stderr || r.stdout)}`);
|
|
167
|
+
// conftest exits 1 on a denial and 2 or more when it could not run; opa exits 0 on a query it ran.
|
|
168
|
+
if (found.denials.length === 0 && r.status !== 0)
|
|
169
|
+
return errorVerdict(engine, `${argv[0]} exited ${r.status}: ${tail(r.stderr)}`);
|
|
170
|
+
return gatePolicyVerdict(engine, found);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
rmSync(work, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Copy the policy directory `path` out of `tree` (the tree at base) into a
|
|
178
|
+
* new temporary directory, so the engine reads the policy as the base has
|
|
179
|
+
* it and never the change's copy. Returns the directory and how to remove
|
|
180
|
+
* it, or the reason there is no policy to read.
|
|
181
|
+
*/
|
|
182
|
+
export function exportGatePolicy(tree, path) {
|
|
183
|
+
const root = path.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
184
|
+
const kind = root === "" || root === "." ? "dir" : tree.stat(root);
|
|
185
|
+
if (kind === undefined)
|
|
186
|
+
return { error: `the policy directory ${path} does not exist at base` };
|
|
187
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-policy-"));
|
|
188
|
+
const cleanup = () => rmSync(dir, { recursive: true, force: true });
|
|
189
|
+
const copy = (from, to) => {
|
|
190
|
+
for (const entry of tree.list(from) ?? []) {
|
|
191
|
+
const src = from === "" || from === "." ? entry.name : `${from}/${entry.name}`;
|
|
192
|
+
const dest = join(to, entry.name);
|
|
193
|
+
if (entry.type === "dir") {
|
|
194
|
+
mkdirSync(dest, { recursive: true });
|
|
195
|
+
copy(src, dest);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
writeFileSync(dest, tree.bytes ? tree.bytes(src) : tree.read(src));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
try {
|
|
203
|
+
if (kind === "file")
|
|
204
|
+
writeFileSync(join(dir, root.split("/").pop()), tree.bytes ? tree.bytes(root) : tree.read(root));
|
|
205
|
+
else
|
|
206
|
+
copy(root, dir);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
cleanup();
|
|
210
|
+
return { error: `the policy at ${path} could not be read at base: ${err instanceof Error ? err.message : String(err)}` };
|
|
211
|
+
}
|
|
212
|
+
return { dir, cleanup };
|
|
213
|
+
}
|
|
214
|
+
/** The settings, checked. Throws with what is wrong. `where` names them, such as `ci.policy`. */
|
|
215
|
+
export function assertGatePolicySettings(where, raw) {
|
|
216
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
217
|
+
throw new Error(`${where} must be { engine?, path?, namespace?, command? }`);
|
|
218
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
219
|
+
if (key === "engine") {
|
|
220
|
+
if (!GATE_POLICY_ENGINES.includes(value))
|
|
221
|
+
throw new Error(`${where}.engine must be ${GATE_POLICY_ENGINES.join(" or ")}, the result shape the command prints`);
|
|
222
|
+
}
|
|
223
|
+
else if (key === "path" || key === "namespace") {
|
|
224
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
225
|
+
throw new Error(`${where}.${key} must be a non-empty string`);
|
|
226
|
+
if (key === "path" && (value.startsWith("/") || value.split("/").includes("..")))
|
|
227
|
+
throw new Error(`${where}.path must be a directory inside the workspace, relative to its declaration`);
|
|
228
|
+
}
|
|
229
|
+
else if (key === "command") {
|
|
230
|
+
if (!Array.isArray(value) || value.length === 0 || !value.every((a) => typeof a === "string" && a !== ""))
|
|
231
|
+
throw new Error(`${where}.command must be argv, a non-empty list of strings`);
|
|
232
|
+
if (!value.some((a) => a.includes("{input}")))
|
|
233
|
+
throw new Error(`${where}.command must pass the input file as {input}`);
|
|
234
|
+
}
|
|
235
|
+
else if (key === "outcomes" || key === "allow_unattended" || key === "allowUnattended") {
|
|
236
|
+
throw new Error(`${where}.${key}: chant's policy step denies and holds only; a non-empty plan always waits for a person (ws-104), so there is no unattended outcome`);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
throw new Error(`${where}.${key} is not a setting; the settings are engine, path, namespace and command`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The run writes the file just before it appends the pending fact, and puts
|
|
7
7
|
* the file's digest on that fact as `checkpointDigest`. `chant approve`
|
|
8
8
|
* copies the digest onto the approval, the way it copies the plan digest, and
|
|
9
|
-
* a seal covers it (
|
|
9
|
+
* a seal covers it (`./gate-seal.ts`). A later run that
|
|
10
10
|
* resumes at the gate reads the file back and checks it against the digest
|
|
11
11
|
* the approver saw, so a value pushed onto the branch after the approval is
|
|
12
12
|
* refused rather than carried into the steps after the gate.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The run writes the file just before it appends the pending fact, and puts
|
|
7
7
|
* the file's digest on that fact as `checkpointDigest`. `chant approve`
|
|
8
8
|
* copies the digest onto the approval, the way it copies the plan digest, and
|
|
9
|
-
* a seal covers it (
|
|
9
|
+
* a seal covers it (`./gate-seal.ts`). A later run that
|
|
10
10
|
* resumes at the gate reads the file back and checks it against the digest
|
|
11
11
|
* the approver saw, so a value pushed onto the branch after the approval is
|
|
12
12
|
* refused rather than carried into the steps after the gate.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* signers line with no `no-touch-required` option (which the signers parser
|
|
18
18
|
* does not accept). An RSA signature over SHA-1 does not verify.
|
|
19
19
|
*/
|
|
20
|
-
import type { Signer } from "
|
|
20
|
+
import type { Signer } from "../workspace/trust/trust-files.js";
|
|
21
21
|
/**
|
|
22
22
|
* The ssh signature namespace of a gate approval's seal (#3163, ws-080), so a
|
|
23
23
|
* verdict, record or commit signature can't stand in for one.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-seal.d.ts","sourceRoot":"","sources":["../../src/lifecycle/gate-seal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gCAAgC,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,mBAAmB,eAAe,CAAC;AAEhD,kIAAkI;AAClI,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iGAAiG;IACjG,WAAW,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzF,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,kBAAkB,GAAG,kBAAkB,CAWxE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAGtG;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAKpE;AA8BD,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAmCD,iDAAiD;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1F;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,iBAAiB,CA0CtI;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU9D;AAID;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,uGAAuG;IACvG,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yFAAyF;IACzF,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAID,kJAAkJ;AAClJ,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAYrF;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,kBAAkB,GAAG,MAAM,GAAG,IAAI,CAmB1F"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { LedgerReadPort } from "./ledger-port.js";
|
|
2
|
+
/** The version of the table schema below. */
|
|
3
|
+
export declare const QUERY_SCHEMA_VERSION = 1;
|
|
4
|
+
/**
|
|
5
|
+
* Each table's columns, in order, with their SQLite type. A column named in
|
|
6
|
+
* the doc as JSON holds a JSON text, for `json_extract`. Every table starts
|
|
7
|
+
* with `member`: the workspace member whose ledger the row came from, or NULL
|
|
8
|
+
* for a project outside a workspace.
|
|
9
|
+
*/
|
|
10
|
+
export declare const QUERY_TABLES: {
|
|
11
|
+
/** One row per resource in each environment's newest snapshot. `attributes` is JSON. */
|
|
12
|
+
readonly resources: readonly [readonly ["member", "TEXT"], readonly ["env", "TEXT"], readonly ["stack", "TEXT"], readonly ["lexicon", "TEXT"], readonly ["name", "TEXT"], readonly ["type", "TEXT"], readonly ["physical_id", "TEXT"], readonly ["status", "TEXT"], readonly ["last_updated", "TEXT"], readonly ["ownership", "TEXT"], readonly ["attributes", "TEXT"], readonly ["git_sha", "TEXT"], readonly ["observed_at", "TEXT"]];
|
|
13
|
+
/** One row per snapshot file: what was observed, when, and how many resources it holds. */
|
|
14
|
+
readonly snapshots: readonly [readonly ["member", "TEXT"], readonly ["env", "TEXT"], readonly ["stack", "TEXT"], readonly ["lexicon", "TEXT"], readonly ["git_sha", "TEXT"], readonly ["observed_at", "TEXT"], readonly ["depth", "TEXT"], readonly ["resources", "INTEGER"], readonly ["unobserved", "INTEGER"]];
|
|
15
|
+
/** One row per Op run record. `labels` and `outcomes` are JSON. */
|
|
16
|
+
readonly runs: readonly [readonly ["member", "TEXT"], readonly ["id", "TEXT"], readonly ["op", "TEXT"], readonly ["env", "TEXT"], readonly ["started", "TEXT"], readonly ["ended", "TEXT"], readonly ["status", "TEXT"], readonly ["git_sha", "TEXT"], readonly ["forge", "TEXT"], readonly ["run_url", "TEXT"], readonly ["failure_code", "TEXT"], readonly ["failure_reason", "TEXT"], readonly ["failed_phase", "TEXT"], readonly ["gate", "TEXT"], readonly ["steward", "TEXT"], readonly ["labels", "TEXT"], readonly ["outcomes", "TEXT"]];
|
|
17
|
+
/** One row per gate ledger line: `kind` is `pending` or `resolution`. `signed` is 1 for a sealed approval. */
|
|
18
|
+
readonly gates: readonly [readonly ["member", "TEXT"], readonly ["op", "TEXT"], readonly ["gate", "TEXT"], readonly ["kind", "TEXT"], readonly ["timestamp", "TEXT"], readonly ["resolved_by", "TEXT"], readonly ["relayed_by", "TEXT"], readonly ["approver_kind", "TEXT"], readonly ["plan_digest", "TEXT"], readonly ["environment", "TEXT"], readonly ["release_digest", "TEXT"], readonly ["git_sha", "TEXT"], readonly ["run_id", "TEXT"], readonly ["url", "TEXT"], readonly ["note", "TEXT"], readonly ["expires_at", "TEXT"], readonly ["origin", "TEXT"], readonly ["empty", "INTEGER"], readonly ["signed", "INTEGER"]];
|
|
19
|
+
/** One row per deploy record in `<env>/releases.jsonl`. `promoted_from`, `restores` and `redeploys` are JSON. */
|
|
20
|
+
readonly deploys: readonly [readonly ["member", "TEXT"], readonly ["env", "TEXT"], readonly ["component", "TEXT"], readonly ["digest", "TEXT"], readonly ["git_sha", "TEXT"], readonly ["run_id", "TEXT"], readonly ["forge", "TEXT"], readonly ["run_url", "TEXT"], readonly ["timestamp", "TEXT"], readonly ["actor", "TEXT"], readonly ["approver", "TEXT"], readonly ["release_digest", "TEXT"], readonly ["digest_kind", "TEXT"], readonly ["promoted_from", "TEXT"], readonly ["restores", "TEXT"], readonly ["redeploys", "TEXT"]];
|
|
21
|
+
/** One row per release file whose content hashes to its digest. `envs` joins its targets' environments with commas; `artifacts` is JSON. */
|
|
22
|
+
readonly releases: readonly [readonly ["member", "TEXT"], readonly ["release_digest", "TEXT"], readonly ["release_member", "TEXT"], readonly ["git_sha", "TEXT"], readonly ["envs", "TEXT"], readonly ["artifacts", "TEXT"]];
|
|
23
|
+
/** One row per line of `_releases/cuts.jsonl`. `signed` is 1 for a signed cut. */
|
|
24
|
+
readonly cuts: readonly [readonly ["member", "TEXT"], readonly ["release_digest", "TEXT"], readonly ["label", "TEXT"], readonly ["actor", "TEXT"], readonly ["timestamp", "TEXT"], readonly ["run_id", "TEXT"], readonly ["forge", "TEXT"], readonly ["channel", "TEXT"], readonly ["signed", "INTEGER"], readonly ["composed_base", "TEXT"]];
|
|
25
|
+
};
|
|
26
|
+
export type QueryTable = keyof typeof QUERY_TABLES;
|
|
27
|
+
/** The table that describes the others: one row per column, with the schema version. */
|
|
28
|
+
export declare const SCHEMA_TABLE = "chant_schema";
|
|
29
|
+
type Value = string | number | null;
|
|
30
|
+
export type QueryRow = Record<string, Value>;
|
|
31
|
+
/** The rows of each table, as loaded. */
|
|
32
|
+
export type QueryData = Record<QueryTable, QueryRow[]>;
|
|
33
|
+
/** One ledger to read: a member's, or the project's own with `member` null. */
|
|
34
|
+
export interface QueryLedger {
|
|
35
|
+
member: string | null;
|
|
36
|
+
port: LedgerReadPort;
|
|
37
|
+
}
|
|
38
|
+
export interface QueryLoad {
|
|
39
|
+
data: QueryData;
|
|
40
|
+
/** Lines and files that could not be read as their kind, per table. Left out of the rows. */
|
|
41
|
+
malformed: Record<QueryTable, number>;
|
|
42
|
+
}
|
|
43
|
+
export interface QueryResult {
|
|
44
|
+
schemaVersion: number;
|
|
45
|
+
columns: string[];
|
|
46
|
+
rows: QueryRow[];
|
|
47
|
+
/** How many rows each table held. */
|
|
48
|
+
tables: Record<QueryTable, number>;
|
|
49
|
+
malformed: Record<QueryTable, number>;
|
|
50
|
+
}
|
|
51
|
+
/** A statement that can't run, or a Node.js with no SQLite. The message is for a person. */
|
|
52
|
+
export declare class QueryError extends Error {
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Read every table's rows from the ledgers, in the order given. A ledger
|
|
56
|
+
* whose branch is absent reads as no rows. A line or file that is not its
|
|
57
|
+
* kind is counted in `malformed` and left out, as each ledger's own reader
|
|
58
|
+
* does.
|
|
59
|
+
*/
|
|
60
|
+
export declare function loadQueryData(ledgers: readonly QueryLedger[]): Promise<QueryLoad>;
|
|
61
|
+
/** The first word of a statement, past whitespace and comments. */
|
|
62
|
+
export declare function firstWord(sql: string): string;
|
|
63
|
+
/** Run one read-only statement over the tables. */
|
|
64
|
+
export declare function runQuery(sql: string, load: QueryLoad): Promise<QueryResult>;
|
|
65
|
+
/** The rows as an aligned table, a header and a count. */
|
|
66
|
+
export declare function formatQueryResult(r: QueryResult): string;
|
|
67
|
+
export {};
|
|
68
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/lifecycle/query.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAMpD,6CAA6C;AAC7C,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAItC;;;;;GAKG;AACH,eAAO,MAAM,YAAY;IACvB,wFAAwF;;IAExF,2FAA2F;;IAE3F,mEAAmE;;IAEnE,8GAA8G;;IAE9G,iHAAiH;;IAEjH,4IAA4I;;IAE5I,kFAAkF;;CAEH,CAAC;AAElF,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,YAAY,CAAC;AAEnD,wFAAwF;AACxF,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,KAAK,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AACpC,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE7C,yCAAyC;AACzC,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;AAEvD,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,cAAc,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,6FAA6F;IAC7F,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,4FAA4F;AAC5F,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAsDxC;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAyEvF;AA6BD,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,mDAAmD;AACnD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAoCjF;AAID,0DAA0D;AAC1D,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAMxD"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { parseGateLedger, GATES_DIR } from "./gate-ledger.js";
|
|
2
|
+
import { parseReleaseLedger, releaseLedgerLines } from "./release-read.js";
|
|
3
|
+
import { CUTS_FILE, parseCuts, parseRelease, RELEASES_DIR, verifyReleaseDigest } from "./release-store.js";
|
|
4
|
+
import { opOfRunLedgerFile, parseRunLedger } from "./run-ledger.js";
|
|
5
|
+
/** The version of the table schema below. */
|
|
6
|
+
export const QUERY_SCHEMA_VERSION = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Each table's columns, in order, with their SQLite type. A column named in
|
|
9
|
+
* the doc as JSON holds a JSON text, for `json_extract`. Every table starts
|
|
10
|
+
* with `member`: the workspace member whose ledger the row came from, or NULL
|
|
11
|
+
* for a project outside a workspace.
|
|
12
|
+
*/
|
|
13
|
+
export const QUERY_TABLES = {
|
|
14
|
+
/** One row per resource in each environment's newest snapshot. `attributes` is JSON. */
|
|
15
|
+
resources: [["member", "TEXT"], ["env", "TEXT"], ["stack", "TEXT"], ["lexicon", "TEXT"], ["name", "TEXT"], ["type", "TEXT"], ["physical_id", "TEXT"], ["status", "TEXT"], ["last_updated", "TEXT"], ["ownership", "TEXT"], ["attributes", "TEXT"], ["git_sha", "TEXT"], ["observed_at", "TEXT"]],
|
|
16
|
+
/** One row per snapshot file: what was observed, when, and how many resources it holds. */
|
|
17
|
+
snapshots: [["member", "TEXT"], ["env", "TEXT"], ["stack", "TEXT"], ["lexicon", "TEXT"], ["git_sha", "TEXT"], ["observed_at", "TEXT"], ["depth", "TEXT"], ["resources", "INTEGER"], ["unobserved", "INTEGER"]],
|
|
18
|
+
/** One row per Op run record. `labels` and `outcomes` are JSON. */
|
|
19
|
+
runs: [["member", "TEXT"], ["id", "TEXT"], ["op", "TEXT"], ["env", "TEXT"], ["started", "TEXT"], ["ended", "TEXT"], ["status", "TEXT"], ["git_sha", "TEXT"], ["forge", "TEXT"], ["run_url", "TEXT"], ["failure_code", "TEXT"], ["failure_reason", "TEXT"], ["failed_phase", "TEXT"], ["gate", "TEXT"], ["steward", "TEXT"], ["labels", "TEXT"], ["outcomes", "TEXT"]],
|
|
20
|
+
/** One row per gate ledger line: `kind` is `pending` or `resolution`. `signed` is 1 for a sealed approval. */
|
|
21
|
+
gates: [["member", "TEXT"], ["op", "TEXT"], ["gate", "TEXT"], ["kind", "TEXT"], ["timestamp", "TEXT"], ["resolved_by", "TEXT"], ["relayed_by", "TEXT"], ["approver_kind", "TEXT"], ["plan_digest", "TEXT"], ["environment", "TEXT"], ["release_digest", "TEXT"], ["git_sha", "TEXT"], ["run_id", "TEXT"], ["url", "TEXT"], ["note", "TEXT"], ["expires_at", "TEXT"], ["origin", "TEXT"], ["empty", "INTEGER"], ["signed", "INTEGER"]],
|
|
22
|
+
/** One row per deploy record in `<env>/releases.jsonl`. `promoted_from`, `restores` and `redeploys` are JSON. */
|
|
23
|
+
deploys: [["member", "TEXT"], ["env", "TEXT"], ["component", "TEXT"], ["digest", "TEXT"], ["git_sha", "TEXT"], ["run_id", "TEXT"], ["forge", "TEXT"], ["run_url", "TEXT"], ["timestamp", "TEXT"], ["actor", "TEXT"], ["approver", "TEXT"], ["release_digest", "TEXT"], ["digest_kind", "TEXT"], ["promoted_from", "TEXT"], ["restores", "TEXT"], ["redeploys", "TEXT"]],
|
|
24
|
+
/** One row per release file whose content hashes to its digest. `envs` joins its targets' environments with commas; `artifacts` is JSON. */
|
|
25
|
+
releases: [["member", "TEXT"], ["release_digest", "TEXT"], ["release_member", "TEXT"], ["git_sha", "TEXT"], ["envs", "TEXT"], ["artifacts", "TEXT"]],
|
|
26
|
+
/** One row per line of `_releases/cuts.jsonl`. `signed` is 1 for a signed cut. */
|
|
27
|
+
cuts: [["member", "TEXT"], ["release_digest", "TEXT"], ["label", "TEXT"], ["actor", "TEXT"], ["timestamp", "TEXT"], ["run_id", "TEXT"], ["forge", "TEXT"], ["channel", "TEXT"], ["signed", "INTEGER"], ["composed_base", "TEXT"]],
|
|
28
|
+
};
|
|
29
|
+
/** The table that describes the others: one row per column, with the schema version. */
|
|
30
|
+
export const SCHEMA_TABLE = "chant_schema";
|
|
31
|
+
/** A statement that can't run, or a Node.js with no SQLite. The message is for a person. */
|
|
32
|
+
export class QueryError extends Error {
|
|
33
|
+
}
|
|
34
|
+
const TABLE_NAMES = Object.keys(QUERY_TABLES);
|
|
35
|
+
const empty = (v) => Object.fromEntries(TABLE_NAMES.map((t) => [t, v()]));
|
|
36
|
+
const json = (v) => (v === undefined || v === null ? null : JSON.stringify(v));
|
|
37
|
+
const str = (v) => (typeof v === "string" ? v : null);
|
|
38
|
+
const bit = (v) => (v ? 1 : 0);
|
|
39
|
+
/** Whether a parsed `.json` file under an environment is a snapshot (an observation baseline or another file is not). */
|
|
40
|
+
function isSnapshot(v) {
|
|
41
|
+
if (!v || typeof v !== "object")
|
|
42
|
+
return false;
|
|
43
|
+
const s = v;
|
|
44
|
+
return typeof s.lexicon === "string" && typeof s.environment === "string" && typeof s.commit === "string" && typeof s.timestamp === "string" && !!s.resources && typeof s.resources === "object" && !Array.isArray(s.resources);
|
|
45
|
+
}
|
|
46
|
+
function snapshotRows(member, env, file, s, data) {
|
|
47
|
+
// The file name folds the stack in as `<stack>__<lexicon>` (git.ts snapshotStorageKey); the record names it too.
|
|
48
|
+
const key = file.slice(0, -".json".length);
|
|
49
|
+
const stack = s.stack ?? (key.includes("__") ? key.slice(0, key.lastIndexOf("__")) : null);
|
|
50
|
+
const at = { git_sha: s.commit, observed_at: s.timestamp };
|
|
51
|
+
const resources = Object.entries(s.resources);
|
|
52
|
+
data.snapshots.push({ member, env, stack, lexicon: s.lexicon, ...at, depth: s.depth ?? "identity", resources: resources.length, unobserved: Object.keys(s.unobserved ?? {}).length });
|
|
53
|
+
for (const [name, r] of resources) {
|
|
54
|
+
data.resources.push({ member, env, stack, lexicon: s.lexicon, name, type: str(r?.type), physical_id: str(r?.physicalId), status: str(r?.status), last_updated: str(r?.lastUpdated), ownership: str(r?.ownership), attributes: json(r?.attributes), ...at });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function gateRow(member, r) {
|
|
58
|
+
const pending = r.kind === "pending";
|
|
59
|
+
const res = pending ? undefined : r;
|
|
60
|
+
const origin = r.origin;
|
|
61
|
+
return {
|
|
62
|
+
member,
|
|
63
|
+
op: r.op,
|
|
64
|
+
gate: r.gate,
|
|
65
|
+
kind: pending ? "pending" : "resolution",
|
|
66
|
+
timestamp: r.timestamp,
|
|
67
|
+
resolved_by: res?.resolvedBy ?? null,
|
|
68
|
+
relayed_by: str(res?.relayedBy),
|
|
69
|
+
approver_kind: str(res?.approver?.kind),
|
|
70
|
+
plan_digest: str(r.planDigest),
|
|
71
|
+
environment: str(r.environment),
|
|
72
|
+
release_digest: str(r.releaseDigest),
|
|
73
|
+
git_sha: str(r.gitSha),
|
|
74
|
+
run_id: str(r.runId),
|
|
75
|
+
url: str(res?.url),
|
|
76
|
+
note: str(res?.note),
|
|
77
|
+
expires_at: pending ? str(r.expiresAt) : null,
|
|
78
|
+
origin: str(origin),
|
|
79
|
+
empty: bit(res?.empty === true),
|
|
80
|
+
signed: bit(res?.seal !== undefined),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read every table's rows from the ledgers, in the order given. A ledger
|
|
85
|
+
* whose branch is absent reads as no rows. A line or file that is not its
|
|
86
|
+
* kind is counted in `malformed` and left out, as each ledger's own reader
|
|
87
|
+
* does.
|
|
88
|
+
*/
|
|
89
|
+
export async function loadQueryData(ledgers) {
|
|
90
|
+
const data = empty(() => []);
|
|
91
|
+
const malformed = empty(() => 0);
|
|
92
|
+
for (const { member, port } of ledgers) {
|
|
93
|
+
const trees = (await port.trees()).sort();
|
|
94
|
+
for (const env of trees.filter((t) => !t.startsWith("_"))) {
|
|
95
|
+
for (const file of (await port.list(env)).sort()) {
|
|
96
|
+
if (file.endsWith(".json")) {
|
|
97
|
+
const text = await port.read(env, file);
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = text === null ? null : JSON.parse(text);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
parsed = null;
|
|
104
|
+
}
|
|
105
|
+
// Other JSON files live beside the snapshots (the observation baseline); only a snapshot-shaped one is read.
|
|
106
|
+
if (isSnapshot(parsed))
|
|
107
|
+
snapshotRows(member, env, file, parsed, data);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (file === "releases.jsonl") {
|
|
111
|
+
const { records, malformed: bad } = parseReleaseLedger(releaseLedgerLines(await port.read(env, file)));
|
|
112
|
+
malformed.deploys += bad;
|
|
113
|
+
for (const d of records) {
|
|
114
|
+
data.deploys.push({ member, env: d.env, component: d.component, digest: d.digest, git_sha: d.gitSha, run_id: d.runId, forge: str(d.runOrigin?.forge), run_url: str(d.runOrigin?.url), timestamp: d.timestamp, actor: d.actor, approver: str(d.approver), release_digest: str(d.releaseDigest), digest_kind: str(d.digestKind), promoted_from: json(d.promotedFrom), restores: json(d.restores), redeploys: json(d.redeploys) });
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (opOfRunLedgerFile(file) !== null) {
|
|
119
|
+
const text = await port.read(env, file);
|
|
120
|
+
if (text === null)
|
|
121
|
+
continue;
|
|
122
|
+
const { records, malformed: bad } = parseRunLedger(text);
|
|
123
|
+
malformed.runs += bad;
|
|
124
|
+
for (const r of records) {
|
|
125
|
+
data.runs.push({ member, id: r.id, op: r.op, env: r.env, started: r.started, ended: r.ended, status: r.status, git_sha: str(r.gitSha), forge: str(r.runOrigin?.forge), run_url: str(r.runOrigin?.url), failure_code: str(r.failure?.code), failure_reason: str(r.failure?.reason), failed_phase: str(r.failure?.phase), gate: str(r.gate?.name), steward: str(r.steward), labels: json(r.labels ?? {}), outcomes: json(r.outcomes ?? {}) });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (trees.includes(GATES_DIR)) {
|
|
131
|
+
for (const file of (await port.list(GATES_DIR)).sort()) {
|
|
132
|
+
if (!file.endsWith(".jsonl"))
|
|
133
|
+
continue;
|
|
134
|
+
const text = await port.read(GATES_DIR, file);
|
|
135
|
+
if (text === null)
|
|
136
|
+
continue;
|
|
137
|
+
const { resolutions, pending, malformed: bad } = parseGateLedger(text);
|
|
138
|
+
malformed.gates += bad;
|
|
139
|
+
// Both kinds share the file; ledger order is time order, so they are merged back by timestamp.
|
|
140
|
+
const lines = [...pending, ...resolutions].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
141
|
+
for (const r of lines)
|
|
142
|
+
data.gates.push(gateRow(member, r));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (trees.includes(RELEASES_DIR)) {
|
|
146
|
+
for (const file of (await port.list(RELEASES_DIR)).sort()) {
|
|
147
|
+
if (file === CUTS_FILE) {
|
|
148
|
+
const { cuts, malformed: bad } = parseCuts(await port.read(RELEASES_DIR, file));
|
|
149
|
+
malformed.cuts += bad;
|
|
150
|
+
for (const c of cuts) {
|
|
151
|
+
data.cuts.push({ member, release_digest: c.releaseDigest, label: c.label, actor: c.actor, timestamp: c.timestamp, run_id: c.runId, forge: str(c.runOrigin?.forge), channel: str(c.channel), signed: bit(c.signature !== null), composed_base: str(c.composed?.base) });
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!file.endsWith(".json"))
|
|
156
|
+
continue;
|
|
157
|
+
const text = await port.read(RELEASES_DIR, file);
|
|
158
|
+
const release = text === null ? null : parseRelease(text);
|
|
159
|
+
// `readAllReleases` (release-store.ts) counts a file whose content does not hash to its digest as malformed; so does this.
|
|
160
|
+
if (!release || release.releaseDigest.replace(/:/g, "_") !== file.slice(0, -".json".length) || !verifyReleaseDigest(release)) {
|
|
161
|
+
malformed.releases++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
data.releases.push({ member, release_digest: release.releaseDigest, release_member: release.member, git_sha: release.commit, envs: release.targets.map((t) => t.env).join(","), artifacts: json(release.artifactDigests) });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { data, malformed };
|
|
169
|
+
}
|
|
170
|
+
/** node:sqlite, without the experimental warning some Node.js versions print when it loads. */
|
|
171
|
+
async function sqlite() {
|
|
172
|
+
const emit = process.emitWarning;
|
|
173
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
174
|
+
if (/SQLite/.test(typeof warning === "string" ? warning : warning.message))
|
|
175
|
+
return;
|
|
176
|
+
return emit.call(process, warning, ...rest);
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
const name = "node:sqlite";
|
|
180
|
+
return (await import(name));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw new QueryError(`chant query runs on the SQLite built into Node.js 22.13 or later; this is Node.js ${process.versions.node}`);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
process.emitWarning = emit;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** The first word of a statement, past whitespace and comments. */
|
|
190
|
+
export function firstWord(sql) {
|
|
191
|
+
const s = sql.replace(/^(\s+|--[^\n]*(\n|$)|\/\*[\s\S]*?\*\/)*/, "");
|
|
192
|
+
return (/^[A-Za-z]+/.exec(s)?.[0] ?? "").toLowerCase();
|
|
193
|
+
}
|
|
194
|
+
/** Run one read-only statement over the tables. */
|
|
195
|
+
export async function runQuery(sql, load) {
|
|
196
|
+
if (sql.trim() === "")
|
|
197
|
+
throw new QueryError('chant query needs a statement: chant query "SELECT * FROM resources"');
|
|
198
|
+
if (!["select", "with", "values"].includes(firstWord(sql)))
|
|
199
|
+
throw new QueryError("chant query runs one SELECT (or WITH, or VALUES) statement; it changes nothing");
|
|
200
|
+
const { DatabaseSync } = await sqlite();
|
|
201
|
+
const db = new DatabaseSync(":memory:");
|
|
202
|
+
try {
|
|
203
|
+
db.exec(`CREATE TABLE ${SCHEMA_TABLE} ("version" INTEGER, "table_name" TEXT, "column_name" TEXT, "type" TEXT, "position" INTEGER)`);
|
|
204
|
+
const describe = db.prepare(`INSERT INTO ${SCHEMA_TABLE} VALUES (?, ?, ?, ?, ?)`);
|
|
205
|
+
for (const table of TABLE_NAMES) {
|
|
206
|
+
const cols = QUERY_TABLES[table];
|
|
207
|
+
db.exec(`CREATE TABLE ${table} (${cols.map(([c, t]) => `"${c}" ${t}`).join(", ")})`);
|
|
208
|
+
cols.forEach(([c, t], i) => describe.run(QUERY_SCHEMA_VERSION, table, c, t, i + 1));
|
|
209
|
+
const insert = db.prepare(`INSERT INTO ${table} VALUES (${cols.map(() => "?").join(", ")})`);
|
|
210
|
+
db.exec("BEGIN");
|
|
211
|
+
for (const row of load.data[table])
|
|
212
|
+
insert.run(...cols.map(([c]) => row[c] ?? null));
|
|
213
|
+
db.exec("COMMIT");
|
|
214
|
+
}
|
|
215
|
+
db.exec("PRAGMA query_only = ON");
|
|
216
|
+
let stmt;
|
|
217
|
+
try {
|
|
218
|
+
stmt = db.prepare(sql);
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
throw new QueryError(`the query does not run: ${e.message}`);
|
|
222
|
+
}
|
|
223
|
+
let rows;
|
|
224
|
+
try {
|
|
225
|
+
rows = stmt.all();
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
throw new QueryError(`the query does not run: ${e.message}`);
|
|
229
|
+
}
|
|
230
|
+
const columns = typeof stmt.columns === "function" ? stmt.columns().map((c) => c.name) : Object.keys(rows[0] ?? {});
|
|
231
|
+
const tables = Object.fromEntries(TABLE_NAMES.map((t) => [t, load.data[t].length]));
|
|
232
|
+
return { schemaVersion: QUERY_SCHEMA_VERSION, columns, rows: rows.map((r) => ({ ...r })), tables, malformed: load.malformed };
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
db.close();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const cell = (v) => (v === null || v === undefined ? "" : String(v));
|
|
239
|
+
/** The rows as an aligned table, a header and a count. */
|
|
240
|
+
export function formatQueryResult(r) {
|
|
241
|
+
const widths = r.columns.map((c) => Math.max(c.length, ...r.rows.map((row) => cell(row[c]).length)));
|
|
242
|
+
const line = (vals) => vals.map((v, i) => v.padEnd(widths[i])).join(" ").trimEnd();
|
|
243
|
+
const out = [line(r.columns), line(widths.map((w) => "-".repeat(w))), ...r.rows.map((row) => line(r.columns.map((c) => cell(row[c]))))];
|
|
244
|
+
out.push(`(${r.rows.length} ${r.rows.length === 1 ? "row" : "rows"})`);
|
|
245
|
+
return out.join("\n");
|
|
246
|
+
}
|
|
@@ -60,4 +60,17 @@ export declare function readRunLedger(environment: string, op: string, opts?: {
|
|
|
60
60
|
records: OpRunRecord[];
|
|
61
61
|
malformed: number;
|
|
62
62
|
}>;
|
|
63
|
+
/** The run ledger file name for `op` under `<env>/`, `runs__<op>.jsonl`. */
|
|
64
|
+
export declare function runLedgerFile(op: string): string;
|
|
65
|
+
/** The op a run ledger file name holds, or null when `file` is not one. */
|
|
66
|
+
export declare function opOfRunLedgerFile(file: string): string | null;
|
|
67
|
+
/**
|
|
68
|
+
* Parse the text of one run ledger file the way {@link readRunLedger} reads
|
|
69
|
+
* it (#4297): a reader that fetches the file itself, such as `chant query`,
|
|
70
|
+
* gets the same records and the same malformed count.
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseRunLedger(content: string): {
|
|
73
|
+
records: OpRunRecord[];
|
|
74
|
+
malformed: number;
|
|
75
|
+
};
|
|
63
76
|
//# sourceMappingURL=run-ledger.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-ledger.d.ts","sourceRoot":"","sources":["../../src/lifecycle/run-ledger.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAkB,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAA+C,MAAM,eAAe,CAAC;AASrI,0EAA0E;AAC1E,eAAO,MAAM,eAAe,UAAU,CAAC;AAMvC,8FAA8F;AAC9F,wBAAgB,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,MAAM,CAEjE;AAQD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC,EACzC,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,KAAK,EAAE;IACL,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC9B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,OAAO,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACjC,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IACrC,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC,GACA,gBAAgB,CAkDlB;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,gBAAgB,EACvB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACtB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,CAAC,CAsBlD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,MAAM,EACV,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"run-ledger.d.ts","sourceRoot":"","sources":["../../src/lifecycle/run-ledger.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAkB,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAA+C,MAAM,eAAe,CAAC;AASrI,0EAA0E;AAC1E,eAAO,MAAM,eAAe,UAAU,CAAC;AAMvC,8FAA8F;AAC9F,wBAAgB,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,MAAM,CAEjE;AAQD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC,EACzC,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,KAAK,EAAE;IACL,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC9B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,OAAO,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACjC,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IACrC,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC,GACA,gBAAgB,CAkDlB;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,gBAAgB,EACvB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACtB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,CAAC,CAsBlD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,MAAM,EACV,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAIxD;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE7D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CA+B7F"}
|