@tinybirdco/sdk 0.0.52 → 0.0.54

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/src/api/deploy.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Deploy resources to Tinybird main workspace
3
- * Uses the /v1/deploy endpoint to create a deployment, then sets it live
3
+ * Uses the /v1/deploy endpoint with auto-promotion enabled
4
4
  */
5
5
 
6
6
  import type { GeneratedResources } from "../generator/index.js";
@@ -14,7 +14,7 @@ const FORWARD_CLASSIC_GUIDANCE =
14
14
  * Feedback item from deployment response
15
15
  */
16
16
  export interface DeploymentFeedback {
17
- resource: string;
17
+ resource: string | null;
18
18
  level: "ERROR" | "WARNING" | "INFO";
19
19
  message: string;
20
20
  }
@@ -31,13 +31,6 @@ export interface Deployment {
31
31
  feedback?: DeploymentFeedback[];
32
32
  }
33
33
 
34
- /**
35
- * Response from /v1/deployments list endpoint
36
- */
37
- export interface DeploymentsListResponse {
38
- deployments: Deployment[];
39
- }
40
-
41
34
  /**
42
35
  * Response from /v1/deploy endpoint
43
36
  */
@@ -48,6 +41,14 @@ export interface DeployResponse {
48
41
  errors?: Array<{ filename?: string; error: string }>;
49
42
  }
50
43
 
44
+ /**
45
+ * Response from /v1/deployments/{id} endpoint
46
+ */
47
+ export interface DeploymentStatusResponse {
48
+ result: string;
49
+ deployment: Deployment;
50
+ }
51
+
51
52
  /**
52
53
  * Detailed deployment information with resource changes
53
54
  */
@@ -74,21 +75,11 @@ export interface DeploymentDetails extends Deployment {
74
75
  errors?: Array<{ filename?: string; error: string }>;
75
76
  }
76
77
 
77
- /**
78
- * Response from /v1/deployments/{id} endpoint
79
- */
80
- export interface DeploymentStatusResponse {
81
- result: string;
82
- deployment: Deployment;
83
- }
84
-
85
78
  /**
86
79
  * Deploy generated resources to Tinybird main workspace
87
80
  *
88
81
  * Uses the /v1/deploy endpoint which accepts all resources in a single
89
- * multipart form request. After creating the deployment, this function:
90
- * 1. Polls until the deployment is ready (status === 'data_ready')
91
- * 2. Sets the deployment as live via /v1/deployments/{id}/set-live
82
+ * multipart form request.
92
83
  *
93
84
  * @param config - Build configuration with API URL and token
94
85
  * @param resources - Generated resources to deploy
@@ -161,13 +152,14 @@ export async function deployToMain(
161
152
  pollIntervalMs?: number;
162
153
  maxPollAttempts?: number;
163
154
  check?: boolean;
155
+ autoPromote?: boolean;
164
156
  allowDestructiveOperations?: boolean;
165
157
  callbacks?: DeployCallbacks;
166
158
  }
167
159
  ): Promise<BuildApiResult> {
168
160
  const debug = options?.debug ?? !!process.env.TINYBIRD_DEBUG;
169
161
  const pollIntervalMs = options?.pollIntervalMs ?? 1000;
170
- const maxPollAttempts = options?.maxPollAttempts ?? 120; // 2 minutes max
162
+ const maxPollAttempts = options?.maxPollAttempts ?? 120;
171
163
  const baseUrl = config.baseUrl.replace(/\/$/, "");
172
164
 
173
165
  const formData = new FormData();
@@ -202,46 +194,17 @@ export async function deployToMain(
202
194
  );
203
195
  }
204
196
 
205
- // Step 0: Clean up any stale non-live deployments that might block the new deployment
206
- try {
207
- const deploymentsUrl = `${baseUrl}/v1/deployments`;
208
- const deploymentsResponse = await tinybirdFetch(deploymentsUrl, {
209
- headers: {
210
- Authorization: `Bearer ${config.token}`,
211
- },
212
- });
213
-
214
- if (deploymentsResponse.ok) {
215
- const deploymentsBody = (await deploymentsResponse.json()) as DeploymentsListResponse;
216
- const staleDeployments = deploymentsBody.deployments.filter(
217
- (d) => !d.live && d.status !== "live"
218
- );
219
-
220
- for (const stale of staleDeployments) {
221
- if (debug) {
222
- console.log(`[debug] Cleaning up stale deployment: ${stale.id} (status: ${stale.status})`);
223
- }
224
- await tinybirdFetch(`${baseUrl}/v1/deployments/${stale.id}`, {
225
- method: "DELETE",
226
- headers: {
227
- Authorization: `Bearer ${config.token}`,
228
- },
229
- });
230
- }
231
- }
232
- } catch (e) {
233
- // Ignore errors during cleanup - we'll try to deploy anyway
234
- if (debug) {
235
- console.log(`[debug] Failed to clean up stale deployments: ${e}`);
236
- }
237
- }
238
-
239
- // Step 1: Create deployment via /v1/deploy
197
+ // Create deployment via /v1/deploy.
198
+ // `auto_promote=true` makes the API promote the deployment automatically.
240
199
  const deployUrlBase = `${baseUrl}/v1/deploy`;
241
200
  const urlParams = new URLSearchParams();
242
201
  if (options?.check) {
243
202
  urlParams.set("check", "true");
244
203
  }
204
+ const autoPromote = options?.autoPromote ?? !options?.check;
205
+ if (autoPromote) {
206
+ urlParams.set("auto_promote", "true");
207
+ }
245
208
  if (options?.allowDestructiveOperations) {
246
209
  urlParams.set("allow_destructive_operations", "true");
247
210
  }
@@ -282,7 +245,7 @@ export async function deployToMain(
282
245
  return feedback
283
246
  .map((f) => {
284
247
  // Extract just the filename from "Datasource events.datasource" format
285
- const resourceName = f.resource.split(" ").pop() ?? f.resource;
248
+ const resourceName = f.resource?.split(" ").pop() ?? f.resource ?? "unknown";
286
249
  return `${resourceName}: ${normalizeDeployErrorMessage(f.message)}`;
287
250
  })
288
251
  .join("\n");
@@ -408,18 +371,22 @@ export async function deployToMain(
408
371
  });
409
372
  }
410
373
 
411
- // Step 2: Poll until deployment is ready
412
- let deployment = body.deployment;
413
- let attempts = 0;
374
+ let deployment = deploymentDetails;
375
+ let statusAttempts = 0;
414
376
 
415
377
  options?.callbacks?.onWaitingForReady?.();
416
378
 
417
- while (deployment.status !== "data_ready" && attempts < maxPollAttempts) {
379
+ while (
380
+ deployment.status !== "data_ready" &&
381
+ deployment.status !== "failed" &&
382
+ deployment.status !== "error" &&
383
+ statusAttempts < maxPollAttempts
384
+ ) {
418
385
  await sleep(pollIntervalMs);
419
- attempts++;
386
+ statusAttempts++;
420
387
 
421
388
  if (debug) {
422
- console.log(`[debug] Polling deployment status (attempt ${attempts})...`);
389
+ console.log(`[debug] Polling deployment status (attempt ${statusAttempts})...`);
423
390
  }
424
391
 
425
392
  const statusUrl = `${baseUrl}/v1/deployments/${deploymentId}`;
@@ -443,30 +410,13 @@ export async function deployToMain(
443
410
 
444
411
  const statusBody = (await statusResponse.json()) as DeploymentStatusResponse;
445
412
  deployment = statusBody.deployment;
446
-
447
- if (debug) {
448
- console.log(`[debug] Deployment status: ${deployment.status}`);
449
- }
450
-
451
- // Check for failed status
452
- if (deployment.status === "failed" || deployment.status === "error") {
453
- return {
454
- success: false,
455
- result: "failed",
456
- error: `Deployment failed with status: ${deployment.status}`,
457
- datasourceCount: resources.datasources.length,
458
- pipeCount: resources.pipes.length,
459
- connectionCount: resources.connections?.length ?? 0,
460
- buildId: deploymentId,
461
- };
462
- }
463
413
  }
464
414
 
465
- if (deployment.status !== "data_ready") {
415
+ if (deployment.status === "failed" || deployment.status === "error") {
466
416
  return {
467
417
  success: false,
468
418
  result: "failed",
469
- error: `Deployment timed out after ${maxPollAttempts} attempts. Last status: ${deployment.status}`,
419
+ error: `Deployment failed with status: ${deployment.status}`,
470
420
  datasourceCount: resources.datasources.length,
471
421
  pipeCount: resources.pipes.length,
472
422
  connectionCount: resources.connections?.length ?? 0,
@@ -474,28 +424,11 @@ export async function deployToMain(
474
424
  };
475
425
  }
476
426
 
477
- options?.callbacks?.onDeploymentReady?.();
478
-
479
- // Step 3: Set the deployment as live
480
- const setLiveUrl = `${baseUrl}/v1/deployments/${deploymentId}/set-live`;
481
-
482
- if (debug) {
483
- console.log(`[debug] POST ${setLiveUrl}`);
484
- }
485
-
486
- const setLiveResponse = await tinybirdFetch(setLiveUrl, {
487
- method: "POST",
488
- headers: {
489
- Authorization: `Bearer ${config.token}`,
490
- },
491
- });
492
-
493
- if (!setLiveResponse.ok) {
494
- const setLiveBody = await setLiveResponse.text();
427
+ if (deployment.status !== "data_ready") {
495
428
  return {
496
429
  success: false,
497
430
  result: "failed",
498
- error: `Failed to set deployment as live: ${setLiveResponse.status} ${setLiveResponse.statusText}\n${setLiveBody}`,
431
+ error: `Deployment timed out after ${maxPollAttempts} attempts. Last status: ${deployment.status}`,
499
432
  datasourceCount: resources.datasources.length,
500
433
  pipeCount: resources.pipes.length,
501
434
  connectionCount: resources.connections?.length ?? 0,
@@ -503,11 +436,66 @@ export async function deployToMain(
503
436
  };
504
437
  }
505
438
 
439
+ options?.callbacks?.onDeploymentReady?.();
440
+
441
+ if (autoPromote && !deployment.live) {
442
+ let promotionAttempts = 0;
443
+ options?.callbacks?.onWaitingForPromote?.();
444
+
445
+ while (!deployment.live && promotionAttempts < maxPollAttempts) {
446
+ await sleep(pollIntervalMs);
447
+ promotionAttempts++;
448
+
449
+ if (debug) {
450
+ console.log(`[debug] Polling auto-promote status (attempt ${promotionAttempts})...`);
451
+ }
452
+
453
+ const statusUrl = `${baseUrl}/v1/deployments/${deploymentId}`;
454
+ const statusResponse = await tinybirdFetch(statusUrl, {
455
+ headers: {
456
+ Authorization: `Bearer ${config.token}`,
457
+ },
458
+ });
459
+
460
+ if (!statusResponse.ok) {
461
+ return {
462
+ success: false,
463
+ result: "failed",
464
+ error: `Failed to check deployment status: ${statusResponse.status} ${statusResponse.statusText}`,
465
+ datasourceCount: resources.datasources.length,
466
+ pipeCount: resources.pipes.length,
467
+ connectionCount: resources.connections?.length ?? 0,
468
+ buildId: deploymentId,
469
+ };
470
+ }
471
+
472
+ const statusBody = (await statusResponse.json()) as DeploymentStatusResponse;
473
+ deployment = statusBody.deployment;
474
+ }
475
+
476
+ if (!deployment.live) {
477
+ return {
478
+ success: false,
479
+ result: "failed",
480
+ error: `Deployment reached data_ready but auto-promote did not complete after ${maxPollAttempts} attempts`,
481
+ datasourceCount: resources.datasources.length,
482
+ pipeCount: resources.pipes.length,
483
+ connectionCount: resources.connections?.length ?? 0,
484
+ buildId: deploymentId,
485
+ };
486
+ }
487
+
488
+ options?.callbacks?.onDeploymentPromoted?.();
489
+ }
490
+
506
491
  if (debug) {
507
- console.log(`[debug] Deployment ${deploymentId} is now live`);
492
+ const stateLabel = deployment.live ? "live" : "ready";
493
+ console.log(`[debug] Deployment ${deploymentId} is now ${stateLabel}`);
508
494
  }
509
495
 
510
- options?.callbacks?.onDeploymentLive?.(deploymentId);
496
+ if (deployment.live) {
497
+ options?.callbacks?.onDeploymentLive?.(deploymentId);
498
+ }
511
499
 
512
500
  return {
513
501
  success: true,
@@ -529,9 +517,6 @@ export async function deployToMain(
529
517
  };
530
518
  }
531
519
 
532
- /**
533
- * Helper function to sleep for a given number of milliseconds
534
- */
535
520
  function sleep(ms: number): Promise<void> {
536
521
  return new Promise((resolve) => setTimeout(resolve, ms));
537
522
  }