@kadi.build/deploy-ability 0.0.5 → 0.0.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/dist/errors/certificate-error.d.ts +2 -0
- package/dist/errors/certificate-error.d.ts.map +1 -1
- package/dist/errors/certificate-error.js +2 -0
- package/dist/errors/certificate-error.js.map +1 -1
- package/dist/targets/akash/certificate-manager.d.ts +31 -0
- package/dist/targets/akash/certificate-manager.d.ts.map +1 -1
- package/dist/targets/akash/certificate-manager.js +75 -1
- package/dist/targets/akash/certificate-manager.js.map +1 -1
- package/dist/targets/akash/index.d.ts +2 -2
- package/dist/targets/akash/index.d.ts.map +1 -1
- package/dist/targets/akash/index.js.map +1 -1
- package/package.json +1 -1
- package/docs/KADI_ABILITY_CONVERSION.md +0 -1365
- package/docs/PIPELINE_BUILDER_DESIGN.md +0 -1149
|
@@ -1,1149 +0,0 @@
|
|
|
1
|
-
# Pipeline Builder Pattern Design Proposal
|
|
2
|
-
|
|
3
|
-
> **Status**: Draft Proposal
|
|
4
|
-
> **Author**: Design discussion with Claude Code
|
|
5
|
-
> **Date**: 2025-11-18
|
|
6
|
-
> **Target**: deploy-ability v0.1.0
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## Table of Contents
|
|
11
|
-
|
|
12
|
-
1. [Executive Summary](#executive-summary)
|
|
13
|
-
2. [Current Design & Problems](#current-design--problems)
|
|
14
|
-
3. [Proposed Solution: Pipeline Builder](#proposed-solution-pipeline-builder)
|
|
15
|
-
4. [API Design](#api-design)
|
|
16
|
-
5. [Before/After Comparison](#beforeafter-comparison)
|
|
17
|
-
6. [Implementation Plan](#implementation-plan)
|
|
18
|
-
7. [Migration Strategy](#migration-strategy)
|
|
19
|
-
8. [Benefits & Trade-offs](#benefits--trade-offs)
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Executive Summary
|
|
24
|
-
|
|
25
|
-
**Problem**: The current deploy-ability API has excellent separation of concerns but poor discoverability and error-prone usage patterns. Users must manually orchestrate registry setup, profile transformation, and cleanup - leading to easy mistakes and confusing errors.
|
|
26
|
-
|
|
27
|
-
**Solution**: Introduce a fluent Pipeline Builder API that makes deployment steps explicit, discoverable, and safe by default. The pipeline handles orchestration, lifecycle management, and automatic cleanup while preserving the underlying composable primitives.
|
|
28
|
-
|
|
29
|
-
**Impact**:
|
|
30
|
-
- **Users**: Simpler, safer API that's hard to misuse
|
|
31
|
-
- **Library**: Backward compatible - builder is new, primitives remain unchanged
|
|
32
|
-
- **Code Quality**: Reduced boilerplate, automatic resource cleanup, better error messages
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Current Design & Problems
|
|
37
|
-
|
|
38
|
-
### How It Works Today
|
|
39
|
-
|
|
40
|
-
The current design separates concerns across multiple layers:
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
44
|
-
│ Layer 1: kadi-deploy (CLI Orchestration) │
|
|
45
|
-
│ │
|
|
46
|
-
│ 1. Load profile from agent.json │
|
|
47
|
-
│ 2. Call setupRegistryIfNeeded() → returns transformed │
|
|
48
|
-
│ 3. Call deployToAkash() with transformed profile │
|
|
49
|
-
│ 4. Remember to call cleanup() in finally block │
|
|
50
|
-
└─────────────────────────────────────────────────────────────┘
|
|
51
|
-
↓
|
|
52
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
53
|
-
│ Layer 2: deploy-ability (Library Primitives) │
|
|
54
|
-
│ │
|
|
55
|
-
│ • setupRegistryIfNeeded() - Optional pre-processing │
|
|
56
|
-
│ • deployToAkash() - Core deployment logic │
|
|
57
|
-
│ • loadProfile() - Profile loading │
|
|
58
|
-
│ • generateAkashSdl() - SDL generation │
|
|
59
|
-
└─────────────────────────────────────────────────────────────┘
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### Current Usage Pattern (kadi-deploy)
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
// File: kadi-deploy/src/targets/akash/akash.ts
|
|
66
|
-
|
|
67
|
-
export async function deployToAkash(ctx: DeploymentContext) {
|
|
68
|
-
const { profile, wallet, certificate, logger } = ctx;
|
|
69
|
-
|
|
70
|
-
// Step 1: Manually setup registry (easy to forget!)
|
|
71
|
-
const registryCtx = await setupRegistryIfNeeded(
|
|
72
|
-
profile,
|
|
73
|
-
logger,
|
|
74
|
-
{
|
|
75
|
-
containerEngine: 'docker',
|
|
76
|
-
tunnelService: 'serveo'
|
|
77
|
-
}
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
// Step 2: Remember the cleanup function for later
|
|
81
|
-
const cleanup = registryCtx.cleanup;
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
// Step 3: Call deployment with transformed profile
|
|
85
|
-
const result = await deployToAkash({
|
|
86
|
-
loadedProfile: {
|
|
87
|
-
profile: registryCtx.deployableProfile // ← Transformed!
|
|
88
|
-
},
|
|
89
|
-
wallet,
|
|
90
|
-
certificate,
|
|
91
|
-
bidSelector: selectCheapestBid,
|
|
92
|
-
logger
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (!result.success) {
|
|
96
|
-
throw new Error(result.error.message);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return result.data;
|
|
100
|
-
|
|
101
|
-
} finally {
|
|
102
|
-
// Step 4: Manual cleanup (easy to forget or get wrong!)
|
|
103
|
-
await cleanup();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Problems with Current Design
|
|
109
|
-
|
|
110
|
-
#### 1. **Discoverability Problem**
|
|
111
|
-
|
|
112
|
-
Looking at `deployToAkash()` signature, there's NO indication that local images require special handling:
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
// What the signature shows:
|
|
116
|
-
export async function deployToAkash(
|
|
117
|
-
params: AkashDeploymentExecution
|
|
118
|
-
): Promise<AkashDeploymentResult>
|
|
119
|
-
|
|
120
|
-
// What it DOESN'T show:
|
|
121
|
-
// ⚠️ If your profile uses local images, call setupRegistryIfNeeded() first!
|
|
122
|
-
// ⚠️ Don't forget to call the cleanup function!
|
|
123
|
-
// ⚠️ Use a try-finally block to ensure cleanup happens!
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
**Result**: New users (or even experienced ones in a hurry) will write:
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
// This looks right but FAILS at runtime! ❌
|
|
130
|
-
const result = await deployToAkash({
|
|
131
|
-
projectRoot: './',
|
|
132
|
-
profile: 'my-profile', // Contains "image": "my-app:latest"
|
|
133
|
-
wallet,
|
|
134
|
-
certificate,
|
|
135
|
-
bidSelector: selectCheapestBid
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Error (on Akash provider, minutes later):
|
|
139
|
-
// "Failed to pull image: my-app:latest - not found"
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
The error happens **remotely** on the Akash provider, not locally where you can catch it early.
|
|
143
|
-
|
|
144
|
-
#### 2. **Manual Lifecycle Management**
|
|
145
|
-
|
|
146
|
-
Users must remember to:
|
|
147
|
-
- Call `setupRegistryIfNeeded()` before deployment
|
|
148
|
-
- Store the `cleanup` function
|
|
149
|
-
- Call `cleanup()` in a finally block
|
|
150
|
-
- Handle cleanup errors gracefully
|
|
151
|
-
|
|
152
|
-
**This is error-prone**:
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
// What if deployment throws before cleanup is set up?
|
|
156
|
-
const registryCtx = await setupRegistryIfNeeded(...);
|
|
157
|
-
const cleanup = registryCtx.cleanup; // ← Not in try block yet!
|
|
158
|
-
|
|
159
|
-
// What if user forgets finally block?
|
|
160
|
-
try {
|
|
161
|
-
await deployToAkash(...);
|
|
162
|
-
} catch (error) {
|
|
163
|
-
console.error(error);
|
|
164
|
-
return; // ← Forgot cleanup! Registry keeps running!
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// What if cleanup throws?
|
|
168
|
-
finally {
|
|
169
|
-
await cleanup(); // ← Could throw and mask deployment errors
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
#### 3. **Split Brain Problem**
|
|
174
|
-
|
|
175
|
-
To understand the complete deployment flow, you need to know about:
|
|
176
|
-
- **deploy-ability**: The core primitives (`deployToAkash`, `setupRegistryIfNeeded`)
|
|
177
|
-
- **kadi-deploy**: The orchestration logic (how primitives are combined)
|
|
178
|
-
|
|
179
|
-
There's no single place that shows the "canonical way" to deploy with local images.
|
|
180
|
-
|
|
181
|
-
#### 4. **TypeScript Can't Help**
|
|
182
|
-
|
|
183
|
-
The type system doesn't encode the relationship between local images and registry setup:
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
// TypeScript is happy with this, but it's wrong at runtime:
|
|
187
|
-
const profile: AkashDeploymentProfile = {
|
|
188
|
-
target: 'akash',
|
|
189
|
-
services: {
|
|
190
|
-
app: {
|
|
191
|
-
image: 'my-local-image:latest' // ← Local image!
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// TypeScript doesn't warn you about this:
|
|
197
|
-
await deployToAkash({
|
|
198
|
-
loadedProfile: { profile }, // ← Missing registry setup!
|
|
199
|
-
wallet,
|
|
200
|
-
certificate
|
|
201
|
-
});
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
#### 5. **Verbose Boilerplate**
|
|
205
|
-
|
|
206
|
-
Every consumer of deploy-ability needs to write the same orchestration logic:
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
// This pattern repeats in every deployment tool:
|
|
210
|
-
const registryCtx = await setupRegistryIfNeeded(...);
|
|
211
|
-
try {
|
|
212
|
-
const result = await deployToAkash({
|
|
213
|
-
loadedProfile: { profile: registryCtx.deployableProfile },
|
|
214
|
-
...
|
|
215
|
-
});
|
|
216
|
-
return result;
|
|
217
|
-
} finally {
|
|
218
|
-
await registryCtx.cleanup();
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
---
|
|
223
|
-
|
|
224
|
-
## Proposed Solution: Pipeline Builder
|
|
225
|
-
|
|
226
|
-
### Core Concept
|
|
227
|
-
|
|
228
|
-
Introduce a **fluent builder API** that:
|
|
229
|
-
1. Makes deployment steps **explicit** and **discoverable**
|
|
230
|
-
2. Handles **lifecycle management** automatically
|
|
231
|
-
3. Provides **clear error messages** for missing steps
|
|
232
|
-
4. Remains **backward compatible** (keeps existing primitives)
|
|
233
|
-
|
|
234
|
-
### Mental Model
|
|
235
|
-
|
|
236
|
-
Think of deployment as building a pipeline with stages:
|
|
237
|
-
|
|
238
|
-
```
|
|
239
|
-
Profile Loading → Registry Setup → Wallet → Certificate → Execution → Cleanup
|
|
240
|
-
(Stage 1) (Stage 2) (Stage 3) (Stage 4) (Stage 5) (Auto)
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
Each stage:
|
|
244
|
-
- Is **explicit** (you call a method to add it)
|
|
245
|
-
- Is **validated** (builder checks you didn't skip required steps)
|
|
246
|
-
- Is **typed** (TypeScript guides you through the steps)
|
|
247
|
-
- Is **automatic** (lifecycle handled for you)
|
|
248
|
-
|
|
249
|
-
### Key Design Principles
|
|
250
|
-
|
|
251
|
-
1. **Fluent & Discoverable**: Chaining methods makes steps obvious
|
|
252
|
-
2. **Safe by Default**: Can't execute without required stages
|
|
253
|
-
3. **Automatic Cleanup**: Resources freed via RAII pattern
|
|
254
|
-
4. **Backward Compatible**: Original functions still work
|
|
255
|
-
5. **Progressive Disclosure**: Simple by default, powerful when needed
|
|
256
|
-
|
|
257
|
-
---
|
|
258
|
-
|
|
259
|
-
## API Design
|
|
260
|
-
|
|
261
|
-
### Basic Usage
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
import { DeploymentPipeline } from '@kadi.build/deploy-ability/akash';
|
|
265
|
-
|
|
266
|
-
// Simple deployment with local images
|
|
267
|
-
const result = await DeploymentPipeline
|
|
268
|
-
.create({ projectRoot: './', profile: 'production' })
|
|
269
|
-
.withLocalImageSupport({ tunnelService: 'serveo' })
|
|
270
|
-
.withWallet(wallet)
|
|
271
|
-
.withCertificate(certificate)
|
|
272
|
-
.execute();
|
|
273
|
-
|
|
274
|
-
// Cleanup happens automatically!
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### Advanced Usage
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
// Full control with custom bid selection
|
|
281
|
-
const result = await DeploymentPipeline
|
|
282
|
-
.create({
|
|
283
|
-
projectRoot: './',
|
|
284
|
-
profile: 'production',
|
|
285
|
-
network: 'mainnet'
|
|
286
|
-
})
|
|
287
|
-
.withLocalImageSupport({
|
|
288
|
-
tunnelService: 'ngrok',
|
|
289
|
-
tunnelAuthToken: process.env.NGROK_TOKEN
|
|
290
|
-
})
|
|
291
|
-
.withWallet(wallet)
|
|
292
|
-
.withCertificate(certificate)
|
|
293
|
-
.withBidSelector((bids) => {
|
|
294
|
-
const filtered = filterBids(bids, {
|
|
295
|
-
maxPricePerMonth: { usd: 50, aktPrice: 0.45 },
|
|
296
|
-
minUptime: { value: 0.95, period: '7d' }
|
|
297
|
-
});
|
|
298
|
-
return selectCheapestBid(filtered);
|
|
299
|
-
})
|
|
300
|
-
.withLogger(customLogger)
|
|
301
|
-
.execute();
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Conditional Registry Setup
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
// Automatic detection - registry only starts if needed
|
|
308
|
-
const result = await DeploymentPipeline
|
|
309
|
-
.create({ projectRoot: './', profile: 'production' })
|
|
310
|
-
.withLocalImageSupport() // ← Auto-detects, no-op if all remote
|
|
311
|
-
.withWallet(wallet)
|
|
312
|
-
.withCertificate(certificate)
|
|
313
|
-
.execute();
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
### Pre-loaded Profile (Advanced)
|
|
317
|
-
|
|
318
|
-
```typescript
|
|
319
|
-
// When you already have a transformed profile
|
|
320
|
-
const result = await DeploymentPipeline
|
|
321
|
-
.createFromProfile(transformedProfile, 'production')
|
|
322
|
-
.withWallet(wallet)
|
|
323
|
-
.withCertificate(certificate)
|
|
324
|
-
.execute();
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
### Dry Run Mode
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
// Generate SDL without deploying
|
|
331
|
-
const result = await DeploymentPipeline
|
|
332
|
-
.create({ projectRoot: './', profile: 'production' })
|
|
333
|
-
.dryRun();
|
|
334
|
-
|
|
335
|
-
console.log('SDL:', result.data.sdl);
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### Error Handling
|
|
339
|
-
|
|
340
|
-
```typescript
|
|
341
|
-
try {
|
|
342
|
-
const result = await DeploymentPipeline
|
|
343
|
-
.create({ projectRoot: './', profile: 'production' })
|
|
344
|
-
.withWallet(wallet)
|
|
345
|
-
.execute(); // ← Missing certificate!
|
|
346
|
-
|
|
347
|
-
} catch (error) {
|
|
348
|
-
if (error instanceof PipelineValidationError) {
|
|
349
|
-
console.error('Pipeline not configured correctly:', error.message);
|
|
350
|
-
// "Certificate is required. Call .withCertificate() before .execute()"
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
### Monitoring & Callbacks
|
|
356
|
-
|
|
357
|
-
```typescript
|
|
358
|
-
const result = await DeploymentPipeline
|
|
359
|
-
.create({ projectRoot: './', profile: 'production' })
|
|
360
|
-
.withLocalImageSupport()
|
|
361
|
-
.withWallet(wallet)
|
|
362
|
-
.withCertificate(certificate)
|
|
363
|
-
.onRegistryStarted((info) => {
|
|
364
|
-
console.log(`Registry available at: ${info.tunnelUrl}`);
|
|
365
|
-
})
|
|
366
|
-
.onBidsReceived((bids) => {
|
|
367
|
-
console.log(`Received ${bids.length} bids`);
|
|
368
|
-
})
|
|
369
|
-
.onLeaseCreated((lease) => {
|
|
370
|
-
console.log(`Lease created with ${lease.provider}`);
|
|
371
|
-
})
|
|
372
|
-
.execute();
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
---
|
|
376
|
-
|
|
377
|
-
## Before/After Comparison
|
|
378
|
-
|
|
379
|
-
### Consumer: kadi-deploy CLI
|
|
380
|
-
|
|
381
|
-
#### Before (Current Design)
|
|
382
|
-
|
|
383
|
-
```typescript
|
|
384
|
-
// File: kadi-deploy/src/targets/akash/akash.ts
|
|
385
|
-
|
|
386
|
-
import {
|
|
387
|
-
deployToAkash,
|
|
388
|
-
setupRegistryIfNeeded,
|
|
389
|
-
selectCheapestBid
|
|
390
|
-
} from '@kadi.build/deploy-ability/akash';
|
|
391
|
-
|
|
392
|
-
export async function deployAkashProfile(ctx: DeploymentContext) {
|
|
393
|
-
const { profile, wallet, certificate, logger } = ctx;
|
|
394
|
-
|
|
395
|
-
// Manual orchestration
|
|
396
|
-
logger.log('Setting up registry for local images...');
|
|
397
|
-
const registryCtx = await setupRegistryIfNeeded(
|
|
398
|
-
profile,
|
|
399
|
-
logger,
|
|
400
|
-
{
|
|
401
|
-
containerEngine: 'docker',
|
|
402
|
-
tunnelService: 'serveo'
|
|
403
|
-
}
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
const cleanup = registryCtx.cleanup;
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
logger.log('Deploying to Akash...');
|
|
410
|
-
|
|
411
|
-
const result = await deployToAkash({
|
|
412
|
-
loadedProfile: {
|
|
413
|
-
profile: registryCtx.deployableProfile
|
|
414
|
-
},
|
|
415
|
-
wallet,
|
|
416
|
-
certificate,
|
|
417
|
-
bidSelector: selectCheapestBid,
|
|
418
|
-
logger
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
if (!result.success) {
|
|
422
|
-
throw new Error(result.error.message);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
logger.log('✅ Deployment successful!');
|
|
426
|
-
return result.data;
|
|
427
|
-
|
|
428
|
-
} catch (error) {
|
|
429
|
-
logger.error('❌ Deployment failed:', error);
|
|
430
|
-
throw error;
|
|
431
|
-
|
|
432
|
-
} finally {
|
|
433
|
-
logger.log('Cleaning up registry...');
|
|
434
|
-
try {
|
|
435
|
-
await cleanup();
|
|
436
|
-
} catch (cleanupError) {
|
|
437
|
-
logger.warn('Warning: Registry cleanup failed:', cleanupError);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
**Issues**:
|
|
444
|
-
- 48 lines of boilerplate
|
|
445
|
-
- Manual lifecycle management
|
|
446
|
-
- Nested try-catch for cleanup errors
|
|
447
|
-
- Easy to forget steps
|
|
448
|
-
|
|
449
|
-
#### After (Pipeline Builder)
|
|
450
|
-
|
|
451
|
-
```typescript
|
|
452
|
-
// File: kadi-deploy/src/targets/akash/akash.ts
|
|
453
|
-
|
|
454
|
-
import {
|
|
455
|
-
DeploymentPipeline,
|
|
456
|
-
selectCheapestBid
|
|
457
|
-
} from '@kadi.build/deploy-ability/akash';
|
|
458
|
-
|
|
459
|
-
export async function deployAkashProfile(ctx: DeploymentContext) {
|
|
460
|
-
const { profile, wallet, certificate, logger } = ctx;
|
|
461
|
-
|
|
462
|
-
// Pipeline handles orchestration
|
|
463
|
-
const result = await DeploymentPipeline
|
|
464
|
-
.createFromProfile(profile, ctx.profileName)
|
|
465
|
-
.withLocalImageSupport({ tunnelService: 'serveo' })
|
|
466
|
-
.withWallet(wallet)
|
|
467
|
-
.withCertificate(certificate)
|
|
468
|
-
.withBidSelector(selectCheapestBid)
|
|
469
|
-
.withLogger(logger)
|
|
470
|
-
.execute();
|
|
471
|
-
|
|
472
|
-
if (!result.success) {
|
|
473
|
-
throw new Error(result.error.message);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return result.data;
|
|
477
|
-
}
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
**Improvements**:
|
|
481
|
-
- 19 lines (60% reduction)
|
|
482
|
-
- No manual lifecycle management
|
|
483
|
-
- Automatic cleanup (even on errors)
|
|
484
|
-
- Clear, readable flow
|
|
485
|
-
|
|
486
|
-
### Consumer: Custom Deployment Script
|
|
487
|
-
|
|
488
|
-
#### Before (Current Design)
|
|
489
|
-
|
|
490
|
-
```typescript
|
|
491
|
-
// deploy.ts - User's custom deployment script
|
|
492
|
-
|
|
493
|
-
import {
|
|
494
|
-
deployToAkash,
|
|
495
|
-
setupRegistryIfNeeded,
|
|
496
|
-
connectWallet,
|
|
497
|
-
ensureCertificate,
|
|
498
|
-
createSigningClient,
|
|
499
|
-
selectCheapestBid
|
|
500
|
-
} from '@kadi.build/deploy-ability/akash';
|
|
501
|
-
|
|
502
|
-
async function deploy() {
|
|
503
|
-
// Step 1: Connect wallet
|
|
504
|
-
const walletResult = await connectWallet({ network: 'mainnet' });
|
|
505
|
-
if (!walletResult.success) throw walletResult.error;
|
|
506
|
-
|
|
507
|
-
// Step 2: Ensure certificate
|
|
508
|
-
const clientResult = await createSigningClient(walletResult.data, 'mainnet');
|
|
509
|
-
if (!clientResult.success) throw clientResult.error;
|
|
510
|
-
|
|
511
|
-
const certResult = await ensureCertificate(
|
|
512
|
-
walletResult.data,
|
|
513
|
-
'mainnet',
|
|
514
|
-
clientResult.data.client
|
|
515
|
-
);
|
|
516
|
-
if (!certResult.success) throw certResult.error;
|
|
517
|
-
|
|
518
|
-
// Step 3: Load profile from disk
|
|
519
|
-
const profile = JSON.parse(
|
|
520
|
-
fs.readFileSync('./agent.json', 'utf-8')
|
|
521
|
-
).deploy['production'];
|
|
522
|
-
|
|
523
|
-
// Step 4: Setup registry
|
|
524
|
-
const registryCtx = await setupRegistryIfNeeded(profile, console, {
|
|
525
|
-
containerEngine: 'docker',
|
|
526
|
-
tunnelService: 'serveo'
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
try {
|
|
530
|
-
// Step 5: Deploy
|
|
531
|
-
const result = await deployToAkash({
|
|
532
|
-
loadedProfile: { profile: registryCtx.deployableProfile },
|
|
533
|
-
wallet: walletResult.data,
|
|
534
|
-
certificate: certResult.data,
|
|
535
|
-
bidSelector: selectCheapestBid
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
if (!result.success) {
|
|
539
|
-
throw new Error(result.error.message);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
console.log('Deployed!', result.data);
|
|
543
|
-
|
|
544
|
-
} finally {
|
|
545
|
-
await registryCtx.cleanup();
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
deploy().catch(console.error);
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
**Issues**:
|
|
553
|
-
- Verbose error checking
|
|
554
|
-
- Manual orchestration
|
|
555
|
-
- Easy to forget cleanup
|
|
556
|
-
|
|
557
|
-
#### After (Pipeline Builder)
|
|
558
|
-
|
|
559
|
-
```typescript
|
|
560
|
-
// deploy.ts - User's custom deployment script
|
|
561
|
-
|
|
562
|
-
import {
|
|
563
|
-
DeploymentPipeline,
|
|
564
|
-
connectWallet,
|
|
565
|
-
ensureCertificate,
|
|
566
|
-
createSigningClient,
|
|
567
|
-
selectCheapestBid
|
|
568
|
-
} from '@kadi.build/deploy-ability/akash';
|
|
569
|
-
|
|
570
|
-
async function deploy() {
|
|
571
|
-
// Setup wallet & certificate (same as before)
|
|
572
|
-
const walletResult = await connectWallet({ network: 'mainnet' });
|
|
573
|
-
if (!walletResult.success) throw walletResult.error;
|
|
574
|
-
|
|
575
|
-
const clientResult = await createSigningClient(walletResult.data, 'mainnet');
|
|
576
|
-
if (!clientResult.success) throw clientResult.error;
|
|
577
|
-
|
|
578
|
-
const certResult = await ensureCertificate(
|
|
579
|
-
walletResult.data,
|
|
580
|
-
'mainnet',
|
|
581
|
-
clientResult.data.client
|
|
582
|
-
);
|
|
583
|
-
if (!certResult.success) throw certResult.error;
|
|
584
|
-
|
|
585
|
-
// Deploy with pipeline - much simpler!
|
|
586
|
-
const result = await DeploymentPipeline
|
|
587
|
-
.create({
|
|
588
|
-
projectRoot: './',
|
|
589
|
-
profile: 'production',
|
|
590
|
-
network: 'mainnet'
|
|
591
|
-
})
|
|
592
|
-
.withLocalImageSupport({ tunnelService: 'serveo' })
|
|
593
|
-
.withWallet(walletResult.data)
|
|
594
|
-
.withCertificate(certResult.data)
|
|
595
|
-
.withBidSelector(selectCheapestBid)
|
|
596
|
-
.execute();
|
|
597
|
-
|
|
598
|
-
if (!result.success) {
|
|
599
|
-
throw new Error(result.error.message);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
console.log('Deployed!', result.data);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
deploy().catch(console.error);
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
**Improvements**:
|
|
609
|
-
- Cleaner deployment orchestration
|
|
610
|
-
- No manual cleanup
|
|
611
|
-
- Easier to read and understand
|
|
612
|
-
|
|
613
|
-
### Consumer: Autonomous Agent (Model Manager)
|
|
614
|
-
|
|
615
|
-
#### Before (Current Design)
|
|
616
|
-
|
|
617
|
-
```typescript
|
|
618
|
-
// src/services/deployment-logic.ts (Model Manager Agent)
|
|
619
|
-
|
|
620
|
-
async deployModel(request: DeploymentRequest) {
|
|
621
|
-
const profile = this.profileGenerator.generate(request);
|
|
622
|
-
|
|
623
|
-
// Manual registry setup
|
|
624
|
-
const registryCtx = await setupRegistryIfNeeded(
|
|
625
|
-
profile,
|
|
626
|
-
this.logger,
|
|
627
|
-
{ containerEngine: 'docker' }
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
try {
|
|
631
|
-
const result = await deployToAkash({
|
|
632
|
-
loadedProfile: { profile: registryCtx.deployableProfile },
|
|
633
|
-
wallet: this.walletContext,
|
|
634
|
-
certificate: this.certificate,
|
|
635
|
-
bidSelector: selectCheapestBid,
|
|
636
|
-
logger: this.logger
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
if (!result.success) {
|
|
640
|
-
throw new Error(result.error.message);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
return {
|
|
644
|
-
dseq: result.data.dseq,
|
|
645
|
-
provider: result.data.providerUri,
|
|
646
|
-
endpoints: result.data.endpoints
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
} finally {
|
|
650
|
-
await registryCtx.cleanup();
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
#### After (Pipeline Builder)
|
|
656
|
-
|
|
657
|
-
```typescript
|
|
658
|
-
// src/services/deployment-logic.ts (Model Manager Agent)
|
|
659
|
-
|
|
660
|
-
async deployModel(request: DeploymentRequest) {
|
|
661
|
-
const profile = this.profileGenerator.generate(request);
|
|
662
|
-
|
|
663
|
-
// Pipeline handles everything
|
|
664
|
-
const result = await DeploymentPipeline
|
|
665
|
-
.createFromProfile(profile, request.model.name)
|
|
666
|
-
.withLocalImageSupport() // Auto-detects, no-op if not needed
|
|
667
|
-
.withWallet(this.walletContext)
|
|
668
|
-
.withCertificate(this.certificate)
|
|
669
|
-
.withBidSelector(selectCheapestBid)
|
|
670
|
-
.withLogger(this.logger)
|
|
671
|
-
.execute();
|
|
672
|
-
|
|
673
|
-
if (!result.success) {
|
|
674
|
-
throw new Error(result.error.message);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
return {
|
|
678
|
-
dseq: result.data.dseq,
|
|
679
|
-
provider: result.data.providerUri,
|
|
680
|
-
endpoints: result.data.endpoints
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
**Improvements**:
|
|
686
|
-
- Simpler agent code
|
|
687
|
-
- Automatic resource cleanup
|
|
688
|
-
- Better error handling
|
|
689
|
-
|
|
690
|
-
---
|
|
691
|
-
|
|
692
|
-
## Implementation Plan
|
|
693
|
-
|
|
694
|
-
### Phase 1: Core Pipeline Builder
|
|
695
|
-
|
|
696
|
-
**Goal**: Implement basic pipeline with existing primitives
|
|
697
|
-
|
|
698
|
-
**Files to Create**:
|
|
699
|
-
```
|
|
700
|
-
src/targets/akash/
|
|
701
|
-
├── pipeline/
|
|
702
|
-
│ ├── index.ts # Public exports
|
|
703
|
-
│ ├── DeploymentPipeline.ts # Main builder class
|
|
704
|
-
│ ├── PipelineStage.ts # Stage definitions
|
|
705
|
-
│ ├── PipelineContext.ts # Internal state
|
|
706
|
-
│ ├── PipelineValidator.ts # Validation logic
|
|
707
|
-
│ └── errors.ts # Pipeline-specific errors
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
**Implementation**:
|
|
711
|
-
|
|
712
|
-
```typescript
|
|
713
|
-
// src/targets/akash/pipeline/DeploymentPipeline.ts
|
|
714
|
-
|
|
715
|
-
export class DeploymentPipeline {
|
|
716
|
-
private context: PipelineContext;
|
|
717
|
-
|
|
718
|
-
private constructor(context: PipelineContext) {
|
|
719
|
-
this.context = context;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// ========================================
|
|
723
|
-
// Factory Methods
|
|
724
|
-
// ========================================
|
|
725
|
-
|
|
726
|
-
static create(options: {
|
|
727
|
-
projectRoot: string;
|
|
728
|
-
profile: string;
|
|
729
|
-
network?: AkashNetwork;
|
|
730
|
-
}): DeploymentPipeline {
|
|
731
|
-
return new DeploymentPipeline({
|
|
732
|
-
stage: 'initialized',
|
|
733
|
-
options
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
static createFromProfile(
|
|
738
|
-
profile: AkashDeploymentProfile,
|
|
739
|
-
profileName: string
|
|
740
|
-
): DeploymentPipeline {
|
|
741
|
-
return new DeploymentPipeline({
|
|
742
|
-
stage: 'profile-loaded',
|
|
743
|
-
loadedProfile: { profile, name: profileName }
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// ========================================
|
|
748
|
-
// Builder Methods
|
|
749
|
-
// ========================================
|
|
750
|
-
|
|
751
|
-
withLocalImageSupport(options?: RegistryOptions): DeploymentPipeline {
|
|
752
|
-
this.context.registryOptions = options || {};
|
|
753
|
-
this.context.enableRegistry = true;
|
|
754
|
-
return this;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
withWallet(wallet: WalletContext): DeploymentPipeline {
|
|
758
|
-
this.context.wallet = wallet;
|
|
759
|
-
return this;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
withCertificate(certificate: AkashProviderTlsCertificate): DeploymentPipeline {
|
|
763
|
-
this.context.certificate = certificate;
|
|
764
|
-
return this;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
withBidSelector(selector: BidSelector): DeploymentPipeline {
|
|
768
|
-
this.context.bidSelector = selector;
|
|
769
|
-
return this;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
withLogger(logger: DeploymentLogger): DeploymentPipeline {
|
|
773
|
-
this.context.logger = logger;
|
|
774
|
-
return this;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// ========================================
|
|
778
|
-
// Execution
|
|
779
|
-
// ========================================
|
|
780
|
-
|
|
781
|
-
async execute(): Promise<AkashDeploymentResult> {
|
|
782
|
-
// Validate pipeline configuration
|
|
783
|
-
this.validate();
|
|
784
|
-
|
|
785
|
-
// Track resources for cleanup
|
|
786
|
-
const resources: ResourceHandle[] = [];
|
|
787
|
-
|
|
788
|
-
try {
|
|
789
|
-
// Stage 1: Load profile if needed
|
|
790
|
-
await this.loadProfileIfNeeded();
|
|
791
|
-
|
|
792
|
-
// Stage 2: Setup registry if needed
|
|
793
|
-
const registryHandle = await this.setupRegistryIfNeeded();
|
|
794
|
-
if (registryHandle) resources.push(registryHandle);
|
|
795
|
-
|
|
796
|
-
// Stage 3: Deploy to Akash
|
|
797
|
-
const result = await this.deploy();
|
|
798
|
-
|
|
799
|
-
return result;
|
|
800
|
-
|
|
801
|
-
} finally {
|
|
802
|
-
// Automatic cleanup (reverse order)
|
|
803
|
-
await this.cleanup(resources);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
async dryRun(): Promise<AkashDeploymentResult> {
|
|
808
|
-
await this.loadProfileIfNeeded();
|
|
809
|
-
// Generate SDL only, no deployment
|
|
810
|
-
const sdl = generateAkashSdl(this.context.loadedProfile!);
|
|
811
|
-
return success({
|
|
812
|
-
dryRun: true,
|
|
813
|
-
sdl,
|
|
814
|
-
profile: this.context.loadedProfile!.name,
|
|
815
|
-
network: this.context.options!.network || 'mainnet'
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// ========================================
|
|
820
|
-
// Internal Methods
|
|
821
|
-
// ========================================
|
|
822
|
-
|
|
823
|
-
private validate(): void {
|
|
824
|
-
if (!this.context.wallet) {
|
|
825
|
-
throw new PipelineValidationError(
|
|
826
|
-
'Wallet is required. Call .withWallet(wallet) before .execute()'
|
|
827
|
-
);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (!this.context.certificate) {
|
|
831
|
-
throw new PipelineValidationError(
|
|
832
|
-
'Certificate is required. Call .withCertificate(cert) before .execute()'
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// More validation...
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
private async loadProfileIfNeeded(): Promise<void> {
|
|
840
|
-
if (this.context.loadedProfile) return;
|
|
841
|
-
|
|
842
|
-
const profile = await loadProfile(
|
|
843
|
-
this.context.options!.projectRoot,
|
|
844
|
-
this.context.options!.profile,
|
|
845
|
-
this.context.logger || defaultLogger
|
|
846
|
-
);
|
|
847
|
-
|
|
848
|
-
this.context.loadedProfile = profile;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
private async setupRegistryIfNeeded(): Promise<ResourceHandle | null> {
|
|
852
|
-
if (!this.context.enableRegistry) return null;
|
|
853
|
-
|
|
854
|
-
const logger = this.context.logger || defaultLogger;
|
|
855
|
-
const registryCtx = await setupRegistryIfNeeded(
|
|
856
|
-
this.context.loadedProfile!.profile,
|
|
857
|
-
logger,
|
|
858
|
-
this.context.registryOptions
|
|
859
|
-
);
|
|
860
|
-
|
|
861
|
-
// Update profile with transformed version
|
|
862
|
-
this.context.loadedProfile!.profile = registryCtx.deployableProfile;
|
|
863
|
-
|
|
864
|
-
// Return cleanup handle
|
|
865
|
-
return {
|
|
866
|
-
name: 'registry',
|
|
867
|
-
cleanup: registryCtx.cleanup
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
private async deploy(): Promise<AkashDeploymentResult> {
|
|
872
|
-
return deployToAkash({
|
|
873
|
-
loadedProfile: this.context.loadedProfile!,
|
|
874
|
-
wallet: this.context.wallet!,
|
|
875
|
-
certificate: this.context.certificate!,
|
|
876
|
-
bidSelector: this.context.bidSelector || selectCheapestBid,
|
|
877
|
-
logger: this.context.logger
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
private async cleanup(resources: ResourceHandle[]): Promise<void> {
|
|
882
|
-
// Cleanup in reverse order (LIFO)
|
|
883
|
-
for (const resource of resources.reverse()) {
|
|
884
|
-
try {
|
|
885
|
-
await resource.cleanup();
|
|
886
|
-
} catch (error) {
|
|
887
|
-
const logger = this.context.logger || defaultLogger;
|
|
888
|
-
logger.warn(`Failed to cleanup ${resource.name}:`, error);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// ========================================
|
|
895
|
-
// Supporting Types
|
|
896
|
-
// ========================================
|
|
897
|
-
|
|
898
|
-
interface PipelineContext {
|
|
899
|
-
stage: 'initialized' | 'profile-loaded' | 'registry-setup' | 'deploying';
|
|
900
|
-
options?: {
|
|
901
|
-
projectRoot: string;
|
|
902
|
-
profile: string;
|
|
903
|
-
network?: AkashNetwork;
|
|
904
|
-
};
|
|
905
|
-
loadedProfile?: {
|
|
906
|
-
profile: AkashDeploymentProfile;
|
|
907
|
-
name: string;
|
|
908
|
-
};
|
|
909
|
-
enableRegistry?: boolean;
|
|
910
|
-
registryOptions?: RegistryOptions;
|
|
911
|
-
wallet?: WalletContext;
|
|
912
|
-
certificate?: AkashProviderTlsCertificate;
|
|
913
|
-
bidSelector?: BidSelector;
|
|
914
|
-
logger?: DeploymentLogger;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
interface ResourceHandle {
|
|
918
|
-
name: string;
|
|
919
|
-
cleanup: () => Promise<void>;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
export class PipelineValidationError extends Error {
|
|
923
|
-
constructor(message: string) {
|
|
924
|
-
super(message);
|
|
925
|
-
this.name = 'PipelineValidationError';
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
```
|
|
929
|
-
|
|
930
|
-
### Phase 2: Enhanced Features
|
|
931
|
-
|
|
932
|
-
**Add lifecycle callbacks**:
|
|
933
|
-
|
|
934
|
-
```typescript
|
|
935
|
-
withCallbacks(callbacks: {
|
|
936
|
-
onRegistryStarted?: (info: RegistryInfo) => void;
|
|
937
|
-
onProfileLoaded?: (profile: AkashDeploymentProfile) => void;
|
|
938
|
-
onBidsReceived?: (bids: EnhancedBid[]) => void;
|
|
939
|
-
onLeaseCreated?: (lease: LeaseDetails) => void;
|
|
940
|
-
}): DeploymentPipeline
|
|
941
|
-
```
|
|
942
|
-
|
|
943
|
-
**Add monitoring integration**:
|
|
944
|
-
|
|
945
|
-
```typescript
|
|
946
|
-
withMonitoring(options: {
|
|
947
|
-
waitForContainers?: boolean;
|
|
948
|
-
containerTimeout?: number;
|
|
949
|
-
streamLogs?: boolean;
|
|
950
|
-
}): DeploymentPipeline
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
### Phase 3: Documentation & Examples
|
|
954
|
-
|
|
955
|
-
1. Update main README with pipeline examples
|
|
956
|
-
2. Create `docs/PIPELINE_API.md` with full API reference
|
|
957
|
-
3. Add examples to `examples/` directory
|
|
958
|
-
4. Update TypeDoc comments
|
|
959
|
-
|
|
960
|
-
---
|
|
961
|
-
|
|
962
|
-
## Migration Strategy
|
|
963
|
-
|
|
964
|
-
### Backward Compatibility
|
|
965
|
-
|
|
966
|
-
**Principle**: The pipeline builder is **additive** - all existing functions remain unchanged.
|
|
967
|
-
|
|
968
|
-
```typescript
|
|
969
|
-
// Old way still works!
|
|
970
|
-
import { deployToAkash, setupRegistryIfNeeded } from '@kadi.build/deploy-ability/akash';
|
|
971
|
-
|
|
972
|
-
const registryCtx = await setupRegistryIfNeeded(...);
|
|
973
|
-
try {
|
|
974
|
-
const result = await deployToAkash({
|
|
975
|
-
loadedProfile: { profile: registryCtx.deployableProfile },
|
|
976
|
-
...
|
|
977
|
-
});
|
|
978
|
-
} finally {
|
|
979
|
-
await registryCtx.cleanup();
|
|
980
|
-
}
|
|
981
|
-
```
|
|
982
|
-
|
|
983
|
-
```typescript
|
|
984
|
-
// New way is optional
|
|
985
|
-
import { DeploymentPipeline } from '@kadi.build/deploy-ability/akash';
|
|
986
|
-
|
|
987
|
-
const result = await DeploymentPipeline
|
|
988
|
-
.create({ projectRoot: './', profile: 'prod' })
|
|
989
|
-
.withLocalImageSupport()
|
|
990
|
-
.withWallet(wallet)
|
|
991
|
-
.withCertificate(cert)
|
|
992
|
-
.execute();
|
|
993
|
-
```
|
|
994
|
-
|
|
995
|
-
### Migration Path
|
|
996
|
-
|
|
997
|
-
#### Step 1: Soft Deprecation (v0.1.0)
|
|
998
|
-
|
|
999
|
-
- Add pipeline builder to library
|
|
1000
|
-
- Update docs to show pipeline as recommended approach
|
|
1001
|
-
- Mark old approach as "still supported"
|
|
1002
|
-
|
|
1003
|
-
```typescript
|
|
1004
|
-
/**
|
|
1005
|
-
* @deprecated Prefer using DeploymentPipeline for better ergonomics
|
|
1006
|
-
* @see DeploymentPipeline
|
|
1007
|
-
*/
|
|
1008
|
-
export async function deployToAkash(...) { ... }
|
|
1009
|
-
```
|
|
1010
|
-
|
|
1011
|
-
#### Step 2: Community Feedback (v0.2.0 - v0.5.0)
|
|
1012
|
-
|
|
1013
|
-
- Gather feedback on pipeline API
|
|
1014
|
-
- Iterate on design based on real usage
|
|
1015
|
-
- Keep both approaches working
|
|
1016
|
-
|
|
1017
|
-
#### Step 3: Hard Deprecation (v1.0.0)
|
|
1018
|
-
|
|
1019
|
-
- Pipeline is primary API
|
|
1020
|
-
- Old functions remain but warn in console
|
|
1021
|
-
- Documentation shows pipeline only
|
|
1022
|
-
|
|
1023
|
-
#### Step 4: Removal (v2.0.0)
|
|
1024
|
-
|
|
1025
|
-
- Remove old functions (breaking change)
|
|
1026
|
-
- Pipeline is the only way
|
|
1027
|
-
|
|
1028
|
-
### Gradual Adoption
|
|
1029
|
-
|
|
1030
|
-
**Users can adopt incrementally**:
|
|
1031
|
-
|
|
1032
|
-
```typescript
|
|
1033
|
-
// Week 1: Just wrap existing code
|
|
1034
|
-
const result = await DeploymentPipeline
|
|
1035
|
-
.createFromProfile(profile, 'prod')
|
|
1036
|
-
.withWallet(wallet)
|
|
1037
|
-
.withCertificate(cert)
|
|
1038
|
-
.execute();
|
|
1039
|
-
|
|
1040
|
-
// Week 2: Add local image support
|
|
1041
|
-
const result = await DeploymentPipeline
|
|
1042
|
-
.create({ projectRoot: './', profile: 'prod' })
|
|
1043
|
-
.withLocalImageSupport() // ← New!
|
|
1044
|
-
.withWallet(wallet)
|
|
1045
|
-
.withCertificate(cert)
|
|
1046
|
-
.execute();
|
|
1047
|
-
|
|
1048
|
-
// Week 3: Add custom bid selection
|
|
1049
|
-
const result = await DeploymentPipeline
|
|
1050
|
-
.create({ projectRoot: './', profile: 'prod' })
|
|
1051
|
-
.withLocalImageSupport()
|
|
1052
|
-
.withWallet(wallet)
|
|
1053
|
-
.withCertificate(cert)
|
|
1054
|
-
.withBidSelector(customSelector) // ← New!
|
|
1055
|
-
.execute();
|
|
1056
|
-
```
|
|
1057
|
-
|
|
1058
|
-
---
|
|
1059
|
-
|
|
1060
|
-
## Benefits & Trade-offs
|
|
1061
|
-
|
|
1062
|
-
### Benefits
|
|
1063
|
-
|
|
1064
|
-
#### 1. **Discoverability**
|
|
1065
|
-
- IDE autocomplete guides you through steps
|
|
1066
|
-
- Method names are self-documenting
|
|
1067
|
-
- Can't forget required steps
|
|
1068
|
-
|
|
1069
|
-
#### 2. **Safety**
|
|
1070
|
-
- Validation catches configuration errors early
|
|
1071
|
-
- Automatic cleanup prevents resource leaks
|
|
1072
|
-
- Type system encodes requirements
|
|
1073
|
-
|
|
1074
|
-
#### 3. **Ergonomics**
|
|
1075
|
-
- Less boilerplate (60% reduction)
|
|
1076
|
-
- No manual lifecycle management
|
|
1077
|
-
- Cleaner, more readable code
|
|
1078
|
-
|
|
1079
|
-
#### 4. **Flexibility**
|
|
1080
|
-
- Stages are optional (progressive disclosure)
|
|
1081
|
-
- Can still use primitives for advanced cases
|
|
1082
|
-
- Backward compatible
|
|
1083
|
-
|
|
1084
|
-
#### 5. **Error Messages**
|
|
1085
|
-
```typescript
|
|
1086
|
-
// Before: Confusing error at runtime on Akash
|
|
1087
|
-
"Failed to pull image: my-app:latest"
|
|
1088
|
-
|
|
1089
|
-
// After: Clear error immediately
|
|
1090
|
-
"Pipeline validation failed: Wallet is required.
|
|
1091
|
-
Call .withWallet(wallet) before .execute()"
|
|
1092
|
-
```
|
|
1093
|
-
|
|
1094
|
-
### Trade-offs
|
|
1095
|
-
|
|
1096
|
-
#### 1. **More Code in Library**
|
|
1097
|
-
- **Cost**: ~500 lines for pipeline implementation
|
|
1098
|
-
- **Benefit**: Shared across all consumers (net reduction overall)
|
|
1099
|
-
|
|
1100
|
-
#### 2. **Learning Curve**
|
|
1101
|
-
- **Cost**: Users need to learn builder pattern
|
|
1102
|
-
- **Benefit**: Pattern is familiar from other libraries (e.g., Axios, TypeORM)
|
|
1103
|
-
|
|
1104
|
-
#### 3. **Less Control**
|
|
1105
|
-
- **Cost**: Some orchestration is hidden
|
|
1106
|
-
- **Benefit**: Can still use primitives for full control
|
|
1107
|
-
|
|
1108
|
-
#### 4. **Testing Complexity**
|
|
1109
|
-
- **Cost**: Need to test pipeline stages
|
|
1110
|
-
- **Benefit**: Better test coverage overall
|
|
1111
|
-
|
|
1112
|
-
### Comparison Table
|
|
1113
|
-
|
|
1114
|
-
| Aspect | Current (Primitives) | Pipeline Builder |
|
|
1115
|
-
|--------|---------------------|------------------|
|
|
1116
|
-
| **Lines of Code** | ~40-50 lines | ~15-20 lines |
|
|
1117
|
-
| **Discoverability** | ❌ Poor | ✅ Excellent |
|
|
1118
|
-
| **Error Messages** | ❌ Confusing | ✅ Clear |
|
|
1119
|
-
| **Lifecycle Management** | ❌ Manual | ✅ Automatic |
|
|
1120
|
-
| **Type Safety** | ⚠️ Partial | ✅ Complete |
|
|
1121
|
-
| **Flexibility** | ✅ Full control | ✅ Still available |
|
|
1122
|
-
| **Learning Curve** | ⚠️ Medium | ⚠️ Medium |
|
|
1123
|
-
| **Code in Library** | ✅ Minimal | ⚠️ More |
|
|
1124
|
-
|
|
1125
|
-
---
|
|
1126
|
-
|
|
1127
|
-
## Conclusion
|
|
1128
|
-
|
|
1129
|
-
The Pipeline Builder pattern addresses the key usability issues with deploy-ability while maintaining its architectural strengths. By making deployment steps explicit and handling lifecycle automatically, we create an API that's both powerful and hard to misuse.
|
|
1130
|
-
|
|
1131
|
-
### Next Steps
|
|
1132
|
-
|
|
1133
|
-
1. **Review**: Gather feedback on this proposal
|
|
1134
|
-
2. **Prototype**: Implement Phase 1 (core pipeline)
|
|
1135
|
-
3. **Test**: Migrate kadi-deploy to use pipeline
|
|
1136
|
-
4. **Iterate**: Refine based on real usage
|
|
1137
|
-
5. **Document**: Write comprehensive guides
|
|
1138
|
-
6. **Release**: Ship as part of deploy-ability v0.1.0
|
|
1139
|
-
|
|
1140
|
-
### Open Questions
|
|
1141
|
-
|
|
1142
|
-
1. Should pipeline builder be in a separate package or same as deploy-ability?
|
|
1143
|
-
2. What's the right balance between automatic and explicit?
|
|
1144
|
-
3. Should we support middleware/plugins for custom stages?
|
|
1145
|
-
4. How should we handle errors in pipeline stages?
|
|
1146
|
-
|
|
1147
|
-
---
|
|
1148
|
-
|
|
1149
|
-
**Feedback Welcome**: Please share thoughts, concerns, or suggestions!
|