@kadi.build/deploy-ability 0.0.4 → 0.0.5

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.
@@ -0,0 +1,1149 @@
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!