@bli-cockpit/cli 0.2.22 → 0.2.23
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/commands/local.js
CHANGED
|
@@ -711,9 +711,16 @@ function sanitizeInstallErrorCode(value) {
|
|
|
711
711
|
.slice(0, 120);
|
|
712
712
|
return normalized || "unknown";
|
|
713
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Posts pending install events. Also the collector's only per-tick listening
|
|
716
|
+
* post: the response carries the server-published `min_cli_version` floor
|
|
717
|
+
* (BLI-2678), so the last one observed is returned for the scheduled
|
|
718
|
+
* self-update step to act on. Every early-out returns null — no receipt, no
|
|
719
|
+
* floor.
|
|
720
|
+
*/
|
|
714
721
|
export async function reportInstallEventsBestEffort(options) {
|
|
715
722
|
if (options.events.length === 0)
|
|
716
|
-
return;
|
|
723
|
+
return null;
|
|
717
724
|
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
718
725
|
try {
|
|
719
726
|
await enqueueInstallEventEntry(paths, {
|
|
@@ -744,17 +751,18 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
744
751
|
if (options.json) {
|
|
745
752
|
writeLine(options.io.stderr, "Install event outbox unavailable: local_write_failed");
|
|
746
753
|
}
|
|
747
|
-
return;
|
|
754
|
+
return null;
|
|
748
755
|
}
|
|
749
756
|
const session = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
750
757
|
if (!session ||
|
|
751
758
|
session.session_state !== "valid" ||
|
|
752
759
|
typeof session.device_token !== "string" ||
|
|
753
760
|
!session.device_token) {
|
|
754
|
-
return;
|
|
761
|
+
return null;
|
|
755
762
|
}
|
|
756
763
|
const pending = (await readPendingInstallEventEntries(paths)).slice(0, 20);
|
|
757
764
|
const failures = [];
|
|
765
|
+
let observedMinCliVersion = null;
|
|
758
766
|
for (let offset = 0; offset < pending.length; offset += 5) {
|
|
759
767
|
await Promise.all(pending.slice(offset, offset + 5).map(async (entry) => {
|
|
760
768
|
const controller = new AbortController();
|
|
@@ -777,6 +785,13 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
777
785
|
if (!response.ok) {
|
|
778
786
|
throw new Error(`http_${response.status}`);
|
|
779
787
|
}
|
|
788
|
+
const receipt = (await response
|
|
789
|
+
.json()
|
|
790
|
+
.catch(() => null));
|
|
791
|
+
if (typeof receipt?.min_cli_version === "string" &&
|
|
792
|
+
receipt.min_cli_version.trim()) {
|
|
793
|
+
observedMinCliVersion = receipt.min_cli_version.trim();
|
|
794
|
+
}
|
|
780
795
|
await removeInstallEventEntry(paths, entry.outbox_id);
|
|
781
796
|
}
|
|
782
797
|
catch (error) {
|
|
@@ -795,6 +810,7 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
795
810
|
if (options.json && failures.length > 0) {
|
|
796
811
|
writeLine(options.io.stderr, `Install event telemetry queued for retry: ${[...new Set(failures)].join(",")}`);
|
|
797
812
|
}
|
|
813
|
+
return observedMinCliVersion;
|
|
798
814
|
}
|
|
799
815
|
function classifyInstallTelemetryError(error) {
|
|
800
816
|
if (error instanceof Error && error.name === "AbortError") {
|
|
@@ -2135,7 +2151,7 @@ async function runSync(command, io) {
|
|
|
2135
2151
|
const paths = getCollectorRuntimePaths(command.homeDir);
|
|
2136
2152
|
const config = await readLocalCollectorConfig(paths).catch(() => null);
|
|
2137
2153
|
const dashboardUrl = command.dashboardUrl ?? config?.dashboard_url ?? DEFAULT_DASHBOARD_URL;
|
|
2138
|
-
await reportInstallEventsBestEffort({
|
|
2154
|
+
const minCliVersionAtStart = await reportInstallEventsBestEffort({
|
|
2139
2155
|
homeDir: command.homeDir,
|
|
2140
2156
|
dashboardUrl,
|
|
2141
2157
|
command: "sync",
|
|
@@ -2145,7 +2161,7 @@ async function runSync(command, io) {
|
|
|
2145
2161
|
});
|
|
2146
2162
|
try {
|
|
2147
2163
|
const result = await runSyncWithHealthReceipt(command, io);
|
|
2148
|
-
await reportInstallEventsBestEffort({
|
|
2164
|
+
const minCliVersion = await reportInstallEventsBestEffort({
|
|
2149
2165
|
homeDir: command.homeDir,
|
|
2150
2166
|
dashboardUrl,
|
|
2151
2167
|
command: "sync",
|
|
@@ -2155,11 +2171,11 @@ async function runSync(command, io) {
|
|
|
2155
2171
|
});
|
|
2156
2172
|
// BLI-2601: self-update runs only after collection's own outcome above is
|
|
2157
2173
|
// already decided and reported, win or lose. See the function doc.
|
|
2158
|
-
await runScheduledSelfUpdateAfterSync(command, io, dashboardUrl);
|
|
2174
|
+
await runScheduledSelfUpdateAfterSync(command, io, dashboardUrl, minCliVersion ?? minCliVersionAtStart);
|
|
2159
2175
|
return result.exitCode;
|
|
2160
2176
|
}
|
|
2161
2177
|
catch (error) {
|
|
2162
|
-
await reportInstallEventsBestEffort({
|
|
2178
|
+
const minCliVersion = await reportInstallEventsBestEffort({
|
|
2163
2179
|
homeDir: command.homeDir,
|
|
2164
2180
|
dashboardUrl,
|
|
2165
2181
|
command: "sync",
|
|
@@ -2174,7 +2190,7 @@ async function runSync(command, io) {
|
|
|
2174
2190
|
json: command.json,
|
|
2175
2191
|
io,
|
|
2176
2192
|
});
|
|
2177
|
-
await runScheduledSelfUpdateAfterSync(command, io, dashboardUrl);
|
|
2193
|
+
await runScheduledSelfUpdateAfterSync(command, io, dashboardUrl, minCliVersion ?? minCliVersionAtStart);
|
|
2178
2194
|
throw error;
|
|
2179
2195
|
}
|
|
2180
2196
|
}
|
|
@@ -2188,10 +2204,10 @@ async function runSync(command, io) {
|
|
|
2188
2204
|
* reported as its own named `update` receipt, never surfaced as a `sync`
|
|
2189
2205
|
* failure or thrown from this function.
|
|
2190
2206
|
*/
|
|
2191
|
-
async function runScheduledSelfUpdateAfterSync(command, io, dashboardUrl) {
|
|
2207
|
+
async function runScheduledSelfUpdateAfterSync(command, io, dashboardUrl, minCliVersion) {
|
|
2192
2208
|
let event;
|
|
2193
2209
|
try {
|
|
2194
|
-
event = await runScheduledSelfUpdateForSync(command, io);
|
|
2210
|
+
event = await runScheduledSelfUpdateForSync(command, io, minCliVersion);
|
|
2195
2211
|
}
|
|
2196
2212
|
catch (error) {
|
|
2197
2213
|
// The throttle/probe/install machinery below is defensive already; this
|
|
@@ -2215,7 +2231,7 @@ async function runScheduledSelfUpdateAfterSync(command, io, dashboardUrl) {
|
|
|
2215
2231
|
io,
|
|
2216
2232
|
});
|
|
2217
2233
|
}
|
|
2218
|
-
async function runScheduledSelfUpdateForSync(command, io) {
|
|
2234
|
+
async function runScheduledSelfUpdateForSync(command, io, minCliVersion) {
|
|
2219
2235
|
const rawExec = io.exec;
|
|
2220
2236
|
if (!rawExec) {
|
|
2221
2237
|
// Only the real production `defaultIo()` supplies a process runner. A
|
|
@@ -2235,7 +2251,7 @@ async function runScheduledSelfUpdateForSync(command, io) {
|
|
|
2235
2251
|
exec,
|
|
2236
2252
|
currentVersion: LOCAL_COLLECTOR_VERSION,
|
|
2237
2253
|
install: (tag) => attemptScheduledSelfUpdateInstall(scheduledIo, tag),
|
|
2238
|
-
}, { env: io.env });
|
|
2254
|
+
}, { env: io.env, minVersion: minCliVersion });
|
|
2239
2255
|
return scheduledSelfUpdateInstallEvent(result);
|
|
2240
2256
|
}
|
|
2241
2257
|
async function attemptScheduledSelfUpdateInstall(io, tag) {
|
|
@@ -2260,9 +2276,21 @@ function scheduledSelfUpdateInstallEvent(result) {
|
|
|
2260
2276
|
// produces a receipt.
|
|
2261
2277
|
if (result.reason === "throttled_recent_attempt")
|
|
2262
2278
|
return null;
|
|
2263
|
-
|
|
2264
|
-
|
|
2279
|
+
// A forced attempt names its trigger in the receipt either way, so the
|
|
2280
|
+
// ledger can tell "converged on the daily cadence" from "the floor pulled
|
|
2281
|
+
// this machine forward" (BLI-2678).
|
|
2282
|
+
const forcedDetail = result.forced && result.min_version
|
|
2283
|
+
? `forced_min_version ${result.min_version}`
|
|
2284
|
+
: null;
|
|
2285
|
+
if (result.status === "ok") {
|
|
2286
|
+
return {
|
|
2287
|
+
step: "update",
|
|
2288
|
+
status: "ok",
|
|
2289
|
+
...(forcedDetail ? { error_detail: forcedDetail } : {}),
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2265
2292
|
const detail = [
|
|
2293
|
+
forcedDetail,
|
|
2266
2294
|
result.target_version ? `target ${result.target_version}` : null,
|
|
2267
2295
|
result.installed_version ? `installed ${result.installed_version}` : null,
|
|
2268
2296
|
]
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.23");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -37,9 +37,22 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
37
37
|
const env = options.env ?? process.env;
|
|
38
38
|
const now = options.now ?? new Date();
|
|
39
39
|
const tag = options.tag ?? "latest";
|
|
40
|
+
// BLI-2678: the fleet forced-update floor. When the server says the minimum
|
|
41
|
+
// is above what this process is running, the daily throttle stops applying —
|
|
42
|
+
// the whole point of the floor is converging faster than once a day. This is
|
|
43
|
+
// NOT remote code execution: being below the floor only makes the exact
|
|
44
|
+
// same npm self-update that runs daily anyway run now. An unparseable floor
|
|
45
|
+
// is ignored rather than obeyed — a typo must fail toward the safe default.
|
|
46
|
+
const minVersion = parseSemverTriple(options.minVersion ?? null)
|
|
47
|
+
? options.minVersion.trim()
|
|
48
|
+
: null;
|
|
49
|
+
const forced = minVersion !== null && isSemverBelow(deps.currentVersion, minVersion);
|
|
50
|
+
const forcedFields = forced ? { forced: true, min_version: minVersion } : {};
|
|
40
51
|
const marker = path.join(paths.state_dir, SELF_UPDATE_THROTTLE_MARKER);
|
|
41
52
|
const lastCheck = await fs.stat(marker).catch(() => null);
|
|
42
|
-
if (
|
|
53
|
+
if (!forced &&
|
|
54
|
+
lastCheck &&
|
|
55
|
+
now.getTime() - lastCheck.mtimeMs < SELF_UPDATE_MIN_INTERVAL_MS) {
|
|
43
56
|
// The steady-state case: already checked today, nothing to do. Not
|
|
44
57
|
// reported as an "attempt" by the caller — this fires on ~95 of every 96
|
|
45
58
|
// ticks and would otherwise be pure noise.
|
|
@@ -56,11 +69,33 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
56
69
|
// disabled machine reports itself at most once per day too, instead of on
|
|
57
70
|
// every tick.
|
|
58
71
|
if (env["COCKPIT_DISABLE_AUTO_UPDATE"] === "1") {
|
|
59
|
-
|
|
72
|
+
// The incident-triage freeze outranks the forced floor: a human pinned
|
|
73
|
+
// this machine on purpose, and the server must not be able to unpin it.
|
|
74
|
+
return { status: "skipped", reason: "disabled_by_env", ...forcedFields };
|
|
60
75
|
}
|
|
61
76
|
const targetVersion = await latestCliVersionFromNpm(deps.exec, tag);
|
|
77
|
+
if (forced &&
|
|
78
|
+
targetVersion &&
|
|
79
|
+
isSemverBelow(targetVersion, minVersion)) {
|
|
80
|
+
// The floor is above what npm actually serves — a typo'd floor, or one set
|
|
81
|
+
// before the release finished publishing. Installing would not clear the
|
|
82
|
+
// floor, so every forced tick would run `npm install` forever. Skip loudly
|
|
83
|
+
// instead: this reason posts a receipt on every tick until the floor is
|
|
84
|
+
// corrected, which is exactly the alarm a misconfig should raise.
|
|
85
|
+
return {
|
|
86
|
+
status: "skipped",
|
|
87
|
+
reason: "min_version_above_npm_latest",
|
|
88
|
+
target_version: targetVersion,
|
|
89
|
+
...forcedFields,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
62
92
|
if (targetVersion && targetVersion === deps.currentVersion) {
|
|
63
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
status: "ok",
|
|
95
|
+
reason: "already_latest",
|
|
96
|
+
target_version: targetVersion,
|
|
97
|
+
...forcedFields,
|
|
98
|
+
};
|
|
64
99
|
}
|
|
65
100
|
const installed = await deps.install(tag);
|
|
66
101
|
if (!installed.ok) {
|
|
@@ -68,6 +103,7 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
68
103
|
status: "fail",
|
|
69
104
|
reason: installed.eacces ? "eacces_needs_chown" : "npm_install_failed",
|
|
70
105
|
...(targetVersion ? { target_version: targetVersion } : {}),
|
|
106
|
+
...forcedFields,
|
|
71
107
|
};
|
|
72
108
|
}
|
|
73
109
|
// Verify against what the platform returns, not the exit code npm handed
|
|
@@ -82,6 +118,7 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
82
118
|
reason: "stale_after_self_update",
|
|
83
119
|
...(targetVersion ? { target_version: targetVersion } : {}),
|
|
84
120
|
...(installedVersion ? { installed_version: installedVersion } : {}),
|
|
121
|
+
...forcedFields,
|
|
85
122
|
};
|
|
86
123
|
}
|
|
87
124
|
return {
|
|
@@ -89,8 +126,37 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
89
126
|
reason: "updated",
|
|
90
127
|
target_version: targetVersion ?? installedVersion,
|
|
91
128
|
installed_version: installedVersion,
|
|
129
|
+
...forcedFields,
|
|
92
130
|
};
|
|
93
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Ordering comparison for the forced-update floor. Mirrors the dashboard's
|
|
134
|
+
* parseSemver/compareParsedSemver (apps/dashboard/src/lib/ambient/rollups.ts)
|
|
135
|
+
* rather than adding a `semver` dependency for two ten-line functions. A
|
|
136
|
+
* version either side fails to parse → false: an unreadable floor or version
|
|
137
|
+
* must never force anything.
|
|
138
|
+
*/
|
|
139
|
+
function parseSemverTriple(value) {
|
|
140
|
+
if (typeof value !== "string")
|
|
141
|
+
return null;
|
|
142
|
+
const match = value.trim().match(/^(\d+)\.(\d+)\.(\d+)/u);
|
|
143
|
+
if (!match)
|
|
144
|
+
return null;
|
|
145
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
146
|
+
}
|
|
147
|
+
function isSemverBelow(left, right) {
|
|
148
|
+
const parsedLeft = parseSemverTriple(left);
|
|
149
|
+
const parsedRight = parseSemverTriple(right);
|
|
150
|
+
if (!parsedLeft || !parsedRight)
|
|
151
|
+
return false;
|
|
152
|
+
for (let part = 0; part < 3; part += 1) {
|
|
153
|
+
if ((parsedLeft[part] ?? 0) < (parsedRight[part] ?? 0))
|
|
154
|
+
return true;
|
|
155
|
+
if ((parsedLeft[part] ?? 0) > (parsedRight[part] ?? 0))
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
94
160
|
async function latestCliVersionFromNpm(exec, tag) {
|
|
95
161
|
const result = await exec("npm", [
|
|
96
162
|
"view",
|