@wopr-network/platform-core 1.23.0 → 1.25.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.
@@ -1,3 +1,4 @@
1
+ export * from "./init-fleet-updater.js";
1
2
  export * from "./repository-types.js";
2
3
  export * from "./rollout-orchestrator.js";
3
4
  export * from "./rollout-strategy.js";
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Wires the fleet auto-update pipeline: ImagePoller → RolloutOrchestrator → ContainerUpdater.
3
+ *
4
+ * Consumers call initFleetUpdater() with a Docker instance, FleetManager, and config.
5
+ * The pipeline detects new image digests, batches updates via a rollout strategy,
6
+ * snapshots volumes before updating, and restores on failure (nuclear rollback).
7
+ *
8
+ * When a new image digest is detected for ANY bot, the orchestrator triggers a
9
+ * fleet-wide rollout across all non-manual bots. This is intentional: the managed
10
+ * Paperclip image is shared across all tenants, so a single digest change means
11
+ * all bots need updating.
12
+ */
13
+
14
+ import type Docker from "dockerode";
15
+ import { logger } from "../config/logger.js";
16
+ import type { IBotProfileRepository } from "./bot-profile-repository.js";
17
+ import type { FleetManager } from "./fleet-manager.js";
18
+ import { ImagePoller } from "./image-poller.js";
19
+ import type { IProfileStore } from "./profile-store.js";
20
+ import { RolloutOrchestrator, type RolloutResult } from "./rollout-orchestrator.js";
21
+ import { createRolloutStrategy, type RollingWaveOptions } from "./rollout-strategy.js";
22
+ import { ContainerUpdater } from "./updater.js";
23
+ import { VolumeSnapshotManager } from "./volume-snapshot-manager.js";
24
+
25
+ export interface FleetUpdaterConfig {
26
+ /** Rollout strategy type. Default: "rolling-wave" */
27
+ strategy?: "rolling-wave" | "single-bot" | "immediate";
28
+ /** Options for RollingWaveStrategy (ignored for other strategies) */
29
+ strategyOptions?: RollingWaveOptions;
30
+ /** Directory for volume snapshots. Default: "/data/fleet/snapshots" */
31
+ snapshotDir?: string;
32
+ /** Called after each bot update */
33
+ onBotUpdated?: (result: { botId: string; success: boolean; volumeRestored: boolean }) => void;
34
+ /** Called when a rollout completes */
35
+ onRolloutComplete?: (result: RolloutResult) => void;
36
+ }
37
+
38
+ export interface FleetUpdaterHandle {
39
+ poller: ImagePoller;
40
+ updater: ContainerUpdater;
41
+ orchestrator: RolloutOrchestrator;
42
+ snapshotManager: VolumeSnapshotManager;
43
+ /** Stop the poller and wait for any active rollout to finish */
44
+ stop: () => Promise<void>;
45
+ }
46
+
47
+ /**
48
+ * Initialize the fleet auto-update pipeline.
49
+ *
50
+ * Creates and wires: ImagePoller → RolloutOrchestrator → ContainerUpdater
51
+ * with VolumeSnapshotManager for nuclear rollback.
52
+ *
53
+ * @param docker - Dockerode instance for container operations
54
+ * @param fleet - FleetManager for container lifecycle
55
+ * @param profileStore - Legacy IProfileStore (used by ImagePoller/ContainerUpdater)
56
+ * @param profileRepo - PostgreSQL-backed IBotProfileRepository (used for updatable profile queries)
57
+ * @param config - Optional pipeline configuration
58
+ */
59
+ export function initFleetUpdater(
60
+ docker: Docker,
61
+ fleet: FleetManager,
62
+ profileStore: IProfileStore,
63
+ profileRepo: IBotProfileRepository,
64
+ config: FleetUpdaterConfig = {},
65
+ ): FleetUpdaterHandle {
66
+ const {
67
+ strategy: strategyType = "rolling-wave",
68
+ strategyOptions,
69
+ snapshotDir = "/data/fleet/snapshots",
70
+ onBotUpdated,
71
+ onRolloutComplete,
72
+ } = config;
73
+
74
+ const poller = new ImagePoller(docker, profileStore);
75
+ const updater = new ContainerUpdater(docker, profileStore, fleet, poller);
76
+ const snapshotManager = new VolumeSnapshotManager(docker, snapshotDir);
77
+ const strategy = createRolloutStrategy(strategyType, strategyOptions);
78
+
79
+ const orchestrator = new RolloutOrchestrator({
80
+ updater,
81
+ snapshotManager,
82
+ strategy,
83
+ getUpdatableProfiles: async () => {
84
+ const profiles = await profileRepo.list();
85
+ return profiles.filter((p) => p.updatePolicy !== "manual");
86
+ },
87
+ onBotUpdated,
88
+ onRolloutComplete,
89
+ });
90
+
91
+ // Wire the detection → orchestration pipeline.
92
+ // Any digest change triggers a fleet-wide rollout because the managed image
93
+ // is shared across all tenants — one new digest means all bots need updating.
94
+ poller.onUpdateAvailable = async (_botId: string, _newDigest: string) => {
95
+ if (orchestrator.isRolling) {
96
+ logger.debug("Skipping update trigger — rollout already in progress");
97
+ return;
98
+ }
99
+ logger.info("New image digest detected — starting fleet-wide rollout");
100
+ await orchestrator.rollout().catch((err) => {
101
+ logger.error("Rollout failed", { err });
102
+ });
103
+ };
104
+
105
+ // Start polling
106
+ poller.start().catch((err) => {
107
+ logger.error("ImagePoller failed to start", { err });
108
+ });
109
+
110
+ logger.info("Fleet auto-update pipeline initialized", {
111
+ strategy: strategyType,
112
+ snapshotDir,
113
+ });
114
+
115
+ return {
116
+ poller,
117
+ updater,
118
+ orchestrator,
119
+ snapshotManager,
120
+ stop: async () => {
121
+ poller.stop();
122
+ // Wait for any in-flight rollout to complete before returning
123
+ if (orchestrator.isRolling) {
124
+ logger.info("Waiting for active rollout to finish before shutdown...");
125
+ // Poll until rollout finishes (max 5 minutes)
126
+ const deadline = Date.now() + 5 * 60 * 1000;
127
+ while (orchestrator.isRolling && Date.now() < deadline) {
128
+ await new Promise((r) => setTimeout(r, 1000));
129
+ }
130
+ }
131
+ logger.info("Fleet auto-update pipeline stopped");
132
+ },
133
+ };
134
+ }