@decocms/blocks-cli 7.0.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/package.json +44 -0
- package/scripts/analyze-traces.mjs +1117 -0
- package/scripts/audit-observability-config.test.ts +446 -0
- package/scripts/audit-observability-config.ts +511 -0
- package/scripts/deco-migrate-cli.ts +444 -0
- package/scripts/fast-deploy-kv.test.ts +131 -0
- package/scripts/generate-blocks.test.ts +94 -0
- package/scripts/generate-blocks.ts +274 -0
- package/scripts/generate-invoke.test.ts +195 -0
- package/scripts/generate-invoke.ts +469 -0
- package/scripts/generate-loaders.ts +217 -0
- package/scripts/generate-schema.ts +1287 -0
- package/scripts/generate-sections.ts +237 -0
- package/scripts/htmx-analyze.ts +226 -0
- package/scripts/lib/blocks-dedupe.test.ts +179 -0
- package/scripts/lib/blocks-dedupe.ts +142 -0
- package/scripts/lib/cf-kv-rest.ts +78 -0
- package/scripts/lib/jsonc.ts +122 -0
- package/scripts/lib/kv-snapshot.ts +51 -0
- package/scripts/lib/read-decofile.ts +70 -0
- package/scripts/lib/sync-helpers.ts +44 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
- package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
- package/scripts/migrate/analyzers/island-classifier.ts +96 -0
- package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
- package/scripts/migrate/analyzers/section-metadata.ts +147 -0
- package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
- package/scripts/migrate/colors.ts +46 -0
- package/scripts/migrate/config.test.ts +202 -0
- package/scripts/migrate/config.ts +186 -0
- package/scripts/migrate/phase-analyze.test.ts +63 -0
- package/scripts/migrate/phase-analyze.ts +782 -0
- package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
- package/scripts/migrate/phase-cleanup-audit.ts +105 -0
- package/scripts/migrate/phase-cleanup.test.ts +141 -0
- package/scripts/migrate/phase-cleanup.ts +1588 -0
- package/scripts/migrate/phase-compile.test.ts +193 -0
- package/scripts/migrate/phase-compile.ts +177 -0
- package/scripts/migrate/phase-report.ts +243 -0
- package/scripts/migrate/phase-scaffold.ts +593 -0
- package/scripts/migrate/phase-transform.ts +310 -0
- package/scripts/migrate/phase-verify.test.ts +127 -0
- package/scripts/migrate/phase-verify.ts +572 -0
- package/scripts/migrate/post-cleanup/rules.ts +1708 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
- package/scripts/migrate/post-cleanup/runner.ts +137 -0
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
- package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
- package/scripts/migrate/post-cleanup/types.ts +106 -0
- package/scripts/migrate/source-layout.test.ts +111 -0
- package/scripts/migrate/source-layout.ts +103 -0
- package/scripts/migrate/templates/app-css.ts +366 -0
- package/scripts/migrate/templates/cache-config.ts +26 -0
- package/scripts/migrate/templates/commerce-loaders.ts +230 -0
- package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
- package/scripts/migrate/templates/cursor-rules.ts +70 -0
- package/scripts/migrate/templates/hooks.test.ts +141 -0
- package/scripts/migrate/templates/hooks.ts +152 -0
- package/scripts/migrate/templates/knip-config.ts +27 -0
- package/scripts/migrate/templates/lib-utils.test.ts +139 -0
- package/scripts/migrate/templates/lib-utils.ts +326 -0
- package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
- package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
- package/scripts/migrate/templates/package-json.ts +177 -0
- package/scripts/migrate/templates/routes.ts +237 -0
- package/scripts/migrate/templates/sdk-gen.ts +59 -0
- package/scripts/migrate/templates/section-loaders.ts +456 -0
- package/scripts/migrate/templates/server-entry.ts +561 -0
- package/scripts/migrate/templates/setup.ts +148 -0
- package/scripts/migrate/templates/tsconfig.ts +21 -0
- package/scripts/migrate/templates/types-gen.ts +174 -0
- package/scripts/migrate/templates/ui-components.ts +144 -0
- package/scripts/migrate/templates/vite-config.ts +101 -0
- package/scripts/migrate/transforms/dead-code.ts +455 -0
- package/scripts/migrate/transforms/deno-isms.ts +85 -0
- package/scripts/migrate/transforms/fresh-apis.ts +223 -0
- package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
- package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
- package/scripts/migrate/transforms/imports.ts +385 -0
- package/scripts/migrate/transforms/jsx.ts +317 -0
- package/scripts/migrate/transforms/section-conventions.ts +210 -0
- package/scripts/migrate/transforms/tailwind.ts +739 -0
- package/scripts/migrate/types.ts +244 -0
- package/scripts/migrate-blocks-to-kv.ts +104 -0
- package/scripts/migrate-post-cleanup.ts +191 -0
- package/scripts/migrate-to-cf-observability.test.ts +215 -0
- package/scripts/migrate-to-cf-observability.ts +699 -0
- package/scripts/migrate.ts +282 -0
- package/scripts/smoke-otlp-errorlog.ts +46 -0
- package/scripts/smoke-otlp-meter.ts +48 -0
- package/scripts/sync-blocks-to-kv.ts +153 -0
- package/scripts/tailwind-lint.ts +518 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* @decocms/start — observability config audit
|
|
4
|
+
*
|
|
5
|
+
* Read-only auditor for a site's `wrangler.jsonc`. Detects drift away
|
|
6
|
+
* from the canonical Cloudflare-native observability block documented
|
|
7
|
+
* in `docs/observability.md`. CI-friendly: exits 0 on a clean audit, 1
|
|
8
|
+
* on findings.
|
|
9
|
+
*
|
|
10
|
+
* This is the **detect** half of D3 ("audit is the safety net"). The
|
|
11
|
+
* matching **fix** half is `migrate-to-cf-observability.ts`, which can
|
|
12
|
+
* rewrite the block back to canonical with `--write`. Every rule here
|
|
13
|
+
* has a corresponding behavior in the codemod — there is no rule we
|
|
14
|
+
* can detect but not auto-fix.
|
|
15
|
+
*
|
|
16
|
+
* Rules (id — severity — what it catches):
|
|
17
|
+
*
|
|
18
|
+
* observability_missing error No `observability` key at all. CF captures nothing.
|
|
19
|
+
* observability_disabled error `observability.enabled: false`. Master switch off.
|
|
20
|
+
* traces_disabled warn `observability.traces.enabled: false`. No traces in dashboard.
|
|
21
|
+
* logs_disabled warn `observability.logs.enabled: false`. No logs in dashboard.
|
|
22
|
+
* head_sampling_rate_elevated error `traces.head_sampling_rate > 0.01`. Fleet-scale cost risk; see docs/observability.md.
|
|
23
|
+
* logs_head_sampling_rate_low warn `logs.head_sampling_rate < 1`. Sampling info/warn logs loses signal cheaply; errors go via the direct-POST channel.
|
|
24
|
+
* persist_disabled_no_destination error `persist: false` with no destination configured. Data captured then discarded.
|
|
25
|
+
*
|
|
26
|
+
* Phase 6 / D-14 — fleet-config drift rules (live outside the
|
|
27
|
+
* `observability` block but still owned by this audit):
|
|
28
|
+
*
|
|
29
|
+
* version_metadata_binding_missing error Missing `version_metadata` binding. `service.version` won't be stamped — regressions can't be attributed to a deploy.
|
|
30
|
+
* analytics_engine_binding_missing warn No `DECO_METRICS` AE binding. AE meter is off; OTLP meter still works but CF dashboard panels go dark.
|
|
31
|
+
* tail_consumer_missing error No `tail_consumers` entry pointing at `deco-otel-tail`. 100% error-capture is broken.
|
|
32
|
+
* otel_metrics_endpoint_missing warn `DECO_OTEL_METRICS_ENDPOINT` not set on `vars`. OTLP meter is off; only AE works.
|
|
33
|
+
* otel_traces_endpoint_missing warn `DECO_OTEL_TRACES_ENDPOINT` not set on `vars`. Framework `deco.*` spans drop unless CF Traces is on.
|
|
34
|
+
* otel_logs_endpoint_missing warn `DECO_OTEL_LOGS_ENDPOINT` not set on `vars`. Error logs ride CF Destinations only (head-sampled).
|
|
35
|
+
*
|
|
36
|
+
* Usage (from a site directory):
|
|
37
|
+
* npx -p @decocms/start deco-audit-observability # audit cwd (warn mode — exit 0)
|
|
38
|
+
* npx -p @decocms/start deco-audit-observability --source ./ # explicit source dir
|
|
39
|
+
* npx -p @decocms/start deco-audit-observability --json # machine-readable
|
|
40
|
+
* npx -p @decocms/start deco-audit-observability --mode block # error findings exit 1 (CI gate)
|
|
41
|
+
* npx -p @decocms/start deco-audit-observability --github # GitHub Actions annotations
|
|
42
|
+
*
|
|
43
|
+
* Options:
|
|
44
|
+
* --source <dir> Site directory (default: .)
|
|
45
|
+
* --json Emit findings as JSON to stdout
|
|
46
|
+
* --mode <m> Gate hardness: "warn" (default — always exit 0 on findings,
|
|
47
|
+
* just print them) or "block" (exit 1 on any `error` finding).
|
|
48
|
+
* See D-16 in MIGRATION_TOOLING_PLAN.md for the rationale on
|
|
49
|
+
* why warn is the v1 default.
|
|
50
|
+
* --github Emit `::warning::` / `::error::` lines for GitHub Actions
|
|
51
|
+
* annotations in addition to the normal text output.
|
|
52
|
+
* --help, -h Show this message
|
|
53
|
+
*
|
|
54
|
+
* Exit codes:
|
|
55
|
+
* 0 — no findings, or `--mode warn` (the default) regardless of findings
|
|
56
|
+
* 1 — `--mode block` and at least one `error`-severity finding
|
|
57
|
+
* 2 — file invalid / can't parse
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import * as fs from "node:fs";
|
|
61
|
+
import * as path from "node:path";
|
|
62
|
+
import { parseJsonc } from "./lib/jsonc";
|
|
63
|
+
|
|
64
|
+
export type Severity = "error" | "warn" | "info";
|
|
65
|
+
|
|
66
|
+
export interface Finding {
|
|
67
|
+
id: string;
|
|
68
|
+
severity: Severity;
|
|
69
|
+
message: string;
|
|
70
|
+
/** Suggested remediation — usually a codemod invocation. */
|
|
71
|
+
fix?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type GateMode = "warn" | "block";
|
|
75
|
+
|
|
76
|
+
interface CliOpts {
|
|
77
|
+
source: string;
|
|
78
|
+
json: boolean;
|
|
79
|
+
help: boolean;
|
|
80
|
+
mode: GateMode;
|
|
81
|
+
github: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseArgs(argv: string[]): CliOpts {
|
|
85
|
+
const opts: CliOpts = {
|
|
86
|
+
source: ".",
|
|
87
|
+
json: false,
|
|
88
|
+
help: false,
|
|
89
|
+
mode: "warn",
|
|
90
|
+
github: false,
|
|
91
|
+
};
|
|
92
|
+
for (let i = 0; i < argv.length; i++) {
|
|
93
|
+
const flag = argv[i];
|
|
94
|
+
switch (flag) {
|
|
95
|
+
case "--source":
|
|
96
|
+
opts.source = argv[++i] ?? ".";
|
|
97
|
+
break;
|
|
98
|
+
case "--json":
|
|
99
|
+
opts.json = true;
|
|
100
|
+
break;
|
|
101
|
+
case "--mode": {
|
|
102
|
+
const value = argv[++i];
|
|
103
|
+
if (value !== "warn" && value !== "block") {
|
|
104
|
+
console.error(
|
|
105
|
+
`audit: --mode must be "warn" or "block" (got "${value ?? ""}")`,
|
|
106
|
+
);
|
|
107
|
+
process.exit(2);
|
|
108
|
+
}
|
|
109
|
+
opts.mode = value;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "--github":
|
|
113
|
+
opts.github = true;
|
|
114
|
+
break;
|
|
115
|
+
case "--help":
|
|
116
|
+
case "-h":
|
|
117
|
+
opts.help = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return opts;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function showHelp(): void {
|
|
125
|
+
console.log(`
|
|
126
|
+
@decocms/start — observability config audit
|
|
127
|
+
|
|
128
|
+
Read-only check for drift from the canonical Cloudflare-native
|
|
129
|
+
observability block in wrangler.jsonc. Pair with
|
|
130
|
+
\`deco-cf-observability --write\` to auto-fix.
|
|
131
|
+
|
|
132
|
+
Usage:
|
|
133
|
+
npx -p @decocms/start deco-audit-observability [options]
|
|
134
|
+
|
|
135
|
+
Options:
|
|
136
|
+
--source <dir> Site directory (default: .)
|
|
137
|
+
--json Emit findings as JSON
|
|
138
|
+
--mode <m> "warn" (default, exit 0) | "block" (exit 1 on errors)
|
|
139
|
+
--github Emit ::warning::/::error:: lines for GitHub Actions
|
|
140
|
+
--help, -h This message
|
|
141
|
+
|
|
142
|
+
Exit codes:
|
|
143
|
+
0 no findings, OR --mode warn (default — annotate and move on)
|
|
144
|
+
1 --mode block AND at least one error-severity finding
|
|
145
|
+
2 wrangler.jsonc missing or unparseable
|
|
146
|
+
|
|
147
|
+
See D-16 in MIGRATION_TOOLING_PLAN.md for the v1 "warn-only" policy.
|
|
148
|
+
`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface ObservabilityBlock {
|
|
152
|
+
enabled?: boolean;
|
|
153
|
+
logs?: {
|
|
154
|
+
enabled?: boolean;
|
|
155
|
+
head_sampling_rate?: number;
|
|
156
|
+
persist?: boolean;
|
|
157
|
+
destinations?: unknown[];
|
|
158
|
+
invocation_logs?: boolean;
|
|
159
|
+
};
|
|
160
|
+
traces?: {
|
|
161
|
+
enabled?: boolean;
|
|
162
|
+
head_sampling_rate?: number;
|
|
163
|
+
persist?: boolean;
|
|
164
|
+
destinations?: unknown[];
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Pure audit function. Exported for unit-testing; the CLI wrapper is the
|
|
170
|
+
* thin sliver below.
|
|
171
|
+
*/
|
|
172
|
+
export function auditObservabilityBlock(
|
|
173
|
+
obs: ObservabilityBlock | undefined,
|
|
174
|
+
): Finding[] {
|
|
175
|
+
const findings: Finding[] = [];
|
|
176
|
+
|
|
177
|
+
if (!obs) {
|
|
178
|
+
findings.push({
|
|
179
|
+
id: "observability_missing",
|
|
180
|
+
severity: "error",
|
|
181
|
+
message:
|
|
182
|
+
"wrangler.jsonc has no `observability` block. Cloudflare won't capture logs or traces.",
|
|
183
|
+
fix: "npx -p @decocms/start deco-cf-observability --write",
|
|
184
|
+
});
|
|
185
|
+
return findings;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (obs.enabled === false) {
|
|
189
|
+
findings.push({
|
|
190
|
+
id: "observability_disabled",
|
|
191
|
+
severity: "error",
|
|
192
|
+
message:
|
|
193
|
+
"`observability.enabled: false` — the master switch is off, sub-block flags do nothing.",
|
|
194
|
+
fix: "npx -p @decocms/start deco-cf-observability --write",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---- traces ----
|
|
199
|
+
if (obs.traces?.enabled === false) {
|
|
200
|
+
findings.push({
|
|
201
|
+
id: "traces_disabled",
|
|
202
|
+
severity: "warn",
|
|
203
|
+
message:
|
|
204
|
+
"`observability.traces.enabled: false` — traces won't reach the CF dashboard or any destination.",
|
|
205
|
+
fix: "npx -p @decocms/start deco-cf-observability --write",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const tracesRate = obs.traces?.head_sampling_rate;
|
|
210
|
+
if (typeof tracesRate === "number" && tracesRate > 0.01) {
|
|
211
|
+
findings.push({
|
|
212
|
+
id: "head_sampling_rate_elevated",
|
|
213
|
+
severity: "error",
|
|
214
|
+
message:
|
|
215
|
+
`traces.head_sampling_rate is ${tracesRate} (> 0.01). At fleet scale this is a cost trap; ` +
|
|
216
|
+
`see docs/observability.md → Sampling. If this is intentional and time-bounded (incident, ` +
|
|
217
|
+
`release window), leave a comment in wrangler.jsonc explaining why, then revert.`,
|
|
218
|
+
fix: "npx -p @decocms/start deco-cf-observability --write --traces-rate 0.01",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---- logs ----
|
|
223
|
+
if (obs.logs?.enabled === false) {
|
|
224
|
+
findings.push({
|
|
225
|
+
id: "logs_disabled",
|
|
226
|
+
severity: "warn",
|
|
227
|
+
message:
|
|
228
|
+
"`observability.logs.enabled: false` — logs won't reach the CF dashboard or any destination.",
|
|
229
|
+
fix: "npx -p @decocms/start deco-cf-observability --write",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const logsRate = obs.logs?.head_sampling_rate;
|
|
234
|
+
if (typeof logsRate === "number" && logsRate < 1) {
|
|
235
|
+
findings.push({
|
|
236
|
+
id: "logs_head_sampling_rate_low",
|
|
237
|
+
severity: "warn",
|
|
238
|
+
message:
|
|
239
|
+
`logs.head_sampling_rate is ${logsRate} (< 1). Info/warn logs are cheap and high-signal; ` +
|
|
240
|
+
`error logs already bypass head sampling via the direct-POST channel, so there's little to ` +
|
|
241
|
+
`gain by sampling logs.`,
|
|
242
|
+
fix: "npx -p @decocms/start deco-cf-observability --write --logs-rate 1",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---- persist / destinations ----
|
|
247
|
+
const hasDestination = (block?: { destinations?: unknown[] }): boolean =>
|
|
248
|
+
Array.isArray(block?.destinations) && block!.destinations!.length > 0;
|
|
249
|
+
|
|
250
|
+
const tracesPersist = obs.traces?.persist ?? true;
|
|
251
|
+
if (
|
|
252
|
+
obs.traces?.enabled !== false &&
|
|
253
|
+
!tracesPersist &&
|
|
254
|
+
!hasDestination(obs.traces)
|
|
255
|
+
) {
|
|
256
|
+
findings.push({
|
|
257
|
+
id: "persist_disabled_no_destination",
|
|
258
|
+
severity: "error",
|
|
259
|
+
message:
|
|
260
|
+
"traces.persist:false with no destinations — traces are captured and discarded. " +
|
|
261
|
+
"Either set persist:true (CF dashboard storage) or configure a destination.",
|
|
262
|
+
fix: "npx -p @decocms/start deco-cf-observability --write --persist",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const logsPersist = obs.logs?.persist ?? true;
|
|
267
|
+
if (
|
|
268
|
+
obs.logs?.enabled !== false &&
|
|
269
|
+
!logsPersist &&
|
|
270
|
+
!hasDestination(obs.logs)
|
|
271
|
+
) {
|
|
272
|
+
findings.push({
|
|
273
|
+
id: "persist_disabled_no_destination",
|
|
274
|
+
severity: "error",
|
|
275
|
+
message:
|
|
276
|
+
"logs.persist:false with no destinations — logs are captured and discarded. " +
|
|
277
|
+
"Either set persist:true (CF dashboard storage) or configure a destination.",
|
|
278
|
+
fix: "npx -p @decocms/start deco-cf-observability --write --persist",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return findings;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Fleet-config drift rules — owned by the same audit because the
|
|
287
|
+
* cumulative effect of "observability block correct, bindings missing"
|
|
288
|
+
* is identical to "observability block missing" (no data lands in
|
|
289
|
+
* ClickHouse).
|
|
290
|
+
*
|
|
291
|
+
* The CLI composes `auditObservabilityBlock` + `auditFleetBindings`.
|
|
292
|
+
* Both return Finding[]; callers concatenate.
|
|
293
|
+
*/
|
|
294
|
+
export interface WranglerLike {
|
|
295
|
+
observability?: ObservabilityBlock;
|
|
296
|
+
version_metadata?: { binding?: string } | unknown;
|
|
297
|
+
analytics_engine_datasets?: Array<{ binding?: string; dataset?: string }> | unknown;
|
|
298
|
+
tail_consumers?: Array<{ service?: string; environment?: string }> | unknown;
|
|
299
|
+
vars?: Record<string, unknown> | unknown;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function auditFleetBindings(wrangler: WranglerLike): Finding[] {
|
|
303
|
+
const findings: Finding[] = [];
|
|
304
|
+
|
|
305
|
+
// version_metadata — required so `service.version` is stamped on every
|
|
306
|
+
// span and log line. Without it, regressions can't be tied to a
|
|
307
|
+
// specific deployment.
|
|
308
|
+
const vmBinding =
|
|
309
|
+
typeof wrangler.version_metadata === "object" &&
|
|
310
|
+
wrangler.version_metadata !== null &&
|
|
311
|
+
"binding" in wrangler.version_metadata
|
|
312
|
+
? (wrangler.version_metadata as { binding?: string }).binding
|
|
313
|
+
: undefined;
|
|
314
|
+
if (!vmBinding) {
|
|
315
|
+
findings.push({
|
|
316
|
+
id: "version_metadata_binding_missing",
|
|
317
|
+
severity: "error",
|
|
318
|
+
message:
|
|
319
|
+
"wrangler.jsonc is missing a `version_metadata.binding` entry. " +
|
|
320
|
+
"`service.version` won't appear on spans/logs and the deploy-correlation " +
|
|
321
|
+
"panel will be empty. Recommended value: `CF_VERSION_METADATA`.",
|
|
322
|
+
fix: "npx -p @decocms/start deco-cf-observability --write",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// DECO_METRICS — Analytics Engine binding. The AE meter is the hot-
|
|
327
|
+
// path CF dashboard view; OTLP works without it but we lose the
|
|
328
|
+
// operator-grade short-window panels.
|
|
329
|
+
const aeDatasets = Array.isArray(wrangler.analytics_engine_datasets)
|
|
330
|
+
? (wrangler.analytics_engine_datasets as Array<{ binding?: string }>)
|
|
331
|
+
: [];
|
|
332
|
+
const hasMetricsBinding = aeDatasets.some((d) => d?.binding === "DECO_METRICS");
|
|
333
|
+
if (!hasMetricsBinding) {
|
|
334
|
+
findings.push({
|
|
335
|
+
id: "analytics_engine_binding_missing",
|
|
336
|
+
severity: "warn",
|
|
337
|
+
message:
|
|
338
|
+
"wrangler.jsonc has no `analytics_engine_datasets[].binding == 'DECO_METRICS'`. " +
|
|
339
|
+
"The AE meter is off; the hot-path operator dashboards in CF will be empty. " +
|
|
340
|
+
"OTLP metrics keep flowing if `DECO_OTEL_METRICS_ENDPOINT` is set.",
|
|
341
|
+
fix: "npx -p @decocms/start deco-cf-observability --write",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// tail_consumers — must list deco-otel-tail. Phase 1 enrichment is
|
|
346
|
+
// useless without the tail consumer firing.
|
|
347
|
+
const tail = Array.isArray(wrangler.tail_consumers)
|
|
348
|
+
? (wrangler.tail_consumers as Array<{ service?: string }>)
|
|
349
|
+
: [];
|
|
350
|
+
const hasTailConsumer = tail.some((t) => t?.service === "deco-otel-tail");
|
|
351
|
+
if (!hasTailConsumer) {
|
|
352
|
+
findings.push({
|
|
353
|
+
id: "tail_consumer_missing",
|
|
354
|
+
severity: "error",
|
|
355
|
+
message:
|
|
356
|
+
"wrangler.jsonc has no `tail_consumers[].service == 'deco-otel-tail'` entry. " +
|
|
357
|
+
"100% error-capture is broken — only the head-sampled CF Destinations path " +
|
|
358
|
+
"will report errors, and isolate crashes will be invisible.",
|
|
359
|
+
fix: "npx -p @decocms/start deco-cf-observability --write",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// DECO_OTEL_*_ENDPOINT env vars. Audit each separately so the message
|
|
364
|
+
// explains which channel is silently no-op.
|
|
365
|
+
const vars =
|
|
366
|
+
typeof wrangler.vars === "object" && wrangler.vars !== null
|
|
367
|
+
? (wrangler.vars as Record<string, unknown>)
|
|
368
|
+
: {};
|
|
369
|
+
const checkVar = (id: string, name: string, severity: Severity, channel: string) => {
|
|
370
|
+
const v = vars[name];
|
|
371
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
372
|
+
findings.push({
|
|
373
|
+
id,
|
|
374
|
+
severity,
|
|
375
|
+
message:
|
|
376
|
+
`wrangler.jsonc \`vars.${name}\` is not set. ${channel} is a no-op; ` +
|
|
377
|
+
`data captured in this channel never lands in ClickHouse. ` +
|
|
378
|
+
`See docs/observability.md for the canonical endpoints.`,
|
|
379
|
+
fix: "npx -p @decocms/start deco-cf-observability --write",
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
checkVar(
|
|
384
|
+
"otel_metrics_endpoint_missing",
|
|
385
|
+
"DECO_OTEL_METRICS_ENDPOINT",
|
|
386
|
+
"warn",
|
|
387
|
+
"OTLP metrics direct-POST",
|
|
388
|
+
);
|
|
389
|
+
checkVar(
|
|
390
|
+
"otel_traces_endpoint_missing",
|
|
391
|
+
"DECO_OTEL_TRACES_ENDPOINT",
|
|
392
|
+
"warn",
|
|
393
|
+
"OTLP traces direct-POST",
|
|
394
|
+
);
|
|
395
|
+
checkVar(
|
|
396
|
+
"otel_logs_endpoint_missing",
|
|
397
|
+
"DECO_OTEL_LOGS_ENDPOINT",
|
|
398
|
+
"warn",
|
|
399
|
+
"OTLP error-log direct-POST",
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
return findings;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* One-stop call for the full wrangler audit — composes the
|
|
407
|
+
* observability-block rules with the fleet-binding rules. Both keep
|
|
408
|
+
* working standalone for fine-grained tests.
|
|
409
|
+
*/
|
|
410
|
+
export function auditWranglerConfig(wrangler: WranglerLike): Finding[] {
|
|
411
|
+
return [
|
|
412
|
+
...auditObservabilityBlock(wrangler.observability),
|
|
413
|
+
...auditFleetBindings(wrangler),
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function findingsToText(file: string, findings: Finding[]): string {
|
|
418
|
+
if (findings.length === 0) {
|
|
419
|
+
return `OK ${file} — observability config looks canonical`;
|
|
420
|
+
}
|
|
421
|
+
const lines = [`Findings in ${file}:`];
|
|
422
|
+
for (const f of findings) {
|
|
423
|
+
lines.push(` [${f.severity.toUpperCase()}] ${f.id}`);
|
|
424
|
+
lines.push(` ${f.message}`);
|
|
425
|
+
if (f.fix) lines.push(` fix: ${f.fix}`);
|
|
426
|
+
}
|
|
427
|
+
lines.push("");
|
|
428
|
+
return lines.join("\n");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function main(): void {
|
|
432
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
433
|
+
if (opts.help) {
|
|
434
|
+
showHelp();
|
|
435
|
+
process.exit(0);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const file = path.resolve(opts.source, "wrangler.jsonc");
|
|
439
|
+
if (!fs.existsSync(file)) {
|
|
440
|
+
console.error(`audit: ${file} not found`);
|
|
441
|
+
process.exit(2);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let parsed: WranglerLike;
|
|
445
|
+
try {
|
|
446
|
+
parsed = parseJsonc(fs.readFileSync(file, "utf8")) as WranglerLike;
|
|
447
|
+
} catch (err) {
|
|
448
|
+
console.error(`audit: ${file} could not be parsed: ${(err as Error).message}`);
|
|
449
|
+
process.exit(2);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const findings = auditWranglerConfig(parsed);
|
|
453
|
+
|
|
454
|
+
if (opts.json) {
|
|
455
|
+
process.stdout.write(
|
|
456
|
+
JSON.stringify({ file, mode: opts.mode, findings }, null, 2) + "\n",
|
|
457
|
+
);
|
|
458
|
+
} else {
|
|
459
|
+
process.stdout.write(findingsToText(file, findings) + "\n");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (opts.github) {
|
|
463
|
+
for (const f of findings) {
|
|
464
|
+
// GitHub Actions workflow command. `error` and `warning` annotate the
|
|
465
|
+
// diff; `notice` is informational. We never emit `error` in warn mode
|
|
466
|
+
// even for error-severity rules — the v1 policy is annotate-don't-fail.
|
|
467
|
+
const level = opts.mode === "block" && f.severity === "error"
|
|
468
|
+
? "error"
|
|
469
|
+
: f.severity === "info" ? "notice" : "warning";
|
|
470
|
+
const msg = `${f.message}${f.fix ? ` (fix: ${f.fix})` : ""}`;
|
|
471
|
+
const escaped = msg.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(
|
|
472
|
+
/\n/g,
|
|
473
|
+
"%0A",
|
|
474
|
+
);
|
|
475
|
+
process.stdout.write(`::${level} title=${f.id}::${escaped}\n`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Exit policy: D-16 / Phase 6 decision.
|
|
480
|
+
// warn — annotate only; always exit 0 (CI sees the findings but ships)
|
|
481
|
+
// block — exit 1 on any `error`-severity finding
|
|
482
|
+
// The default is `warn` because storefronts are upgraded over weeks; a
|
|
483
|
+
// day-one block would fail PRs that have nothing to do with observability.
|
|
484
|
+
const shouldFail = opts.mode === "block" &&
|
|
485
|
+
findings.some((f) => f.severity === "error");
|
|
486
|
+
process.exit(shouldFail ? 1 : 0);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Only run when invoked directly, not when imported by tests.
|
|
490
|
+
// Works under both CJS (require.main === module) and ESM (import.meta.url
|
|
491
|
+
// matches argv[1]) because the package is `"type": "module"` but the
|
|
492
|
+
// codemod siblings ship .cjs bundles via tsup.
|
|
493
|
+
const isCjsEntry =
|
|
494
|
+
typeof require !== "undefined" &&
|
|
495
|
+
typeof module !== "undefined" &&
|
|
496
|
+
// biome-ignore lint/correctness/noNodejsModules: entry-point check
|
|
497
|
+
require.main === module;
|
|
498
|
+
let isEsmEntry = false;
|
|
499
|
+
try {
|
|
500
|
+
// import.meta is a syntax error in CJS, but we're in an ESM source file.
|
|
501
|
+
isEsmEntry =
|
|
502
|
+
typeof process !== "undefined" &&
|
|
503
|
+
Array.isArray(process.argv) &&
|
|
504
|
+
process.argv[1] !== undefined &&
|
|
505
|
+
import.meta.url === `file://${process.argv[1]}`;
|
|
506
|
+
} catch {
|
|
507
|
+
// ignore in CJS
|
|
508
|
+
}
|
|
509
|
+
if (isCjsEntry || isEsmEntry) {
|
|
510
|
+
main();
|
|
511
|
+
}
|