@tinybirdco/sdk 0.0.54 → 0.0.56

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.
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeAll, afterEach, afterAll, vi } from "vitest";
1
+ import { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest";
2
2
  import { setupServer } from "msw/node";
3
3
  import { http, HttpResponse } from "msw";
4
4
  import { deployToMain } from "./deploy.js";
@@ -6,14 +6,25 @@ import type { BuildConfig } from "./build.js";
6
6
  import {
7
7
  BASE_URL,
8
8
  createDeploySuccessResponse,
9
+ createDeploymentStatusResponse,
10
+ createSetLiveSuccessResponse,
9
11
  createBuildFailureResponse,
10
12
  createBuildMultipleErrorsResponse,
13
+ createDeploymentsListResponse,
11
14
  } from "../test/handlers.js";
12
15
  import type { GeneratedResources } from "../generator/index.js";
13
16
 
14
17
  const server = setupServer();
15
18
 
16
19
  beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
20
+ beforeEach(() => {
21
+ // Set up default handler for deployments list (used by stale deployment cleanup)
22
+ server.use(
23
+ http.get(`${BASE_URL}/v1/deployments`, () => {
24
+ return HttpResponse.json(createDeploymentsListResponse());
25
+ })
26
+ );
27
+ });
17
28
  afterEach(() => server.resetHandlers());
18
29
  afterAll(() => server.close());
19
30
 
@@ -24,12 +35,17 @@ describe("Deploy API", () => {
24
35
  };
25
36
 
26
37
  const resources: GeneratedResources = {
27
- datasources: [{ name: "events", content: "SCHEMA > timestamp DateTime" }],
28
- pipes: [{ name: "top_events", content: "NODE main\nSQL > SELECT * FROM events" }],
38
+ datasources: [
39
+ { name: "events", content: "SCHEMA > timestamp DateTime" },
40
+ ],
41
+ pipes: [
42
+ { name: "top_events", content: "NODE main\nSQL > SELECT * FROM events" },
43
+ ],
29
44
  connections: [],
30
45
  };
31
46
 
32
- function setupAutoPromoteSuccessFlow(deploymentId = "deploy-abc") {
47
+ // Helper to set up successful deploy flow
48
+ function setupSuccessfulDeployFlow(deploymentId = "deploy-abc") {
33
49
  server.use(
34
50
  http.post(`${BASE_URL}/v1/deploy`, () => {
35
51
  return HttpResponse.json(
@@ -37,42 +53,64 @@ describe("Deploy API", () => {
37
53
  );
38
54
  }),
39
55
  http.get(`${BASE_URL}/v1/deployments/${deploymentId}`, () => {
40
- return HttpResponse.json({
41
- result: "success",
42
- deployment: {
43
- id: deploymentId,
44
- status: "data_ready",
45
- live: true,
46
- },
47
- });
56
+ return HttpResponse.json(
57
+ createDeploymentStatusResponse({ deploymentId, status: "data_ready" })
58
+ );
59
+ }),
60
+ http.post(`${BASE_URL}/v1/deployments/${deploymentId}/set-live`, () => {
61
+ return HttpResponse.json(createSetLiveSuccessResponse());
48
62
  })
49
63
  );
50
64
  }
51
65
 
52
66
  describe("deployToMain", () => {
53
- it("successfully deploys resources with auto-promote flow", async () => {
54
- setupAutoPromoteSuccessFlow("deploy-abc");
67
+ it("successfully deploys resources with full flow", async () => {
68
+ setupSuccessfulDeployFlow("deploy-abc");
55
69
 
56
- const onDeploymentLive = vi.fn();
57
- const result = await deployToMain(config, resources, {
58
- pollIntervalMs: 1,
59
- callbacks: { onDeploymentLive },
60
- });
70
+ const result = await deployToMain(config, resources, { pollIntervalMs: 1 });
61
71
 
62
72
  expect(result.success).toBe(true);
63
73
  expect(result.result).toBe("success");
64
74
  expect(result.buildId).toBe("deploy-abc");
65
75
  expect(result.datasourceCount).toBe(1);
66
76
  expect(result.pipeCount).toBe(1);
67
- expect(onDeploymentLive).toHaveBeenCalledWith("deploy-abc");
77
+ });
78
+
79
+ it("polls until deployment is ready", async () => {
80
+ let pollCount = 0;
81
+
82
+ server.use(
83
+ http.post(`${BASE_URL}/v1/deploy`, () => {
84
+ return HttpResponse.json(
85
+ createDeploySuccessResponse({ deploymentId: "deploy-poll", status: "pending" })
86
+ );
87
+ }),
88
+ http.get(`${BASE_URL}/v1/deployments/deploy-poll`, () => {
89
+ pollCount++;
90
+ // Return pending for first 2 polls, then data_ready
91
+ const status = pollCount < 3 ? "pending" : "data_ready";
92
+ return HttpResponse.json(
93
+ createDeploymentStatusResponse({ deploymentId: "deploy-poll", status })
94
+ );
95
+ }),
96
+ http.post(`${BASE_URL}/v1/deployments/deploy-poll/set-live`, () => {
97
+ return HttpResponse.json(createSetLiveSuccessResponse());
98
+ })
99
+ );
100
+
101
+ const result = await deployToMain(config, resources, { pollIntervalMs: 1 });
102
+
103
+ expect(result.success).toBe(true);
104
+ expect(pollCount).toBe(3);
68
105
  });
69
106
 
70
107
  it("handles deploy failure with single error", async () => {
71
108
  server.use(
72
109
  http.post(`${BASE_URL}/v1/deploy`, () => {
73
- return HttpResponse.json(createBuildFailureResponse("Permission denied"), {
74
- status: 200,
75
- });
110
+ return HttpResponse.json(
111
+ createBuildFailureResponse("Permission denied"),
112
+ { status: 200 }
113
+ );
76
114
  })
77
115
  );
78
116
 
@@ -130,7 +168,10 @@ describe("Deploy API", () => {
130
168
  it("handles HTTP error responses", async () => {
131
169
  server.use(
132
170
  http.post(`${BASE_URL}/v1/deploy`, () => {
133
- return HttpResponse.json({ result: "failed", error: "Forbidden" }, { status: 403 });
171
+ return HttpResponse.json(
172
+ { result: "failed", error: "Forbidden" },
173
+ { status: 403 }
174
+ );
134
175
  })
135
176
  );
136
177
 
@@ -150,24 +191,28 @@ describe("Deploy API", () => {
150
191
  })
151
192
  );
152
193
 
153
- await expect(deployToMain(config, resources)).rejects.toThrow("Failed to parse response");
194
+ await expect(deployToMain(config, resources)).rejects.toThrow(
195
+ "Failed to parse response"
196
+ );
154
197
  });
155
198
 
156
- it("uses /v1/deploy endpoint and sends auto_promote by default", async () => {
199
+ it("uses /v1/deploy endpoint (not /v1/build)", async () => {
157
200
  let capturedUrl: string | null = null;
158
201
 
159
202
  server.use(
160
203
  http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
161
204
  capturedUrl = request.url;
162
205
  return HttpResponse.json(
163
- createDeploySuccessResponse({ deploymentId: "deploy-url-test", status: "pending" })
206
+ createDeploySuccessResponse({ deploymentId: "deploy-url-test" })
164
207
  );
165
208
  }),
166
209
  http.get(`${BASE_URL}/v1/deployments/deploy-url-test`, () => {
167
- return HttpResponse.json({
168
- result: "success",
169
- deployment: { id: "deploy-url-test", status: "data_ready", live: true },
170
- });
210
+ return HttpResponse.json(
211
+ createDeploymentStatusResponse({ deploymentId: "deploy-url-test", status: "data_ready" })
212
+ );
213
+ }),
214
+ http.post(`${BASE_URL}/v1/deployments/deploy-url-test/set-live`, () => {
215
+ return HttpResponse.json(createSetLiveSuccessResponse());
171
216
  })
172
217
  );
173
218
 
@@ -176,7 +221,6 @@ describe("Deploy API", () => {
176
221
  const parsed = new URL(capturedUrl ?? "");
177
222
  expect(parsed.pathname).toBe("/v1/deploy");
178
223
  expect(parsed.searchParams.get("from")).toBe("ts-sdk");
179
- expect(parsed.searchParams.get("auto_promote")).toBe("true");
180
224
  });
181
225
 
182
226
  it("passes allow_destructive_operations when explicitly enabled", async () => {
@@ -186,14 +230,19 @@ describe("Deploy API", () => {
186
230
  http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
187
231
  capturedUrl = request.url;
188
232
  return HttpResponse.json(
189
- createDeploySuccessResponse({ deploymentId: "deploy-destructive", status: "pending" })
233
+ createDeploySuccessResponse({ deploymentId: "deploy-destructive" })
190
234
  );
191
235
  }),
192
236
  http.get(`${BASE_URL}/v1/deployments/deploy-destructive`, () => {
193
- return HttpResponse.json({
194
- result: "success",
195
- deployment: { id: "deploy-destructive", status: "data_ready", live: true },
196
- });
237
+ return HttpResponse.json(
238
+ createDeploymentStatusResponse({
239
+ deploymentId: "deploy-destructive",
240
+ status: "data_ready",
241
+ })
242
+ );
243
+ }),
244
+ http.post(`${BASE_URL}/v1/deployments/deploy-destructive/set-live`, () => {
245
+ return HttpResponse.json(createSetLiveSuccessResponse());
197
246
  })
198
247
  );
199
248
 
@@ -204,25 +253,6 @@ describe("Deploy API", () => {
204
253
 
205
254
  const parsed = new URL(capturedUrl ?? "");
206
255
  expect(parsed.searchParams.get("allow_destructive_operations")).toBe("true");
207
- expect(parsed.searchParams.get("auto_promote")).toBe("true");
208
- });
209
-
210
- it("does not send auto_promote in check mode", async () => {
211
- let capturedUrl: string | null = null;
212
-
213
- server.use(
214
- http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
215
- capturedUrl = request.url;
216
- return HttpResponse.json({ result: "success" });
217
- })
218
- );
219
-
220
- const result = await deployToMain(config, resources, { check: true });
221
-
222
- expect(result.success).toBe(true);
223
- const parsed = new URL(capturedUrl ?? "");
224
- expect(parsed.searchParams.get("check")).toBe("true");
225
- expect(parsed.searchParams.get("auto_promote")).toBeNull();
226
256
  });
227
257
 
228
258
  it("adds actionable guidance to Forward/Classic workspace errors", async () => {
@@ -248,6 +278,49 @@ describe("Deploy API", () => {
248
278
  );
249
279
  });
250
280
 
281
+ it("handles failed deployment status", async () => {
282
+ server.use(
283
+ http.post(`${BASE_URL}/v1/deploy`, () => {
284
+ return HttpResponse.json(
285
+ createDeploySuccessResponse({ deploymentId: "deploy-fail", status: "pending" })
286
+ );
287
+ }),
288
+ http.get(`${BASE_URL}/v1/deployments/deploy-fail`, () => {
289
+ return HttpResponse.json(
290
+ createDeploymentStatusResponse({ deploymentId: "deploy-fail", status: "failed" })
291
+ );
292
+ })
293
+ );
294
+
295
+ const result = await deployToMain(config, resources, { pollIntervalMs: 1 });
296
+
297
+ expect(result.success).toBe(false);
298
+ expect(result.error).toContain("Deployment failed with status: failed");
299
+ });
300
+
301
+ it("handles set-live failure", async () => {
302
+ server.use(
303
+ http.post(`${BASE_URL}/v1/deploy`, () => {
304
+ return HttpResponse.json(
305
+ createDeploySuccessResponse({ deploymentId: "deploy-setlive-fail" })
306
+ );
307
+ }),
308
+ http.get(`${BASE_URL}/v1/deployments/deploy-setlive-fail`, () => {
309
+ return HttpResponse.json(
310
+ createDeploymentStatusResponse({ deploymentId: "deploy-setlive-fail", status: "data_ready" })
311
+ );
312
+ }),
313
+ http.post(`${BASE_URL}/v1/deployments/deploy-setlive-fail/set-live`, () => {
314
+ return HttpResponse.json({ error: "Set live failed" }, { status: 500 });
315
+ })
316
+ );
317
+
318
+ const result = await deployToMain(config, resources, { pollIntervalMs: 1 });
319
+
320
+ expect(result.success).toBe(false);
321
+ expect(result.error).toContain("Failed to set deployment as live");
322
+ });
323
+
251
324
  it("normalizes baseUrl with trailing slash", async () => {
252
325
  let capturedUrl: string | null = null;
253
326
 
@@ -255,14 +328,16 @@ describe("Deploy API", () => {
255
328
  http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
256
329
  capturedUrl = request.url;
257
330
  return HttpResponse.json(
258
- createDeploySuccessResponse({ deploymentId: "deploy-slash", status: "pending" })
331
+ createDeploySuccessResponse({ deploymentId: "deploy-slash" })
259
332
  );
260
333
  }),
261
334
  http.get(`${BASE_URL}/v1/deployments/deploy-slash`, () => {
262
- return HttpResponse.json({
263
- result: "success",
264
- deployment: { id: "deploy-slash", status: "data_ready", live: true },
265
- });
335
+ return HttpResponse.json(
336
+ createDeploymentStatusResponse({ deploymentId: "deploy-slash", status: "data_ready" })
337
+ );
338
+ }),
339
+ http.post(`${BASE_URL}/v1/deployments/deploy-slash/set-live`, () => {
340
+ return HttpResponse.json(createSetLiveSuccessResponse());
266
341
  })
267
342
  );
268
343
 
@@ -275,7 +350,71 @@ describe("Deploy API", () => {
275
350
  const parsed = new URL(capturedUrl ?? "");
276
351
  expect(parsed.pathname).toBe("/v1/deploy");
277
352
  expect(parsed.searchParams.get("from")).toBe("ts-sdk");
278
- expect(parsed.searchParams.get("auto_promote")).toBe("true");
353
+ });
354
+
355
+ it("times out when deployment never becomes ready", async () => {
356
+ server.use(
357
+ http.post(`${BASE_URL}/v1/deploy`, () => {
358
+ return HttpResponse.json(
359
+ createDeploySuccessResponse({ deploymentId: "deploy-timeout", status: "pending" })
360
+ );
361
+ }),
362
+ http.get(`${BASE_URL}/v1/deployments/deploy-timeout`, () => {
363
+ return HttpResponse.json(
364
+ createDeploymentStatusResponse({ deploymentId: "deploy-timeout", status: "pending" })
365
+ );
366
+ })
367
+ );
368
+
369
+ const result = await deployToMain(config, resources, {
370
+ pollIntervalMs: 1,
371
+ maxPollAttempts: 3,
372
+ });
373
+
374
+ expect(result.success).toBe(false);
375
+ expect(result.error).toContain("Deployment timed out");
376
+ });
377
+
378
+ it("includes connections in deploy form data", async () => {
379
+ const resourcesWithConnections: GeneratedResources = {
380
+ ...resources,
381
+ connections: [
382
+ {
383
+ name: "my_kafka",
384
+ content: "TYPE kafka\nKAFKA_BROKERS kafka:9092\nKAFKA_TOPIC events\n",
385
+ },
386
+ ],
387
+ };
388
+
389
+ let capturedFormData: FormData | null = null;
390
+
391
+ server.use(
392
+ http.post(`${BASE_URL}/v1/deploy`, async ({ request }) => {
393
+ capturedFormData = await request.formData();
394
+ return HttpResponse.json(
395
+ createDeploySuccessResponse({ deploymentId: "deploy-conn", status: "pending" })
396
+ );
397
+ }),
398
+ http.get(`${BASE_URL}/v1/deployments/deploy-conn`, () => {
399
+ return HttpResponse.json(
400
+ createDeploymentStatusResponse({ deploymentId: "deploy-conn", status: "data_ready" })
401
+ );
402
+ }),
403
+ http.post(`${BASE_URL}/v1/deployments/deploy-conn/set-live`, () => {
404
+ return HttpResponse.json(createSetLiveSuccessResponse());
405
+ })
406
+ );
407
+
408
+ const result = await deployToMain(config, resourcesWithConnections, {
409
+ pollIntervalMs: 1,
410
+ });
411
+
412
+ expect(result.success).toBe(true);
413
+ expect(result.connectionCount).toBe(1);
414
+ expect(capturedFormData).not.toBeNull();
415
+ // 1 datasource + 1 pipe + 1 connection
416
+ const allValues = capturedFormData!.getAll("data_project://");
417
+ expect(allValues.length).toBe(3);
279
418
  });
280
419
  });
281
420
  });
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";
@@ -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,61 @@ 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
+ // Add connections
206
+ for (const conn of resources.connections ?? []) {
207
+ const fieldName = `data_project://`;
208
+ const fileName = `${conn.name}.connection`;
209
+ if (debug) {
210
+ console.log(`[debug] Adding connection: ${fieldName} (filename: ${fileName})`);
211
+ console.log(`[debug] Content:\n${conn.content}\n`);
212
+ }
213
+ formData.append(
214
+ fieldName,
215
+ new Blob([conn.content], { type: "text/plain" }),
216
+ fileName
217
+ );
218
+ }
219
+
220
+ // Step 0: Clean up any stale non-live deployments that might block the new deployment
221
+ try {
222
+ const deploymentsUrl = `${baseUrl}/v1/deployments`;
223
+ const deploymentsResponse = await tinybirdFetch(deploymentsUrl, {
224
+ headers: {
225
+ Authorization: `Bearer ${config.token}`,
226
+ },
227
+ });
228
+
229
+ if (deploymentsResponse.ok) {
230
+ const deploymentsBody = (await deploymentsResponse.json()) as DeploymentsListResponse;
231
+ const staleDeployments = deploymentsBody.deployments.filter(
232
+ (d) => !d.live && d.status !== "live"
233
+ );
234
+
235
+ for (const stale of staleDeployments) {
236
+ if (debug) {
237
+ console.log(`[debug] Cleaning up stale deployment: ${stale.id} (status: ${stale.status})`);
238
+ }
239
+ await tinybirdFetch(`${baseUrl}/v1/deployments/${stale.id}`, {
240
+ method: "DELETE",
241
+ headers: {
242
+ Authorization: `Bearer ${config.token}`,
243
+ },
244
+ });
245
+ }
246
+ }
247
+ } catch (e) {
248
+ // Ignore errors during cleanup - we'll try to deploy anyway
249
+ if (debug) {
250
+ console.log(`[debug] Failed to clean up stale deployments: ${e}`);
251
+ }
252
+ }
253
+
254
+ // Step 1: Create deployment via /v1/deploy
199
255
  const deployUrlBase = `${baseUrl}/v1/deploy`;
200
256
  const urlParams = new URLSearchParams();
201
257
  if (options?.check) {
202
258
  urlParams.set("check", "true");
203
259
  }
204
- const autoPromote = options?.autoPromote ?? !options?.check;
205
- if (autoPromote) {
206
- urlParams.set("auto_promote", "true");
207
- }
208
260
  if (options?.allowDestructiveOperations) {
209
261
  urlParams.set("allow_destructive_operations", "true");
210
262
  }
@@ -371,22 +423,18 @@ export async function deployToMain(
371
423
  });
372
424
  }
373
425
 
374
- let deployment = deploymentDetails;
375
- let statusAttempts = 0;
426
+ // Step 2: Poll until deployment is ready
427
+ let deployment = body.deployment;
428
+ let attempts = 0;
376
429
 
377
430
  options?.callbacks?.onWaitingForReady?.();
378
431
 
379
- while (
380
- deployment.status !== "data_ready" &&
381
- deployment.status !== "failed" &&
382
- deployment.status !== "error" &&
383
- statusAttempts < maxPollAttempts
384
- ) {
432
+ while (deployment.status !== "data_ready" && attempts < maxPollAttempts) {
385
433
  await sleep(pollIntervalMs);
386
- statusAttempts++;
434
+ attempts++;
387
435
 
388
436
  if (debug) {
389
- console.log(`[debug] Polling deployment status (attempt ${statusAttempts})...`);
437
+ console.log(`[debug] Polling deployment status (attempt ${attempts})...`);
390
438
  }
391
439
 
392
440
  const statusUrl = `${baseUrl}/v1/deployments/${deploymentId}`;
@@ -410,18 +458,23 @@ export async function deployToMain(
410
458
 
411
459
  const statusBody = (await statusResponse.json()) as DeploymentStatusResponse;
412
460
  deployment = statusBody.deployment;
413
- }
414
461
 
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
- };
462
+ if (debug) {
463
+ console.log(`[debug] Deployment status: ${deployment.status}`);
464
+ }
465
+
466
+ // Check for failed status
467
+ if (deployment.status === "failed" || deployment.status === "error") {
468
+ return {
469
+ success: false,
470
+ result: "failed",
471
+ error: `Deployment failed with status: ${deployment.status}`,
472
+ datasourceCount: resources.datasources.length,
473
+ pipeCount: resources.pipes.length,
474
+ connectionCount: resources.connections?.length ?? 0,
475
+ buildId: deploymentId,
476
+ };
477
+ }
425
478
  }
426
479
 
427
480
  if (deployment.status !== "data_ready") {
@@ -438,64 +491,38 @@ export async function deployToMain(
438
491
 
439
492
  options?.callbacks?.onDeploymentReady?.();
440
493
 
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
- }
494
+ // Step 3: Set the deployment as live
495
+ const setLiveUrl = `${baseUrl}/v1/deployments/${deploymentId}/set-live`;
471
496
 
472
- const statusBody = (await statusResponse.json()) as DeploymentStatusResponse;
473
- deployment = statusBody.deployment;
474
- }
497
+ if (debug) {
498
+ console.log(`[debug] POST ${setLiveUrl}`);
499
+ }
475
500
 
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
- }
501
+ const setLiveResponse = await tinybirdFetch(setLiveUrl, {
502
+ method: "POST",
503
+ headers: {
504
+ Authorization: `Bearer ${config.token}`,
505
+ },
506
+ });
487
507
 
488
- options?.callbacks?.onDeploymentPromoted?.();
508
+ if (!setLiveResponse.ok) {
509
+ const setLiveBody = await setLiveResponse.text();
510
+ return {
511
+ success: false,
512
+ result: "failed",
513
+ error: `Failed to set deployment as live: ${setLiveResponse.status} ${setLiveResponse.statusText}\n${setLiveBody}`,
514
+ datasourceCount: resources.datasources.length,
515
+ pipeCount: resources.pipes.length,
516
+ connectionCount: resources.connections?.length ?? 0,
517
+ buildId: deploymentId,
518
+ };
489
519
  }
490
520
 
491
521
  if (debug) {
492
- const stateLabel = deployment.live ? "live" : "ready";
493
- console.log(`[debug] Deployment ${deploymentId} is now ${stateLabel}`);
522
+ console.log(`[debug] Deployment ${deploymentId} is now live`);
494
523
  }
495
524
 
496
- if (deployment.live) {
497
- options?.callbacks?.onDeploymentLive?.(deploymentId);
498
- }
525
+ options?.callbacks?.onDeploymentLive?.(deploymentId);
499
526
 
500
527
  return {
501
528
  success: true,
@@ -517,6 +544,9 @@ export async function deployToMain(
517
544
  };
518
545
  }
519
546
 
547
+ /**
548
+ * Helper function to sleep for a given number of milliseconds
549
+ */
520
550
  function sleep(ms: number): Promise<void> {
521
551
  return new Promise((resolve) => setTimeout(resolve, ms));
522
552
  }