@codeyam-editor/codeyam-editor 0.1.4 → 0.1.6
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/cli.js +5 -1
- package/npm/cloud-branch-switch.js +1 -1
- package/npm/cloud.js +3166 -150
- package/npm/codesign-plan.js +68 -0
- package/npm/container.js +3 -0
- package/npm/disk-pressure.js +78 -0
- package/npm/editor-dev.js +96 -15
- package/npm/editor.js +85 -37
- package/npm/empty-project-dir.js +92 -7
- package/npm/fleet-add-failure.js +252 -0
- package/npm/fleet-asset-version.js +37 -0
- package/npm/fleet-auth-health.js +213 -0
- package/npm/fleet-auth.js +6 -1
- package/npm/fleet-build-data.js +109 -5
- package/npm/fleet-cleanup.js +232 -16
- package/npm/fleet-destroy-safety.js +272 -0
- package/npm/fleet-early-roster.js +87 -0
- package/npm/fleet-effect-poll.js +110 -0
- package/npm/fleet-effect-status.js +33 -32
- package/npm/fleet-env.js +86 -0
- package/npm/fleet-ghcr-secret.js +129 -0
- package/npm/fleet-git-maintenance.js +142 -0
- package/npm/fleet-host-op-limiter.js +68 -0
- package/npm/fleet-host.js +74 -0
- package/npm/fleet-iap-token.js +165 -0
- package/npm/fleet-job-badge.js +1 -1
- package/npm/fleet-launchd.js +171 -0
- package/npm/fleet-live-status.js +155 -0
- package/npm/fleet-memory.js +245 -9
- package/npm/fleet-plans.js +233 -4
- package/npm/fleet-pool.js +75 -16
- package/npm/fleet-prewarm.js +241 -0
- package/npm/fleet-project-drift.js +58 -0
- package/npm/fleet-public-url.js +394 -0
- package/npm/fleet-queue.js +95 -1
- package/npm/fleet-reachability-debounce.js +69 -0
- package/npm/fleet-reachability.js +80 -0
- package/npm/fleet-reconcile.js +43 -1
- package/npm/fleet-reset.js +330 -13
- package/npm/fleet-roster.js +6 -8
- package/npm/fleet-running-plan.js +85 -0
- package/npm/fleet-self-update.js +142 -0
- package/npm/fleet-session-token-cache.js +65 -0
- package/npm/fleet-setup-steps.js +112 -0
- package/npm/fleet-sleep-status.js +33 -0
- package/npm/fleet-status-refs.js +96 -0
- package/npm/fleet-strand.js +181 -0
- package/npm/fleet-stuck-status.js +117 -0
- package/npm/fleet-vm-access.js +404 -0
- package/npm/fleet-vm-probe-target.js +282 -0
- package/npm/fleet-vm-readiness.js +237 -0
- package/npm/fleet-vm-slugs.js +116 -0
- package/npm/google-ingress.js +226 -0
- package/npm/ingress-invoke.js +770 -0
- package/npm/launch-config.js +386 -9
- package/npm/scenario-check.js +880 -37
- package/npm/scenario-handlers.js +154 -30
- package/npm/scenario-interactivity.js +145 -6
- package/npm/scenario-issues.js +13 -1
- package/npm/scenario-metrics.js +116 -1
- package/npm/scenario-mocks.js +144 -21
- package/npm/scenario-playwright.js +649 -38
- package/npm/script-drift.js +152 -0
- package/npm/utils.js +365 -52
- package/npm/vm-urls.js +69 -8
- package/package.json +5 -5
- package/ui/dist/assets/ContainerInspector-DgM0pTAx.js +2 -0
- package/ui/dist/assets/EditorShell-B7Q06uJ_.js +252 -0
- package/ui/dist/assets/EditorShell-DP5UY20G.css +1 -0
- package/ui/dist/assets/{Markdown-BHBqz-Mj.js → Markdown-Chgdf3PW.js} +1 -1
- package/ui/dist/assets/ProjectLauncherScreen-BRrEr8Ng.js +1 -0
- package/ui/dist/assets/ScenarioDataPanel-BeQfa28g.js +9 -0
- package/ui/dist/assets/{ScenarioDataPanel-DYP7pi_n.css → ScenarioDataPanel-C1LukYI6.css} +1 -1
- package/ui/dist/assets/ScenarioDataPanelFullPage-BLqRCxQX.js +1 -0
- package/ui/dist/assets/ScenarioRenderer-CIAxfHPh.js +21 -0
- package/ui/dist/assets/StepsSimulator-CymGgfbu.js +1 -0
- package/ui/dist/assets/index-BygCiJSb.css +1 -0
- package/ui/dist/assets/{index-BuK0lizt.js → index-DCHqRrwO.js} +9 -9
- package/ui/dist/assets/{markdown-v0F0UEt3.js → markdown-C9tbsVHX.js} +1 -1
- package/ui/dist/assets/react-nrLBr15I.js +28 -0
- package/ui/dist/assets/useEvents-S3Qk1Y_d.js +1 -0
- package/ui/dist/assets/useServerIdentity-B7rI0u3O.js +1 -0
- package/ui/dist/codeyam-mark.svg +4 -0
- package/ui/dist/codeyam-wordmark.svg +5 -0
- package/ui/dist/index.html +4 -4
- package/ui/dist/open-source/codeyam-counter.png +0 -0
- package/ui/dist/open-source/el-carot.png +0 -0
- package/ui/dist/open-source/tabcommand.png +0 -0
- package/ui/dist/assets/ContainerInspector-C8EkH4PP.js +0 -2
- package/ui/dist/assets/EditorShell-tMMdarZD.js +0 -187
- package/ui/dist/assets/ScenarioDataPanel-xWYGZOpo.js +0 -5
- package/ui/dist/assets/ScenarioDataPanelFullPage-5G6KuZgw.js +0 -1
- package/ui/dist/assets/ScenarioRenderer-CSdhX1Ak.js +0 -1
- package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +0 -1
- package/ui/dist/assets/index-BGqairFV.css +0 -1
- package/ui/dist/assets/react-CSS0HapR.js +0 -28
- package/ui/dist/assets/useEvents-DggWrtHe.js +0 -1
package/npm/fleet-pool.js
CHANGED
|
@@ -22,8 +22,11 @@
|
|
|
22
22
|
//
|
|
23
23
|
// On-disk shape (tolerant: a missing or malformed file is the empty pool):
|
|
24
24
|
// { "vms": { "<n>": { status, branch, freshAt, claimedAt } } }
|
|
25
|
-
// status: "pool"
|
|
26
|
-
// "
|
|
25
|
+
// status: "pool" idle, claim-ready (authenticated + push-ready)
|
|
26
|
+
// "pending-auth" reachable + LB-wired but awaiting operator auth;
|
|
27
|
+
// counts toward the provisioning target (so the
|
|
28
|
+
// keeper converges) but is NOT claimable
|
|
29
|
+
// "assigned" claimed by an operator, rebranded to `branch`
|
|
27
30
|
// branch: the branch the VM is on (base branch while pooled)
|
|
28
31
|
// freshAt: ISO timestamp of the last successful auth/health refresh —
|
|
29
32
|
// drives evict-before-claim of a member whose token lapsed
|
|
@@ -45,6 +48,20 @@ const POOL_STATE_FILE = process.env.POOL_STATE_FILE
|
|
|
45
48
|
|
|
46
49
|
const POOL = 'pool';
|
|
47
50
|
const ASSIGNED = 'assigned';
|
|
51
|
+
// A VM that passed every readiness item EXCEPT provider auth (reachable,
|
|
52
|
+
// on-branch, broker up, editor+preview 200, git-push-ready). Provisioning has
|
|
53
|
+
// converged — the VM exists and is LB-wired — so it counts toward the refill
|
|
54
|
+
// target and the keeper stops churning; but it is never claimable, so the claim
|
|
55
|
+
// path still hands out only fully-authenticated VMs (the 2026-06-29 guarantee).
|
|
56
|
+
const PENDING_AUTH = 'pending-auth';
|
|
57
|
+
|
|
58
|
+
// Canonical status coercion: an unrecognized status is not a pool member.
|
|
59
|
+
function coerceStatus(status) {
|
|
60
|
+
if (status === ASSIGNED) return ASSIGNED;
|
|
61
|
+
if (status === PENDING_AUTH) return PENDING_AUTH;
|
|
62
|
+
if (status === POOL) return POOL;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
48
65
|
|
|
49
66
|
// ── Pure state helpers ──────────────────────────────────────────────────────
|
|
50
67
|
|
|
@@ -61,7 +78,7 @@ function normalizeState(raw) {
|
|
|
61
78
|
const n = Number(key);
|
|
62
79
|
if (!Number.isInteger(n) || n <= 0) continue;
|
|
63
80
|
const e = vms[key] || {};
|
|
64
|
-
const status = e.status
|
|
81
|
+
const status = coerceStatus(e.status);
|
|
65
82
|
if (!status) continue;
|
|
66
83
|
out.vms[n] = {
|
|
67
84
|
status,
|
|
@@ -91,6 +108,13 @@ function assignedMembers(state) {
|
|
|
91
108
|
return membersByStatus(state, ASSIGNED);
|
|
92
109
|
}
|
|
93
110
|
|
|
111
|
+
// VMs provisioned + LB-wired but awaiting operator auth. They count toward the
|
|
112
|
+
// refill target (provisioning has converged) yet are excluded from poolMembers,
|
|
113
|
+
// so selectForClaim never hands one out.
|
|
114
|
+
function pendingAuthMembers(state) {
|
|
115
|
+
return membersByStatus(state, PENDING_AUTH);
|
|
116
|
+
}
|
|
117
|
+
|
|
94
118
|
// True when a pool entry's auth/health is older than `ttlMs` (its token may
|
|
95
119
|
// have lapsed). An entry with no `freshAt` is treated as stale — "freshness
|
|
96
120
|
// not proven" fails closed, mirroring fleet-ready's auth gate. Only meaningful
|
|
@@ -109,12 +133,16 @@ function staleMembers(state, nowMs, ttlMs) {
|
|
|
109
133
|
return poolMembers(state).filter((n) => isStale(state.vms[n], nowMs, ttlMs));
|
|
110
134
|
}
|
|
111
135
|
|
|
112
|
-
// How many NEW
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
136
|
+
// How many NEW VMs the keeper must provision to reach `target`. `have` counts
|
|
137
|
+
// BOTH claim-ready pool members AND pending-auth members: provisioning has
|
|
138
|
+
// converged once N reachable+wired VMs exist, regardless of auth, so the deficit
|
|
139
|
+
// closes and the keeper stops churning (the 2026-07-09 endless-refill fix). Only
|
|
140
|
+
// the provisioning denominator widens — poolMembers (claim eligibility) is
|
|
141
|
+
// untouched, so a claim still selects only fully-authenticated VMs. Assigned VMs
|
|
142
|
+
// do not count (claimed → gone from the warm set until released). Never negative
|
|
143
|
+
// (an over-target pool is shrunk by policy, not here).
|
|
116
144
|
function refillCount(state, target) {
|
|
117
|
-
const have = poolMembers(state).length;
|
|
145
|
+
const have = poolMembers(state).length + pendingAuthMembers(state).length;
|
|
118
146
|
return Math.max(0, target - have);
|
|
119
147
|
}
|
|
120
148
|
|
|
@@ -141,16 +169,18 @@ function selectForClaim(state, count, branch, nowIso) {
|
|
|
141
169
|
}
|
|
142
170
|
|
|
143
171
|
// Register (or overwrite) a VM in the pool state. The keeper calls this after
|
|
144
|
-
// provisioning a
|
|
145
|
-
//
|
|
146
|
-
// VM
|
|
172
|
+
// provisioning: status=pool for a fully-READY VM, status=pending-auth for one
|
|
173
|
+
// that is reachable+wired but still awaiting operator auth; the claim path can
|
|
174
|
+
// re-add an assigned VM. `freshAt` is stamped to now for a pool OR pending-auth
|
|
175
|
+
// member (both were just verified live) so neither is immediately stale; an
|
|
176
|
+
// unknown status coerces to pool.
|
|
147
177
|
function applyAdd(state, n, status, branch, nowIso) {
|
|
148
178
|
const next = normalizeState(state);
|
|
149
|
-
const st = status
|
|
179
|
+
const st = coerceStatus(status) || POOL;
|
|
150
180
|
next.vms[n] = {
|
|
151
181
|
status: st,
|
|
152
182
|
branch: branch || null,
|
|
153
|
-
freshAt: st ===
|
|
183
|
+
freshAt: st === ASSIGNED ? (next.vms[n] && next.vms[n].freshAt) || null : nowIso,
|
|
154
184
|
claimedAt: st === ASSIGNED ? nowIso : null,
|
|
155
185
|
};
|
|
156
186
|
return next;
|
|
@@ -188,6 +218,18 @@ function applyMarkFresh(state, n, nowIso) {
|
|
|
188
218
|
return next;
|
|
189
219
|
}
|
|
190
220
|
|
|
221
|
+
// Promote a pending-auth VM to a claim-ready pool member — the dashboard calls
|
|
222
|
+
// this the instant an operator completes that VM's OAuth, so it becomes
|
|
223
|
+
// claimable with NO re-provision. `freshAt` resets to now (it was just proven
|
|
224
|
+
// authenticated). A no-op unless the VM is currently pending-auth, so it is safe
|
|
225
|
+
// to call for any authenticated VM (an already-pooled or absent VM is untouched).
|
|
226
|
+
function applyPromote(state, n, nowIso) {
|
|
227
|
+
const next = normalizeState(state);
|
|
228
|
+
if (!next.vms[n] || next.vms[n].status !== PENDING_AUTH) return next;
|
|
229
|
+
next.vms[n] = { ...next.vms[n], status: POOL, freshAt: nowIso, claimedAt: null };
|
|
230
|
+
return next;
|
|
231
|
+
}
|
|
232
|
+
|
|
191
233
|
// ── Atomic file layer ───────────────────────────────────────────────────────
|
|
192
234
|
|
|
193
235
|
// Synchronous millisecond sleep without a busy CPU spin — used by withLock's
|
|
@@ -260,9 +302,11 @@ module.exports = {
|
|
|
260
302
|
POOL_STATE_FILE,
|
|
261
303
|
POOL,
|
|
262
304
|
ASSIGNED,
|
|
305
|
+
PENDING_AUTH,
|
|
263
306
|
normalizeState,
|
|
264
307
|
poolMembers,
|
|
265
308
|
assignedMembers,
|
|
309
|
+
pendingAuthMembers,
|
|
266
310
|
isStale,
|
|
267
311
|
staleMembers,
|
|
268
312
|
refillCount,
|
|
@@ -271,6 +315,7 @@ module.exports = {
|
|
|
271
315
|
applyRelease,
|
|
272
316
|
applyEvict,
|
|
273
317
|
applyMarkFresh,
|
|
318
|
+
applyPromote,
|
|
274
319
|
withLock,
|
|
275
320
|
readState,
|
|
276
321
|
writeState,
|
|
@@ -312,6 +357,9 @@ function main(argv) {
|
|
|
312
357
|
case 'assigned-members':
|
|
313
358
|
process.stdout.write(`${assignedMembers(readState()).join(' ')}\n`);
|
|
314
359
|
return 0;
|
|
360
|
+
case 'pending-auth-members':
|
|
361
|
+
process.stdout.write(`${pendingAuthMembers(readState()).join(' ')}\n`);
|
|
362
|
+
return 0;
|
|
315
363
|
case 'refill-count': {
|
|
316
364
|
const target = Number(flags.target);
|
|
317
365
|
if (!Number.isInteger(target) || target < 0) {
|
|
@@ -357,8 +405,10 @@ function main(argv) {
|
|
|
357
405
|
process.stderr.write('fleet-pool: add needs --vm <pos int>\n');
|
|
358
406
|
return 2;
|
|
359
407
|
}
|
|
360
|
-
|
|
361
|
-
|
|
408
|
+
// applyAdd coerces the status (pool | pending-auth | assigned; unknown →
|
|
409
|
+
// pool), so pass the raw flag through — this is how the keeper adds a
|
|
410
|
+
// reachable-but-unauthed VM with `--status pending-auth`.
|
|
411
|
+
withLock(() => writeState(applyAdd(readState(), vm, flags.status, flags.branch, now)));
|
|
362
412
|
return 0;
|
|
363
413
|
}
|
|
364
414
|
case 'release': {
|
|
@@ -388,9 +438,18 @@ function main(argv) {
|
|
|
388
438
|
withLock(() => writeState(applyMarkFresh(readState(), vm, now)));
|
|
389
439
|
return 0;
|
|
390
440
|
}
|
|
441
|
+
case 'promote': {
|
|
442
|
+
const vm = Number(flags.vm);
|
|
443
|
+
if (!Number.isInteger(vm) || vm <= 0) {
|
|
444
|
+
process.stderr.write('fleet-pool: promote needs --vm <pos int>\n');
|
|
445
|
+
return 2;
|
|
446
|
+
}
|
|
447
|
+
withLock(() => writeState(applyPromote(readState(), vm, now)));
|
|
448
|
+
return 0;
|
|
449
|
+
}
|
|
391
450
|
default:
|
|
392
451
|
process.stderr.write(
|
|
393
|
-
'fleet-pool: usage: <list|pool-members|assigned-members|refill-count|stale|claim|add|release|evict|mark-fresh> [--flags]\n',
|
|
452
|
+
'fleet-pool: usage: <list|pool-members|assigned-members|pending-auth-members|refill-count|stale|claim|add|release|evict|mark-fresh|promote> [--flags]\n',
|
|
394
453
|
);
|
|
395
454
|
return 2;
|
|
396
455
|
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// npm/fleet-prewarm.js — pure verdicts for the fleet dashboard's concurrent
|
|
4
|
+
// cold-add pre-warm (vm--concurrent-add-prewarm-branch-image).
|
|
5
|
+
//
|
|
6
|
+
// When several VMs are added at once on a branch whose prebuilt image is not yet
|
|
7
|
+
// published, EVERY VM compiles the editor from source (~10 min each) in
|
|
8
|
+
// parallel — none has finished compiling-and-publishing before its siblings
|
|
9
|
+
// start, so the self-feeding fast path (publish_compiled_binary in
|
|
10
|
+
// scripts/gcp-bootstrap.sh) never gets a chance to prime. The fix is to bring up
|
|
11
|
+
// ONE "pacer" VM first, wait until it has published the branch binary (or
|
|
12
|
+
// settled on a definite won't-publish), then release the rest so they pull it.
|
|
13
|
+
//
|
|
14
|
+
// This module holds the *decisions* — pace-or-fan-out, which tags constitute a
|
|
15
|
+
// usable image, parsing the bootstrap's machine markers, and the push-token
|
|
16
|
+
// diagnostic — so each case is unit-tested without a live VM or registry. The
|
|
17
|
+
// I/O (the manifest probe, reading the add log, threading the held queue) lives
|
|
18
|
+
// in the server / cloud.js shells that call these.
|
|
19
|
+
|
|
20
|
+
// Machine-detectable markers gcp-bootstrap.sh emits into the cloud:up add log
|
|
21
|
+
// (streamed to /tmp/dash-add-vm-<n>.log). They are stable contracts the
|
|
22
|
+
// dashboard greps for — keep them in sync with scripts/gcp-bootstrap.sh.
|
|
23
|
+
const MARKER_PUBLISHED = 'CY_BINARY_PUBLISHED'; // pacer published the branch binary; held adds can pull it
|
|
24
|
+
const MARKER_PUBLISH_SKIPPED = 'CY_BINARY_PUBLISH_SKIPPED'; // pacer will NOT publish (no token / no branch / failure)
|
|
25
|
+
const MARKER_PATH_PULL = 'CY_PROVISION_PATH pull'; // this VM pulled a prebuilt image (~2 min fast path)
|
|
26
|
+
const MARKER_PATH_COMPILE = 'CY_PROVISION_PATH compile'; // this VM compiled from source (~10 min slow path)
|
|
27
|
+
|
|
28
|
+
// Markers scripts/cloud-refresh-binary.sh emits into the between-session
|
|
29
|
+
// refresh log (/tmp/dash-refresh-binary-vm-<n>.log, or the systemd journal when
|
|
30
|
+
// the timer runs it unattended). Same stable-contract rule as the four above.
|
|
31
|
+
const MARKER_REFRESHED = 'CY_BINARY_REFRESHED'; // pulled + swapped a newer published binary, container restarted
|
|
32
|
+
const MARKER_REFRESH_CURRENT = 'CY_BINARY_CURRENT'; // already on the newest published binary; nothing to do
|
|
33
|
+
const MARKER_REFRESH_DEFERRED = 'CY_BINARY_REFRESH_DEFERRED'; // newer binary exists but a session is live (or state unknown)
|
|
34
|
+
const MARKER_REFRESH_DEGRADED = 'CY_DEGRADED binary-refresh'; // the refresh could NOT complete — this VM may be running stale
|
|
35
|
+
|
|
36
|
+
// Mirror scripts/gcp-bootstrap.sh's BRANCH_SLUG: '/' → '-', lowercased, capped
|
|
37
|
+
// at 128 chars. The dashboard and the VM MUST agree on the slug or the probe
|
|
38
|
+
// checks different tags than the bootstrap pulls. Empty / missing branch → the
|
|
39
|
+
// universal "main" slug, matching the bootstrap's detached-HEAD fallback.
|
|
40
|
+
function branchSlug(branch) {
|
|
41
|
+
if (!branch || typeof branch !== 'string') return 'main';
|
|
42
|
+
const slug = branch.replace(/\//g, '-').toLowerCase().slice(0, 128);
|
|
43
|
+
return slug || 'main';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The tag chain that constitutes a "usable" prebuilt editor image, in the same
|
|
47
|
+
// precedence the dev bootstrap pulls (build_editor_image in gcp-bootstrap.sh):
|
|
48
|
+
// 1. cloud-smoke:<slug>-latest — the branch base image
|
|
49
|
+
// 2. cloud-smoke:main-latest — the universal CI fallback (skipped when
|
|
50
|
+
// the branch already IS main, no duplicate)
|
|
51
|
+
// 3. binary:<slug>-latest — the branch binary overlay
|
|
52
|
+
// If a manifest HEAD on ANY of these succeeds, a cold add can pull instead of
|
|
53
|
+
// compiling, so no pacer is needed. Registry defaults to the same
|
|
54
|
+
// CODEYAM_REGISTRY the bootstrap reads.
|
|
55
|
+
function usableImageTags({ registry, branch } = {}) {
|
|
56
|
+
const reg = registry || 'ghcr.io/codeyam-ai';
|
|
57
|
+
const slug = branchSlug(branch);
|
|
58
|
+
const tags = [`${reg}/codeyam-editor-cloud-smoke:${slug}-latest`];
|
|
59
|
+
if (slug !== 'main') tags.push(`${reg}/codeyam-editor-cloud-smoke:main-latest`);
|
|
60
|
+
tags.push(`${reg}/codeyam-editor-binary:${slug}-latest`);
|
|
61
|
+
return tags;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The core verdict: should a cold add pace one VM to compile+publish first?
|
|
65
|
+
//
|
|
66
|
+
// count how many VMs the operator asked to add
|
|
67
|
+
// hasUsableImage the probe result — true (image present), false (definitely
|
|
68
|
+
// absent), or null (couldn't determine: no token, probe error)
|
|
69
|
+
// hasPushToken a write:packages token is present, so the pacer CAN publish
|
|
70
|
+
//
|
|
71
|
+
// Pace ONLY when it strictly pays AND can work:
|
|
72
|
+
// - count >= 1 — a batch (k>1) serializes ONE ahead so the rest pull;
|
|
73
|
+
// a single cold add (k==1) still paces so it PUBLISHES
|
|
74
|
+
// the branch image, making every FUTURE add (single or
|
|
75
|
+
// batch) pull instead of source-building. Gating the
|
|
76
|
+
// publish behind k>1 was a bug: adding VMs one at a time
|
|
77
|
+
// bypassed the pacer entirely, so no image was ever
|
|
78
|
+
// produced and every single add recompiled.
|
|
79
|
+
// - hasUsableImage !== true — a usable image lets all N pull concurrently, so
|
|
80
|
+
// only pace when one is NOT known to exist. Both
|
|
81
|
+
// `false` (known-absent) AND `null` (undetermined)
|
|
82
|
+
// pace: publishing is cheap + idempotent, and pacing
|
|
83
|
+
// is the ONLY path that bootstraps self-feeding.
|
|
84
|
+
// Fanning out on `null` is exactly what permanently
|
|
85
|
+
// wedges a docker-less operator host whose probe can
|
|
86
|
+
// never confirm absence (see
|
|
87
|
+
// npm/cloud.js::probeUsableEditorImage) — republishing
|
|
88
|
+
// an image that already existed is the harmless worst
|
|
89
|
+
// case.
|
|
90
|
+
// - hasPushToken — without it the pacer can't publish, so pacing would
|
|
91
|
+
// just serialize N source compiles for no gain
|
|
92
|
+
// Fan out immediately only when a usable image is KNOWN to exist, or there is no
|
|
93
|
+
// push token (today's behavior for those two cases, strictly additive).
|
|
94
|
+
function decideBatchPrewarm({ count, hasUsableImage, hasPushToken } = {}) {
|
|
95
|
+
const k = Math.round(Number(count) || 0);
|
|
96
|
+
if (k < 1) return { pace: false, reason: 'no VMs requested' };
|
|
97
|
+
if (hasUsableImage === true)
|
|
98
|
+
return { pace: false, reason: 'usable prebuilt image exists — all pull concurrently' };
|
|
99
|
+
if (!hasPushToken)
|
|
100
|
+
return { pace: false, reason: 'no push token — pacer could not publish, fan out' };
|
|
101
|
+
// hasUsableImage is `false` (known-absent) or `null` (undetermined); with a push
|
|
102
|
+
// token, pace either way to bootstrap self-feeding.
|
|
103
|
+
const basis = hasUsableImage === false
|
|
104
|
+
? 'branch image absent'
|
|
105
|
+
: 'image presence undetermined — publishing to bootstrap self-feeding';
|
|
106
|
+
return {
|
|
107
|
+
pace: true,
|
|
108
|
+
reason: k > 1
|
|
109
|
+
? `pacing 1 of ${k} — ${basis}, will publish for the rest`
|
|
110
|
+
: `single cold add — ${basis}, will compile and publish so future adds pull`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Has the pacer's add log settled to a definite publish outcome the held batch
|
|
115
|
+
// can act on? Returns 'published' (binary is now pullable — release and pull),
|
|
116
|
+
// 'skipped' (pacer will NOT publish — release; they recompile, today's
|
|
117
|
+
// behavior), or null (still in flight — keep waiting). A SKIPPED that is later
|
|
118
|
+
// followed by a PUBLISHED still reports published (the success wins), so marker
|
|
119
|
+
// ordering can't strand the batch on a transient earlier skip.
|
|
120
|
+
function detectPublishSettled(logText) {
|
|
121
|
+
if (!logText || typeof logText !== 'string') return null;
|
|
122
|
+
if (logText.includes(MARKER_PUBLISHED)) return 'published';
|
|
123
|
+
if (logText.includes(MARKER_PUBLISH_SKIPPED)) return 'skipped';
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The pure release decision for the pre-warm hold (startPrewarmWatch's per-tick
|
|
128
|
+
// verdict): given the pacer's current signals, should the held batch be released
|
|
129
|
+
// now, and why? Returns a human reason string to release, or null to keep
|
|
130
|
+
// waiting. Releases on the FIRST of: a settled publish marker, the pacer job
|
|
131
|
+
// terminating without one, or the bounded timeout — so a publish failure or a
|
|
132
|
+
// stuck pacer can never strand the batch.
|
|
133
|
+
//
|
|
134
|
+
// settled detectPublishSettled() result: 'published' | 'skipped' | null
|
|
135
|
+
// pacerJob the pacer add job record ({ state, ... }) or null/undefined
|
|
136
|
+
// elapsedMs ms since the watch started
|
|
137
|
+
// timeoutMs the bounded fallback after which the batch releases regardless
|
|
138
|
+
function prewarmReleaseDecision({ settled, pacerJob, elapsedMs, timeoutMs } = {}) {
|
|
139
|
+
if (settled) return `pacer ${settled}`;
|
|
140
|
+
const st = pacerJob && pacerJob.state;
|
|
141
|
+
if (st === 'done' || st === 'error') return `pacer ${st}`;
|
|
142
|
+
if (Number(elapsedMs) > Number(timeoutMs)) return 'timeout';
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Which provision path did a VM take, parsed from its add log's path marker?
|
|
147
|
+
// Returns 'pull' (~2 min prebuilt pull), 'compile' (~10 min source compile), or
|
|
148
|
+
// null (not yet determined). The LAST marker wins — a VM that tries a pull,
|
|
149
|
+
// misses, then falls back to a compile correctly reads 'compile'.
|
|
150
|
+
function provisionPath(logText) {
|
|
151
|
+
if (!logText || typeof logText !== 'string') return null;
|
|
152
|
+
const pull = logText.lastIndexOf(MARKER_PATH_PULL);
|
|
153
|
+
const compile = logText.lastIndexOf(MARKER_PATH_COMPILE);
|
|
154
|
+
if (pull < 0 && compile < 0) return null;
|
|
155
|
+
return compile > pull ? 'compile' : 'pull';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Operator-facing one-liner for a provision path, for the add card sub-status.
|
|
159
|
+
// null path → null (render nothing).
|
|
160
|
+
function provisionPathLabel(path) {
|
|
161
|
+
if (path === 'pull') return 'pulling prebuilt image (~2 min)';
|
|
162
|
+
if (path === 'compile') return 'compiling from source (~10 min)';
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The push-token diagnostic surfaced on the dashboard. Without a write:packages
|
|
167
|
+
// token the self-feeding publish is silently off, so EVERY cold add recompiles
|
|
168
|
+
// (~10 min) and the pacer can't help either — a state the operator must see, not
|
|
169
|
+
// discover from a buried per-VM log line.
|
|
170
|
+
function pushTokenDiagnostic({ hasPushToken } = {}) {
|
|
171
|
+
if (hasPushToken) return { warn: false, message: null };
|
|
172
|
+
return {
|
|
173
|
+
warn: true,
|
|
174
|
+
message:
|
|
175
|
+
'Self-feeding disabled: no CODEYAM_GHCR_PUSH_TOKEN (write:packages). ' +
|
|
176
|
+
'Cold adds will always recompile the editor from source (~10 min each) ' +
|
|
177
|
+
'and concurrent batches cannot pre-warm. Install a write-scoped token and ' +
|
|
178
|
+
'reinstall the fleet agents to enable the fast path.',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// What did the last between-session binary refresh do? Parsed from the refresh
|
|
183
|
+
// log for the VM card, so an operator can tell "up to date" from "could not
|
|
184
|
+
// update" without opening a log.
|
|
185
|
+
//
|
|
186
|
+
// Returns 'refreshed' | 'current' | 'deferred' | 'degraded' | null (no refresh
|
|
187
|
+
// has reported yet).
|
|
188
|
+
//
|
|
189
|
+
// The LAST marker wins, for the same reason provisionPath() takes the last one:
|
|
190
|
+
// a tick that defers and a later tick that refreshes must read as refreshed,
|
|
191
|
+
// not stay stuck on the older verdict. Ordering — not precedence between marker
|
|
192
|
+
// kinds — is what makes this correct across repeated timer runs.
|
|
193
|
+
//
|
|
194
|
+
// `degraded` is deliberately a distinct state from `deferred`. Deferring is the
|
|
195
|
+
// mechanism working as designed (a session is live, so we wait). Degraded means
|
|
196
|
+
// the VM wanted a newer binary and could NOT get one — the silent-staleness
|
|
197
|
+
// case this whole mechanism exists to make visible, and the only one that
|
|
198
|
+
// warrants an operator's attention.
|
|
199
|
+
function binaryRefreshState(logText) {
|
|
200
|
+
if (!logText || typeof logText !== 'string') return null;
|
|
201
|
+
const at = (marker) => logText.lastIndexOf(marker);
|
|
202
|
+
const candidates = [
|
|
203
|
+
['refreshed', at(MARKER_REFRESHED)],
|
|
204
|
+
['current', at(MARKER_REFRESH_CURRENT)],
|
|
205
|
+
['deferred', at(MARKER_REFRESH_DEFERRED)],
|
|
206
|
+
['degraded', at(MARKER_REFRESH_DEGRADED)],
|
|
207
|
+
].filter(([, idx]) => idx >= 0);
|
|
208
|
+
if (!candidates.length) return null;
|
|
209
|
+
return candidates.reduce((best, cur) => (cur[1] > best[1] ? cur : best))[0];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Operator-facing one-liner for a refresh state, for the VM card. null state →
|
|
213
|
+
// null (render nothing — a VM that has never run a refresh is not a problem).
|
|
214
|
+
function binaryRefreshLabel(state) {
|
|
215
|
+
if (state === 'refreshed') return 'editor binary refreshed to the newest published build';
|
|
216
|
+
if (state === 'current') return 'editor binary up to date';
|
|
217
|
+
if (state === 'deferred') return 'newer editor binary available — waiting for the session to end';
|
|
218
|
+
if (state === 'degraded') return 'could NOT refresh the editor binary — may be running stale';
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
MARKER_PUBLISHED,
|
|
224
|
+
MARKER_PUBLISH_SKIPPED,
|
|
225
|
+
MARKER_PATH_PULL,
|
|
226
|
+
MARKER_PATH_COMPILE,
|
|
227
|
+
MARKER_REFRESHED,
|
|
228
|
+
MARKER_REFRESH_CURRENT,
|
|
229
|
+
MARKER_REFRESH_DEFERRED,
|
|
230
|
+
MARKER_REFRESH_DEGRADED,
|
|
231
|
+
binaryRefreshState,
|
|
232
|
+
binaryRefreshLabel,
|
|
233
|
+
branchSlug,
|
|
234
|
+
usableImageTags,
|
|
235
|
+
decideBatchPrewarm,
|
|
236
|
+
detectPublishSettled,
|
|
237
|
+
prewarmReleaseDecision,
|
|
238
|
+
provisionPath,
|
|
239
|
+
provisionPathLabel,
|
|
240
|
+
pushTokenDiagnostic,
|
|
241
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// npm/fleet-project-drift.js — pure classifier for the fleet dashboard's
|
|
4
|
+
// "project-dir-drift" surface: does a VM's LIVE project identity conflict with
|
|
5
|
+
// the identity the operator asked for at Add time?
|
|
6
|
+
//
|
|
7
|
+
// The drift surface exists to catch a genuine misprovision — a VM whose
|
|
8
|
+
// /workspace checked out the WRONG repo, so its live project name no longer
|
|
9
|
+
// matches the expected one. But a brand-new GREENFIELD project (added with
|
|
10
|
+
// --new-project / newProjectName, no clientRepo) has no origin remote and, on a
|
|
11
|
+
// cloud VM, is always bind-mounted at /workspace. Before the editor seeds its
|
|
12
|
+
// projectTitle, the live identity falls back to the bare mount basename
|
|
13
|
+
// "workspace", which never equals the expected newProjectName — a PERMANENT
|
|
14
|
+
// FALSE drift that made every healthy greenfield add read amber and then get
|
|
15
|
+
// misclassified as "Add failed". This module suppresses drift for exactly that
|
|
16
|
+
// un-onboarded greenfield shape while leaving every true-positive intact.
|
|
17
|
+
//
|
|
18
|
+
// Pure like fleet-add-failure.js: server.js is the thin I/O shell that gathers
|
|
19
|
+
// the poll state and renders the returned reasons; the decision is unit-tested
|
|
20
|
+
// here without a live VM.
|
|
21
|
+
|
|
22
|
+
// The bare basename of the cloud-VM bind mount. Every fleet VM mounts the
|
|
23
|
+
// client project at /workspace (see server.js's in-container gather and the
|
|
24
|
+
// `-w /workspace` exec target), so an un-onboarded greenfield project with no
|
|
25
|
+
// origin resolves its live name to this literal string until the editor seeds a
|
|
26
|
+
// real projectTitle.
|
|
27
|
+
const MOUNT_BASENAME = 'workspace';
|
|
28
|
+
|
|
29
|
+
// Decide the project-dir-drift reasons for one VM.
|
|
30
|
+
//
|
|
31
|
+
// expectedName the identity the operator asked for: repoBasename(clientRepo)
|
|
32
|
+
// for a clone, else newProjectName for greenfield, else null
|
|
33
|
+
// liveName the VM's live clientProject.name (null when unreachable)
|
|
34
|
+
// liveRepo the VM's live clientProject.repo — null for a project with no
|
|
35
|
+
// origin remote (always the case for a fresh greenfield add)
|
|
36
|
+
// hasClientRepo whether the Add was configured with a clientRepo (a clone);
|
|
37
|
+
// false for a --new-project greenfield add
|
|
38
|
+
//
|
|
39
|
+
// Returns { drifted, reasons }. `reasons` is [] when not drifted so the caller
|
|
40
|
+
// can spread it into the card's reason list unconditionally.
|
|
41
|
+
function classifyProjectDrift({ expectedName, liveName, liveRepo, hasClientRepo } = {}) {
|
|
42
|
+
// Un-onboarded greenfield: no configured clientRepo, no live origin remote,
|
|
43
|
+
// and the live name is still the bare mount basename. This is a healthy fresh
|
|
44
|
+
// project that simply has not been wired to a remote yet — never real drift.
|
|
45
|
+
// Note the live-origin check: once a greenfield project DOES gain an origin
|
|
46
|
+
// whose basename conflicts with expected, `liveRepo` is non-null and this
|
|
47
|
+
// suppression no longer applies, so a genuine later misprovision still drifts.
|
|
48
|
+
const greenfieldUnonboarded =
|
|
49
|
+
!hasClientRepo && !liveRepo && liveName === MOUNT_BASENAME;
|
|
50
|
+
const drifted =
|
|
51
|
+
!greenfieldUnonboarded && !!(expectedName && liveName && expectedName !== liveName);
|
|
52
|
+
const reasons = drifted
|
|
53
|
+
? [`project-dir-drift (expected: ${expectedName}, running: ${liveName})`]
|
|
54
|
+
: [];
|
|
55
|
+
return { drifted, reasons };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { classifyProjectDrift, MOUNT_BASENAME };
|