@intentius/chant 0.45.0 → 0.49.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/audit/core.d.ts +21 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +3 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +14 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +10 -0
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +8 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/config.d.ts +54 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +15 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +223 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +132 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +12 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +4 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/builders.d.ts +134 -8
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/types.d.ts +31 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +11 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +11 -2
- package/src/audit/rules-doc.ts +11 -1
- package/src/build.test.ts +41 -0
- package/src/build.ts +34 -6
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/audit.test.ts +53 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.test.ts +211 -0
- package/src/cli/commands/build.ts +126 -8
- package/src/cli/commands/check-lexicon.test.ts +45 -1
- package/src/cli/commands/check-lexicon.ts +45 -0
- package/src/cli/commands/lexicon-surface-diff.ts +9 -0
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +32 -7
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.ts +4 -4
- package/src/cli/handlers/lifecycle.test.ts +345 -1
- package/src/cli/handlers/lifecycle.ts +300 -11
- package/src/cli/handlers/search.ts +5 -2
- package/src/cli/main.ts +12 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +58 -1
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +7 -0
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/lexicon-regen.ts +19 -1
- package/src/codegen/validate.test.ts +33 -0
- package/src/codegen/validate.ts +21 -2
- package/src/components/auto-release.ts +6 -0
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +8 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/config.test.ts +55 -0
- package/src/config.ts +88 -1
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/fold/subset.test.ts +26 -0
- package/src/fold/subset.ts +45 -19
- package/src/index.ts +4 -0
- package/src/lexicon.ts +227 -2
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/index.ts +3 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/teardown.test.ts +568 -0
- package/src/lifecycle/teardown.ts +359 -0
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.ts +15 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +27 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +8 -1
- package/src/lint/rules/index.ts +10 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/builders.ts +179 -8
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/index.ts +10 -3
- package/src/op/local-executor.ts +172 -25
- package/src/op/op.test.ts +25 -2
- package/src/op/receipt-store.ts +211 -0
- package/src/op/types.ts +33 -1
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +12 -0
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wrangler-deploy` / `wrangler-versions-promote` — the Cloudflare Workers
|
|
3
|
+
* apply leaves (chant #1293, epic #1296). The epic cedes the Workers plane
|
|
4
|
+
* entirely to `wrangler` (first-party, schema-backed, already owns the
|
|
5
|
+
* config format, bindings model, local dev, and deploy) — these two verbs
|
|
6
|
+
* are a typed wrapper over that CLI, not a reimplementation of it.
|
|
7
|
+
*
|
|
8
|
+
* Placement: core, not a cloudflare lexicon. #1293's own analysis weighed
|
|
9
|
+
* three options — core (generic verb, fastest, costs a little conceptual
|
|
10
|
+
* tidiness since `wrangler-deploy` shells a specific vendor CLI), a
|
|
11
|
+
* capability-only cloudflare lexicon (the "correct" home, but `LexiconPlugin`
|
|
12
|
+
* tier-1 completeness — serializer, lint rules, post-synth, LSP, examples,
|
|
13
|
+
* docs — is heavy ceremony for three verbs and no resource types), or
|
|
14
|
+
* generalizing to a `cli-deploy` verb parameterised by tool (tempting, but
|
|
15
|
+
* premature and it re-opens the verb set the bounded-primitives thesis
|
|
16
|
+
* depends on staying closed). Core wins on the same precedent `sign`/
|
|
17
|
+
* `attest-provenance` (./sign.ts) already set: a specific vendor CLI
|
|
18
|
+
* (`cosign`) shelled out through the injectable `ProcessRunner`
|
|
19
|
+
* (./process-runner.ts), no cloud-specific SDK client needed the way AWS's
|
|
20
|
+
* `CloudExecutor` (./cloud-executor.ts) models CloudFormation/ECS/Lambda.
|
|
21
|
+
* `wrangler` fits that exact shape. Revisit if/when the zone-plane lexicon
|
|
22
|
+
* (#1294) lands and gives these three verbs a natural cloudflare-owned home.
|
|
23
|
+
*
|
|
24
|
+
* `wrangler-versions-promote` is also the direct evidence for #1296's
|
|
25
|
+
* "three new verbs cover a whole new cloud" claim: Cloudflare's native
|
|
26
|
+
* Worker version rollback maps onto the existing `RollbackPolicy: "native"`
|
|
27
|
+
* (../capability.ts) with no compensation code to hand-write — both verbs
|
|
28
|
+
* here declare a `rollback` (auto-derived "native" per ../capability.ts's
|
|
29
|
+
* `Capability.rollbackPolicy` doc) that re-promotes to whichever version was
|
|
30
|
+
* live before the step ran, the same best-effort captured-previous-state
|
|
31
|
+
* pattern lexicons/aws/src/components/apply.ts's `lambda-deploy` uses for
|
|
32
|
+
* its alias rollback.
|
|
33
|
+
*
|
|
34
|
+
* No real Cloudflare control-plane emulator exists yet (#1295, epic #1296),
|
|
35
|
+
* so — like ./sign.ts — every real path here shells out through the
|
|
36
|
+
* injectable `ProcessRunner` and every test substitutes `MockProcessRunner`
|
|
37
|
+
* (./__tests__/mock-process-runner.ts): no live `wrangler`, no network, ever,
|
|
38
|
+
* in a test run. CI coverage is plan-shape/invocation-shape assertions only,
|
|
39
|
+
* per #1293's own "Verification" section.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import type { Capability } from "../capability";
|
|
43
|
+
import { defaultProcessRunner, q, requireTool, type ProcessRunner } from "./process-runner";
|
|
44
|
+
|
|
45
|
+
const WRANGLER_TOOL = "wrangler";
|
|
46
|
+
|
|
47
|
+
/** Distinguishes one deploy target (a wrangler config + optional named environment) from another, so the best-effort previous-version tracking below never conflates two different Workers sharing a process. */
|
|
48
|
+
function targetKey(config: string, env?: string): string {
|
|
49
|
+
return env ? `${config}#${env}` : config;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildWranglerDeployArgs(input: WranglerDeployInput): string {
|
|
53
|
+
const args = ["wrangler", "deploy", "--config", q(input.config)];
|
|
54
|
+
if (input.env) args.push("--env", q(input.env));
|
|
55
|
+
return args.join(" ");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildVersionsListArgs(config: string, env?: string): string {
|
|
59
|
+
const args = ["wrangler", "versions", "list", "--config", q(config), "--json"];
|
|
60
|
+
if (env) args.push("--env", q(env));
|
|
61
|
+
return args.join(" ");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildVersionsPromoteArgs(config: string, versionId: string, percentage: number, env?: string): string {
|
|
65
|
+
const args = ["wrangler", "versions", "deploy", q(`${versionId}@${percentage}`), "--config", q(config), "--yes"];
|
|
66
|
+
if (env) args.push("--env", q(env));
|
|
67
|
+
return args.join(" ");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Thrown when `wrangler deploy`'s stdout carries no parseable Version ID — fail-closed rather than returning a capability output downstream steps (`wrangler-versions-promote`, wired via `"@Deploy.versionId"`) would silently receive as `undefined`. */
|
|
71
|
+
export class WranglerVersionIdNotFoundError extends Error {
|
|
72
|
+
constructor(public readonly stdout: string) {
|
|
73
|
+
super(`wrangler-deploy: could not find a Version ID in wrangler's output. Got:\n${stdout}`);
|
|
74
|
+
this.name = "WranglerVersionIdNotFoundError";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// wrangler's deploy/versions-upload output names the new version as e.g.
|
|
79
|
+
// "Version ID: 07bcb198-... " (gradual deployments) — accept either "Version
|
|
80
|
+
// ID:" or the bare "Version:" wording across wrangler versions, and require a
|
|
81
|
+
// UUID-shaped token so a stray "Version: 2" summary line can't be mistaken
|
|
82
|
+
// for it.
|
|
83
|
+
const VERSION_ID_RE = /Version(?: ID)?:\s*([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/;
|
|
84
|
+
|
|
85
|
+
/** Extract the deployed Worker version id from `wrangler deploy`'s stdout. Exported so tests can assert on it directly, mirroring ./sign.ts's `buildSignArgs`. */
|
|
86
|
+
export function parseWranglerVersionId(stdout: string): string {
|
|
87
|
+
const match = VERSION_ID_RE.exec(stdout);
|
|
88
|
+
if (!match) throw new WranglerVersionIdNotFoundError(stdout);
|
|
89
|
+
return match[1]!;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface WranglerVersionsListEntry {
|
|
93
|
+
id: string;
|
|
94
|
+
percentage?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Best-effort: the currently-live (100%) version id, from `wrangler versions
|
|
99
|
+
* list --json` — `undefined` for a first deploy (no versions yet) or if
|
|
100
|
+
* listing fails/parses oddly. Never throws: capturing "what to roll back to"
|
|
101
|
+
* must not block the deploy/promote itself, the same fail-soft stance
|
|
102
|
+
* `lambda-deploy`'s alias-version capture takes.
|
|
103
|
+
*/
|
|
104
|
+
async function currentLiveVersionId(
|
|
105
|
+
runner: ProcessRunner,
|
|
106
|
+
config: string,
|
|
107
|
+
env?: string,
|
|
108
|
+
): Promise<string | undefined> {
|
|
109
|
+
try {
|
|
110
|
+
const { stdout } = await runner.run(buildVersionsListArgs(config, env));
|
|
111
|
+
const entries = JSON.parse(stdout) as WranglerVersionsListEntry[];
|
|
112
|
+
return entries.find((entry) => entry.percentage === 100)?.id;
|
|
113
|
+
} catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── wrangler-deploy ──────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export interface WranglerDeployInput {
|
|
121
|
+
/** Path to the Worker's `wrangler.jsonc`/`wrangler.toml`. */
|
|
122
|
+
config: string;
|
|
123
|
+
/** Named environment within the config (`wrangler deploy --env <env>`). */
|
|
124
|
+
env?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface WranglerDeployOutput {
|
|
128
|
+
/** The version id wrangler assigned this deploy — wire into `wrangler-versions-promote` (e.g. `"@Deploy.versionId"`). */
|
|
129
|
+
versionId: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Deploy a Worker from a `wrangler.jsonc`/`wrangler.toml` via `wrangler
|
|
134
|
+
* deploy`, and return the version id it published — the analogue of
|
|
135
|
+
* `lambda-deploy` (../../lexicons/aws/src/components/apply.ts) for the
|
|
136
|
+
* Workers plane.
|
|
137
|
+
*
|
|
138
|
+
* Rollback: `wrangler-versions-promote` back to whichever version was live
|
|
139
|
+
* before this step ran (captured up front via `wrangler versions list`, the
|
|
140
|
+
* same best-effort captured-previous pattern `lambda-deploy` uses for its
|
|
141
|
+
* alias). A no-op on a first deploy — nothing was live to restore.
|
|
142
|
+
*/
|
|
143
|
+
export function createWranglerDeployCapability(
|
|
144
|
+
processRunner: ProcessRunner = defaultProcessRunner(),
|
|
145
|
+
): Capability<WranglerDeployInput, WranglerDeployOutput> {
|
|
146
|
+
const previousVersionByTarget = new Map<string, string | undefined>();
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
kind: "wrangler-deploy",
|
|
150
|
+
async run(_ctx, input) {
|
|
151
|
+
await requireTool(processRunner, WRANGLER_TOOL, `deploy the Worker in ${input.config}`);
|
|
152
|
+
const target = targetKey(input.config, input.env);
|
|
153
|
+
if (!previousVersionByTarget.has(target)) {
|
|
154
|
+
previousVersionByTarget.set(target, await currentLiveVersionId(processRunner, input.config, input.env));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { stdout } = await processRunner.run(buildWranglerDeployArgs(input));
|
|
158
|
+
return { versionId: parseWranglerVersionId(stdout) };
|
|
159
|
+
},
|
|
160
|
+
async rollback(_ctx, input) {
|
|
161
|
+
const target = targetKey(input.config, input.env);
|
|
162
|
+
const previousVersionId = previousVersionByTarget.get(target);
|
|
163
|
+
if (!previousVersionId) return; // first deploy — nothing was live before it.
|
|
164
|
+
await requireTool(processRunner, WRANGLER_TOOL, `roll back the Worker in ${input.config}`);
|
|
165
|
+
await processRunner.run(buildVersionsPromoteArgs(input.config, previousVersionId, 100, input.env));
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Default `wrangler-deploy` capability, backed by the real `ProcessRunner`. */
|
|
171
|
+
export const wranglerDeployCapability: Capability<WranglerDeployInput, WranglerDeployOutput> =
|
|
172
|
+
createWranglerDeployCapability();
|
|
173
|
+
|
|
174
|
+
// ── wrangler-versions-promote ─────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
export interface WranglerVersionsPromoteInput {
|
|
177
|
+
/** Path to the Worker's `wrangler.jsonc`/`wrangler.toml`. */
|
|
178
|
+
config: string;
|
|
179
|
+
/** Version id to promote — typically wired from a prior `wrangler-deploy` step (`"@Deploy.versionId"`) or a prior version id when this step composes as an explicit rollback. */
|
|
180
|
+
versionId: string;
|
|
181
|
+
/** Traffic percentage to route to `versionId`. Default: 100 (full promote/rollback). Below 100 is the gradual-deployment lever. */
|
|
182
|
+
percentage?: number;
|
|
183
|
+
/** Named environment within the config (`wrangler versions deploy --env <env>`). */
|
|
184
|
+
env?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface WranglerVersionsPromoteOutput {
|
|
188
|
+
/** The version id that was promoted. */
|
|
189
|
+
versionId: string;
|
|
190
|
+
/** The traffic percentage actually routed to it. */
|
|
191
|
+
percentage: number;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Promote a Worker version to (some percentage of) live traffic via
|
|
196
|
+
* `wrangler versions deploy <version-id>@<percentage> --yes` — both the
|
|
197
|
+
* gradual-deployment lever (`percentage` < 100) and, at `percentage: 100`,
|
|
198
|
+
* the rollback mechanism: Cloudflare's native version rollback is a promote
|
|
199
|
+
* to a prior version id, not a redeploy, so this same verb composes as the
|
|
200
|
+
* explicit compensation step a component wires up (#1293's "verification"
|
|
201
|
+
* example: forced failure after `wrangler-deploy` -> `wrangler-versions-promote`
|
|
202
|
+
* back to the prior version, no hand-written compensation needed).
|
|
203
|
+
*
|
|
204
|
+
* Also declares its own `rollback` (native, no `rollbackPolicy` override
|
|
205
|
+
* needed — see ../capability.ts): re-promotes to whichever version was live
|
|
206
|
+
* before *this* promote call, for the case where the promote step itself is
|
|
207
|
+
* composed directly (not just as `wrangler-deploy`'s compensation).
|
|
208
|
+
*/
|
|
209
|
+
export function createWranglerVersionsPromoteCapability(
|
|
210
|
+
processRunner: ProcessRunner = defaultProcessRunner(),
|
|
211
|
+
): Capability<WranglerVersionsPromoteInput, WranglerVersionsPromoteOutput> {
|
|
212
|
+
const previousVersionByTarget = new Map<string, string | undefined>();
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
kind: "wrangler-versions-promote",
|
|
216
|
+
async run(_ctx, input) {
|
|
217
|
+
await requireTool(processRunner, WRANGLER_TOOL, `promote version ${input.versionId} in ${input.config}`);
|
|
218
|
+
const percentage = input.percentage ?? 100;
|
|
219
|
+
const target = targetKey(input.config, input.env);
|
|
220
|
+
if (!previousVersionByTarget.has(target)) {
|
|
221
|
+
previousVersionByTarget.set(target, await currentLiveVersionId(processRunner, input.config, input.env));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await processRunner.run(buildVersionsPromoteArgs(input.config, input.versionId, percentage, input.env));
|
|
225
|
+
return { versionId: input.versionId, percentage };
|
|
226
|
+
},
|
|
227
|
+
async rollback(_ctx, input) {
|
|
228
|
+
const target = targetKey(input.config, input.env);
|
|
229
|
+
const previousVersionId = previousVersionByTarget.get(target);
|
|
230
|
+
if (!previousVersionId) return; // nothing was live before this promote.
|
|
231
|
+
await requireTool(processRunner, WRANGLER_TOOL, `roll back version promotion in ${input.config}`);
|
|
232
|
+
await processRunner.run(buildVersionsPromoteArgs(input.config, previousVersionId, 100, input.env));
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Default `wrangler-versions-promote` capability, backed by the real `ProcessRunner`. */
|
|
238
|
+
export const wranglerVersionsPromoteCapability: Capability<
|
|
239
|
+
WranglerVersionsPromoteInput,
|
|
240
|
+
WranglerVersionsPromoteOutput
|
|
241
|
+
> = createWranglerVersionsPromoteCapability();
|
package/src/config.test.ts
CHANGED
|
@@ -6,9 +6,12 @@ import {
|
|
|
6
6
|
resolveAutoReleaseDisabled,
|
|
7
7
|
resolveFoldEnabled,
|
|
8
8
|
resolveSbomFormat,
|
|
9
|
+
resolveKnowledgeDir,
|
|
9
10
|
environmentName,
|
|
10
11
|
environmentNames,
|
|
11
12
|
environmentEndpoint,
|
|
13
|
+
matchesEnvironmentPattern,
|
|
14
|
+
matchesDeclaredEnvironment,
|
|
12
15
|
} from "./config";
|
|
13
16
|
import { writeFileSync, mkdirSync, rmSync } from "fs";
|
|
14
17
|
import { join } from "path";
|
|
@@ -336,6 +339,44 @@ describe("environmentName / environmentNames / environmentEndpoint (#1166)", ()
|
|
|
336
339
|
expect(environmentEndpoint(environments, "unknown")).toBeUndefined(); // not declared at all
|
|
337
340
|
expect(environmentEndpoint(undefined, "floci")).toBeUndefined();
|
|
338
341
|
});
|
|
342
|
+
|
|
343
|
+
test("environmentEndpoint resolves through a pattern entry, literal entry winning (#1221)", () => {
|
|
344
|
+
const environments = [
|
|
345
|
+
{ name: "pr-special", endpoint: "http://special:1111" },
|
|
346
|
+
{ name: "pr-*", endpoint: "http://preview:4566" },
|
|
347
|
+
];
|
|
348
|
+
expect(environmentEndpoint(environments, "pr-42")).toBe("http://preview:4566");
|
|
349
|
+
expect(environmentEndpoint(environments, "pr-special")).toBe("http://special:1111"); // literal wins over the pattern
|
|
350
|
+
expect(environmentEndpoint(environments, "prod")).toBeUndefined();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe("environment glob patterns (#1221)", () => {
|
|
355
|
+
test("matchesEnvironmentPattern: a * matches any run of characters, anchored", () => {
|
|
356
|
+
expect(matchesEnvironmentPattern("pr-*", "pr-42")).toBe(true);
|
|
357
|
+
expect(matchesEnvironmentPattern("pr-*", "pr-")).toBe(true); // empty run is a run
|
|
358
|
+
expect(matchesEnvironmentPattern("pr-*", "pr")).toBe(false);
|
|
359
|
+
expect(matchesEnvironmentPattern("pr-*", "xpr-42")).toBe(false); // anchored at the start
|
|
360
|
+
expect(matchesEnvironmentPattern("*-preview", "42-preview")).toBe(true);
|
|
361
|
+
expect(matchesEnvironmentPattern("*-preview", "42-preview-old")).toBe(false); // anchored at the end
|
|
362
|
+
expect(matchesEnvironmentPattern("test-*-eu", "test-a-eu")).toBe(true);
|
|
363
|
+
expect(matchesEnvironmentPattern("test-*-eu", "test-a-us")).toBe(false);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("matchesEnvironmentPattern without a * is plain equality", () => {
|
|
367
|
+
expect(matchesEnvironmentPattern("prod", "prod")).toBe(true);
|
|
368
|
+
expect(matchesEnvironmentPattern("prod", "prod2")).toBe(false);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("matchesDeclaredEnvironment: literal first, then pattern, across entry forms", () => {
|
|
372
|
+
const environments = ["dev", "prod", "pr-*", { name: "test-*", endpoint: "http://localhost:4566" }];
|
|
373
|
+
expect(matchesDeclaredEnvironment(environments, "prod")).toBe(true);
|
|
374
|
+
expect(matchesDeclaredEnvironment(environments, "pr-42")).toBe(true);
|
|
375
|
+
expect(matchesDeclaredEnvironment(environments, "test-suite-a")).toBe(true); // object-form pattern entry
|
|
376
|
+
expect(matchesDeclaredEnvironment(environments, "stage")).toBe(false);
|
|
377
|
+
expect(matchesDeclaredEnvironment(undefined, "prod")).toBe(false);
|
|
378
|
+
expect(matchesDeclaredEnvironment([], "prod")).toBe(false);
|
|
379
|
+
});
|
|
339
380
|
});
|
|
340
381
|
|
|
341
382
|
describe("resolveFoldEnabled (#1134 — fold is the default build path)", () => {
|
|
@@ -393,3 +434,17 @@ describe("resolveSbomFormat (#606)", () => {
|
|
|
393
434
|
expect(resolveSbomFormat({}, "cyclonedx")).toBe("cyclonedx");
|
|
394
435
|
});
|
|
395
436
|
});
|
|
437
|
+
|
|
438
|
+
describe("resolveKnowledgeDir (#1864, design #1059)", () => {
|
|
439
|
+
test("convention: knowledge/ beside the project root when config is silent", () => {
|
|
440
|
+
expect(resolveKnowledgeDir({}, "/proj")).toBe(join("/proj", "knowledge"));
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("config override honored", () => {
|
|
444
|
+
expect(resolveKnowledgeDir({ knowledge: { dir: "docs/knowledge" } }, "/proj")).toBe(join("/proj", "docs/knowledge"));
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("an empty knowledge object still falls back to the convention name", () => {
|
|
448
|
+
expect(resolveKnowledgeDir({ knowledge: {} }, "/proj")).toBe(join("/proj", "knowledge"));
|
|
449
|
+
});
|
|
450
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -44,6 +44,56 @@ export function environmentNames(environments: EnvironmentDeclaration[] | undefi
|
|
|
44
44
|
return environments?.map(environmentName);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Whether a declared environment name is a glob pattern rather than a literal
|
|
49
|
+
* (#1221) — it contains at least one `*`.
|
|
50
|
+
*/
|
|
51
|
+
export function isEnvironmentPattern(declaredName: string): boolean {
|
|
52
|
+
return declaredName.includes("*");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Anchored `*`-glob match (#1221): each `*` in `pattern` matches any run of
|
|
57
|
+
* characters (possibly empty); everything else matches itself. Deliberately
|
|
58
|
+
* the whole surface — no `?`, no character classes, no regex. A pattern with
|
|
59
|
+
* no `*` degenerates to string equality.
|
|
60
|
+
*/
|
|
61
|
+
export function matchesEnvironmentPattern(pattern: string, name: string): boolean {
|
|
62
|
+
const parts = pattern.split("*");
|
|
63
|
+
if (parts.length === 1) return pattern === name;
|
|
64
|
+
const first = parts[0];
|
|
65
|
+
const last = parts[parts.length - 1];
|
|
66
|
+
if (!name.startsWith(first)) return false;
|
|
67
|
+
let cursor = first.length;
|
|
68
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
69
|
+
const part = parts[i];
|
|
70
|
+
if (part === "") continue;
|
|
71
|
+
const at = name.indexOf(part, cursor);
|
|
72
|
+
if (at === -1) return false;
|
|
73
|
+
cursor = at + part.length;
|
|
74
|
+
}
|
|
75
|
+
return name.length - cursor >= last.length && name.endsWith(last);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Environment-membership test (#1221): is `name` covered by this project's
|
|
80
|
+
* declared `environments`? A literal entry matches by equality; an entry
|
|
81
|
+
* containing `*` matches as a glob pattern (`"pr-*"` covers `pr-42`).
|
|
82
|
+
* Literals are checked first across the whole list, then patterns — so a
|
|
83
|
+
* name that is declared outright never depends on pattern order. `undefined`
|
|
84
|
+
* / empty `environments` returns `false`; callers that treat "no declared
|
|
85
|
+
* environments" as "anything goes" (e.g. {@link unknownEnvError} in
|
|
86
|
+
* `./env.ts`) guard that case themselves.
|
|
87
|
+
*/
|
|
88
|
+
export function matchesDeclaredEnvironment(
|
|
89
|
+
environments: EnvironmentDeclaration[] | undefined,
|
|
90
|
+
name: string,
|
|
91
|
+
): boolean {
|
|
92
|
+
const names = environmentNames(environments) ?? [];
|
|
93
|
+
if (names.includes(name)) return true;
|
|
94
|
+
return names.some((declared) => isEnvironmentPattern(declared) && matchesEnvironmentPattern(declared, name));
|
|
95
|
+
}
|
|
96
|
+
|
|
47
97
|
/**
|
|
48
98
|
* The endpoint `name` declares (chant #1166) — `undefined` for a bare-string
|
|
49
99
|
* entry, an entry with no `endpoint` set, or a name this project doesn't
|
|
@@ -51,9 +101,16 @@ export function environmentNames(environments: EnvironmentDeclaration[] | undefi
|
|
|
51
101
|
* it injects this into the ambient env var each observing lexicon's CLI
|
|
52
102
|
* shell-out reads (e.g. `AWS_ENDPOINT_URL`), unless that var is already set —
|
|
53
103
|
* ambient always wins.
|
|
104
|
+
*
|
|
105
|
+
* A pattern entry (#1221) can carry an endpoint too: `{ name: "pr-*",
|
|
106
|
+
* endpoint }` supplies the endpoint for every `pr-<n>` environment. A literal
|
|
107
|
+
* entry always wins over a pattern — declaring `pr-special` alongside
|
|
108
|
+
* `pr-*` resolves `pr-special` to the literal entry's endpoint (or none).
|
|
54
109
|
*/
|
|
55
110
|
export function environmentEndpoint(environments: EnvironmentDeclaration[] | undefined, name: string): string | undefined {
|
|
56
|
-
const found =
|
|
111
|
+
const found =
|
|
112
|
+
environments?.find((e) => environmentName(e) === name) ??
|
|
113
|
+
environments?.find((e) => isEnvironmentPattern(environmentName(e)) && matchesEnvironmentPattern(environmentName(e), name));
|
|
57
114
|
return found && typeof found !== "string" ? found.endpoint : undefined;
|
|
58
115
|
}
|
|
59
116
|
|
|
@@ -124,6 +181,9 @@ export const ChantConfigSchema = z.object({
|
|
|
124
181
|
scanner: z.enum(["grype", "trivy"]).optional(),
|
|
125
182
|
vexSources: z.array(z.string()).optional(),
|
|
126
183
|
}).optional(),
|
|
184
|
+
knowledge: z.object({
|
|
185
|
+
dir: z.string().min(1).optional(),
|
|
186
|
+
}).optional(),
|
|
127
187
|
}).passthrough();
|
|
128
188
|
|
|
129
189
|
/**
|
|
@@ -343,6 +403,21 @@ export interface ChantConfig {
|
|
|
343
403
|
/** Default VEX document paths (OpenVEX/CycloneDX) applied to every gate. Read where the gate step is composed. */
|
|
344
404
|
vexSources?: string[];
|
|
345
405
|
};
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* OKF knowledge bundle location (#1864, design #1059, epic #1057) — the
|
|
409
|
+
* *input* side of `chant explain --format okf` (#1058): a project may
|
|
410
|
+
* author knowledge as an OKF v0.2 bundle (a directory of markdown files
|
|
411
|
+
* with YAML frontmatter) that `./okf-read.ts`'s `loadOkfBundle` reads and
|
|
412
|
+
* binds to discovered entities via each concept's `binds` frontmatter key.
|
|
413
|
+
* Convention-first: `knowledge/` beside `chant.config.ts` is used when this
|
|
414
|
+
* is unset. Set `dir` only when that name is already taken by something
|
|
415
|
+
* else in the project. See {@link resolveKnowledgeDir}.
|
|
416
|
+
*/
|
|
417
|
+
knowledge?: {
|
|
418
|
+
/** Bundle directory, relative to the project root. Defaults to `"knowledge"`. */
|
|
419
|
+
dir?: string;
|
|
420
|
+
};
|
|
346
421
|
}
|
|
347
422
|
|
|
348
423
|
/**
|
|
@@ -726,6 +801,18 @@ export function resolveVulnPolicy(config: ChantConfig): Partial<VulnPolicy> {
|
|
|
726
801
|
return out;
|
|
727
802
|
}
|
|
728
803
|
|
|
804
|
+
/**
|
|
805
|
+
* Resolve the OKF knowledge bundle directory (#1864, design #1059):
|
|
806
|
+
* `knowledge.dir` relative to `projectPath` when configured, else the
|
|
807
|
+
* `knowledge/` convention beside `chant.config.ts`. Never checks existence —
|
|
808
|
+
* a project with no bundle yet resolves a path all the same, and
|
|
809
|
+
* `okf-read.ts`'s `loadOkfBundle` treats a missing directory as an empty
|
|
810
|
+
* bundle rather than an error.
|
|
811
|
+
*/
|
|
812
|
+
export function resolveKnowledgeDir(config: ChantConfig, projectPath: string): string {
|
|
813
|
+
return join(projectPath, config.knowledge?.dir ?? "knowledge");
|
|
814
|
+
}
|
|
815
|
+
|
|
729
816
|
/**
|
|
730
817
|
* Validate and normalize a raw config object into ChantConfig shape.
|
|
731
818
|
*/
|
|
@@ -48,6 +48,25 @@ describe("normalizeDeepProperties", () => {
|
|
|
48
48
|
expect(Object.keys(out.mid as Record<string, unknown>)).toEqual(["a", "z"]);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
test("the mask hook collapses a structural secret path to MASKED, on either side (#1830)", () => {
|
|
52
|
+
const hooks: DeepNormalizationHooks = {
|
|
53
|
+
mask: (n) => n.entityType === "K8s::Core::Secret" && n.pattern.startsWith("data."),
|
|
54
|
+
};
|
|
55
|
+
for (const side of ["declared", "live"] as const) {
|
|
56
|
+
const out = normalizeDeepProperties(
|
|
57
|
+
{ data: { "app.conf": "c2VjcmV0LWJ5dGVz" }, type: "Opaque" },
|
|
58
|
+
{ entityType: "K8s::Core::Secret", side, hooks },
|
|
59
|
+
);
|
|
60
|
+
expect(out).toEqual({ data: { "app.conf": MASKED }, type: "Opaque" });
|
|
61
|
+
}
|
|
62
|
+
// Another entity type sails through the same hook untouched.
|
|
63
|
+
const other = normalizeDeepProperties(
|
|
64
|
+
{ data: { "app.conf": "plain" } },
|
|
65
|
+
{ entityType: "K8s::Core::ConfigMap", side: "live", hooks },
|
|
66
|
+
);
|
|
67
|
+
expect(other).toEqual({ data: { "app.conf": "plain" } });
|
|
68
|
+
});
|
|
69
|
+
|
|
51
70
|
test("leaves array order alone with no ordering hook", () => {
|
|
52
71
|
const out = normalizeDeepProperties({ Tags: [{ Key: "z" }, { Key: "a" }] }, { entityType: "T", side: "live" });
|
|
53
72
|
expect(out.Tags).toEqual([{ Key: "z" }, { Key: "a" }]);
|
package/src/deep-observation.ts
CHANGED
|
@@ -266,6 +266,20 @@ export interface DeepNormalizationHooks {
|
|
|
266
266
|
* reads as permanent drift.
|
|
267
267
|
*/
|
|
268
268
|
unresolved?(node: DeepNode): boolean;
|
|
269
|
+
/**
|
|
270
|
+
* Return true when this value is secret material the diff must never hold —
|
|
271
|
+
* it is collapsed to {@link MASKED} on BOTH sides (the hook runs over the
|
|
272
|
+
* declared and the live tree alike), so presence and key names still
|
|
273
|
+
* classify while values never reach a diff row, a log line, or a snapshot.
|
|
274
|
+
*
|
|
275
|
+
* The pass masks by key name on its own ({@link isSensitiveKey}); this hook
|
|
276
|
+
* exists for the lexicons whose secret-bearing paths are structural rather
|
|
277
|
+
* than name-shaped — a Kubernetes Secret's `data` carries arbitrary key
|
|
278
|
+
* names (`app.conf`), and #1365 decision 6 draws the hard line: drift on a
|
|
279
|
+
* secret observes presence, declared key-set, and metadata — never a value
|
|
280
|
+
* or a value-derived hash.
|
|
281
|
+
*/
|
|
282
|
+
mask?(node: DeepNode): boolean;
|
|
269
283
|
}
|
|
270
284
|
|
|
271
285
|
/** Everything the pass needs besides the tree itself. */
|
|
@@ -405,6 +419,9 @@ export function normalizeDeepProperties(
|
|
|
405
419
|
|
|
406
420
|
const normalizeValue = (value: unknown, path: string, pattern: string, key: string): unknown => {
|
|
407
421
|
if (isSensitiveKey(key)) return MASKED;
|
|
422
|
+
// The lexicon's structural mask — same collapse, path-shaped rather than
|
|
423
|
+
// key-named (a k8s Secret's `data.*` no matter what the key is called).
|
|
424
|
+
if (hooks?.mask?.(nodeOf(path, pattern, key, value))) return MASKED;
|
|
408
425
|
// A value the lexicon says cannot be known without deploying — an
|
|
409
426
|
// expression-string reference — collapses exactly like a class-instance
|
|
410
427
|
// intrinsic does below.
|