@codeyam-editor/codeyam-editor 0.1.8 → 0.1.9
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/npm/cloud.js +361 -18
- package/npm/editor-dev.js +24 -2
- package/npm/editor.js +15 -5
- package/npm/fleet-add-failure.js +355 -21
- package/npm/fleet-branch.js +124 -1
- package/npm/fleet-build-data.js +12 -1
- package/npm/fleet-cleanup.js +84 -3
- package/npm/fleet-datastore-health.js +107 -0
- package/npm/fleet-revision.js +108 -0
- package/npm/fleet-run-setup.js +66 -0
- package/npm/fleet-session-activity.js +162 -0
- package/npm/fleet-vm-readiness.js +57 -16
- package/npm/launch-config.js +126 -2
- package/npm/scenario-check.js +697 -34
- package/npm/scenario-interactivity.js +477 -23
- package/npm/scenario-issues.js +61 -0
- package/npm/scenario-playwright.js +67 -14
- package/npm/utils.js +262 -22
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-DSVZvEkB.js → ContainerInspector-C7I9wctz.js} +1 -1
- package/ui/dist/assets/EditorShell-9aYMeOdZ.js +330 -0
- package/ui/dist/assets/EditorShell-CE8iG_EM.css +1 -0
- package/ui/dist/assets/ScenarioDataPanel-CLwkrex0.js +18 -0
- package/ui/dist/assets/{ScenarioDataPanelFullPage-Cyk6efiI.js → ScenarioDataPanelFullPage-DT0TZC4M.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-Bsarhcrl.js +21 -0
- package/ui/dist/assets/index-BRoAenc1.css +1 -0
- package/ui/dist/assets/{index-BPLKgwon.js → index-iMR5siiG.js} +9 -9
- package/ui/dist/index.html +2 -2
- package/ui/dist/preview-subdir-demo/index.html +62 -0
- package/ui/dist/preview-subdir-demo/sibling.html +48 -0
- package/ui/dist/assets/EditorShell-DnXdHrar.js +0 -256
- package/ui/dist/assets/EditorShell-FvbXyqqD.css +0 -1
- package/ui/dist/assets/ScenarioDataPanel-6qJE-0lA.js +0 -15
- package/ui/dist/assets/ScenarioRenderer-Uyt54vHI.js +0 -21
- package/ui/dist/assets/index-DKpgJLo3.css +0 -1
package/npm/fleet-add-failure.js
CHANGED
|
@@ -33,7 +33,11 @@ const { parseRosterMarkers } = require('./fleet-early-roster');
|
|
|
33
33
|
//
|
|
34
34
|
// 1. NOTHING WAS PROVISIONED. The classifier can prove no cloud resource was
|
|
35
35
|
// created (`preflight-drift` aborts before the first gcloud call;
|
|
36
|
-
// `instance-vanished` confirms the instance is gone
|
|
36
|
+
// `instance-vanished` confirms the instance is gone; `capacity-stockout`
|
|
37
|
+
// pairs a GCE stockout signature with the `Failed to create instance` line
|
|
38
|
+
// that `ensureInstance` throws from exactly one place — the branch where the
|
|
39
|
+
// instance was not found AND the create call failed, so no instance, disk,
|
|
40
|
+
// NEG or backend service can exist). Retry starts clean.
|
|
37
41
|
//
|
|
38
42
|
// 2. THE REMAINING WORK IS RE-ENTRANT. A real VM exists, but every step the
|
|
39
43
|
// retry would re-run is skip-on-exists or re-attaching, so the retry
|
|
@@ -50,6 +54,7 @@ const { parseRosterMarkers } = require('./fleet-early-roster');
|
|
|
50
54
|
const ADD_FAILURE_RETRYABLE = new Set([
|
|
51
55
|
'preflight-drift',
|
|
52
56
|
'instance-vanished',
|
|
57
|
+
'capacity-stockout',
|
|
53
58
|
'project-bootstrap-timeout',
|
|
54
59
|
]);
|
|
55
60
|
|
|
@@ -171,6 +176,180 @@ function nextValidActionRemedy(text) {
|
|
|
171
176
|
return i === -1 ? '' : lines.slice(i).join('\n').trim();
|
|
172
177
|
}
|
|
173
178
|
|
|
179
|
+
// Name the capacity GCE was short of, from whatever the stockout transcript
|
|
180
|
+
// happens to expose — for the capacity-stockout recovery line. GCE reports this
|
|
181
|
+
// inconsistently: the 2026-08-20 VM-11 transcript carried `vmType:` / `zone:`
|
|
182
|
+
// bullets under errorInfo.metadatas, while the 2026-08-18 VM-13 spelling
|
|
183
|
+
// (ZONE_RESOURCE_POOL_EXHAUSTED_WITH_DETAILS) carried neither. So every part is
|
|
184
|
+
// optional and the phrase degrades to a generic-but-true sentence rather than
|
|
185
|
+
// interpolating `undefined` into operator-facing copy. Extracted alongside
|
|
186
|
+
// nextValidActionRemedy (the same shape: a pure transcript reader that enriches
|
|
187
|
+
// one arm's recovery text) so the degradation is asserted directly.
|
|
188
|
+
//
|
|
189
|
+
// `zone:` is anchored with ^\s* so the neighbouring `zonesAvailable:` bullet —
|
|
190
|
+
// which lists the zones that DID have capacity — cannot be mistaken for the zone
|
|
191
|
+
// that failed. Never throws; returns the generic phrase for empty input.
|
|
192
|
+
function stockoutCapacityPhrase(logTail) {
|
|
193
|
+
const log = String(logTail || '');
|
|
194
|
+
const vmType = (log.match(/^\s*vmType:\s*(\S+)/m) || [])[1];
|
|
195
|
+
const zone = (log.match(/^\s*zone:\s*(\S+)/m) || [])[1];
|
|
196
|
+
return [vmType, 'capacity', zone ? `in ${zone}` : 'in the target zone']
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.join(' ');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// The spellings git uses for "that branch is not on the remote", each capturing
|
|
202
|
+
// the ref name. The union is not defensive padding — the two bootstrap scripts
|
|
203
|
+
// reach the remote with DIFFERENT git commands, and git words the same condition
|
|
204
|
+
// differently for each:
|
|
205
|
+
// `git fetch <url> <branch>` (clone_editor_repo, scripts/gcp-bootstrap.sh)
|
|
206
|
+
// → fatal: couldn't find remote ref <ref>
|
|
207
|
+
// `git clone --branch <b>` (clone_project, scripts/gcp-project-bootstrap.sh)
|
|
208
|
+
// → fatal: Remote branch <b> not found in upstream origin
|
|
209
|
+
// Keying on only the first — the one the 2026-08-25 transcripts happened to
|
|
210
|
+
// carry — would miss every client-side clone failure. Same union-of-spellings
|
|
211
|
+
// lesson capacity-stockout learned from GCE.
|
|
212
|
+
//
|
|
213
|
+
// Deliberately NOT included: git's `warning: Could not find remote branch …`,
|
|
214
|
+
// which is a submodule-update warning that accompanies a SUCCEEDING command.
|
|
215
|
+
// Matching it here would let an add that failed for an unrelated reason claim
|
|
216
|
+
// this category — and this arm sits above the generic one, so a false positive
|
|
217
|
+
// would suppress the true cause rather than merely add noise.
|
|
218
|
+
const MISSING_REF_PATTERNS = [
|
|
219
|
+
/couldn't find remote ref (\S+)/i,
|
|
220
|
+
/Remote branch (\S+) not found in upstream/i,
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
// Which branch died, and which config field carried it — read from the transcript.
|
|
224
|
+
//
|
|
225
|
+
// The field matters because clientBranch and editorBranch are configured in
|
|
226
|
+
// different places: an operator who corrects only one re-issues a request that
|
|
227
|
+
// fails identically. It is derived from the log prefix of the nearest preceding
|
|
228
|
+
// marker line, because the two bootstrap scripts brand their output distinctly
|
|
229
|
+
// (`[gcp-bootstrap]` builds the editor image from the editor branch;
|
|
230
|
+
// `[project-bootstrap]` clones the client repo at the client branch). When the
|
|
231
|
+
// transcript carries neither marker the field is left null and the copy names
|
|
232
|
+
// both fields rather than guessing — the same degrade-to-true-but-generic
|
|
233
|
+
// discipline stockoutCapacityPhrase uses. Never throws; returns
|
|
234
|
+
// { ref: null, field: null } for input with no missing-ref signature.
|
|
235
|
+
function missingBranchRefFacts(logTail) {
|
|
236
|
+
const lines = String(logTail || '').split('\n');
|
|
237
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
238
|
+
let ref = null;
|
|
239
|
+
for (const re of MISSING_REF_PATTERNS) {
|
|
240
|
+
const m = lines[i].match(re);
|
|
241
|
+
if (m) { ref = m[1]; break; }
|
|
242
|
+
}
|
|
243
|
+
if (!ref) continue;
|
|
244
|
+
let field = null;
|
|
245
|
+
for (let j = i; j >= 0; j -= 1) {
|
|
246
|
+
if (/\[project-bootstrap\]/.test(lines[j])) { field = 'clientBranch'; break; }
|
|
247
|
+
if (/\[gcp-bootstrap\]/.test(lines[j])) { field = 'editorBranch'; break; }
|
|
248
|
+
}
|
|
249
|
+
return { ref, field };
|
|
250
|
+
}
|
|
251
|
+
return { ref: null, field: null };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Operator-facing recovery copy for the missing-branch-ref arm.
|
|
255
|
+
//
|
|
256
|
+
// Extracted from the arm for the same reason stockoutCapacityPhrase was — it is
|
|
257
|
+
// the one arm whose copy VARIES with what the transcript happened to expose, so
|
|
258
|
+
// pulling it out lets the wording be asserted directly instead of only through
|
|
259
|
+
// classifyAddFailure, and keeps the arm itself a two-line decision.
|
|
260
|
+
//
|
|
261
|
+
// Two properties are load-bearing and are what the tests pin:
|
|
262
|
+
// - It states the outcome FIRST and never borrows the generic arm's "a partial
|
|
263
|
+
// instance may exist — reconcile or destroy it" language. At this point git
|
|
264
|
+
// has aborted before any image build or container work, so that advice sends
|
|
265
|
+
// the operator hunting for a box that was never created — the inversion that
|
|
266
|
+
// cost the 2026-08-25 recovery its first hour.
|
|
267
|
+
// - The remedy is a DIFFERENT action, never "retry". Re-issuing the same
|
|
268
|
+
// config against a deleted ref fails identically forever, which is exactly
|
|
269
|
+
// why this category is kept out of ADD_FAILURE_RETRYABLE.
|
|
270
|
+
//
|
|
271
|
+
// `field` is optional: when the transcript carries no bootstrap marker the copy
|
|
272
|
+
// names both fields rather than guessing which one carried the dead ref.
|
|
273
|
+
// Pure; no I/O.
|
|
274
|
+
function missingBranchRefRecovery({ ref, field } = {}) {
|
|
275
|
+
const carried = field
|
|
276
|
+
? `Its ${field} still points at it`
|
|
277
|
+
: 'Its clientBranch and editorBranch both need checking';
|
|
278
|
+
return (
|
|
279
|
+
`Branch '${ref}' is not on origin any more — it was almost certainly pruned when it merged. ` +
|
|
280
|
+
'Nothing was provisioned: git aborted before any image build or container work, so there is no instance, ' +
|
|
281
|
+
`disk, NEG or backend service to clean up, and nothing to destroy. ${carried}, so retrying as-is will fail ` +
|
|
282
|
+
'identically every time. Re-add on a branch that still exists (the current fleet branch is the usual answer).'
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Facts for the wedged-insert arm: a GCE instance insert that was ACCEPTED and
|
|
287
|
+
// then never finished. Returns { wedged, code, instance } — `code` is the GCE
|
|
288
|
+
// support code when the transcript carried one, so the recovery copy can hand the
|
|
289
|
+
// operator the exact string a Support case needs instead of making them dig it out
|
|
290
|
+
// of the transcript by hand.
|
|
291
|
+
//
|
|
292
|
+
// Two spellings reach here and both are proof, so the match is a UNION:
|
|
293
|
+
// A. The raw GCE prose, which is what the 2026-09-04 VM-8 transcript carried:
|
|
294
|
+
// `Internal error. Please try again or contact Google Support. (Code: '…')`
|
|
295
|
+
// PAIRED with evidence this run actually attempted a create. The pairing is
|
|
296
|
+
// what keeps the arm narrow — the same prose surfacing from some other
|
|
297
|
+
// gcloud call is not an insert that wedged, and must not claim to be one.
|
|
298
|
+
// B. ensureInstance's own fast-fail line (npm/cloud.js), which fires when a
|
|
299
|
+
// re-add finds the slot still parked in PROVISIONING behind an operation
|
|
300
|
+
// that never reached DONE. That path exists precisely so the transcript
|
|
301
|
+
// carries a signature instead of an uninformative 2-minute timeout, so this
|
|
302
|
+
// arm keys on it directly.
|
|
303
|
+
//
|
|
304
|
+
// Deliberately NOT keyed on elapsed time. A duration heuristic ("PROVISIONING for
|
|
305
|
+
// too long") would misfire on a legitimately slow create; an INTERNAL_ERROR on an
|
|
306
|
+
// accepted insert is provable evidence, which is the same signature-over-duration
|
|
307
|
+
// discipline the capacity-stockout arm uses.
|
|
308
|
+
const WEDGED_INSERT_PROSE = /Internal error\. Please try again or contact Google Support/i;
|
|
309
|
+
const WEDGED_INSERT_FASTFAIL = /stuck in PROVISIONING behind a wedged create operation/i;
|
|
310
|
+
const WEDGED_INSERT_CREATE_ATTEMPTED = /not found\s*—\s*creating|Failed to create instance|did not reach RUNNING/i;
|
|
311
|
+
|
|
312
|
+
function wedgedInsertFacts(logTail) {
|
|
313
|
+
const log = String(logTail || '');
|
|
314
|
+
const wedged = WEDGED_INSERT_FASTFAIL.test(log)
|
|
315
|
+
|| (WEDGED_INSERT_PROSE.test(log) && WEDGED_INSERT_CREATE_ATTEMPTED.test(log));
|
|
316
|
+
if (!wedged) return { wedged: false, code: null, instance: null };
|
|
317
|
+
return {
|
|
318
|
+
wedged: true,
|
|
319
|
+
code: (log.match(/\(Code:\s*'(-?\d+)'\)/) || [])[1] || null,
|
|
320
|
+
instance: (log.match(/Checking instance\s+(\S+?)\.\.\./) || [])[1] || null,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Operator-facing recovery copy for the wedged-insert arm, extracted for the same
|
|
325
|
+
// reason stockoutCapacityPhrase and missingBranchRefRecovery were: its wording
|
|
326
|
+
// varies with what the transcript exposed, so pulling it out lets the copy be
|
|
327
|
+
// asserted directly rather than only through classifyAddFailure.
|
|
328
|
+
//
|
|
329
|
+
// Three properties are load-bearing and are what the tests pin:
|
|
330
|
+
// - It never borrows the generic arm's "reconcile or destroy it" line. A delete
|
|
331
|
+
// against a wedged slot is ACCEPTED and then queues behind the wedged insert
|
|
332
|
+
// rather than clearing it, so that advice sends the operator to an action that
|
|
333
|
+
// provably does not work — the same inversion that cost the 2026-08-25
|
|
334
|
+
// missing-branch-ref recovery its first hour.
|
|
335
|
+
// - It says plainly that this is not the operator's fault and not their fix.
|
|
336
|
+
// The only thing that resolves it is GCE-side.
|
|
337
|
+
// - It names the Support code when one was captured, because that string is
|
|
338
|
+
// what a Google Cloud Support case is keyed on.
|
|
339
|
+
function wedgedInsertRecovery({ code, instance } = {}) {
|
|
340
|
+
const named = instance || 'the instance';
|
|
341
|
+
const cite = code ? ` citing operation error code ${code}` : '';
|
|
342
|
+
return (
|
|
343
|
+
`GCE accepted the create for ${named} and then never finished it: the insert stayed at status RUNNING, progress 0 ` +
|
|
344
|
+
'and eventually attached an INTERNAL_ERROR without ever reaching DONE, leaving the instance in PROVISIONING and its ' +
|
|
345
|
+
'boot disk in CREATING. Nothing on our side failed and there is nothing here to reconcile. Do NOT retry — the ' +
|
|
346
|
+
'instance name is held by the wedged operation, so a re-add cannot succeed while it is outstanding. Do NOT try to ' +
|
|
347
|
+
'destroy it either — a delete is accepted but queues behind the wedged insert instead of clearing it. This needs ' +
|
|
348
|
+
`GCE-side resolution: open a Google Cloud Support case${cite}. If you need the slot before Google releases it, add ` +
|
|
349
|
+
'on a different VM number.'
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
174
353
|
// Build a verdict, stamping `retryable` from the single-source-of-truth Set so it
|
|
175
354
|
// can never drift from the category.
|
|
176
355
|
function buildFailureVerdict(category, headline, recovery, extra) {
|
|
@@ -200,11 +379,20 @@ function buildFailureVerdict(category, headline, recovery, extra) {
|
|
|
200
379
|
// Returns { category, headline, recovery, retryable, files? }.
|
|
201
380
|
//
|
|
202
381
|
// Detection is ORDER-SENSITIVE. The provision shell echoes cloud:up's tail, so a
|
|
203
|
-
// drift / instance-vanished / container-stuck
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// operator would lose the
|
|
382
|
+
// drift / instance-vanished / container-stuck / capacity-stockout /
|
|
383
|
+
// missing-branch-ref / wedged-insert transcript ALSO contains the generic `cloud:up FAILED
|
|
384
|
+
// (exit N)` line. Each specific (and every retryable) signature is therefore
|
|
385
|
+
// tested BEFORE the generic cloud-up-failed signature, or a retryable drift would
|
|
386
|
+
// misclassify as a non-retryable cloud:up failure and the operator would lose the
|
|
387
|
+
// safe one-click recovery. That is not a hypothetical: it is exactly the bug
|
|
388
|
+
// `capacity-stockout` (arm 6) was added to fix, and its placement immediately
|
|
389
|
+
// above cloud-up-failed is load-bearing. `missing-branch-ref` (arm 7) has the
|
|
390
|
+
// same property for the same reason — it also sits immediately above the generic
|
|
391
|
+
// arm, and its ordering is what stops a dead ref reporting as a half-provisioned
|
|
392
|
+
// instance. `wedged-insert` (arm 8) is the third: it sits directly above the
|
|
393
|
+
// generic arm so a create GCE accepted-and-abandoned cannot report as an ordinary
|
|
394
|
+
// provisioning failure whose advice is to destroy the box — a destroy that, on a
|
|
395
|
+
// wedged slot, queues behind the wedge instead of clearing it.
|
|
208
396
|
function classifyAddFailure({ logTail, exitCode, instanceAlive, containerHealthy, wiringDeadlineBlown, wiringInFlight } = {}) {
|
|
209
397
|
const log = String(logTail || '');
|
|
210
398
|
|
|
@@ -272,33 +460,127 @@ function classifyAddFailure({ logTail, exitCode, instanceAlive, containerHealthy
|
|
|
272
460
|
'The container came up but the project was never scaffolded (no .codeyam/editor.json on the VM), so provisioning did not finish onboarding. Reconcile this VM to complete the scaffold, then re-add.');
|
|
273
461
|
}
|
|
274
462
|
|
|
275
|
-
// 6.
|
|
276
|
-
//
|
|
277
|
-
//
|
|
463
|
+
// 6. capacity-stockout — GCE had no capacity for the machine type in the target
|
|
464
|
+
// zone, so `ensureInstance` (npm/cloud.js) threw from its create branch. This
|
|
465
|
+
// fired twice in three days — VM-13 / payper on 2026-08-18 and VM-11 /
|
|
466
|
+
// codeyam-cms on 2026-08-20 — and both times classified as the generic
|
|
467
|
+
// cloud-up-failed below, whose copy wrongly warned that a partial instance may
|
|
468
|
+
// exist and whose non-retryable verdict refused the one failure that needs
|
|
469
|
+
// nothing but a retry. Each incident cost manual state surgery on the operator
|
|
470
|
+
// host.
|
|
471
|
+
//
|
|
472
|
+
// Retryable on ground 1 (NOTHING WAS PROVISIONED), proven STRUCTURALLY rather
|
|
473
|
+
// than by trusting the error prose: the arm fires only on a stockout signature
|
|
474
|
+
// AND the `Failed to create instance` line, which ensureInstance throws from
|
|
475
|
+
// exactly one place — the branch where the instance was not found and the
|
|
476
|
+
// create call then failed. That conjunction is positive proof no cloud
|
|
477
|
+
// resource exists, the same standing preflight-drift and instance-vanished
|
|
478
|
+
// already have; both incidents were verified by hand to have left no instance,
|
|
479
|
+
// disk, NEG or backend service behind. The not-rostered guard is belt-and-
|
|
480
|
+
// braces: if a future transcript stockouts on one attempt and then creates a
|
|
481
|
+
// box, a roster marker means a real VM exists and this arm must not fire.
|
|
482
|
+
//
|
|
483
|
+
// The signature is a UNION because GCE spells this more than one way: the
|
|
484
|
+
// 2026-08-18 transcript carried `ZONE_RESOURCE_POOL_EXHAUSTED_WITH_DETAILS`
|
|
485
|
+
// with `reason: stockout`, while 2026-08-20 carried `reason:
|
|
486
|
+
// resource_availability` with `state:STOCKOUT, sub-state:STOCKOUT` and the
|
|
487
|
+
// prose below. Keying on only the first would have missed the second. The
|
|
488
|
+
// prose alternative uses `\s+` because the message wraps mid-sentence.
|
|
489
|
+
//
|
|
490
|
+
// Tested BEFORE cloud-up-failed because the provision shell echoes cloud:up's
|
|
491
|
+
// tail, so a stockout transcript ALSO carries `cloud:up FAILED (exit N)` —
|
|
492
|
+
// matching that first is precisely the bug being fixed here.
|
|
493
|
+
if (/ZONE_RESOURCE_POOL_EXHAUSTED|\bSTOCKOUT\b|reason:\s*resource_availability|does not have\s+enough resources available/i.test(log)
|
|
494
|
+
&& /Failed to create instance/i.test(log)
|
|
495
|
+
&& !transcriptRostered(log)) {
|
|
496
|
+
return buildFailureVerdict('capacity-stockout', 'No capacity in the zone',
|
|
497
|
+
`GCE had no ${stockoutCapacityPhrase(log)} at that moment, so the create call failed before anything existed — there is no instance, disk, NEG or backend service to clean up, and nothing to undo. Capacity is transient but its return is not scheduled: wait a few minutes, then Retry add. Do NOT destroy anything.`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 7. missing-branch-ref — the requested branch no longer exists on origin, so
|
|
501
|
+
// git aborted the clone/fetch. Observed live on 2026-08-25: VMs 2, 4, 5 and
|
|
502
|
+
// 7 each died on `fatal: couldn't find remote ref editor-improvements-75`,
|
|
503
|
+
// a branch pruned from origin earlier that day when it merged. This repo
|
|
504
|
+
// prunes on merge, so ANY stored feature-branch name expires silently —
|
|
505
|
+
// this is a recurring condition, not a one-off.
|
|
506
|
+
//
|
|
507
|
+
// NOT retryable, and deliberately NOT a member of ADD_FAILURE_RETRYABLE
|
|
508
|
+
// even though it satisfies ground 1 (nothing was provisioned). Membership
|
|
509
|
+
// means "re-issuing the SAME request can succeed"; re-issuing an add
|
|
510
|
+
// against a deleted ref fails identically forever. Admitting it would
|
|
511
|
+
// automate the guaranteed-fail loop the operator was stuck in by hand. The
|
|
512
|
+
// honest remedy is a DIFFERENT request — re-add on a live branch — which is
|
|
513
|
+
// what the recovery copy names.
|
|
514
|
+
//
|
|
515
|
+
// Tested BEFORE cloud-up-failed for the reason arm 6 documents: the
|
|
516
|
+
// provision shell echoes cloud:up's tail, so this transcript ALSO carries
|
|
517
|
+
// the generic `cloud:up FAILED (exit N)` line. Matching that first is the
|
|
518
|
+
// bug being fixed — it is what made the real incident report the OPPOSITE
|
|
519
|
+
// of the truth, warning that "a partial instance may exist — reconcile or
|
|
520
|
+
// destroy it" when nothing had been provisioned at all.
|
|
521
|
+
const missingRef = missingBranchRefFacts(log);
|
|
522
|
+
if (missingRef.ref) {
|
|
523
|
+
return buildFailureVerdict('missing-branch-ref', 'Branch no longer exists',
|
|
524
|
+
missingBranchRefRecovery(missingRef));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 8. wedged-insert — GCE accepted the instance insert and then never finished
|
|
528
|
+
// it. Observed live on 2026-09-04 (VM-8): the insert was submitted at
|
|
529
|
+
// 14:29:50Z, sat at `status: RUNNING, progress: 0` for hours, and eventually
|
|
530
|
+
// attached `INTERNAL_ERROR` WITHOUT ever transitioning to DONE. The instance
|
|
531
|
+
// stayed in PROVISIONING and its boot disk in CREATING indefinitely, and a
|
|
532
|
+
// subsequent `instances delete` was accepted but queued at PENDING behind the
|
|
533
|
+
// wedged insert — so the slot could be neither created nor destroyed.
|
|
534
|
+
//
|
|
535
|
+
// It needs its own category because every neighbour misdescribes it: it is
|
|
536
|
+
// NOT capacity-stockout (GCE accepted the request rather than rejecting it),
|
|
537
|
+
// NOT cloud-up-failed (nothing in the provisioning script failed), and
|
|
538
|
+
// emphatically not the `verifying` fallback it actually landed on — whose
|
|
539
|
+
// copy told the operator the add "may still be converging" and to wait, about
|
|
540
|
+
// a VM that never would.
|
|
541
|
+
//
|
|
542
|
+
// NOT retryable, and deliberately not a member of ADD_FAILURE_RETRYABLE: a
|
|
543
|
+
// retry cannot succeed while the instance name is held by a wedged operation,
|
|
544
|
+
// so admitting it would automate a guaranteed-fail loop — the same reasoning
|
|
545
|
+
// missing-branch-ref documents. Not in CALM_ADD_CATEGORIES either: it is
|
|
546
|
+
// terminal, not calm, and there is nothing left to converge.
|
|
547
|
+
//
|
|
548
|
+
// Tested BEFORE cloud-up-failed for the reason arm 6 documents: the provision
|
|
549
|
+
// shell echoes cloud:up's tail into every transcript, so matching the generic
|
|
550
|
+
// line first is precisely the bug being fixed.
|
|
551
|
+
const wedgedInsert = wedgedInsertFacts(log);
|
|
552
|
+
if (wedgedInsert.wedged) {
|
|
553
|
+
return buildFailureVerdict('wedged-insert', 'Create wedged in Google Cloud',
|
|
554
|
+
wedgedInsertRecovery(wedgedInsert));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// 9. cloud-up-failed — a generic cloud:up failure, reached ONLY when the drift /
|
|
558
|
+
// vanished / stuck / stockout / missing-ref / wedged-insert signatures above
|
|
559
|
+
// did not match. A partial instance may exist, so not retryable.
|
|
278
560
|
if (/cloud:up FAILED \(exit/i.test(log)) {
|
|
279
561
|
return buildFailureVerdict('cloud-up-failed', 'Provisioning failed',
|
|
280
562
|
'cloud:up failed during provisioning, so a partial instance may exist. Reconcile or destroy this VM before re-adding.');
|
|
281
563
|
}
|
|
282
564
|
|
|
283
|
-
//
|
|
565
|
+
// 10. interrupted — a signal kill (OOM 137, SIGTERM 143, …) or an explicit
|
|
284
566
|
// Killed / out-of-memory marker. The real VM state is unknown. Not retryable.
|
|
285
567
|
if (fc.isSignalKillExit(Number(exitCode)) || /\bKilled\b/.test(log) || /Out of memory/i.test(log)) {
|
|
286
568
|
return buildFailureVerdict('interrupted', 'Provisioning interrupted',
|
|
287
569
|
'The provision was interrupted (killed or out of memory), so the VM state is unknown. Reconcile or destroy this VM before re-adding.');
|
|
288
570
|
}
|
|
289
571
|
|
|
290
|
-
//
|
|
572
|
+
// 11. The post-add IAP read-back verdicts. Unlike every signature above, this one
|
|
291
573
|
// is not produced by the provision transcript at all — it is the BLOCKED
|
|
292
574
|
// reason iapReadbackOutcome hands to annotateAddFailure, which is what
|
|
293
575
|
// stops a read-back failure from being the one add failure that reaches the
|
|
294
576
|
// card with no category at all (and so no differentiated headline, no
|
|
295
577
|
// recovery, no guarded Retry, and no chance for holdConvergingBadge to hold it
|
|
296
|
-
// calm). Tested AFTER all
|
|
578
|
+
// calm). Tested AFTER all eight hard-failure signatures so a proven failure
|
|
297
579
|
// always wins if both texts are ever present, and BEFORE converging/verifying
|
|
298
580
|
// because it is strictly more specific than either.
|
|
299
581
|
if (/IAP policy authorizes no human/i.test(log)) {
|
|
300
582
|
const remedy = nextValidActionRemedy(log);
|
|
301
|
-
//
|
|
583
|
+
// 9a. access-pending — no accessors YET. The grant is the LAST step of the
|
|
302
584
|
// detached wire-add-tail, so while that tail is alive an empty policy is
|
|
303
585
|
// the expected mid-flight reading, not a verdict. Calm: this is the state
|
|
304
586
|
// that used to render a red "Add failed" on healthy VM-4/VM-7 minutes
|
|
@@ -307,7 +589,7 @@ function classifyAddFailure({ logTail, exitCode, instanceAlive, containerHealthy
|
|
|
307
589
|
return buildFailureVerdict('access-pending', 'Access not granted yet',
|
|
308
590
|
'The VM is up and rostered, but its IAP policy has no human accessors yet — the add\'s background wiring applies the grant as its last step and is still running. This resolves on its own once that tail finishes; no action is needed unless it persists past the wiring deadline.');
|
|
309
591
|
}
|
|
310
|
-
//
|
|
592
|
+
// 9b. iap-unauthorized — the wiring is done and the policy STILL authorizes
|
|
311
593
|
// nobody, so the public URL returns "Error code 51" for every human. This
|
|
312
594
|
// is the 2026-08-09 case the read-back exists to catch (one step granted
|
|
313
595
|
// the operator, the next revoked him) and it is a genuine hard failure.
|
|
@@ -317,12 +599,12 @@ function classifyAddFailure({ logTail, exitCode, instanceAlive, containerHealthy
|
|
|
317
599
|
`The VM is up but its IAP policy authorizes no human, so the public URL returns "Error code 51" for everyone. Its background wiring is no longer running, so this will not resolve on its own — grant access to fix it. Do NOT destroy or re-add this VM.${remedy ? `\n\n${remedy}` : ''}`);
|
|
318
600
|
}
|
|
319
601
|
|
|
320
|
-
//
|
|
602
|
+
// 12. converging — the add reached the early-roster marker (`CY_ROSTER`): the
|
|
321
603
|
// container gate passed, the instance is real, and the VM is rostered, yet it
|
|
322
604
|
// exited non-zero WITHOUT matching any specific hard-failure signature above.
|
|
323
605
|
// By construction of the provision script, every genuine hard failure (drift,
|
|
324
|
-
// instance-vanished, container-stuck, incomplete-workspace,
|
|
325
|
-
// interrupted) is printed BEFORE `CY_ROSTER`, so a rostered transcript that
|
|
606
|
+
// instance-vanished, container-stuck, incomplete-workspace, capacity-stockout,
|
|
607
|
+
// cloud-up-failed, interrupted) is printed BEFORE `CY_ROSTER`, so a rostered transcript that
|
|
326
608
|
// fell through to here is a still-wiring box owned by the detached
|
|
327
609
|
// wire-add-tail / reconcile-ingress — NOT an add failure. Surface it as an
|
|
328
610
|
// honest in-flight state instead of the scary red `unknown`. Not retryable:
|
|
@@ -335,7 +617,7 @@ function classifyAddFailure({ logTail, exitCode, instanceAlive, containerHealthy
|
|
|
335
617
|
'The container is up and the VM is rostered; the LB ingress is still wiring in the background. This resolves on its own once the editor is reachable over the LB — no action needed unless it persists past the wiring deadline.');
|
|
336
618
|
}
|
|
337
619
|
|
|
338
|
-
//
|
|
620
|
+
// 13. The residue: no failure signature above matched. The polarity here is
|
|
339
621
|
// INVERTED from the original design. It used to read "no recognized success
|
|
340
622
|
// signature ⇒ failed", which made red the DEFAULT for "we don't know" — so a
|
|
341
623
|
// VM whose container was Up and whose editor answered /api/health 200 got a
|
|
@@ -345,7 +627,7 @@ function classifyAddFailure({ logTail, exitCode, instanceAlive, containerHealthy
|
|
|
345
627
|
//
|
|
346
628
|
// Red now requires positive proof of death, and everything else is in-flight:
|
|
347
629
|
//
|
|
348
|
-
//
|
|
630
|
+
// 11a. unknown — a non-zero exit CORROBORATED by a durable fact (see
|
|
349
631
|
// addFailureCorroborated). The box is provably dead or provably never
|
|
350
632
|
// came up, so the loud, terminal escalation is warranted and unchanged.
|
|
351
633
|
// Still never invites an unproven retry.
|
|
@@ -354,7 +636,7 @@ function classifyAddFailure({ logTail, exitCode, instanceAlive, containerHealthy
|
|
|
354
636
|
'The add failed for an unclassified reason and the VM is confirmed dead or never came up — inspect the transcript below, then reconcile or destroy this VM before re-adding.');
|
|
355
637
|
}
|
|
356
638
|
|
|
357
|
-
//
|
|
639
|
+
// 11b. verifying — the honest default. No failure signature, and nothing
|
|
358
640
|
// durable proves this VM is dead: it may still be provisioning or
|
|
359
641
|
// wiring, and the ingress can keep propagating for minutes after the
|
|
360
642
|
// wrapper is gone. Calm copy, transcript still one click away. NOT
|
|
@@ -405,4 +687,56 @@ function holdConvergingBadge({ category, wiringDeadlineBlown } = {}) {
|
|
|
405
687
|
return CALM_ADD_CATEGORIES.has(category) && !wiringDeadlineBlown;
|
|
406
688
|
}
|
|
407
689
|
|
|
408
|
-
|
|
690
|
+
// Refusal copy for a Retry on a card whose branch was deleted from origin.
|
|
691
|
+
//
|
|
692
|
+
// The generic non-retryable refusal says the add "may have left a
|
|
693
|
+
// half-provisioned instance — reconcile or destroy it". For this category that
|
|
694
|
+
// is the OPPOSITE of the truth: git aborts before any image build or container
|
|
695
|
+
// work, so nothing exists. Sending an operator to hunt for a box that was never
|
|
696
|
+
// created is what cost the 2026-08-25 recovery its first hour, so this copy
|
|
697
|
+
// leads with the outcome (nothing to clean up) before naming the mechanism.
|
|
698
|
+
//
|
|
699
|
+
// The remedy is deliberately not "retry". Re-issuing the same config against a
|
|
700
|
+
// deleted ref fails identically forever — which is exactly why the category is
|
|
701
|
+
// kept OUT of ADD_FAILURE_RETRYABLE — so the honest advice is a different
|
|
702
|
+
// action. For a self-host VM the fleet pin names the live branch and a re-add
|
|
703
|
+
// now picks it up, so the copy can be specific; for a client VM the branch is a
|
|
704
|
+
// per-project choice only the operator can make, so it asks instead of guessing.
|
|
705
|
+
//
|
|
706
|
+
// `ref` / `field` come from missingBranchRefFacts and are both optional: the copy
|
|
707
|
+
// degrades to true-but-generic rather than interpolating `undefined`, the same
|
|
708
|
+
// discipline stockoutCapacityPhrase uses. Pure; no I/O.
|
|
709
|
+
function deadRefRetryMessage({ n, ref, field, selfHost = false, pin } = {}) {
|
|
710
|
+
const which = ref ? `'${ref}'` : 'the requested branch';
|
|
711
|
+
const carried = field ? ` (${field})` : '';
|
|
712
|
+
const remedy = selfHost && pin
|
|
713
|
+
? `Add VM again — it now picks up the current fleet branch (${pin}) instead of the stored one.`
|
|
714
|
+
: 'Add VM again with a branch that still exists on origin.';
|
|
715
|
+
return (
|
|
716
|
+
`VM-${n}: branch ${which}${carried} no longer exists on origin, so nothing was provisioned — ` +
|
|
717
|
+
'there is no instance, disk or backend service to clean up. Retrying as-is re-targets ' +
|
|
718
|
+
`the same dead branch and fails identically every time. ${remedy}`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Refusal copy for a one-click "Retry add" against a slot held by a wedged
|
|
723
|
+
// insert, extracted for the same reason deadRefRetryMessage was: the refusal
|
|
724
|
+
// decision belongs to canRetryAdd, the wording belongs here, and only the wording
|
|
725
|
+
// needs asserting.
|
|
726
|
+
//
|
|
727
|
+
// It exists because the generic refusal line is not merely unhelpful here, it is
|
|
728
|
+
// WRONG in a way the operator cannot detect. "may have left a half-provisioned
|
|
729
|
+
// instance — reconcile or destroy it" describes a cleanup that is accepted by GCE
|
|
730
|
+
// and then silently queues behind the wedged insert instead of clearing it, so
|
|
731
|
+
// following it looks like progress and achieves nothing. Say what is true: this is
|
|
732
|
+
// GCE-side, neither retry nor destroy resolves it, and the unblocking move is a
|
|
733
|
+
// Support case (or a different VM number if a box is needed now). Pure; no I/O.
|
|
734
|
+
function wedgedRetryMessage(n) {
|
|
735
|
+
return (
|
|
736
|
+
`VM-${n} is stuck inside Google Cloud: GCE accepted the create and never finished it, so the instance name is ` +
|
|
737
|
+
'held by a wedged operation. A retry cannot succeed while it is outstanding, and a destroy will queue behind it ' +
|
|
738
|
+
'rather than clear it — this needs a Google Cloud Support case. Add on a different VM number if you need a box now.'
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
module.exports = { classifyAddFailure, extractDriftFiles, lastNonEmptyLines, canRetryAdd, transcriptRostered, holdConvergingBadge, addFailureCorroborated, containerHealthWitness, stockoutCapacityPhrase, missingBranchRefFacts, missingBranchRefRecovery, deadRefRetryMessage, wedgedInsertFacts, wedgedInsertRecovery, wedgedRetryMessage, CALM_ADD_CATEGORIES, ADD_FAILURE_RETRYABLE };
|
package/npm/fleet-branch.js
CHANGED
|
@@ -46,4 +46,127 @@ function resolveFleetBranch({ envBranch, gitBranch, fallback } = {}) {
|
|
|
46
46
|
return cleanBranch(fallback) || UNKNOWN_BRANCH;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// ---------- Dead-pin detection ----------
|
|
50
|
+
// `fleet-advance-to.sh` refuses to WRITE a pin for a branch absent from origin,
|
|
51
|
+
// but that guard runs once and nothing re-checks the pin already in place —
|
|
52
|
+
// while the branch it names can be deleted by anyone merging a PR with the
|
|
53
|
+
// delete-branch box ticked. Observed 2026-08-15: PR #104 merged
|
|
54
|
+
// `editor-improvements-72` and deleted it; hours later the operator host still
|
|
55
|
+
// read `FLEET_BRANCH=editor-improvements-72` and nothing said so.
|
|
56
|
+
//
|
|
57
|
+
// The probe itself (`git ls-remote`) is I/O and belongs to server.js, on the
|
|
58
|
+
// SLOW memo cadence rather than the ~3s /data poll. What lives here is the pure
|
|
59
|
+
// classification of its outcome — which is the part a test can actually reach,
|
|
60
|
+
// since a function defined in server.js can never be executed by a test (that
|
|
61
|
+
// file has top-level side effects on require).
|
|
62
|
+
//
|
|
63
|
+
// The tri-state mirrors scripts/operator/lib/branch-on-origin.sh exactly, and
|
|
64
|
+
// the mirroring is the point: `gone` is a DEFINITIVE absence, `indeterminate`
|
|
65
|
+
// means absence was NOT proven (no network, no remote, auth trouble). Widening
|
|
66
|
+
// `indeterminate` into `gone` would turn every offline operator host into a
|
|
67
|
+
// fleet-wide "your pin is dead" alarm.
|
|
68
|
+
|
|
69
|
+
/** Map a `git ls-remote --exit-code --heads` exit code to the shared tri-state.
|
|
70
|
+
* 0 -> 'live', 2 -> 'gone', anything else (including a non-numeric or absent
|
|
71
|
+
* code) -> 'indeterminate'. Pure. */
|
|
72
|
+
function lsRemoteStatus(exitCode) {
|
|
73
|
+
if (exitCode === 0) return 'live';
|
|
74
|
+
if (exitCode === 2) return 'gone';
|
|
75
|
+
return 'indeterminate';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Classify the health of the fleet's branch pin.
|
|
79
|
+
*
|
|
80
|
+
* Returns `{ state, branch, pinned, dead, blocks, recovery }` where `state` is
|
|
81
|
+
* 'live' | 'gone' | 'indeterminate' | 'unknown'.
|
|
82
|
+
*
|
|
83
|
+
* 'unknown' is the un-probeable case — no branch, or the UNKNOWN_BRANCH
|
|
84
|
+
* sentinel that `resolveFleetBranch` yields when there is neither a pin nor a
|
|
85
|
+
* readable git HEAD. There is no ref to ask origin about, so reporting it as
|
|
86
|
+
* `gone` would invent a deletion that never happened.
|
|
87
|
+
*
|
|
88
|
+
* `dead` is true ONLY for 'gone'. It is the single field a caller should gate a
|
|
89
|
+
* degradation banner on, so no consumer has to re-derive which states are
|
|
90
|
+
* actionable.
|
|
91
|
+
*
|
|
92
|
+
* `blocks` names what a dead pin is actively breaking — both consequences,
|
|
93
|
+
* because neither announces itself: the operator-repo refresh cannot fetch a
|
|
94
|
+
* deleted ref, and the pin also drives `cloud.json editorBranch`, so newly
|
|
95
|
+
* provisioned VMs check out a ref that is not there. Pure; no I/O. */
|
|
96
|
+
function classifyBranchPin({ branch, lsRemoteExitCode, pinned = false } = {}) {
|
|
97
|
+
const name = cleanBranch(branch);
|
|
98
|
+
const probeable = name !== null && name !== UNKNOWN_BRANCH;
|
|
99
|
+
const state = probeable ? lsRemoteStatus(lsRemoteExitCode) : 'unknown';
|
|
100
|
+
const dead = state === 'gone';
|
|
101
|
+
return {
|
|
102
|
+
state,
|
|
103
|
+
branch: name,
|
|
104
|
+
pinned: Boolean(pinned),
|
|
105
|
+
dead,
|
|
106
|
+
blocks: dead
|
|
107
|
+
? [
|
|
108
|
+
'refresh-operator-repo.sh cannot fetch the pinned branch, so this host keeps serving whatever its clone already had',
|
|
109
|
+
'cloud.json editorBranch reads the same pin, so newly provisioned VMs check out a ref that does not exist',
|
|
110
|
+
]
|
|
111
|
+
: [],
|
|
112
|
+
recovery: dead
|
|
113
|
+
? `scripts/operator/fleet-advance-to.sh <BRANCH> — move the fleet to a live branch (it refuses a branch that is itself absent from origin)`
|
|
114
|
+
: null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Decide whether the fleet pin should override a stored per-VM branch when an
|
|
119
|
+
* add is RE-ISSUED, and report every override so the caller can log it.
|
|
120
|
+
*
|
|
121
|
+
* Returns `{ config, overrides }` — a NEW config (the input is never mutated)
|
|
122
|
+
* and `[{ field, from, to }]`, empty when nothing changed.
|
|
123
|
+
*
|
|
124
|
+
* The problem this solves: a per-VM config that recorded `editor-improvements-75`
|
|
125
|
+
* was not wrong when it was written — it EXPIRED, silently, the moment that
|
|
126
|
+
* branch merged and origin pruned it. This repo prunes on merge, so every stored
|
|
127
|
+
* feature-branch name has that property. The pin does not expire: it is the
|
|
128
|
+
* durable statement of fleet-wide policy, and on 2026-08-25 it was correct
|
|
129
|
+
* throughout while four retried cards each failed on their stale snapshot (a
|
|
130
|
+
* fresh pool add read the pin and succeeded at the same moment).
|
|
131
|
+
*
|
|
132
|
+
* Three guards, each declining to act rather than acting wrongly:
|
|
133
|
+
* - `selfHost` false ⇒ untouched. A client VM's branch is a deliberate
|
|
134
|
+
* per-project choice; overwriting it with a fleet pin would silently
|
|
135
|
+
* retarget someone else's project. Only the self-host path, where the branch
|
|
136
|
+
* IS fleet policy, falls back to the pin.
|
|
137
|
+
* - no pin, or the UNKNOWN_BRANCH sentinel ⇒ untouched. There is no branch to
|
|
138
|
+
* prefer, and stamping the sentinel would guarantee the failure this exists
|
|
139
|
+
* to prevent.
|
|
140
|
+
* - `pinState === 'gone'` ⇒ untouched. A pin PROVEN absent from origin is not
|
|
141
|
+
* an improvement on a stale-but-possibly-live stored branch; swapping one
|
|
142
|
+
* dead ref for another just moves the failure. Note the deliberate
|
|
143
|
+
* asymmetry with 'indeterminate', which does NOT decline: absence was not
|
|
144
|
+
* proven there, and refusing to converge every time the network blips would
|
|
145
|
+
* strand the fleet on stale branches for the sake of a maybe.
|
|
146
|
+
*
|
|
147
|
+
* Pure; no I/O. */
|
|
148
|
+
function pinCorrectedBranches({ cfg, pin, selfHost = false, pinState } = {}) {
|
|
149
|
+
const config = { ...(cfg || {}) };
|
|
150
|
+
const overrides = [];
|
|
151
|
+
const name = cleanBranch(pin);
|
|
152
|
+
if (!selfHost || !name || name === UNKNOWN_BRANCH || pinState === 'gone') {
|
|
153
|
+
return { config, overrides };
|
|
154
|
+
}
|
|
155
|
+
for (const field of ['clientBranch', 'editorBranch']) {
|
|
156
|
+
const from = cleanBranch(config[field]);
|
|
157
|
+
if (from && from !== name) {
|
|
158
|
+
config[field] = name;
|
|
159
|
+
overrides.push({ field, from, to: name });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { config, overrides };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
UNKNOWN_BRANCH,
|
|
167
|
+
cleanBranch,
|
|
168
|
+
resolveFleetBranch,
|
|
169
|
+
lsRemoteStatus,
|
|
170
|
+
classifyBranchPin,
|
|
171
|
+
pinCorrectedBranches,
|
|
172
|
+
};
|
package/npm/fleet-build-data.js
CHANGED
|
@@ -120,6 +120,17 @@ function buildVmEnvelope(v, ctx) {
|
|
|
120
120
|
// unreachable VM's last-known config is stale, and an unreachable card has
|
|
121
121
|
// louder problems to report than its preview origin.
|
|
122
122
|
previewMisconfigured: (v.reachable && v.previewMisconfigured) || null,
|
|
123
|
+
// The project's DECLARED datastore and whether it is answering:
|
|
124
|
+
// { type, target, local, reachable, lastError }, or null when the project
|
|
125
|
+
// declares none (a Rust CLI, a static site) — which is the common case and
|
|
126
|
+
// must render nothing at all. `reachable` is tri-state exactly like
|
|
127
|
+
// previewServing/previewIngress above: true = probe reached it, false =
|
|
128
|
+
// confirmed down, null = unknown (editor just booted, or the probe went
|
|
129
|
+
// stale). Never conflate "down" with "unknown" — only a confirmed false
|
|
130
|
+
// raises the amber reason. Gated on `reachable` like providerAuth: an
|
|
131
|
+
// unreachable VM's last-known datastore verdict is stale, and it has louder
|
|
132
|
+
// problems to report.
|
|
133
|
+
datastore: (v.reachable && v.datastore) || null,
|
|
123
134
|
effects: v.effects || null,
|
|
124
135
|
// The VM's live-configured build agent (lowercase provider), reported by
|
|
125
136
|
// /api/session-info and merged (sticky) onto the poll state. Drives the
|
|
@@ -177,7 +188,7 @@ const REQUIRED_CARD_FIELDS = [
|
|
|
177
188
|
'degradedReasons', 'setupSteps', 'attentionReasons',
|
|
178
189
|
'isHead', 'headHolderUnreachable', 'queuePos', 'queueLen',
|
|
179
190
|
'job', 'config', 'project', 'variant', 'cardIdentity', 'brokerHistory',
|
|
180
|
-
'brokerDeath', 'providerAuth', 'previewMisconfigured', 'effects', 'currentProvider',
|
|
191
|
+
'brokerDeath', 'providerAuth', 'previewMisconfigured', 'datastore', 'effects', 'currentProvider',
|
|
181
192
|
'readiness', 'backendHealth', 'vmAccess',
|
|
182
193
|
'startedAt', 'featureStartedAt', 'attention', 'envOverride',
|
|
183
194
|
];
|