@tinybirdco/sdk 0.0.53 → 0.0.55

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 with auto-promotion enabled
3
+ * Uses the /v1/deploy endpoint to create a deployment, then sets it live
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,6 +31,13 @@ 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
+
34
41
  /**
35
42
  * Response from /v1/deploy endpoint
36
43
  */
@@ -41,14 +48,6 @@ export interface DeployResponse {
41
48
  errors?: Array<{ filename?: string; error: string }>;
42
49
  }
43
50
 
44
- /**
45
- * Response from /v1/deployments/{id} endpoint
46
- */
47
- export interface DeploymentStatusResponse {
48
- result: string;
49
- deployment: Deployment;
50
- }
51
-
52
51
  /**
53
52
  * Detailed deployment information with resource changes
54
53
  */
@@ -75,11 +74,21 @@ export interface DeploymentDetails extends Deployment {
75
74
  errors?: Array<{ filename?: string; error: string }>;
76
75
  }
77
76
 
77
+ /**
78
+ * Response from /v1/deployments/{id} endpoint
79
+ */
80
+ export interface DeploymentStatusResponse {
81
+ result: string;
82
+ deployment: Deployment;
83
+ }
84
+
78
85
  /**
79
86
  * Deploy generated resources to Tinybird main workspace
80
87
  *
81
88
  * Uses the /v1/deploy endpoint which accepts all resources in a single
82
- * multipart form request.
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
83
92
  *
84
93
  * @param config - Build configuration with API URL and token
85
94
  * @param resources - Generated resources to deploy
@@ -152,14 +161,13 @@ export async function deployToMain(
152
161
  pollIntervalMs?: number;
153
162
  maxPollAttempts?: number;
154
163
  check?: boolean;
155
- autoPromote?: boolean;
156
164
  allowDestructiveOperations?: boolean;
157
165
  callbacks?: DeployCallbacks;
158
166
  }
159
167
  ): Promise<BuildApiResult> {
160
168
  const debug = options?.debug ?? !!process.env.TINYBIRD_DEBUG;
161
169
  const pollIntervalMs = options?.pollIntervalMs ?? 1000;
162
- const maxPollAttempts = options?.maxPollAttempts ?? 120;
170
+ const maxPollAttempts = options?.maxPollAttempts ?? 120; // 2 minutes max
163
171
  const baseUrl = config.baseUrl.replace(/\/$/, "");
164
172
 
165
173
  const formData = new FormData();
@@ -194,17 +202,46 @@ export async function deployToMain(
194
202
  );
195
203
  }
196
204
 
197
- // Create deployment via /v1/deploy.
198
- // `auto_promote=true` makes the API promote the deployment automatically.
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
199
240
  const deployUrlBase = `${baseUrl}/v1/deploy`;
200
241
  const urlParams = new URLSearchParams();
201
242
  if (options?.check) {
202
243
  urlParams.set("check", "true");
203
244
  }
204
- const autoPromote = options?.autoPromote ?? !options?.check;
205
- if (autoPromote) {
206
- urlParams.set("auto_promote", "true");
207
- }
208
245
  if (options?.allowDestructiveOperations) {
209
246
  urlParams.set("allow_destructive_operations", "true");
210
247
  }
@@ -245,7 +282,7 @@ export async function deployToMain(
245
282
  return feedback
246
283
  .map((f) => {
247
284
  // Extract just the filename from "Datasource events.datasource" format
248
- const resourceName = f.resource.split(" ").pop() ?? f.resource;
285
+ const resourceName = f.resource?.split(" ").pop() ?? f.resource ?? "unknown";
249
286
  return `${resourceName}: ${normalizeDeployErrorMessage(f.message)}`;
250
287
  })
251
288
  .join("\n");
@@ -371,22 +408,18 @@ export async function deployToMain(
371
408
  });
372
409
  }
373
410
 
374
- let deployment = deploymentDetails;
375
- let statusAttempts = 0;
411
+ // Step 2: Poll until deployment is ready
412
+ let deployment = body.deployment;
413
+ let attempts = 0;
376
414
 
377
415
  options?.callbacks?.onWaitingForReady?.();
378
416
 
379
- while (
380
- deployment.status !== "data_ready" &&
381
- deployment.status !== "failed" &&
382
- deployment.status !== "error" &&
383
- statusAttempts < maxPollAttempts
384
- ) {
417
+ while (deployment.status !== "data_ready" && attempts < maxPollAttempts) {
385
418
  await sleep(pollIntervalMs);
386
- statusAttempts++;
419
+ attempts++;
387
420
 
388
421
  if (debug) {
389
- console.log(`[debug] Polling deployment status (attempt ${statusAttempts})...`);
422
+ console.log(`[debug] Polling deployment status (attempt ${attempts})...`);
390
423
  }
391
424
 
392
425
  const statusUrl = `${baseUrl}/v1/deployments/${deploymentId}`;
@@ -410,18 +443,23 @@ export async function deployToMain(
410
443
 
411
444
  const statusBody = (await statusResponse.json()) as DeploymentStatusResponse;
412
445
  deployment = statusBody.deployment;
413
- }
414
446
 
415
- if (deployment.status === "failed" || deployment.status === "error") {
416
- return {
417
- success: false,
418
- result: "failed",
419
- error: `Deployment failed with status: ${deployment.status}`,
420
- datasourceCount: resources.datasources.length,
421
- pipeCount: resources.pipes.length,
422
- connectionCount: resources.connections?.length ?? 0,
423
- buildId: deploymentId,
424
- };
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
+ }
425
463
  }
426
464
 
427
465
  if (deployment.status !== "data_ready") {
@@ -438,64 +476,38 @@ export async function deployToMain(
438
476
 
439
477
  options?.callbacks?.onDeploymentReady?.();
440
478
 
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++;
479
+ // Step 3: Set the deployment as live
480
+ const setLiveUrl = `${baseUrl}/v1/deployments/${deploymentId}/set-live`;
448
481
 
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
- }
482
+ if (debug) {
483
+ console.log(`[debug] POST ${setLiveUrl}`);
484
+ }
475
485
 
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
- }
486
+ const setLiveResponse = await tinybirdFetch(setLiveUrl, {
487
+ method: "POST",
488
+ headers: {
489
+ Authorization: `Bearer ${config.token}`,
490
+ },
491
+ });
487
492
 
488
- options?.callbacks?.onDeploymentPromoted?.();
493
+ if (!setLiveResponse.ok) {
494
+ const setLiveBody = await setLiveResponse.text();
495
+ return {
496
+ success: false,
497
+ result: "failed",
498
+ error: `Failed to set deployment as live: ${setLiveResponse.status} ${setLiveResponse.statusText}\n${setLiveBody}`,
499
+ datasourceCount: resources.datasources.length,
500
+ pipeCount: resources.pipes.length,
501
+ connectionCount: resources.connections?.length ?? 0,
502
+ buildId: deploymentId,
503
+ };
489
504
  }
490
505
 
491
506
  if (debug) {
492
- const stateLabel = deployment.live ? "live" : "ready";
493
- console.log(`[debug] Deployment ${deploymentId} is now ${stateLabel}`);
507
+ console.log(`[debug] Deployment ${deploymentId} is now live`);
494
508
  }
495
509
 
496
- if (deployment.live) {
497
- options?.callbacks?.onDeploymentLive?.(deploymentId);
498
- }
510
+ options?.callbacks?.onDeploymentLive?.(deploymentId);
499
511
 
500
512
  return {
501
513
  success: true,
@@ -517,6 +529,9 @@ export async function deployToMain(
517
529
  };
518
530
  }
519
531
 
532
+ /**
533
+ * Helper function to sleep for a given number of milliseconds
534
+ */
520
535
  function sleep(ms: number): Promise<void> {
521
536
  return new Promise((resolve) => setTimeout(resolve, ms));
522
537
  }