@synth-deploy/server 0.1.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/dist/agent/debrief-retention.d.ts +12 -0
- package/dist/agent/debrief-retention.d.ts.map +1 -0
- package/dist/agent/debrief-retention.js +27 -0
- package/dist/agent/debrief-retention.js.map +1 -0
- package/dist/agent/envoy-client.d.ts +216 -0
- package/dist/agent/envoy-client.d.ts.map +1 -0
- package/dist/agent/envoy-client.js +266 -0
- package/dist/agent/envoy-client.js.map +1 -0
- package/dist/agent/envoy-registry.d.ts +102 -0
- package/dist/agent/envoy-registry.d.ts.map +1 -0
- package/dist/agent/envoy-registry.js +319 -0
- package/dist/agent/envoy-registry.js.map +1 -0
- package/dist/agent/health-checker.d.ts +39 -0
- package/dist/agent/health-checker.d.ts.map +1 -0
- package/dist/agent/health-checker.js +49 -0
- package/dist/agent/health-checker.js.map +1 -0
- package/dist/agent/mcp-client-manager.d.ts +36 -0
- package/dist/agent/mcp-client-manager.d.ts.map +1 -0
- package/dist/agent/mcp-client-manager.js +106 -0
- package/dist/agent/mcp-client-manager.js.map +1 -0
- package/dist/agent/stale-deployment-detector.d.ts +15 -0
- package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
- package/dist/agent/stale-deployment-detector.js +50 -0
- package/dist/agent/stale-deployment-detector.js.map +1 -0
- package/dist/agent/step-runner.d.ts +31 -0
- package/dist/agent/step-runner.d.ts.map +1 -0
- package/dist/agent/step-runner.js +80 -0
- package/dist/agent/step-runner.js.map +1 -0
- package/dist/agent/synth-agent.d.ts +168 -0
- package/dist/agent/synth-agent.d.ts.map +1 -0
- package/dist/agent/synth-agent.js +1195 -0
- package/dist/agent/synth-agent.js.map +1 -0
- package/dist/api/agent.d.ts +36 -0
- package/dist/api/agent.d.ts.map +1 -0
- package/dist/api/agent.js +867 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/api-keys.d.ts +4 -0
- package/dist/api/api-keys.d.ts.map +1 -0
- package/dist/api/api-keys.js +118 -0
- package/dist/api/api-keys.js.map +1 -0
- package/dist/api/artifacts.d.ts +5 -0
- package/dist/api/artifacts.d.ts.map +1 -0
- package/dist/api/artifacts.js +142 -0
- package/dist/api/artifacts.js.map +1 -0
- package/dist/api/auth.d.ts +4 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +280 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/deployments.d.ts +11 -0
- package/dist/api/deployments.d.ts.map +1 -0
- package/dist/api/deployments.js +1098 -0
- package/dist/api/deployments.js.map +1 -0
- package/dist/api/environments.d.ts +5 -0
- package/dist/api/environments.d.ts.map +1 -0
- package/dist/api/environments.js +69 -0
- package/dist/api/environments.js.map +1 -0
- package/dist/api/envoy-reports.d.ts +17 -0
- package/dist/api/envoy-reports.d.ts.map +1 -0
- package/dist/api/envoy-reports.js +138 -0
- package/dist/api/envoy-reports.js.map +1 -0
- package/dist/api/envoys.d.ts +5 -0
- package/dist/api/envoys.d.ts.map +1 -0
- package/dist/api/envoys.js +192 -0
- package/dist/api/envoys.js.map +1 -0
- package/dist/api/fleet.d.ts +11 -0
- package/dist/api/fleet.d.ts.map +1 -0
- package/dist/api/fleet.js +394 -0
- package/dist/api/fleet.js.map +1 -0
- package/dist/api/graph.d.ts +8 -0
- package/dist/api/graph.d.ts.map +1 -0
- package/dist/api/graph.js +355 -0
- package/dist/api/graph.js.map +1 -0
- package/dist/api/health.d.ts +20 -0
- package/dist/api/health.d.ts.map +1 -0
- package/dist/api/health.js +248 -0
- package/dist/api/health.js.map +1 -0
- package/dist/api/idp-schemas.d.ts +41 -0
- package/dist/api/idp-schemas.d.ts.map +1 -0
- package/dist/api/idp-schemas.js +17 -0
- package/dist/api/idp-schemas.js.map +1 -0
- package/dist/api/idp.d.ts +6 -0
- package/dist/api/idp.d.ts.map +1 -0
- package/dist/api/idp.js +620 -0
- package/dist/api/idp.js.map +1 -0
- package/dist/api/intake.d.ts +10 -0
- package/dist/api/intake.d.ts.map +1 -0
- package/dist/api/intake.js +418 -0
- package/dist/api/intake.js.map +1 -0
- package/dist/api/partitions.d.ts +5 -0
- package/dist/api/partitions.d.ts.map +1 -0
- package/dist/api/partitions.js +113 -0
- package/dist/api/partitions.js.map +1 -0
- package/dist/api/progress-event-store.d.ts +62 -0
- package/dist/api/progress-event-store.d.ts.map +1 -0
- package/dist/api/progress-event-store.js +118 -0
- package/dist/api/progress-event-store.js.map +1 -0
- package/dist/api/schemas.d.ts +1000 -0
- package/dist/api/schemas.d.ts.map +1 -0
- package/dist/api/schemas.js +328 -0
- package/dist/api/schemas.js.map +1 -0
- package/dist/api/security-boundaries.d.ts +4 -0
- package/dist/api/security-boundaries.d.ts.map +1 -0
- package/dist/api/security-boundaries.js +32 -0
- package/dist/api/security-boundaries.js.map +1 -0
- package/dist/api/settings.d.ts +4 -0
- package/dist/api/settings.d.ts.map +1 -0
- package/dist/api/settings.js +99 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/system.d.ts +75 -0
- package/dist/api/system.d.ts.map +1 -0
- package/dist/api/system.js +558 -0
- package/dist/api/system.js.map +1 -0
- package/dist/api/telemetry.d.ts +4 -0
- package/dist/api/telemetry.d.ts.map +1 -0
- package/dist/api/telemetry.js +24 -0
- package/dist/api/telemetry.js.map +1 -0
- package/dist/api/users.d.ts +4 -0
- package/dist/api/users.d.ts.map +1 -0
- package/dist/api/users.js +173 -0
- package/dist/api/users.js.map +1 -0
- package/dist/archive-unpacker.d.ts +24 -0
- package/dist/archive-unpacker.d.ts.map +1 -0
- package/dist/archive-unpacker.js +239 -0
- package/dist/archive-unpacker.js.map +1 -0
- package/dist/artifact-analyzer.d.ts +59 -0
- package/dist/artifact-analyzer.d.ts.map +1 -0
- package/dist/artifact-analyzer.js +334 -0
- package/dist/artifact-analyzer.js.map +1 -0
- package/dist/auth/idp/index.d.ts +9 -0
- package/dist/auth/idp/index.d.ts.map +1 -0
- package/dist/auth/idp/index.js +5 -0
- package/dist/auth/idp/index.js.map +1 -0
- package/dist/auth/idp/ldap.d.ts +56 -0
- package/dist/auth/idp/ldap.d.ts.map +1 -0
- package/dist/auth/idp/ldap.js +276 -0
- package/dist/auth/idp/ldap.js.map +1 -0
- package/dist/auth/idp/oidc.d.ts +27 -0
- package/dist/auth/idp/oidc.d.ts.map +1 -0
- package/dist/auth/idp/oidc.js +97 -0
- package/dist/auth/idp/oidc.js.map +1 -0
- package/dist/auth/idp/role-mapping.d.ts +9 -0
- package/dist/auth/idp/role-mapping.d.ts.map +1 -0
- package/dist/auth/idp/role-mapping.js +16 -0
- package/dist/auth/idp/role-mapping.js.map +1 -0
- package/dist/auth/idp/saml.d.ts +40 -0
- package/dist/auth/idp/saml.d.ts.map +1 -0
- package/dist/auth/idp/saml.js +117 -0
- package/dist/auth/idp/saml.js.map +1 -0
- package/dist/auth/idp/types.d.ts +23 -0
- package/dist/auth/idp/types.d.ts.map +1 -0
- package/dist/auth/idp/types.js +2 -0
- package/dist/auth/idp/types.js.map +1 -0
- package/dist/fleet/fleet-executor.d.ts +35 -0
- package/dist/fleet/fleet-executor.d.ts.map +1 -0
- package/dist/fleet/fleet-executor.js +228 -0
- package/dist/fleet/fleet-executor.js.map +1 -0
- package/dist/fleet/fleet-store.d.ts +13 -0
- package/dist/fleet/fleet-store.d.ts.map +1 -0
- package/dist/fleet/fleet-store.js +13 -0
- package/dist/fleet/fleet-store.js.map +1 -0
- package/dist/fleet/index.d.ts +5 -0
- package/dist/fleet/index.d.ts.map +1 -0
- package/dist/fleet/index.js +4 -0
- package/dist/fleet/index.js.map +1 -0
- package/dist/fleet/representative-selector.d.ts +15 -0
- package/dist/fleet/representative-selector.d.ts.map +1 -0
- package/dist/fleet/representative-selector.js +71 -0
- package/dist/fleet/representative-selector.js.map +1 -0
- package/dist/graph/graph-executor.d.ts +36 -0
- package/dist/graph/graph-executor.d.ts.map +1 -0
- package/dist/graph/graph-executor.js +348 -0
- package/dist/graph/graph-executor.js.map +1 -0
- package/dist/graph/graph-inference.d.ts +22 -0
- package/dist/graph/graph-inference.d.ts.map +1 -0
- package/dist/graph/graph-inference.js +149 -0
- package/dist/graph/graph-inference.js.map +1 -0
- package/dist/graph/graph-store.d.ts +12 -0
- package/dist/graph/graph-store.d.ts.map +1 -0
- package/dist/graph/graph-store.js +61 -0
- package/dist/graph/graph-store.js.map +1 -0
- package/dist/graph/index.d.ts +5 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +837 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/index.d.ts +6 -0
- package/dist/intake/index.d.ts.map +1 -0
- package/dist/intake/index.js +5 -0
- package/dist/intake/index.js.map +1 -0
- package/dist/intake/intake-processor.d.ts +17 -0
- package/dist/intake/intake-processor.d.ts.map +1 -0
- package/dist/intake/intake-processor.js +99 -0
- package/dist/intake/intake-processor.js.map +1 -0
- package/dist/intake/intake-store.d.ts +7 -0
- package/dist/intake/intake-store.d.ts.map +1 -0
- package/dist/intake/intake-store.js +7 -0
- package/dist/intake/intake-store.js.map +1 -0
- package/dist/intake/registry-poller.d.ts +41 -0
- package/dist/intake/registry-poller.d.ts.map +1 -0
- package/dist/intake/registry-poller.js +202 -0
- package/dist/intake/registry-poller.js.map +1 -0
- package/dist/intake/webhook-handlers.d.ts +37 -0
- package/dist/intake/webhook-handlers.d.ts.map +1 -0
- package/dist/intake/webhook-handlers.js +268 -0
- package/dist/intake/webhook-handlers.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/resources.d.ts +9 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +88 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/middleware/auth.d.ts +29 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +76 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/permissions.d.ts +13 -0
- package/dist/middleware/permissions.d.ts.map +1 -0
- package/dist/middleware/permissions.js +32 -0
- package/dist/middleware/permissions.js.map +1 -0
- package/dist/pattern-store.d.ts +104 -0
- package/dist/pattern-store.d.ts.map +1 -0
- package/dist/pattern-store.js +299 -0
- package/dist/pattern-store.js.map +1 -0
- package/package.json +54 -0
- package/src/agent/debrief-retention.ts +44 -0
- package/src/agent/envoy-client.ts +474 -0
- package/src/agent/envoy-registry.ts +384 -0
- package/src/agent/health-checker.ts +70 -0
- package/src/agent/mcp-client-manager.ts +131 -0
- package/src/agent/stale-deployment-detector.ts +79 -0
- package/src/agent/step-runner.ts +124 -0
- package/src/agent/synth-agent.ts +1567 -0
- package/src/api/agent.ts +1075 -0
- package/src/api/api-keys.ts +129 -0
- package/src/api/artifacts.ts +194 -0
- package/src/api/auth.ts +320 -0
- package/src/api/deployments.ts +1347 -0
- package/src/api/environments.ts +97 -0
- package/src/api/envoy-reports.ts +159 -0
- package/src/api/envoys.ts +237 -0
- package/src/api/fleet.ts +510 -0
- package/src/api/graph.ts +516 -0
- package/src/api/health.ts +311 -0
- package/src/api/idp-schemas.ts +19 -0
- package/src/api/idp.ts +735 -0
- package/src/api/intake.ts +537 -0
- package/src/api/partitions.ts +147 -0
- package/src/api/progress-event-store.ts +153 -0
- package/src/api/schemas.ts +376 -0
- package/src/api/security-boundaries.ts +54 -0
- package/src/api/settings.ts +118 -0
- package/src/api/system.ts +704 -0
- package/src/api/telemetry.ts +32 -0
- package/src/api/users.ts +210 -0
- package/src/archive-unpacker.ts +271 -0
- package/src/artifact-analyzer.ts +438 -0
- package/src/auth/idp/index.ts +8 -0
- package/src/auth/idp/ldap.ts +340 -0
- package/src/auth/idp/oidc.ts +117 -0
- package/src/auth/idp/role-mapping.ts +22 -0
- package/src/auth/idp/saml.ts +148 -0
- package/src/auth/idp/types.ts +22 -0
- package/src/fleet/fleet-executor.ts +309 -0
- package/src/fleet/fleet-store.ts +13 -0
- package/src/fleet/index.ts +4 -0
- package/src/fleet/representative-selector.ts +83 -0
- package/src/graph/graph-executor.ts +446 -0
- package/src/graph/graph-inference.ts +184 -0
- package/src/graph/graph-store.ts +75 -0
- package/src/graph/index.ts +4 -0
- package/src/index.ts +916 -0
- package/src/intake/index.ts +5 -0
- package/src/intake/intake-processor.ts +111 -0
- package/src/intake/intake-store.ts +7 -0
- package/src/intake/registry-poller.ts +230 -0
- package/src/intake/webhook-handlers.ts +328 -0
- package/src/logger.ts +19 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/server.ts +34 -0
- package/src/mcp/tools.ts +117 -0
- package/src/middleware/auth.ts +103 -0
- package/src/middleware/permissions.ts +35 -0
- package/src/pattern-store.ts +409 -0
- package/tests/agent-mode.test.ts +536 -0
- package/tests/api-handlers.test.ts +1245 -0
- package/tests/archive-unpacker.test.ts +179 -0
- package/tests/artifact-analyzer.test.ts +240 -0
- package/tests/auth-middleware.test.ts +189 -0
- package/tests/decision-diary.test.ts +957 -0
- package/tests/diary-reader.test.ts +782 -0
- package/tests/envoy-client.test.ts +342 -0
- package/tests/envoy-reports.test.ts +156 -0
- package/tests/mcp-tools.test.ts +213 -0
- package/tests/orchestration.test.ts +536 -0
- package/tests/partition-deletion.test.ts +143 -0
- package/tests/partition-isolation.test.ts +830 -0
- package/tests/pattern-store.test.ts +371 -0
- package/tests/rbac-enforcement.test.ts +409 -0
- package/tests/ssrf-validation.test.ts +56 -0
- package/tests/stale-deployment.test.ts +85 -0
- package/tests/step-runner.test.ts +308 -0
- package/tests/ui-journey.test.ts +330 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FleetDeployment,
|
|
3
|
+
FleetProgress,
|
|
4
|
+
FleetValidationResult,
|
|
5
|
+
EnvoyValidationResult,
|
|
6
|
+
DeploymentPlan,
|
|
7
|
+
RolloutConfig,
|
|
8
|
+
} from "@synth-deploy/core";
|
|
9
|
+
import type { EnvoyRegistry } from "../agent/envoy-registry.js";
|
|
10
|
+
import { EnvoyClient } from "../agent/envoy-client.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Fleet progress event types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface FleetProgressEvent {
|
|
17
|
+
type:
|
|
18
|
+
| "validation-started"
|
|
19
|
+
| "validation-complete"
|
|
20
|
+
| "batch-started"
|
|
21
|
+
| "envoy-started"
|
|
22
|
+
| "envoy-completed"
|
|
23
|
+
| "envoy-failed"
|
|
24
|
+
| "batch-completed"
|
|
25
|
+
| "fleet-completed"
|
|
26
|
+
| "fleet-failed"
|
|
27
|
+
| "fleet-paused";
|
|
28
|
+
envoyId?: string;
|
|
29
|
+
envoyName?: string;
|
|
30
|
+
batchIndex?: number;
|
|
31
|
+
progress: FleetProgress;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function delay(ms: number): Promise<void> {
|
|
40
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeProgress(
|
|
44
|
+
totalEnvoys: number,
|
|
45
|
+
validated: number,
|
|
46
|
+
executing: number,
|
|
47
|
+
succeeded: number,
|
|
48
|
+
failed: number,
|
|
49
|
+
currentBatch?: number,
|
|
50
|
+
totalBatches?: number,
|
|
51
|
+
): FleetProgress {
|
|
52
|
+
return {
|
|
53
|
+
totalEnvoys,
|
|
54
|
+
validated,
|
|
55
|
+
executing,
|
|
56
|
+
succeeded,
|
|
57
|
+
failed,
|
|
58
|
+
pending: totalEnvoys - succeeded - failed - executing,
|
|
59
|
+
currentBatch,
|
|
60
|
+
totalBatches,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// FleetExecutor — orchestrates validation and progressive rollout
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export class FleetExecutor {
|
|
69
|
+
constructor(
|
|
70
|
+
private envoyRegistry: EnvoyRegistry,
|
|
71
|
+
private createEnvoyClient: (url: string, token: string) => EnvoyClient,
|
|
72
|
+
) {}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validate a deployment plan against all target envoys in the fleet.
|
|
76
|
+
* Probes each envoy and asks it to validate the plan steps.
|
|
77
|
+
*/
|
|
78
|
+
async validateFleet(
|
|
79
|
+
fleetDeployment: FleetDeployment,
|
|
80
|
+
plan: DeploymentPlan,
|
|
81
|
+
): Promise<FleetValidationResult> {
|
|
82
|
+
const targetEnvoyIds =
|
|
83
|
+
fleetDeployment.envoyFilter ?? this.getEnvironmentEnvoyIds(fleetDeployment.environmentId);
|
|
84
|
+
|
|
85
|
+
const results: EnvoyValidationResult[] = [];
|
|
86
|
+
|
|
87
|
+
for (const envoyId of targetEnvoyIds) {
|
|
88
|
+
const entry = this.envoyRegistry.get(envoyId);
|
|
89
|
+
if (!entry) {
|
|
90
|
+
results.push({
|
|
91
|
+
envoyId,
|
|
92
|
+
envoyName: "unknown",
|
|
93
|
+
validated: false,
|
|
94
|
+
issues: [`Envoy ${envoyId} not found in registry`],
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const client = this.createEnvoyClient(entry.url, entry.token);
|
|
100
|
+
try {
|
|
101
|
+
const validation = await client.validatePlan(plan.steps);
|
|
102
|
+
results.push({
|
|
103
|
+
envoyId,
|
|
104
|
+
envoyName: entry.name,
|
|
105
|
+
validated: validation.valid,
|
|
106
|
+
issues: validation.violations?.map((v) => v.reason),
|
|
107
|
+
});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
results.push({
|
|
110
|
+
envoyId,
|
|
111
|
+
envoyName: entry.name,
|
|
112
|
+
validated: false,
|
|
113
|
+
issues: [
|
|
114
|
+
`Validation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
total: results.length,
|
|
122
|
+
validated: results.filter((r) => r.validated).length,
|
|
123
|
+
failed: results.filter((r) => !r.validated).length,
|
|
124
|
+
results,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Execute a progressive rollout across the fleet.
|
|
130
|
+
* Yields progress events as an async generator so callers can stream them.
|
|
131
|
+
*/
|
|
132
|
+
async *executeRollout(
|
|
133
|
+
fleetDeployment: FleetDeployment,
|
|
134
|
+
plan: DeploymentPlan,
|
|
135
|
+
rollbackPlan?: DeploymentPlan,
|
|
136
|
+
): AsyncGenerator<FleetProgressEvent> {
|
|
137
|
+
const validatedEnvoys =
|
|
138
|
+
fleetDeployment.validationResult?.results.filter((r) => r.validated) ?? [];
|
|
139
|
+
const config = fleetDeployment.rolloutConfig;
|
|
140
|
+
const totalEnvoys = validatedEnvoys.length;
|
|
141
|
+
|
|
142
|
+
if (totalEnvoys === 0) {
|
|
143
|
+
yield {
|
|
144
|
+
type: "fleet-failed",
|
|
145
|
+
progress: makeProgress(0, 0, 0, 0, 0),
|
|
146
|
+
error: "No validated envoys to execute against",
|
|
147
|
+
};
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Build batches based on rollout strategy
|
|
152
|
+
const batches = this.buildBatches(validatedEnvoys, config);
|
|
153
|
+
const totalBatches = batches.length;
|
|
154
|
+
|
|
155
|
+
let succeeded = 0;
|
|
156
|
+
let failureCount = 0;
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < batches.length; i++) {
|
|
159
|
+
yield {
|
|
160
|
+
type: "batch-started",
|
|
161
|
+
batchIndex: i,
|
|
162
|
+
progress: makeProgress(totalEnvoys, totalEnvoys, batches[i].length, succeeded, failureCount, i, totalBatches),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
for (const envoy of batches[i]) {
|
|
166
|
+
yield {
|
|
167
|
+
type: "envoy-started",
|
|
168
|
+
envoyId: envoy.envoyId,
|
|
169
|
+
envoyName: envoy.envoyName,
|
|
170
|
+
batchIndex: i,
|
|
171
|
+
progress: makeProgress(totalEnvoys, totalEnvoys, 1, succeeded, failureCount, i, totalBatches),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const entry = this.envoyRegistry.get(envoy.envoyId);
|
|
175
|
+
if (!entry) {
|
|
176
|
+
failureCount++;
|
|
177
|
+
yield {
|
|
178
|
+
type: "envoy-failed",
|
|
179
|
+
envoyId: envoy.envoyId,
|
|
180
|
+
envoyName: envoy.envoyName,
|
|
181
|
+
batchIndex: i,
|
|
182
|
+
progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
|
|
183
|
+
error: `Envoy ${envoy.envoyId} not found in registry`,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (failureCount >= config.haltOnFailureCount) {
|
|
187
|
+
yield {
|
|
188
|
+
type: "fleet-failed",
|
|
189
|
+
progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
|
|
190
|
+
error: `Halted: ${failureCount} failure(s) reached threshold of ${config.haltOnFailureCount}`,
|
|
191
|
+
};
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const client = this.createEnvoyClient(entry.url, entry.token);
|
|
198
|
+
try {
|
|
199
|
+
await client.executeApprovedPlan({
|
|
200
|
+
deploymentId: fleetDeployment.id,
|
|
201
|
+
plan,
|
|
202
|
+
rollbackPlan: rollbackPlan ?? plan,
|
|
203
|
+
artifactType: "fleet",
|
|
204
|
+
artifactName: fleetDeployment.artifactId,
|
|
205
|
+
environmentId: fleetDeployment.environmentId,
|
|
206
|
+
});
|
|
207
|
+
succeeded++;
|
|
208
|
+
yield {
|
|
209
|
+
type: "envoy-completed",
|
|
210
|
+
envoyId: envoy.envoyId,
|
|
211
|
+
envoyName: envoy.envoyName,
|
|
212
|
+
batchIndex: i,
|
|
213
|
+
progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
|
|
214
|
+
};
|
|
215
|
+
} catch (err) {
|
|
216
|
+
failureCount++;
|
|
217
|
+
yield {
|
|
218
|
+
type: "envoy-failed",
|
|
219
|
+
envoyId: envoy.envoyId,
|
|
220
|
+
envoyName: envoy.envoyName,
|
|
221
|
+
batchIndex: i,
|
|
222
|
+
progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
|
|
223
|
+
error: err instanceof Error ? err.message : String(err),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (failureCount >= config.haltOnFailureCount) {
|
|
227
|
+
yield {
|
|
228
|
+
type: "fleet-failed",
|
|
229
|
+
progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
|
|
230
|
+
error: `Halted: ${failureCount} failure(s) reached threshold of ${config.haltOnFailureCount}`,
|
|
231
|
+
};
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
yield {
|
|
238
|
+
type: "batch-completed",
|
|
239
|
+
batchIndex: i,
|
|
240
|
+
progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Health check wait between batches
|
|
244
|
+
if (config.healthCheckWaitMs > 0 && i < batches.length - 1) {
|
|
245
|
+
await delay(config.healthCheckWaitMs);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Pause between batches if configured — yield paused event and return.
|
|
249
|
+
// Caller resumes by calling executeRollout again with remaining batches.
|
|
250
|
+
if (config.pauseBetweenBatches && i < batches.length - 1) {
|
|
251
|
+
yield {
|
|
252
|
+
type: "fleet-paused",
|
|
253
|
+
batchIndex: i,
|
|
254
|
+
progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, i, totalBatches),
|
|
255
|
+
};
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
yield {
|
|
261
|
+
type: "fleet-completed",
|
|
262
|
+
progress: makeProgress(totalEnvoys, totalEnvoys, 0, succeeded, failureCount, totalBatches - 1, totalBatches),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Build execution batches based on the rollout strategy.
|
|
268
|
+
*/
|
|
269
|
+
private buildBatches(
|
|
270
|
+
envoys: EnvoyValidationResult[],
|
|
271
|
+
config: RolloutConfig,
|
|
272
|
+
): EnvoyValidationResult[][] {
|
|
273
|
+
if (envoys.length === 0) return [];
|
|
274
|
+
|
|
275
|
+
if (config.strategy === "all-at-once") {
|
|
276
|
+
return [envoys];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (config.strategy === "canary") {
|
|
280
|
+
// First batch: single canary, second batch: everything else
|
|
281
|
+
return [[envoys[0]], envoys.slice(1)];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// batched strategy
|
|
285
|
+
const size =
|
|
286
|
+
config.batchSize ??
|
|
287
|
+
Math.max(1, Math.ceil(envoys.length * (config.batchPercent ?? 10) / 100));
|
|
288
|
+
|
|
289
|
+
const batches: EnvoyValidationResult[][] = [];
|
|
290
|
+
for (let i = 0; i < envoys.length; i += size) {
|
|
291
|
+
batches.push(envoys.slice(i, i + size));
|
|
292
|
+
}
|
|
293
|
+
return batches;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get all envoy IDs assigned to a given environment.
|
|
298
|
+
*/
|
|
299
|
+
private getEnvironmentEnvoyIds(environmentId: string): string[] {
|
|
300
|
+
return this.envoyRegistry
|
|
301
|
+
.list()
|
|
302
|
+
.filter(
|
|
303
|
+
(e) =>
|
|
304
|
+
e.assignedEnvironments.length === 0 ||
|
|
305
|
+
e.assignedEnvironments.includes(environmentId),
|
|
306
|
+
)
|
|
307
|
+
.map((e) => e.id);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports the persistent fleet deployment store from @synth-deploy/core.
|
|
3
|
+
* Fleet deployments are now SQLite-backed to survive server restarts.
|
|
4
|
+
*
|
|
5
|
+
* Note: In-flight fleet operations that are mid-execution when the server
|
|
6
|
+
* crashes cannot be resumed — their status is persisted, but the active
|
|
7
|
+
* orchestration state (batch progress, in-flight envoy connections) is
|
|
8
|
+
* ephemeral. On restart, in-flight operations will appear as stale entries
|
|
9
|
+
* that users can inspect and manually re-trigger. Terminal states (completed,
|
|
10
|
+
* failed, rolled_back) are fully durable.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { PersistentFleetDeploymentStore as FleetDeploymentStore } from "@synth-deploy/core";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { EnvoyRegistryEntry } from "../agent/envoy-registry.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maximum number of representative envoys to select for plan validation.
|
|
5
|
+
* Keeps validation fast while covering platform diversity.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_REPRESENTATIVES = 3;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Select a representative subset of envoys for plan validation.
|
|
11
|
+
*
|
|
12
|
+
* Strategy:
|
|
13
|
+
* 1. Filter to healthy envoys only (health === "OK")
|
|
14
|
+
* 2. Group by platform (e.g. linux, darwin, windows)
|
|
15
|
+
* 3. Pick one per platform group, preferring most recently seen
|
|
16
|
+
* 4. Cap at MAX_REPRESENTATIVES
|
|
17
|
+
* 5. If all envoys share the same platform, pick 1
|
|
18
|
+
*
|
|
19
|
+
* Returns an array of envoy IDs.
|
|
20
|
+
*/
|
|
21
|
+
export function selectRepresentatives(
|
|
22
|
+
envoys: EnvoyRegistryEntry[],
|
|
23
|
+
_artifactId: string,
|
|
24
|
+
): string[] {
|
|
25
|
+
// 1. Filter to healthy envoys
|
|
26
|
+
const healthy = envoys.filter((e) => e.health === "OK");
|
|
27
|
+
if (healthy.length === 0) return [];
|
|
28
|
+
|
|
29
|
+
// 2. Group by platform
|
|
30
|
+
const byPlatform = new Map<string, EnvoyRegistryEntry[]>();
|
|
31
|
+
for (const envoy of healthy) {
|
|
32
|
+
// Normalize: null/undefined platform treated as "unknown"
|
|
33
|
+
const platform = (envoy as { platform?: string | null }).platform ?? "unknown";
|
|
34
|
+
const group = byPlatform.get(platform) ?? [];
|
|
35
|
+
group.push(envoy);
|
|
36
|
+
byPlatform.set(platform, group);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 3. If all same platform, pick the most recently seen one
|
|
40
|
+
if (byPlatform.size <= 1) {
|
|
41
|
+
const best = pickMostRecentlySeen(healthy);
|
|
42
|
+
return best ? [best.id] : [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 4. Pick one per platform group (prefer most recently seen)
|
|
46
|
+
const representatives: EnvoyRegistryEntry[] = [];
|
|
47
|
+
for (const group of byPlatform.values()) {
|
|
48
|
+
const best = pickMostRecentlySeen(group);
|
|
49
|
+
if (best) representatives.push(best);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 5. Cap at MAX_REPRESENTATIVES (sort by lastSeen descending, take top N)
|
|
53
|
+
representatives.sort((a, b) => {
|
|
54
|
+
const aTime = a.lastSeen ? new Date(a.lastSeen).getTime() : 0;
|
|
55
|
+
const bTime = b.lastSeen ? new Date(b.lastSeen).getTime() : 0;
|
|
56
|
+
return bTime - aTime;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return representatives.slice(0, MAX_REPRESENTATIVES).map((e) => e.id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pick the envoy with the most recent `lastSeen` timestamp from a group.
|
|
64
|
+
*/
|
|
65
|
+
function pickMostRecentlySeen(
|
|
66
|
+
group: EnvoyRegistryEntry[],
|
|
67
|
+
): EnvoyRegistryEntry | undefined {
|
|
68
|
+
if (group.length === 0) return undefined;
|
|
69
|
+
if (group.length === 1) return group[0];
|
|
70
|
+
|
|
71
|
+
let best = group[0];
|
|
72
|
+
let bestTime = best.lastSeen ? new Date(best.lastSeen).getTime() : 0;
|
|
73
|
+
|
|
74
|
+
for (let i = 1; i < group.length; i++) {
|
|
75
|
+
const t = group[i].lastSeen ? new Date(group[i].lastSeen!).getTime() : 0;
|
|
76
|
+
if (t > bestTime) {
|
|
77
|
+
best = group[i];
|
|
78
|
+
bestTime = t;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return best;
|
|
83
|
+
}
|