@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.
- package/dist/api/build.test.js +23 -0
- package/dist/api/build.test.js.map +1 -1
- package/dist/api/deploy.d.ts +17 -10
- package/dist/api/deploy.d.ts.map +1 -1
- package/dist/api/deploy.js +86 -68
- package/dist/api/deploy.js.map +1 -1
- package/dist/api/deploy.test.js +119 -55
- package/dist/api/deploy.test.js.map +1 -1
- package/dist/schema/types.d.ts +34 -25
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +15 -3
- package/dist/schema/types.js.map +1 -1
- package/dist/schema/types.test.js +230 -94
- package/dist/schema/types.test.js.map +1 -1
- package/package.json +1 -1
- package/src/api/build.test.ts +30 -0
- package/src/api/deploy.test.ts +201 -62
- package/src/api/deploy.ts +119 -89
- package/src/schema/types.test.ts +294 -96
- package/src/schema/types.ts +145 -68
package/src/api/deploy.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterEach, afterAll
|
|
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: [
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
54
|
-
|
|
67
|
+
it("successfully deploys resources with full flow", async () => {
|
|
68
|
+
setupSuccessfulDeployFlow("deploy-abc");
|
|
55
69
|
|
|
56
|
-
const
|
|
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
|
-
|
|
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(
|
|
74
|
-
|
|
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(
|
|
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(
|
|
194
|
+
await expect(deployToMain(config, resources)).rejects.toThrow(
|
|
195
|
+
"Failed to parse response"
|
|
196
|
+
);
|
|
154
197
|
});
|
|
155
198
|
|
|
156
|
-
it("uses /v1/deploy endpoint
|
|
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"
|
|
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
|
-
|
|
169
|
-
|
|
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"
|
|
233
|
+
createDeploySuccessResponse({ deploymentId: "deploy-destructive" })
|
|
190
234
|
);
|
|
191
235
|
}),
|
|
192
236
|
http.get(`${BASE_URL}/v1/deployments/deploy-destructive`, () => {
|
|
193
|
-
return HttpResponse.json(
|
|
194
|
-
|
|
195
|
-
|
|
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"
|
|
331
|
+
createDeploySuccessResponse({ deploymentId: "deploy-slash" })
|
|
259
332
|
);
|
|
260
333
|
}),
|
|
261
334
|
http.get(`${BASE_URL}/v1/deployments/deploy-slash`, () => {
|
|
262
|
-
return HttpResponse.json(
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
198
|
-
|
|
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
|
-
|
|
375
|
-
let
|
|
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
|
-
|
|
434
|
+
attempts++;
|
|
387
435
|
|
|
388
436
|
if (debug) {
|
|
389
|
-
console.log(`[debug] Polling deployment status (attempt ${
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
497
|
+
if (debug) {
|
|
498
|
+
console.log(`[debug] POST ${setLiveUrl}`);
|
|
499
|
+
}
|
|
475
500
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
console.log(`[debug] Deployment ${deploymentId} is now ${stateLabel}`);
|
|
522
|
+
console.log(`[debug] Deployment ${deploymentId} is now live`);
|
|
494
523
|
}
|
|
495
524
|
|
|
496
|
-
|
|
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
|
}
|