@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/dist/api/deploy.d.ts +18 -11
- package/dist/api/deploy.d.ts.map +1 -1
- package/dist/api/deploy.js +77 -69
- package/dist/api/deploy.js.map +1 -1
- package/dist/api/deploy.test.js +106 -55
- package/dist/api/deploy.test.js.map +1 -1
- package/package.json +1 -1
- package/src/api/deploy.test.ts +183 -62
- package/src/api/deploy.ts +106 -91
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";
|
|
@@ -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
|
-
//
|
|
198
|
-
|
|
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
|
|
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
|
-
|
|
375
|
-
let
|
|
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
|
-
|
|
419
|
+
attempts++;
|
|
387
420
|
|
|
388
421
|
if (debug) {
|
|
389
|
-
console.log(`[debug] Polling deployment status (attempt ${
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
console.log(`[debug] Deployment ${deploymentId} is now ${stateLabel}`);
|
|
507
|
+
console.log(`[debug] Deployment ${deploymentId} is now live`);
|
|
494
508
|
}
|
|
495
509
|
|
|
496
|
-
|
|
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
|
}
|