@tpsdev-ai/flair 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -377
- package/dist/cli.js +1355 -281
- package/dist/deploy.js +212 -24
- package/dist/fabric-upgrade.js +16 -1
- package/dist/federation/scheduler.js +500 -0
- package/dist/install/clients.js +111 -53
- package/dist/lib/mcp-spec.js +128 -0
- package/dist/lib/safe-snapshot-extract.js +231 -0
- package/dist/lib/scheduler-platform.js +128 -0
- package/dist/lib/xml-escape.js +54 -0
- package/dist/rem/scheduler.js +35 -87
- package/dist/rem/snapshot.js +13 -0
- package/dist/replication-convergence.js +505 -0
- package/dist/resources/MemoryBootstrap.js +7 -8
- package/dist/resources/SemanticSearch.js +17 -45
- package/dist/resources/abstention.js +1 -1
- package/dist/resources/embeddings-boot.js +10 -12
- package/dist/resources/embeddings-provider.js +10 -7
- package/dist/resources/health.js +24 -19
- package/dist/resources/in-process.js +225 -0
- package/dist/resources/mcp-tools.js +23 -17
- package/dist/resources/migration-boot.js +80 -10
- package/dist/resources/migrations/data-dir.js +205 -0
- package/dist/resources/migrations/progress.js +33 -0
- package/dist/resources/migrations/runner.js +29 -2
- package/dist/resources/migrations/state.js +13 -2
- package/dist/resources/models-dir.js +18 -9
- package/dist/resources/semantic-retrieval-core.js +5 -4
- package/dist/src/lib/scheduler-platform.js +128 -0
- package/dist/src/lib/xml-escape.js +54 -0
- package/dist/src/rem/scheduler.js +35 -87
- package/docs/deploying-on-fabric.md +267 -0
- package/docs/deployment.md +5 -0
- package/docs/embedding-in-a-harper-app.md +299 -0
- package/docs/federation.md +61 -4
- package/docs/integrations.md +3 -0
- package/docs/mcp-clients.md +16 -7
- package/docs/quickstart.md +80 -54
- package/docs/releasing.md +72 -38
- package/docs/supply-chain-policy.md +36 -0
- package/docs/troubleshooting.md +24 -0
- package/docs/upgrade.md +98 -3
- package/package.json +1 -11
- package/templates/bin/flair-federation-sync.sh.tmpl +28 -0
- package/templates/launchd/dev.flair.federation.sync.plist.tmpl +47 -0
- package/templates/systemd/flair-federation-sync.service.tmpl +21 -0
- package/templates/systemd/flair-federation-sync.timer.tmpl +16 -0
- package/dist/resources/rerank-provider.js +0 -569
- package/docs/rerank-provisioning.md +0 -101
package/dist/deploy.js
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
3
3
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
+
import { awaitOriginQuiescent, awaitReplicationConvergence, defaultConvergenceDeps, parseReplicationFailure, } from "./replication-convergence.js";
|
|
6
7
|
// Files that must be present in a Flair package for deployment.
|
|
7
8
|
// Mirrors the `files` array in package.json — keep in sync.
|
|
8
9
|
export const REQUIRED_PACKAGE_FILES = [
|
|
@@ -215,7 +216,34 @@ export function buildHarperDeployArgs(opts, url, project) {
|
|
|
215
216
|
}
|
|
216
217
|
// Flaky-peer-replication resilience defaults. See DeployOptions
|
|
217
218
|
// for the full incident writeup this closes.
|
|
218
|
-
|
|
219
|
+
//
|
|
220
|
+
// ── Why this default is 0 (flair#878) ───────────────────────────────────────
|
|
221
|
+
// It was 2 (three attempts). The reasoning behind that — "a bare manual re-run
|
|
222
|
+
// cleared it, so let the tool self-heal" — was right about the symptom and
|
|
223
|
+
// wrong about the mechanism. What actually clears a peer-replication error is
|
|
224
|
+
// Harper finishing its own asynchronous replication; the re-run merely happened
|
|
225
|
+
// to take long enough for that to complete. Retrying is not what fixed it, and
|
|
226
|
+
// the retry is not free:
|
|
227
|
+
//
|
|
228
|
+
// 1. It races work that was going to succeed anyway. Harper keeps replicating
|
|
229
|
+
// after the deploy call returns, so the retry re-issues a full deploy into
|
|
230
|
+
// a cluster mid-heal.
|
|
231
|
+
// 2. It is not idempotent. A retry re-runs the whole deploy including
|
|
232
|
+
// Harper's own `npm install` into the component directory on every node.
|
|
233
|
+
// Overlapping that with the previous attempt's still-running install is
|
|
234
|
+
// how the reported upgrade died with
|
|
235
|
+
// `ENOTEMPTY: directory not empty, rmdir '.../node_modules/<pkg>/dist'`
|
|
236
|
+
// on a native module — a hard failure the original error never was.
|
|
237
|
+
// 3. It is now redundant. The convergence poll added in flair#878 covers
|
|
238
|
+
// exactly the window a retry was buying, WITHOUT touching the cluster:
|
|
239
|
+
// it waits and looks instead of waiting and re-deploying.
|
|
240
|
+
//
|
|
241
|
+
// So the remedy was strictly worse than the problem it retried, and the thing
|
|
242
|
+
// it was compensating for is now observed directly. Retrying is kept as an
|
|
243
|
+
// explicit opt-in (`--deploy-retries <n>`) for the genuine case — replication
|
|
244
|
+
// observed NOT to converge — where it is additionally gated on the origin
|
|
245
|
+
// having gone quiescent first (see awaitOriginQuiescent).
|
|
246
|
+
export const DEFAULT_DEPLOY_RETRIES = 0;
|
|
219
247
|
export const DEPLOY_RETRY_BACKOFF_MS = [5_000, 10_000];
|
|
220
248
|
// The signature of a Fabric PEER-REPLICATION failure specifically — harper
|
|
221
249
|
// deploys fine to the origin node, but pushing the component to a peer
|
|
@@ -228,6 +256,78 @@ export const DEPLOY_RETRY_BACKOFF_MS = [5_000, 10_000];
|
|
|
228
256
|
// none of which mention peer replication and must never be retried).
|
|
229
257
|
// Literal regex, no interpolation.
|
|
230
258
|
export const REPLICATION_FAILURE_RE = /failed to replicate to \d+ (of \d+ )?peer|connection closed\s+1006|ignore_replication_errors/i;
|
|
259
|
+
const HARPER_OUTPUT_SUMMARY_MAX = 300;
|
|
260
|
+
/**
|
|
261
|
+
* Pull the most useful single line out of harper's combined output. Prefers the
|
|
262
|
+
* last line that looks like an error, falling back to the last non-empty line —
|
|
263
|
+
* harper prints its fatal reason last on both paths.
|
|
264
|
+
*/
|
|
265
|
+
export function summarizeHarperOutput(output) {
|
|
266
|
+
const lines = (output ?? "")
|
|
267
|
+
.split("\n")
|
|
268
|
+
.map((l) => l.replace(/\s+$/, "").replace(/^\s*[!>]\s?/, "").trim())
|
|
269
|
+
.filter((l) => l.length > 0);
|
|
270
|
+
if (lines.length === 0)
|
|
271
|
+
return "(no output)";
|
|
272
|
+
const errorish = [...lines].reverse().find((l) => /error|failed|cannot|denied|exit code/i.test(l));
|
|
273
|
+
const chosen = errorish ?? lines[lines.length - 1];
|
|
274
|
+
return chosen.length > HARPER_OUTPUT_SUMMARY_MAX
|
|
275
|
+
? chosen.slice(0, HARPER_OUTPUT_SUMMARY_MAX - 1) + "…"
|
|
276
|
+
: chosen;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Build the error message for a failed deploy from the FULL attempt history.
|
|
280
|
+
*
|
|
281
|
+
* ── The rule this encodes (flair#878, the core defect) ──────────────────────
|
|
282
|
+
* The reported failure is always the FIRST attempt's failure. A later attempt
|
|
283
|
+
* can only exist because this CLI chose to retry, so a later, different failure
|
|
284
|
+
* describes the state the CLI's own remedy created — not the state the operator
|
|
285
|
+
* needs to act on. In the reported incident attempt 1 was a transient
|
|
286
|
+
* peer-replication warning and attempt 2 died with npm `ENOTEMPTY` on a native
|
|
287
|
+
* module; surfacing the ENOTEMPTY sent the diagnosis into the component's
|
|
288
|
+
* node_modules when the actual event was "a peer link blipped and then healed".
|
|
289
|
+
*
|
|
290
|
+
* The later failure is still REPORTED — suppressing it would hide that the
|
|
291
|
+
* cluster may have been left in a worse state — but it is explicitly labelled
|
|
292
|
+
* as a consequence of retrying, with the remedy (stop retrying) named inline.
|
|
293
|
+
* Errors have to enable a response, and "ENOTEMPTY on node-llama-cpp/dist"
|
|
294
|
+
* enables the wrong one.
|
|
295
|
+
*/
|
|
296
|
+
export function describeDeployFailure(failures, convergence) {
|
|
297
|
+
if (failures.length === 0)
|
|
298
|
+
return "harper deploy failed with no recorded attempt";
|
|
299
|
+
const primary = failures[0];
|
|
300
|
+
const escalations = failures
|
|
301
|
+
.slice(1)
|
|
302
|
+
.filter((f) => f.kind !== primary.kind || f.summary !== primary.summary);
|
|
303
|
+
const lines = [];
|
|
304
|
+
if (primary.kind === "replication") {
|
|
305
|
+
lines.push(`harper deploy exited with code ${primary.code}: the component deployed to the origin node but ` +
|
|
306
|
+
`peer replication was reported failed, and this CLI could not confirm the peers converged.`);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
lines.push(`harper deploy exited with code ${primary.code}: ${primary.summary}`);
|
|
310
|
+
}
|
|
311
|
+
if (convergence) {
|
|
312
|
+
lines.push(` convergence check: ${convergence.detail}`);
|
|
313
|
+
}
|
|
314
|
+
if (failures.length > 1) {
|
|
315
|
+
lines.push(` attempt 1 of ${primary.totalAttempts} is the failure reported above (${primary.summary}).`);
|
|
316
|
+
}
|
|
317
|
+
for (const f of escalations) {
|
|
318
|
+
const consequence = primary.kind === "replication" && f.kind === "other"
|
|
319
|
+
? ` This is a CONSEQUENCE of retrying, not the original problem: re-running a deploy over a component ` +
|
|
320
|
+
`directory the previous attempt is still installing into is not idempotent (npm ENOTEMPTY on a native ` +
|
|
321
|
+
`module is the known shape). Diagnose the replication failure above, not this. Run with ` +
|
|
322
|
+
`--deploy-retries 0 to remove this class of failure entirely.`
|
|
323
|
+
: "";
|
|
324
|
+
lines.push(` attempt ${f.attempt} of ${f.totalAttempts} then failed with: ${f.summary}.${consequence}`);
|
|
325
|
+
}
|
|
326
|
+
if (primary.kind === "replication") {
|
|
327
|
+
lines.push(` Pass --ignore-replication-errors to accept an origin-only deploy, or re-run once the peer link recovers.`);
|
|
328
|
+
}
|
|
329
|
+
return lines.join("\n");
|
|
330
|
+
}
|
|
231
331
|
// Tee-style capture: streams harper's stdout/stderr to the user in real time
|
|
232
332
|
// (unchanged UX — a multi-minute deploy needs live progress, not a black
|
|
233
333
|
// box) while ALSO buffering the combined text so the caller can
|
|
@@ -282,42 +382,122 @@ function sleep(ms) {
|
|
|
282
382
|
// what worked by hand, and harper's CLI has no "retry replication only"
|
|
283
383
|
// entry point. Any other failure (bad package, auth, missing files, ...)
|
|
284
384
|
// is never retried — it fails fast, same as before this change.
|
|
285
|
-
async function runHarperDeploy(bin, args, cwd, env, opts) {
|
|
385
|
+
async function runHarperDeploy(bin, args, cwd, env, opts, targetUrl, project) {
|
|
286
386
|
const maxRetries = opts.deployRetries ?? DEFAULT_DEPLOY_RETRIES;
|
|
287
387
|
const backoff = opts.deployRetryBackoffMs ?? DEPLOY_RETRY_BACKOFF_MS;
|
|
288
388
|
const totalAttempts = Math.max(1, maxRetries + 1);
|
|
389
|
+
const convergenceDeps = opts.convergenceDeps ??
|
|
390
|
+
defaultConvergenceDeps(opts.fabricUser, opts.fabricPassword, opts.onProgress);
|
|
391
|
+
// The attempt history is what makes the anti-escalation rule possible —
|
|
392
|
+
// describeDeployFailure() always reports failures[0], never the newest.
|
|
393
|
+
const failures = [];
|
|
394
|
+
let lastConvergence = null;
|
|
289
395
|
for (let attempt = 1; attempt <= totalAttempts; attempt++) {
|
|
290
396
|
const { code, output } = await spawnHarperCaptured(bin, args, cwd, env);
|
|
291
397
|
if (code === 0)
|
|
292
|
-
return { replicationWarning: false };
|
|
398
|
+
return { replicationWarning: false, convergedAfterReplicationError: false };
|
|
293
399
|
const isReplicationFailure = REPLICATION_FAILURE_RE.test(output);
|
|
294
400
|
const isLastAttempt = attempt === totalAttempts;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
401
|
+
failures.push({
|
|
402
|
+
attempt,
|
|
403
|
+
totalAttempts,
|
|
404
|
+
code,
|
|
405
|
+
kind: isReplicationFailure ? "replication" : "other",
|
|
406
|
+
summary: summarizeHarperOutput(output),
|
|
407
|
+
});
|
|
408
|
+
// A non-replication failure has never been retried and still isn't. It
|
|
409
|
+
// fails fast — but through describeDeployFailure(), so that when it lands
|
|
410
|
+
// as attempt 2+ it is reported as the consequence of a retry rather than
|
|
411
|
+
// as the headline. That reordering IS the flair#878 fix.
|
|
412
|
+
if (!isReplicationFailure) {
|
|
413
|
+
throw new Error(describeDeployFailure(failures, lastConvergence));
|
|
414
|
+
}
|
|
415
|
+
// ── flair#878 step 1: check convergence BEFORE deciding anything ────────
|
|
416
|
+
// Harper is still replicating after the call returned. Look before
|
|
417
|
+
// declaring, and before retrying — a converged upgrade must report success.
|
|
418
|
+
if (opts.convergenceCheck !== false) {
|
|
419
|
+
const parsed = parseReplicationFailure(output);
|
|
420
|
+
opts.onProgress?.(parsed.peers.length
|
|
421
|
+
? `peer replication reported failed for ${parsed.peers.length} node(s) — checking whether it converged anyway...`
|
|
422
|
+
: `peer replication reported failed — harper named no peer nodes, so convergence cannot be checked`);
|
|
423
|
+
lastConvergence = await awaitReplicationConvergence({
|
|
424
|
+
targetUrl,
|
|
425
|
+
project,
|
|
426
|
+
peers: parsed.peers,
|
|
427
|
+
timeoutMs: opts.convergenceTimeoutMs,
|
|
428
|
+
pollIntervalMs: opts.convergencePollIntervalMs,
|
|
429
|
+
}, convergenceDeps);
|
|
430
|
+
if (lastConvergence.converged) {
|
|
431
|
+
console.warn(`⚠ flair deploy: harper reported a peer-replication failure on attempt ${attempt}/${totalAttempts}, ` +
|
|
432
|
+
`but replication CONVERGED on its own — ${lastConvergence.detail}. Harper replicates components ` +
|
|
433
|
+
`asynchronously, so that error was a snapshot, not a verdict. Treating this deploy as SUCCESSFUL.`);
|
|
434
|
+
opts.onProgress?.(`peer replication converged after ${lastConvergence.elapsedMs}ms — deploy succeeded`);
|
|
435
|
+
return { replicationWarning: false, convergedAfterReplicationError: true };
|
|
436
|
+
}
|
|
306
437
|
}
|
|
307
|
-
|
|
438
|
+
// ── flair#878 steps 2 + 4: retry only when it is justified AND safe ─────
|
|
439
|
+
// Justified: we positively OBSERVED non-convergence. An unknown is not a
|
|
440
|
+
// licence to re-deploy — the retry is the destructive operation here.
|
|
441
|
+
// Safe: the origin's component tree has stopped changing, so attempt N+1
|
|
442
|
+
// cannot collide with attempt N's still-running server-side install (the
|
|
443
|
+
// ENOTEMPTY shape).
|
|
444
|
+
if (!isLastAttempt) {
|
|
445
|
+
let refusal = null;
|
|
446
|
+
let retryReason = "";
|
|
447
|
+
if (opts.convergenceCheck === false) {
|
|
448
|
+
// --no-convergence-check is an explicit "do what you did before
|
|
449
|
+
// flair#878": no operations-API polling at all, retry on the
|
|
450
|
+
// replication signature alone. Honour it rather than silently
|
|
451
|
+
// downgrading it to "never retry" — an operator who set BOTH this and
|
|
452
|
+
// --deploy-retries has asked for the old behaviour, hazard included.
|
|
453
|
+
// The anti-escalation rule still applies, so a retry that fails
|
|
454
|
+
// differently still cannot hijack what gets reported.
|
|
455
|
+
retryReason = "convergence checking is disabled (--no-convergence-check)";
|
|
456
|
+
}
|
|
457
|
+
else if (!lastConvergence || !lastConvergence.conclusive) {
|
|
458
|
+
refusal =
|
|
459
|
+
`peer replication could not be confirmed either way ` +
|
|
460
|
+
`(${lastConvergence?.detail ?? "no convergence result"}). Re-deploying without knowing whether the ` +
|
|
461
|
+
`cluster converged risks colliding with an in-flight replication or install; reporting the original ` +
|
|
462
|
+
`failure instead.`;
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
const quiescence = await awaitOriginQuiescent({ targetUrl, project }, convergenceDeps);
|
|
466
|
+
if (!quiescence.quiescent) {
|
|
467
|
+
refusal =
|
|
468
|
+
`${quiescence.detail}. Re-deploying while the origin's component directory is still being written ` +
|
|
469
|
+
`is the overlap that turns a replication warning into a hard install failure (npm ENOTEMPTY over an ` +
|
|
470
|
+
`existing native-module tree).`;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
retryReason = `origin is quiescent (${quiescence.detail})`;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (refusal) {
|
|
477
|
+
console.warn(`⚠ flair deploy: NOT retrying — ${refusal}`);
|
|
478
|
+
opts.onProgress?.(`not retrying — ${refusal}`);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
const waitMs = backoff[Math.min(attempt - 1, backoff.length - 1)];
|
|
482
|
+
// Self-healing must be visible, never silent — console.warn directly
|
|
483
|
+
// (not gated behind onProgress, which some callers like
|
|
484
|
+
// fabric-upgrade.ts don't wire up) so this is loud regardless of caller.
|
|
485
|
+
console.warn(`⚠ flair deploy: replication did not converge on attempt ${attempt}/${totalAttempts} ` +
|
|
486
|
+
`(harper deploy exited ${code}); ${retryReason} — retrying in ${Math.round(waitMs / 1000)}s...`);
|
|
487
|
+
opts.onProgress?.(`replication did not converge on attempt ${attempt}/${totalAttempts} — retrying in ${Math.round(waitMs / 1000)}s...`);
|
|
488
|
+
await sleep(waitMs);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (opts.ignoreReplicationErrors) {
|
|
308
493
|
console.warn(`⚠ flair deploy: peer replication still failing after ${attempt} attempt(s), ` +
|
|
309
494
|
`but --ignore-replication-errors is set — treating this as a WARNED SUCCESS ` +
|
|
310
495
|
`(deployed to the origin node only; the peer will need to catch up via normal ` +
|
|
311
496
|
`federation sync or a later deploy).`);
|
|
312
497
|
opts.onProgress?.(`WARNING: proceeding origin-only — peer replication did not complete after ${attempt} attempt(s)`);
|
|
313
|
-
return { replicationWarning: true };
|
|
498
|
+
return { replicationWarning: true, convergedAfterReplicationError: false };
|
|
314
499
|
}
|
|
315
|
-
|
|
316
|
-
throw new Error(`harper deploy exited with code ${code}: peer replication failed after ${attempt} attempt(s) ` +
|
|
317
|
-
`(retries exhausted). Pass --ignore-replication-errors to accept an origin-only deploy, ` +
|
|
318
|
-
`or re-run once the peer link recovers.`);
|
|
319
|
-
}
|
|
320
|
-
throw new Error(`harper deploy exited with code ${code}`);
|
|
500
|
+
throw new Error(describeDeployFailure(failures, lastConvergence));
|
|
321
501
|
}
|
|
322
502
|
// Unreachable — the loop always returns or throws — but keeps TS happy.
|
|
323
503
|
throw new Error("harper deploy: exhausted retry loop without resolving");
|
|
@@ -427,7 +607,7 @@ export async function deploy(opts) {
|
|
|
427
607
|
CLI_TARGET_USERNAME: opts.fabricUser,
|
|
428
608
|
CLI_TARGET_PASSWORD: opts.fabricPassword,
|
|
429
609
|
};
|
|
430
|
-
const { replicationWarning } = await runHarperDeploy(harperBin, args, packageRoot, childEnv, opts);
|
|
610
|
+
const { replicationWarning, convergedAfterReplicationError } = await runHarperDeploy(harperBin, args, packageRoot, childEnv, opts, url, project);
|
|
431
611
|
// harper can print "Successfully deployed" for a component that isn't
|
|
432
612
|
// actually serving anything (the incident this closes: an empty deploy,
|
|
433
613
|
// reported success, /Memory 404ing in prod). Verify by curling the served
|
|
@@ -443,5 +623,13 @@ export async function deploy(opts) {
|
|
|
443
623
|
onProgress: opts.onProgress,
|
|
444
624
|
});
|
|
445
625
|
}
|
|
446
|
-
return {
|
|
626
|
+
return {
|
|
627
|
+
url,
|
|
628
|
+
project,
|
|
629
|
+
version,
|
|
630
|
+
packageRoot,
|
|
631
|
+
dryRun: false,
|
|
632
|
+
replicationWarning,
|
|
633
|
+
convergedAfterReplicationError,
|
|
634
|
+
};
|
|
447
635
|
}
|
package/dist/fabric-upgrade.js
CHANGED
|
@@ -373,6 +373,14 @@ export async function fabricUpgrade(opts, injected) {
|
|
|
373
373
|
restart: opts.restart,
|
|
374
374
|
replicated: opts.replicated,
|
|
375
375
|
packageRoot,
|
|
376
|
+
// flair#878 — see FabricUpgradeOptions: these used to stop at this
|
|
377
|
+
// boundary, which is why the upgrade path could not act on harper's own
|
|
378
|
+
// "pass ignore_replication_errors: true" advice.
|
|
379
|
+
ignoreReplicationErrors: opts.ignoreReplicationErrors,
|
|
380
|
+
deployRetries: opts.deployRetries,
|
|
381
|
+
convergenceCheck: opts.convergenceCheck,
|
|
382
|
+
convergenceTimeoutMs: opts.convergenceTimeoutMs,
|
|
383
|
+
onProgress: (m) => log(` ${m}`),
|
|
376
384
|
});
|
|
377
385
|
// Step 4: verify the deployed version (best-effort).
|
|
378
386
|
let verifiedVersion = null;
|
|
@@ -396,7 +404,14 @@ export async function fabricUpgrade(opts, injected) {
|
|
|
396
404
|
else {
|
|
397
405
|
log(`✓ Deployed ${result.version} (Fabric did not report a version to verify against)`);
|
|
398
406
|
}
|
|
399
|
-
return {
|
|
407
|
+
return {
|
|
408
|
+
plan,
|
|
409
|
+
deployed: true,
|
|
410
|
+
verifiedVersion,
|
|
411
|
+
stagingDir,
|
|
412
|
+
replicationWarning: result.replicationWarning,
|
|
413
|
+
convergedAfterReplicationError: result.convergedAfterReplicationError,
|
|
414
|
+
};
|
|
400
415
|
}
|
|
401
416
|
finally {
|
|
402
417
|
// SAFETY: always clean up the temp dir.
|