@a11ign/screenreader-fleet 0.0.0-reserved.0 → 0.2.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/LICENSE +661 -0
- package/README.md +94 -2
- package/dist/capture-client.d.mts +49 -0
- package/dist/capture-client.d.mts.map +1 -0
- package/dist/capture-client.mjs +352 -0
- package/dist/capture-client.mjs.map +1 -0
- package/dist/check-worker-code.d.mts +34 -0
- package/dist/check-worker-code.d.mts.map +1 -0
- package/dist/check-worker-code.mjs +142 -0
- package/dist/check-worker-code.mjs.map +1 -0
- package/dist/cli-flags.d.mts +71 -0
- package/dist/cli-flags.d.mts.map +1 -0
- package/dist/cli-flags.mjs +207 -0
- package/dist/cli-flags.mjs.map +1 -0
- package/dist/code-drift.d.mts +140 -0
- package/dist/code-drift.d.mts.map +1 -0
- package/dist/code-drift.mjs +284 -0
- package/dist/code-drift.mjs.map +1 -0
- package/dist/command-line-census.d.mts +33 -0
- package/dist/command-line-census.d.mts.map +1 -0
- package/dist/command-line-census.mjs +96 -0
- package/dist/command-line-census.mjs.map +1 -0
- package/dist/compare-workers.d.mts +3 -0
- package/dist/compare-workers.d.mts.map +1 -0
- package/dist/compare-workers.mjs +332 -0
- package/dist/compare-workers.mjs.map +1 -0
- package/dist/control-plane-isolation.d.mts +45 -0
- package/dist/control-plane-isolation.d.mts.map +1 -0
- package/dist/control-plane-isolation.mjs +67 -0
- package/dist/control-plane-isolation.mjs.map +1 -0
- package/dist/deploy-worker.d.mts +3 -0
- package/dist/deploy-worker.d.mts.map +1 -0
- package/dist/deploy-worker.mjs +333 -0
- package/dist/deploy-worker.mjs.map +1 -0
- package/dist/doctor.d.mts +216 -0
- package/dist/doctor.d.mts.map +1 -0
- package/dist/doctor.mjs +962 -0
- package/dist/doctor.mjs.map +1 -0
- package/dist/fleet-consistency.d.mts +235 -0
- package/dist/fleet-consistency.d.mts.map +1 -0
- package/dist/fleet-consistency.mjs +436 -0
- package/dist/fleet-consistency.mjs.map +1 -0
- package/dist/fleet-env.d.mts +228 -0
- package/dist/fleet-env.d.mts.map +1 -0
- package/dist/fleet-env.mjs +509 -0
- package/dist/fleet-env.mjs.map +1 -0
- package/dist/fleet-scripts.d.mts +11 -0
- package/dist/fleet-scripts.d.mts.map +1 -0
- package/dist/fleet-scripts.mjs +41 -0
- package/dist/fleet-scripts.mjs.map +1 -0
- package/dist/git-safe-env.d.mts +10 -0
- package/dist/git-safe-env.d.mts.map +1 -0
- package/dist/git-safe-env.mjs +44 -0
- package/dist/git-safe-env.mjs.map +1 -0
- package/dist/guest-run.d.mts +26 -0
- package/dist/guest-run.d.mts.map +1 -0
- package/dist/guest-run.mjs +164 -0
- package/dist/guest-run.mjs.map +1 -0
- package/dist/host-address.d.mts +33 -0
- package/dist/host-address.d.mts.map +1 -0
- package/dist/host-address.mjs +105 -0
- package/dist/host-address.mjs.map +1 -0
- package/dist/host-capacity.d.mts +64 -0
- package/dist/host-capacity.d.mts.map +1 -0
- package/dist/host-capacity.mjs +152 -0
- package/dist/host-capacity.mjs.map +1 -0
- package/dist/host-metrics.d.mts +116 -0
- package/dist/host-metrics.d.mts.map +1 -0
- package/dist/host-metrics.mjs +201 -0
- package/dist/host-metrics.mjs.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/local-vm.d.ts +125 -0
- package/dist/local-vm.d.ts.map +1 -0
- package/dist/local-vm.js +360 -0
- package/dist/local-vm.js.map +1 -0
- package/dist/measure-guard.d.mts +34 -0
- package/dist/measure-guard.d.mts.map +1 -0
- package/dist/measure-guard.mjs +73 -0
- package/dist/measure-guard.mjs.map +1 -0
- package/dist/normalise-fleet.d.mts +2 -0
- package/dist/normalise-fleet.d.mts.map +1 -0
- package/dist/normalise-fleet.mjs +76 -0
- package/dist/normalise-fleet.mjs.map +1 -0
- package/dist/npm-cli-executable.d.mts +42 -0
- package/dist/npm-cli-executable.d.mts.map +1 -0
- package/dist/npm-cli-executable.mjs +159 -0
- package/dist/npm-cli-executable.mjs.map +1 -0
- package/dist/probe-outcome.d.mts +89 -0
- package/dist/probe-outcome.d.mts.map +1 -0
- package/dist/probe-outcome.mjs +104 -0
- package/dist/probe-outcome.mjs.map +1 -0
- package/dist/protocol-guard.d.mts +34 -0
- package/dist/protocol-guard.d.mts.map +1 -0
- package/dist/protocol-guard.mjs +121 -0
- package/dist/protocol-guard.mjs.map +1 -0
- package/dist/source-walk.d.mts +12 -0
- package/dist/source-walk.d.mts.map +1 -0
- package/dist/source-walk.mjs +56 -0
- package/dist/source-walk.mjs.map +1 -0
- package/dist/transient-fault.d.mts +6 -0
- package/dist/transient-fault.d.mts.map +1 -0
- package/dist/transient-fault.mjs +86 -0
- package/dist/transient-fault.mjs.map +1 -0
- package/dist/utm-deprecated.d.mts +6 -0
- package/dist/utm-deprecated.d.mts.map +1 -0
- package/dist/utm-deprecated.mjs +23 -0
- package/dist/utm-deprecated.mjs.map +1 -0
- package/dist/worker-code-check.d.mts +29 -0
- package/dist/worker-code-check.d.mts.map +1 -0
- package/dist/worker-code-check.mjs +85 -0
- package/dist/worker-code-check.mjs.map +1 -0
- package/dist/worker-health.d.mts +56 -0
- package/dist/worker-health.d.mts.map +1 -0
- package/dist/worker-health.mjs +73 -0
- package/dist/worker-health.mjs.map +1 -0
- package/dist/worker-http.d.mts +103 -0
- package/dist/worker-http.d.mts.map +1 -0
- package/dist/worker-http.mjs +277 -0
- package/dist/worker-http.mjs.map +1 -0
- package/dist/worker-stats.d.mts +66 -0
- package/dist/worker-stats.d.mts.map +1 -0
- package/dist/worker-stats.mjs +143 -0
- package/dist/worker-stats.mjs.map +1 -0
- package/package.json +96 -4
- package/src/local-worker/autounattend.xml +280 -0
- package/src/local-worker/build-vm.sh +218 -0
- package/src/local-worker/clone-worker.sh +141 -0
- package/src/local-worker/create-utm-vm.sh +202 -0
- package/src/local-worker/fetch-windows-iso.sh +238 -0
- package/src/local-worker/first-boot.cmd +58 -0
- package/src/local-worker/worker-ctl.sh +442 -0
- package/src/provisioning/README.md +28 -0
- package/src/provisioning/apply-foreground-lock-timeout.ps1 +71 -0
- package/src/provisioning/bare-metal/README.md +213 -0
- package/src/provisioning/bare-metal/a11y-bootstrap.service +58 -0
- package/src/provisioning/bare-metal/autounattend.xml +428 -0
- package/src/provisioning/bare-metal/serve-bootstrap.sh +86 -0
- package/src/provisioning/bootstrap-control-plane.sh +463 -0
- package/src/provisioning/bootstrap-windows-worker.ps1 +649 -0
- package/src/provisioning/build-lean-worker-image.ps1 +275 -0
- package/src/provisioning/diagnose-nvda-worker.ps1 +174 -0
- package/src/provisioning/provision-nvda-worker.ps1 +827 -0
- package/src/provisioning/set-display-mode.ps1 +411 -0
- package/src/provisioning/stamp-provision-revision.ps1 +184 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
// Is every worker running the code in this checkout?
|
|
4
|
+
//
|
|
5
|
+
// npm run worker:code
|
|
6
|
+
//
|
|
7
|
+
// Deploying is push-then-restart and both halves fail silently: `utmctl exec` reports success
|
|
8
|
+
// whether or not it ran, so a worker can serve the previous process indefinitely. Reading the
|
|
9
|
+
// guest's file hash does not help, because that read goes through exec too -- when exec is
|
|
10
|
+
// dead the check returns empty rather than mismatched, which reads as a flaky tool instead of a
|
|
11
|
+
// failed deploy. That cost an hour, and the stale workers looked exactly like a logic bug.
|
|
12
|
+
//
|
|
13
|
+
// This asks each worker over HTTP, which is reachable exactly when the worker is usable and
|
|
14
|
+
// involves no guest agent. Exit 0 when every worker matches, 1 otherwise.
|
|
15
|
+
import { realpathSync } from "node:fs";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import { fleetScriptPaths } from "./fleet-scripts.mjs";
|
|
19
|
+
import { configuredWorkers, inventoryWorkerUrls, resolveWorkerPool } from "./fleet-env.mjs";
|
|
20
|
+
// The comparison, the remedy and the expected hash live in ONE place, because the capture entry points ask
|
|
21
|
+
// the same question before every run and a second copy of "is this worker stale" is a second answer.
|
|
22
|
+
import { expectedWorkerCode, codeDrift, remedyLines } from "./worker-code-check.mjs";
|
|
23
|
+
import { refuseUnknownFlags } from "./cli-flags.mjs";
|
|
24
|
+
import { errorText } from "@a11ign/screenreader-worker/error-text";
|
|
25
|
+
import { requestJson } from "./worker-http.mjs";
|
|
26
|
+
/**
|
|
27
|
+
* takes NO flags — it asks every worker what code it is running and compares. Any flag passed to it
|
|
28
|
+
* today is discarded in silence.
|
|
29
|
+
*
|
|
30
|
+
* An unrecognised flag is otherwise IGNORED, so it runs the default and reports success.
|
|
31
|
+
*/
|
|
32
|
+
refuseUnknownFlags([], { entry: import.meta.url, command: "npm run worker:code" });
|
|
33
|
+
// Resolved from THIS module: the fleet scripts ship with this package, so a cwd-relative path was only ever
|
|
34
|
+
// right when run from the repo root.
|
|
35
|
+
const CTL = fleetScriptPaths().workerCtl;
|
|
36
|
+
// /health now reports installed runtime versions as well as the code hash. The first
|
|
37
|
+
// request after a Windows boot may need PowerShell file-version discovery, so four seconds
|
|
38
|
+
// was too tight and made a healthy worker look unreachable.
|
|
39
|
+
const HEALTH_TIMEOUT_MS = 15000;
|
|
40
|
+
/**
|
|
41
|
+
* Which workers to ask, AND WHERE THAT LIST CAME FROM — the second half is not decoration.
|
|
42
|
+
*
|
|
43
|
+
* This used to answer the local UTM pool or nothing, and print "no worker is running — nothing to compare"
|
|
44
|
+
* when it found neither. Measured 2026-08-28 with five bare-metal workers serving `/health` and all five
|
|
45
|
+
* STALE against this checkout: the bare command reported nothing to compare, and the same command with
|
|
46
|
+
* `A11Y_WORKERS` set reported `5 stale worker(s)`. One env var apart, and the quiet answer was the wrong one.
|
|
47
|
+
*
|
|
48
|
+
* That is `lab:inventory`'s lesson at a different layer — *"'none here' and 'none anywhere' are different
|
|
49
|
+
* answers, and it now refuses to turn the first into the second"* — and it lands harder here, because this
|
|
50
|
+
* command exists to stop a corpus being captured on the wrong code. A false clean from it is the failure it
|
|
51
|
+
* was written to prevent, delivered by the tool itself.
|
|
52
|
+
*
|
|
53
|
+
* The inventory was already imported and already read, twelve lines below, to print the REMEDY. So the
|
|
54
|
+
* command could name the five workers it should have checked while insisting it had none to check.
|
|
55
|
+
*
|
|
56
|
+
* Reading it here is safe in a way it would not be for a capture run: this probes `/health` and starts
|
|
57
|
+
* nothing, so the rule that naming workers means you are managing them does not apply.
|
|
58
|
+
*/
|
|
59
|
+
export function workerUrls({ named = configuredWorkers, local = localPoolUrls, inventory = inventoryWorkerUrls, } = {}) {
|
|
60
|
+
// ONE PRECEDENCE, in fleet-env.mjs. This function held its own -- named, then the LOCAL UTM POOL, then
|
|
61
|
+
// the inventory -- while `doctor` went named then inventory, so on any Mac with a registered guest the
|
|
62
|
+
// two commands described different fleets. That is the divergence the comment here used to claim it had
|
|
63
|
+
// closed; it closed the NAMED half only.
|
|
64
|
+
return resolveWorkerPool({ named, inventory, local });
|
|
65
|
+
}
|
|
66
|
+
/** The local UTM pool, or none — `utmctl` is absent on a machine that never had one. */
|
|
67
|
+
function localPoolUrls() {
|
|
68
|
+
try {
|
|
69
|
+
const pool = JSON.parse(execFileSync(CTL, ["pool"], { encoding: "utf8" }));
|
|
70
|
+
return pool.filter((/** @type {{ip?: string}} */ vm) => vm.ip)
|
|
71
|
+
.map((/** @type {{ip: string, port: number}} */ vm) => `http://${vm.ip}:${vm.port}`);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
// Never silent: "there is no UTM here" and "utmctl failed" are different, and the second is the one
|
|
75
|
+
// that would otherwise send somebody to the inventory believing the local pool was empty.
|
|
76
|
+
if (process.env.A11Y_DEBUG)
|
|
77
|
+
console.log(` (local pool unavailable: ${errorText(e)})`);
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** @param {string} url */
|
|
82
|
+
async function versionOf(url) {
|
|
83
|
+
// `requestJson`, not `fetch`: gains a real error CODE (`ECONNREFUSED`, `EHOSTUNREACH`) for the per-worker
|
|
84
|
+
// `errorText(e)` line below, in place of fetch's undifferentiated `TypeError: fetch failed`.
|
|
85
|
+
const response = await requestJson(`${url.replace(/\/$/, "")}/health`, { timeoutMs: HEALTH_TIMEOUT_MS });
|
|
86
|
+
// `requestJson` returns `undefined` for unparseable JSON rather than throwing (its own docstring: a cache
|
|
87
|
+
// miss is a normal outcome for its usual callers) -- `fetch`'s `.json()` threw, and the caller here relies
|
|
88
|
+
// on that to report "unreachable" for a worker that answered garbage. Restored explicitly.
|
|
89
|
+
if (response.json === undefined)
|
|
90
|
+
throw new Error(`invalid JSON from ${url}`);
|
|
91
|
+
return response.json.code ?? "absent";
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Nothing here runs on import, for the same reason as `deploy-worker.mjs`: a module that probes every worker
|
|
95
|
+
* over HTTP should be invoked, not merely mentioned. It also lets `code-version.test.ts` import this rather
|
|
96
|
+
* than parse its source as text.
|
|
97
|
+
*/
|
|
98
|
+
async function main() {
|
|
99
|
+
const expected = expectedWorkerCode();
|
|
100
|
+
const { urls, source } = workerUrls();
|
|
101
|
+
console.log(`this checkout: ${expected}`);
|
|
102
|
+
if (!urls.length) {
|
|
103
|
+
console.log("no worker configured, running locally, or listed in inventory.yml — nothing to compare");
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
// Say which list this is. A reading of "all current" means nothing until you know whether it examined
|
|
107
|
+
// the fleet you are about to capture on or the empty pool on your laptop.
|
|
108
|
+
console.log(`checking ${urls.length} worker(s) from ${source}`);
|
|
109
|
+
// Read first, CLASSIFY second, and the classifier is the one the capture preflight uses. This loop used
|
|
110
|
+
// to decide staleness itself with `actual === expected` — the same judgement in a second place, which is
|
|
111
|
+
// exactly what `worker-code-check.mjs` exists to stop. `versionOf` stays only because the CLI reports the
|
|
112
|
+
// network error text per worker, which a preflight has no use for.
|
|
113
|
+
const readings = [];
|
|
114
|
+
for (const url of urls) {
|
|
115
|
+
try {
|
|
116
|
+
// "absent" means the worker predates /health.code, which is itself a stale deploy.
|
|
117
|
+
readings.push({ worker: url, code: await versionOf(url) });
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
console.log(` ${url} unreachable (${errorText(e)})`);
|
|
121
|
+
readings.push({ worker: url, code: null });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const { stale } = codeDrift(expected, readings);
|
|
125
|
+
const staleUrls = stale.map((s) => s.worker);
|
|
126
|
+
const isStale = new Set(staleUrls);
|
|
127
|
+
for (const { worker, code } of readings.filter((r) => r.code !== null)) {
|
|
128
|
+
console.log(` ${worker} ${code} ${isStale.has(worker) ? "STALE — redeploy and REBOOT the guest" : "matches"}`);
|
|
129
|
+
}
|
|
130
|
+
if (staleUrls.length) {
|
|
131
|
+
for (const line of remedyLines(staleUrls, inventoryWorkerUrls()))
|
|
132
|
+
console.log(line);
|
|
133
|
+
}
|
|
134
|
+
process.exit(staleUrls.length ? 1 : 0);
|
|
135
|
+
}
|
|
136
|
+
// REALPATH'D: `import.meta.url` is resolved through symlinks by Node's ESM loader and `process.argv[1]`
|
|
137
|
+
// is not, so a bin reached via its `.bin` symlink (which is how npm always installs one) mismatched here
|
|
138
|
+
// and this guard silently read false — the tool loaded, did nothing, and exited 0. `/var` and `/tmp` are
|
|
139
|
+
// themselves symlinks on macOS, so this fired every time. Same defect, same fix, as `cli.ts`'s `isProgram`.
|
|
140
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ? realpathSync(process.argv[1]) : "").href)
|
|
141
|
+
await main();
|
|
142
|
+
//# sourceMappingURL=check-worker-code.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-worker-code.mjs","sourceRoot":"","sources":["../src/check-worker-code.mjs"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flag name alone: `--shard=0/4` and `--shard` both name `--shard`.
|
|
3
|
+
* @param {string} argument @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function nameOf(argument: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* `--name=value`'s value, or `undefined` if `--name=` was never passed. audit §9's "argv parsing" row:
|
|
8
|
+
* VALIDATION is owned here (`refuseUnknownFlags`), but 15+ files each hand-rolled this exact three-line
|
|
9
|
+
* idiom for EXTRACTION, and one of them had already drifted from the rest.
|
|
10
|
+
*
|
|
11
|
+
* MEASURED before writing this, across all fifteen, against five vectors (a normal value, a missing flag,
|
|
12
|
+
* an empty value, a repeated flag, and a value containing its own `=`): fourteen agreed on all five —
|
|
13
|
+
* `argv.find((a) => a.startsWith("--name=")).slice("--name=".length)`, verbatim or with `name` templated
|
|
14
|
+
* in. `fleet-discover.mjs`'s `arg()` used `.split("=")[1]` instead, which is identical on four vectors and
|
|
15
|
+
* silently WRONG on the fifth: `--url=http://host?a=b` came back as `"http://host?a"`, truncated at the
|
|
16
|
+
* value's own `=`. Dormant today — that helper is only ever asked for `--cidr=` and `--port=`, neither of
|
|
17
|
+
* which can contain one — but a live discrepancy the day it is asked for a URL or a `key=value` pair.
|
|
18
|
+
*
|
|
19
|
+
* Kept as `.slice`, matching the fourteen rather than the one, and `fleet-discover.mjs` converted to it.
|
|
20
|
+
*
|
|
21
|
+
* @param {readonly string[]} argv @param {string} name
|
|
22
|
+
* @returns {string | undefined}
|
|
23
|
+
*/
|
|
24
|
+
export function flagValue(argv: readonly string[], name: string): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Which of `argv` are flags this command does not know. PURE, so it is testable without a process.
|
|
27
|
+
*
|
|
28
|
+
* A bare `--` is npm's separator and never a flag. Anything not starting with `-` is positional — a URL,
|
|
29
|
+
* a worker address, a page path — and is not this guard's business.
|
|
30
|
+
*
|
|
31
|
+
* SINGLE-DASH FLAGS ARE INSPECTED TOO, AND THE OMISSION COST A 14-MINUTE FLEET OPERATION.
|
|
32
|
+
*
|
|
33
|
+
* This read `startsWith("--")`, on the reasoning that only long flags are ever this repo's own. But an
|
|
34
|
+
* ANSIBLE-shaped argument is single-dash, and several of these commands wrap `ansible-playbook` — so
|
|
35
|
+
* `npm run fleet:provision -- -e worker_edge_allow_downgrade=true` passed straight through the guard,
|
|
36
|
+
* was never forwarded by the wrapper, and the whole fleet was provisioned WITHOUT the authorisation the
|
|
37
|
+
* operator believed they had given. Measured 2026-09-05. The role then refused, correctly, with a message
|
|
38
|
+
* telling the operator to pass the very flag they had just passed.
|
|
39
|
+
*
|
|
40
|
+
* That is precisely the defect this file exists to prevent — "an ignored flag runs the default and reports
|
|
41
|
+
* success" — surviving inside its own remedy, because the remedy was written to match one flag SHAPE
|
|
42
|
+
* rather than the idea of a flag. `-e` is not a URL and not a page path; nothing positional here begins
|
|
43
|
+
* with a dash followed by a letter, which is what makes this safe to refuse rather than merely warn on.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* @param {string[]} argv @param {string[]} known @returns {string[]}
|
|
47
|
+
*/
|
|
48
|
+
export function unknownFlags(argv: string[], known: string[]): string[];
|
|
49
|
+
/**
|
|
50
|
+
* The closest known flag, when there is one close enough to be a likely typo rather than a guess.
|
|
51
|
+
* @param {string} flag @param {string[]} known @returns {string | undefined}
|
|
52
|
+
*/
|
|
53
|
+
export function didYouMean(flag: string, known: string[]): string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Refuse, naming the flag and what this command does take. Exits 2; does not return on failure.
|
|
56
|
+
*
|
|
57
|
+
* `command` names the thing a human typed — the npm script, not the file — because that is what they will
|
|
58
|
+
* retype. Defaults to the script's basename, which is right for the ones invoked directly.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* @param {string[]} known Every flag this command reads, `--name` or `--name=`.
|
|
62
|
+
* @param {{entry: string, argv?: string[], command?: string}} options
|
|
63
|
+
* `entry` is the caller's `import.meta.url`, and it is REQUIRED. `command` names the thing a HUMAN
|
|
64
|
+
* typed — the npm script, not the file — because that is what they will retype.
|
|
65
|
+
*/
|
|
66
|
+
export function refuseUnknownFlags(known: string[], { entry, argv, command }: {
|
|
67
|
+
entry: string;
|
|
68
|
+
argv?: string[];
|
|
69
|
+
command?: string;
|
|
70
|
+
}): void;
|
|
71
|
+
//# sourceMappingURL=cli-flags.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-flags.d.mts","sourceRoot":"","sources":["../src/cli-flags.mjs"],"names":[],"mappings":"AAgDA;;;GAGG;AACH,iCAFW,MAAM,GAAqB,MAAM,CAK3C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,gCAHW,SAAS,MAAM,EAAE,QAAe,MAAM,GACpC,MAAM,GAAG,SAAS,CAM9B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH;;GAEG;AACH,mCAFW,MAAM,EAAE,SAAe,MAAM,EAAE,GAAkB,MAAM,EAAE,CAQnE;AAED;;;GAGG;AACH,iCAFW,MAAM,SAAe,MAAM,EAAE,GAAkB,MAAM,GAAG,SAAS,CAO3E;AAED;;;;;GAKG;AACH;;;;;GAKG;AAMH,0CAVW,MAAM,EAAE,4BACR;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAC,QA+E5D"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Refuse a flag this command does not read.
|
|
4
|
+
*
|
|
5
|
+
* Every CLI here parses argv the same way — `process.argv.find((a) => a.startsWith("--only="))` or
|
|
6
|
+
* `process.argv.includes("--resume")` — and every one of them therefore IGNORES anything it does not
|
|
7
|
+
* recognise. A mistyped, renamed or hallucinated flag runs the default and reports success, and the
|
|
8
|
+
* operator believes their value was applied. That is the same defect as an Ansible extra var a job does
|
|
9
|
+
* not read, one layer out, and this repo has paid for it twice:
|
|
10
|
+
*
|
|
11
|
+
* - a blocker's own message told the reader to run `--write-baseline`; the flag is `--update-baseline`
|
|
12
|
+
* - `--only=route-title-stale` covered 1 of the 7 cases in that family, because the match was exact-id
|
|
13
|
+
*
|
|
14
|
+
* Neither produced an error. Both produced a plausible wrong answer, which this file's governing rule
|
|
15
|
+
* says to replace with a refusal that names the cause.
|
|
16
|
+
*
|
|
17
|
+
* ## Why the known list is passed in rather than read from the caller's source
|
|
18
|
+
*
|
|
19
|
+
* Deriving it at runtime — reading the calling module and regexing out its `--flags` — needs no
|
|
20
|
+
* maintenance, and is wrong for one reason that matters: a CLI whose flags are consumed by a HELPER in
|
|
21
|
+
* another module would refuse them, so the guard would break exactly the commands with the most moving
|
|
22
|
+
* parts. The list is explicit here and `cli-flags.test.ts` derives the same set from source and pins the
|
|
23
|
+
* two equal, which is this repo's remedy when a duplication is forced.
|
|
24
|
+
*/
|
|
25
|
+
import { basename } from "node:path";
|
|
26
|
+
import { realpathSync } from "node:fs";
|
|
27
|
+
import { pathToFileURL } from "node:url";
|
|
28
|
+
/** How far apart two flags may be and still be worth suggesting. One typo, or one word. */
|
|
29
|
+
const NEAR = 4;
|
|
30
|
+
/**
|
|
31
|
+
* Levenshtein, small and iterative — the inputs are flag names, never long strings.
|
|
32
|
+
* @param {string} a @param {string} b @returns {number}
|
|
33
|
+
*/
|
|
34
|
+
function distance(a, b) {
|
|
35
|
+
let previous = Array.from({ length: b.length + 1 }, (_unused, index) => index);
|
|
36
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
37
|
+
const row = [i];
|
|
38
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
39
|
+
row[j] = Math.min(row[j - 1] + 1, previous[j] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
40
|
+
}
|
|
41
|
+
previous = row;
|
|
42
|
+
}
|
|
43
|
+
return previous[b.length];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The flag name alone: `--shard=0/4` and `--shard` both name `--shard`.
|
|
47
|
+
* @param {string} argument @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function nameOf(argument) {
|
|
50
|
+
const equals = argument.indexOf("=");
|
|
51
|
+
return equals === -1 ? argument : argument.slice(0, equals);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* `--name=value`'s value, or `undefined` if `--name=` was never passed. audit §9's "argv parsing" row:
|
|
55
|
+
* VALIDATION is owned here (`refuseUnknownFlags`), but 15+ files each hand-rolled this exact three-line
|
|
56
|
+
* idiom for EXTRACTION, and one of them had already drifted from the rest.
|
|
57
|
+
*
|
|
58
|
+
* MEASURED before writing this, across all fifteen, against five vectors (a normal value, a missing flag,
|
|
59
|
+
* an empty value, a repeated flag, and a value containing its own `=`): fourteen agreed on all five —
|
|
60
|
+
* `argv.find((a) => a.startsWith("--name=")).slice("--name=".length)`, verbatim or with `name` templated
|
|
61
|
+
* in. `fleet-discover.mjs`'s `arg()` used `.split("=")[1]` instead, which is identical on four vectors and
|
|
62
|
+
* silently WRONG on the fifth: `--url=http://host?a=b` came back as `"http://host?a"`, truncated at the
|
|
63
|
+
* value's own `=`. Dormant today — that helper is only ever asked for `--cidr=` and `--port=`, neither of
|
|
64
|
+
* which can contain one — but a live discrepancy the day it is asked for a URL or a `key=value` pair.
|
|
65
|
+
*
|
|
66
|
+
* Kept as `.slice`, matching the fourteen rather than the one, and `fleet-discover.mjs` converted to it.
|
|
67
|
+
*
|
|
68
|
+
* @param {readonly string[]} argv @param {string} name
|
|
69
|
+
* @returns {string | undefined}
|
|
70
|
+
*/
|
|
71
|
+
export function flagValue(argv, name) {
|
|
72
|
+
const prefix = `--${name}=`;
|
|
73
|
+
const hit = argv.find((a) => a.startsWith(prefix));
|
|
74
|
+
return hit === undefined ? undefined : hit.slice(prefix.length);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Which of `argv` are flags this command does not know. PURE, so it is testable without a process.
|
|
78
|
+
*
|
|
79
|
+
* A bare `--` is npm's separator and never a flag. Anything not starting with `-` is positional — a URL,
|
|
80
|
+
* a worker address, a page path — and is not this guard's business.
|
|
81
|
+
*
|
|
82
|
+
* SINGLE-DASH FLAGS ARE INSPECTED TOO, AND THE OMISSION COST A 14-MINUTE FLEET OPERATION.
|
|
83
|
+
*
|
|
84
|
+
* This read `startsWith("--")`, on the reasoning that only long flags are ever this repo's own. But an
|
|
85
|
+
* ANSIBLE-shaped argument is single-dash, and several of these commands wrap `ansible-playbook` — so
|
|
86
|
+
* `npm run fleet:provision -- -e worker_edge_allow_downgrade=true` passed straight through the guard,
|
|
87
|
+
* was never forwarded by the wrapper, and the whole fleet was provisioned WITHOUT the authorisation the
|
|
88
|
+
* operator believed they had given. Measured 2026-09-05. The role then refused, correctly, with a message
|
|
89
|
+
* telling the operator to pass the very flag they had just passed.
|
|
90
|
+
*
|
|
91
|
+
* That is precisely the defect this file exists to prevent — "an ignored flag runs the default and reports
|
|
92
|
+
* success" — surviving inside its own remedy, because the remedy was written to match one flag SHAPE
|
|
93
|
+
* rather than the idea of a flag. `-e` is not a URL and not a page path; nothing positional here begins
|
|
94
|
+
* with a dash followed by a letter, which is what makes this safe to refuse rather than merely warn on.
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* @param {string[]} argv @param {string[]} known @returns {string[]}
|
|
98
|
+
*/
|
|
99
|
+
export function unknownFlags(argv, known) {
|
|
100
|
+
const accepted = new Set(known.map(nameOf));
|
|
101
|
+
return argv
|
|
102
|
+
.filter((argument) => argument !== "--" && (argument.startsWith("--") || /^-[A-Za-z]/.test(argument)))
|
|
103
|
+
.map(nameOf)
|
|
104
|
+
.filter((flag) => !accepted.has(flag));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The closest known flag, when there is one close enough to be a likely typo rather than a guess.
|
|
108
|
+
* @param {string} flag @param {string[]} known @returns {string | undefined}
|
|
109
|
+
*/
|
|
110
|
+
export function didYouMean(flag, known) {
|
|
111
|
+
const ranked = known.map(nameOf)
|
|
112
|
+
.map((candidate) => ({ candidate, gap: distance(flag, candidate) }))
|
|
113
|
+
.sort((left, right) => left.gap - right.gap);
|
|
114
|
+
return ranked[0] && ranked[0].gap <= NEAR ? ranked[0].candidate : undefined;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Refuse, naming the flag and what this command does take. Exits 2; does not return on failure.
|
|
118
|
+
*
|
|
119
|
+
* `command` names the thing a human typed — the npm script, not the file — because that is what they will
|
|
120
|
+
* retype. Defaults to the script's basename, which is right for the ones invoked directly.
|
|
121
|
+
*/
|
|
122
|
+
/**
|
|
123
|
+
* @param {string[]} known Every flag this command reads, `--name` or `--name=`.
|
|
124
|
+
* @param {{entry: string, argv?: string[], command?: string}} options
|
|
125
|
+
* `entry` is the caller's `import.meta.url`, and it is REQUIRED. `command` names the thing a HUMAN
|
|
126
|
+
* typed — the npm script, not the file — because that is what they will retype.
|
|
127
|
+
*/
|
|
128
|
+
// NO `= {}` DEFAULT, because `entry` is required and the docstring above has always said so. A default
|
|
129
|
+
// that lets the whole options object be omitted contradicts that: it produces `entry === undefined`, and
|
|
130
|
+
// the guard below decides whether THIS module is the command by comparing `entry` against argv[1] -- so a
|
|
131
|
+
// caller who forgot it would get a guard that silently never fires. Types found the contradiction the
|
|
132
|
+
// moment this file entered the program. Every one of the 60 real call sites passes it.
|
|
133
|
+
export function refuseUnknownFlags(known, { entry, argv = process.argv.slice(2), command }) {
|
|
134
|
+
// ONLY WHEN THIS MODULE IS THE COMMAND, never when it is imported.
|
|
135
|
+
//
|
|
136
|
+
// These calls sit at module top level, so they run on IMPORT — and then inspect the IMPORTING process's
|
|
137
|
+
// argv. Measured 2026-08-27, an hour after the guards went in: `capture-real-pages --role=calibration`
|
|
138
|
+
// imports `fleet-env.mjs`, whose guard woke up, saw `--role`, decided it did not know it, and killed a
|
|
139
|
+
// 50-page capture with "unknown flag --role — did you mean --list?". The guard was right about its own
|
|
140
|
+
// flags and asking the wrong process.
|
|
141
|
+
//
|
|
142
|
+
// `entry` is REQUIRED rather than defaulted, for the reason `createHostThrottle`'s `minGapMs` is: a
|
|
143
|
+
// default here would silently restore exactly this behaviour for any caller who forgot it, and the
|
|
144
|
+
// failure mode is a guard that fires on somebody else's command line.
|
|
145
|
+
if (!entry) {
|
|
146
|
+
throw new TypeError("refuseUnknownFlags needs { entry: import.meta.url } — without it the guard runs "
|
|
147
|
+
+ "on import and inspects the importing process's flags");
|
|
148
|
+
}
|
|
149
|
+
// REALPATH'D, and without it this guard silently does not fire through a symlink — #237.
|
|
150
|
+
//
|
|
151
|
+
// An entry guard in the realpath'd form reads
|
|
152
|
+
// `import.meta.url === pathToFileURL(process.argv[1] ? realpathSync(process.argv[1]) : "").href`, and
|
|
153
|
+
// this comparison had no `realpathSync`. So when `argv[1]` reaches such a script through a symlink — npm's
|
|
154
|
+
// own `.bin` links are symlinks, which is why those call sites resolve — the OUTER condition is true and
|
|
155
|
+
// this one is FALSE. `main()` runs; the flag guard returns early and inspects nothing.
|
|
156
|
+
//
|
|
157
|
+
// NOT EVERY CALL SITE HAS THAT FORM (#1248). Some still compare the plain, symlink-blind
|
|
158
|
+
// `pathToFileURL(process.argv[1] ?? "")`, which fails the other way through a symlink: the OUTER condition is
|
|
159
|
+
// false, `main()` never runs, and the tool exits 0. Which files still do is `KNOWN_PLAIN_ENTRY_GUARDS` in
|
|
160
|
+
// `entry-points.test.ts` (#1086), a ratchet that may shrink and may not grow. It is the list of record, so no
|
|
161
|
+
// count is repeated here.
|
|
162
|
+
//
|
|
163
|
+
// Measured on `piped-exit-status-guard.mjs`, same file, same flag:
|
|
164
|
+
//
|
|
165
|
+
// node scripts/tmp-symlink-probe.mjs --bogus 'echo hi' -> ran, exit 0, flag IGNORED
|
|
166
|
+
// node packages/guards/src/piped-exit-status-guard.mjs --bogus '...' -> refused, exit 2
|
|
167
|
+
//
|
|
168
|
+
// Through the symlink the mistyped flag is ignored and the command reports success — which is the
|
|
169
|
+
// sentence this refusal itself prints as the reason it exists.
|
|
170
|
+
//
|
|
171
|
+
// It is a remedy whose TRIGGER is narrower than the thing it guards, the `refreshBrowseBuffer` shape:
|
|
172
|
+
// reachable from the right path, with a condition that could not be true there. And the census
|
|
173
|
+
// (`cli-flags.test.ts`) cannot see it, because it asserts a file CONTAINS `refuseUnknownFlags(` — it
|
|
174
|
+
// cannot ask whether that call can FIRE, so every guarded CLI reads GUARDED either way.
|
|
175
|
+
//
|
|
176
|
+
// `realpathSync` THROWS on a path that does not exist, and `process.argv[1]` is absent for `node -e`
|
|
177
|
+
// and `node --eval`. Absent stays absent rather than becoming a throw: the guard must be inert when
|
|
178
|
+
// there is no script, never fatal.
|
|
179
|
+
/** The invoking script's REAL path as a URL, so a symlinked `argv[1]` still matches `entry`. */
|
|
180
|
+
let invoked;
|
|
181
|
+
try {
|
|
182
|
+
invoked = process.argv[1] ? pathToFileURL(realpathSync(process.argv[1])).href : "";
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
invoked = pathToFileURL(process.argv[1] ?? "").href; // unresolvable: compare what we were given
|
|
186
|
+
}
|
|
187
|
+
if (entry !== invoked)
|
|
188
|
+
return;
|
|
189
|
+
const unknown = unknownFlags(argv, known);
|
|
190
|
+
if (unknown.length === 0)
|
|
191
|
+
return;
|
|
192
|
+
const name = command ?? basename(process.argv[1] ?? "this command");
|
|
193
|
+
for (const flag of unknown) {
|
|
194
|
+
const near = didYouMean(flag, known);
|
|
195
|
+
console.error(` ${name}: unknown flag ${flag}${near ? ` — did you mean ${near}?` : ""}`);
|
|
196
|
+
}
|
|
197
|
+
// "It takes: " with nothing after it is what a command taking NO flags printed, and that reads like the
|
|
198
|
+
// guard failed to find its own list rather than like an answer. First hit by `release:provenance`, the
|
|
199
|
+
// first zero-flag CLI here -- the branch existed for months with no caller to exercise it.
|
|
200
|
+
const accepted = [...known].map(nameOf).sort();
|
|
201
|
+
console.error(accepted.length === 0
|
|
202
|
+
? " It takes no flags at all."
|
|
203
|
+
: ` It takes: ${accepted.join(" ")}`);
|
|
204
|
+
console.error(" Refusing rather than ignoring it: an ignored flag runs the default and reports success.");
|
|
205
|
+
process.exit(2);
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=cli-flags.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-flags.mjs","sourceRoot":"","sources":["../src/cli-flags.mjs"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a set of `/health.code` readings into stale and unreachable. PURE.
|
|
3
|
+
*
|
|
4
|
+
* `null` is UNREACHABLE and is not a finding, matching `assertOneBrowserAcross`: a box that is asleep
|
|
5
|
+
* contributes no evidence and no mismatch, and treating silence as a fault is how a check earns a
|
|
6
|
+
* reputation for crying wolf. `"absent"` IS a finding — a worker predating `/health.code` is itself a
|
|
7
|
+
* stale deploy.
|
|
8
|
+
*
|
|
9
|
+
* `answered` is returned rather than left to be inferred from the two lists, because "nothing was wrong"
|
|
10
|
+
* and "nothing was examined" produce the SAME empty `stale` — and a caller with only the lists cannot
|
|
11
|
+
* distinguish 3 clean workers from 3 silent ones. Every number carrying what it was computed from is this
|
|
12
|
+
* file's parent rule; this is the one number that was missing.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} expected
|
|
15
|
+
* @param {Array<{worker: string, code: string|null|undefined}>} readings
|
|
16
|
+
* @returns {{expected: string, stale: Array<{worker: string, serving: string}>, unreachable: string[],
|
|
17
|
+
* answered: number}}
|
|
18
|
+
*/
|
|
19
|
+
export function codeDrift(expected: string, readings: Array<{
|
|
20
|
+
worker: string;
|
|
21
|
+
code: string | null | undefined;
|
|
22
|
+
}>): {
|
|
23
|
+
expected: string;
|
|
24
|
+
stale: Array<{
|
|
25
|
+
worker: string;
|
|
26
|
+
serving: string;
|
|
27
|
+
}>;
|
|
28
|
+
unreachable: string[];
|
|
29
|
+
answered: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Name the deploy route that can actually reach these workers.
|
|
33
|
+
*
|
|
34
|
+
* There are two, they share no mechanism, and the wrong one wastes real time. `worker:deploy` is
|
|
35
|
+
* `utmctl file push` plus a `utmctl` reboot: it takes a VM UUID and fails immediately off macOS, so it
|
|
36
|
+
* cannot touch a physical box. Bare-metal workers are git-cloned and deploy by PULLING, through Ansible.
|
|
37
|
+
*
|
|
38
|
+
* This printed the utmctl advice unconditionally, including to a fleet of four mini PCs where none of it
|
|
39
|
+
* applies — a tool confidently prescribing a remedy for a different kind of machine. Which kind a worker is
|
|
40
|
+
* is not a guess: `inventory.yml` is the single source of truth for the bare-metal fleet (ADR 0012).
|
|
41
|
+
*
|
|
42
|
+
* PURE, and the bare-metal list is a parameter rather than a read, because a remedy that only appears when
|
|
43
|
+
* something is already broken is one nobody sees until it matters. This repo has shipped an inert remedy
|
|
44
|
+
* before (`refreshBrowseBuffer`, whose trigger was never set) and confirmed it by results it had no part in
|
|
45
|
+
* producing. Returning lines makes both branches assertable with nothing stale and no fleet.
|
|
46
|
+
*
|
|
47
|
+
* @param {string[]} staleUrls
|
|
48
|
+
* @param {string[]} bareMetalUrls
|
|
49
|
+
* @returns {string[]}
|
|
50
|
+
*/
|
|
51
|
+
export function remedyLines(staleUrls: string[], bareMetalUrls: string[]): string[];
|
|
52
|
+
/**
|
|
53
|
+
* The refusal text, or `null` when the fleet is running this checkout. PURE.
|
|
54
|
+
*
|
|
55
|
+
* Every number it reports carries what it was computed from, which is this file's parent rule: the hash,
|
|
56
|
+
* which side is dirty, and how many boxes were silent rather than merely absent from the count.
|
|
57
|
+
*
|
|
58
|
+
* @param {{expected: string, stale: Array<{worker: string, serving: string}>, unreachable: string[],
|
|
59
|
+
* answered?: number}} drift
|
|
60
|
+
* @param {{when?: string, bareMetalUrls?: string[], sourceDirty?: string}} options
|
|
61
|
+
* @returns {string|null}
|
|
62
|
+
*/
|
|
63
|
+
export function describeCodeDrift(drift: {
|
|
64
|
+
expected: string;
|
|
65
|
+
stale: Array<{
|
|
66
|
+
worker: string;
|
|
67
|
+
serving: string;
|
|
68
|
+
}>;
|
|
69
|
+
unreachable: string[];
|
|
70
|
+
answered?: number;
|
|
71
|
+
}, { when, bareMetalUrls, sourceDirty }?: {
|
|
72
|
+
when?: string;
|
|
73
|
+
bareMetalUrls?: string[];
|
|
74
|
+
sourceDirty?: string;
|
|
75
|
+
}): string | null;
|
|
76
|
+
/** What a single worker is serving, or `null` when it did not answer. */
|
|
77
|
+
/** @param {string} url */
|
|
78
|
+
export function readWorkerCode(url: string): Promise<any>;
|
|
79
|
+
/**
|
|
80
|
+
* Is the worker source in `sourceDir` modified against HEAD?
|
|
81
|
+
*
|
|
82
|
+
* The DIRECTORY IS THE CALLER'S, never a path guessed here: this file is reached from `control` (which asks
|
|
83
|
+
* `layer-checkouts.mjs`) and from `worker-fleet` (which asks the worker package by name), and a guessed
|
|
84
|
+
* monorepo path would read the wrong tree the day the layer lives in another repository (#3394).
|
|
85
|
+
*
|
|
86
|
+
* Guarded, exactly like `protocolBumpNote`: outside a git checkout there is simply nothing to add, and a
|
|
87
|
+
* precondition that throws because `git` is missing is worse than the drift it was checking for.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} sourceDir
|
|
90
|
+
*/
|
|
91
|
+
export function workerSourceDirty(sourceDir: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* AN EMPTY POOL IS NOT A CLEAN FLEET — the refusal, as a value, so it can be tested without exiting.
|
|
94
|
+
*
|
|
95
|
+
* `assertFleetRunsThisCheckout([])` used to print "Fleet runs this checkout (worker code …, 0 worker(s)
|
|
96
|
+
* checked)" and return: an affirmative claim about a fleet it had not looked at. The count sitting in the
|
|
97
|
+
* sentence is the only reason that was ever arguable, and "0 worker(s) checked" under a heading saying the
|
|
98
|
+
* fleet is fine is precisely how "verified" comes to mean "unexamined".
|
|
99
|
+
*
|
|
100
|
+
* REFUSED, not reported-and-continued. The pre-push hook's loud skip is right for `runs/`, whose absence is
|
|
101
|
+
* legitimate; an empty pool at a capture boundary never is. Both callers pass a pool a capture is about to
|
|
102
|
+
* dispatch to, so failing here with a named cause beats failing later as "0 captured".
|
|
103
|
+
*
|
|
104
|
+
* Returned rather than written, and that split is this file's own convention: the classification and the
|
|
105
|
+
* message are pure and driven directly by the tests, because a function that calls `process.exit` cannot be
|
|
106
|
+
* asserted on without stubbing the runtime out from under it.
|
|
107
|
+
*
|
|
108
|
+
* @param {string[] | undefined} workers
|
|
109
|
+
* @param {string} expected
|
|
110
|
+
* @returns {string | null} the refusal, or null when there is a pool to check
|
|
111
|
+
*/
|
|
112
|
+
export function describeEmptyPool(workers: string[] | undefined, expected: string): string | null;
|
|
113
|
+
/**
|
|
114
|
+
* Refuse to run against a fleet that is not serving `expected` — the shared body of
|
|
115
|
+
* `assertFleetRunsThisCheckout`, taking the hash as a parameter rather than computing it.
|
|
116
|
+
*
|
|
117
|
+
* That split is the whole reason this function exists rather than living only in `worker-code-check.mjs`:
|
|
118
|
+
* computing `expected` needs `codeVersion`/`workerSourceDir`, and reaching those from `packages/control`
|
|
119
|
+
* cannot go through a relative path (TS5055 in `worker-fleet`'s own build) or a package-name import (no
|
|
120
|
+
* `node_modules` on the control plane, ADR 0012) at once — see this file's header. So the CALLER computes
|
|
121
|
+
* `expected` however it can safely reach a hasher, and this function starts from the answer.
|
|
122
|
+
*
|
|
123
|
+
* Exits 3, matching `assertOneBrowserAcross`: a precondition the operator must act on, distinct from 1
|
|
124
|
+
* (captures failed) and 2 (the request was malformed).
|
|
125
|
+
*
|
|
126
|
+
* @param {string} expected
|
|
127
|
+
* @param {string[]} workers
|
|
128
|
+
* `sourceDir` is the worker source the `expected` hash was computed over, so a dirty tree is read from the
|
|
129
|
+
* same place the hash came from.
|
|
130
|
+
*
|
|
131
|
+
* @param {{when?: string, allow?: boolean, read?: (url: string) => Promise<string|null>, bareMetalUrls?: string[], sourceDir: string}} options
|
|
132
|
+
*/
|
|
133
|
+
export function assertWorkersServe(expected: string, workers: string[], options: {
|
|
134
|
+
when?: string;
|
|
135
|
+
allow?: boolean;
|
|
136
|
+
read?: (url: string) => Promise<string | null>;
|
|
137
|
+
bareMetalUrls?: string[];
|
|
138
|
+
sourceDir: string;
|
|
139
|
+
}): Promise<void>;
|
|
140
|
+
//# sourceMappingURL=code-drift.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-drift.d.mts","sourceRoot":"","sources":["../src/code-drift.mjs"],"names":[],"mappings":"AAoCA;;;;;;;;;;;;;;;;;GAiBG;AACH,oCALW,MAAM,YACN,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAC,IAAI,GAAC,SAAS,CAAA;CAAC,CAAC,GAClD;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAC;IACzF,QAAQ,EAAE,MAAM,CAAA;CAAC,CAgB9B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,uCAJW,MAAM,EAAE,iBACR,MAAM,EAAE,GACN,MAAM,EAAE,CAmBpB;AAED;;;;;;;;;;GAUG;AACH,yCALW;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAC;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAC,yCACpB;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAC,GAC7D,MAAM,GAAC,IAAI,CA0DvB;AAED,yEAAyE;AACzE,0BAA0B;AAC1B,oCADY,MAAM,gBAoBjB;AAED;;;;;;;;;;;GAWG;AACH,6CAFW,MAAM,UAahB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,2CAJW,MAAM,EAAE,GAAG,SAAS,YACpB,MAAM,GACJ,MAAM,GAAG,IAAI,CAOzB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,6CAPW,MAAM,WACN,MAAM,EAAE,WAIR;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAC,IAAI,CAAC,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAC,iBA2BrI"}
|