@tinybirdco/sdk 0.0.51 → 0.0.53
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/README.md +5 -0
- package/dist/api/deploy.d.ts +10 -17
- package/dist/api/deploy.d.ts.map +1 -1
- package/dist/api/deploy.js +81 -79
- package/dist/api/deploy.js.map +1 -1
- package/dist/api/deploy.test.js +74 -77
- package/dist/api/deploy.test.js.map +1 -1
- package/dist/cli/commands/deploy.d.ts +2 -0
- package/dist/cli/commands/deploy.d.ts.map +1 -1
- package/dist/cli/commands/deploy.js +1 -0
- package/dist/cli/commands/deploy.js.map +1 -1
- package/dist/cli/commands/deploy.test.d.ts +2 -0
- package/dist/cli/commands/deploy.test.d.ts.map +1 -0
- package/dist/cli/commands/deploy.test.js +68 -0
- package/dist/cli/commands/deploy.test.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/deploy.test.ts +86 -128
- package/src/api/deploy.ts +108 -107
- package/src/cli/commands/deploy.test.ts +82 -0
- package/src/cli/commands/deploy.ts +3 -0
- package/src/cli/index.ts +5 -0
package/src/api/deploy.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll,
|
|
1
|
+
import { describe, it, expect, beforeAll, afterEach, afterAll, vi } from "vitest";
|
|
2
2
|
import { setupServer } from "msw/node";
|
|
3
3
|
import { http, HttpResponse } from "msw";
|
|
4
4
|
import { deployToMain } from "./deploy.js";
|
|
@@ -6,25 +6,14 @@ import type { BuildConfig } from "./build.js";
|
|
|
6
6
|
import {
|
|
7
7
|
BASE_URL,
|
|
8
8
|
createDeploySuccessResponse,
|
|
9
|
-
createDeploymentStatusResponse,
|
|
10
|
-
createSetLiveSuccessResponse,
|
|
11
9
|
createBuildFailureResponse,
|
|
12
10
|
createBuildMultipleErrorsResponse,
|
|
13
|
-
createDeploymentsListResponse,
|
|
14
11
|
} from "../test/handlers.js";
|
|
15
12
|
import type { GeneratedResources } from "../generator/index.js";
|
|
16
13
|
|
|
17
14
|
const server = setupServer();
|
|
18
15
|
|
|
19
16
|
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
|
-
});
|
|
28
17
|
afterEach(() => server.resetHandlers());
|
|
29
18
|
afterAll(() => server.close());
|
|
30
19
|
|
|
@@ -35,17 +24,12 @@ describe("Deploy API", () => {
|
|
|
35
24
|
};
|
|
36
25
|
|
|
37
26
|
const resources: GeneratedResources = {
|
|
38
|
-
datasources: [
|
|
39
|
-
|
|
40
|
-
],
|
|
41
|
-
pipes: [
|
|
42
|
-
{ name: "top_events", content: "NODE main\nSQL > SELECT * FROM events" },
|
|
43
|
-
],
|
|
27
|
+
datasources: [{ name: "events", content: "SCHEMA > timestamp DateTime" }],
|
|
28
|
+
pipes: [{ name: "top_events", content: "NODE main\nSQL > SELECT * FROM events" }],
|
|
44
29
|
connections: [],
|
|
45
30
|
};
|
|
46
31
|
|
|
47
|
-
|
|
48
|
-
function setupSuccessfulDeployFlow(deploymentId = "deploy-abc") {
|
|
32
|
+
function setupAutoPromoteSuccessFlow(deploymentId = "deploy-abc") {
|
|
49
33
|
server.use(
|
|
50
34
|
http.post(`${BASE_URL}/v1/deploy`, () => {
|
|
51
35
|
return HttpResponse.json(
|
|
@@ -53,64 +37,42 @@ describe("Deploy API", () => {
|
|
|
53
37
|
);
|
|
54
38
|
}),
|
|
55
39
|
http.get(`${BASE_URL}/v1/deployments/${deploymentId}`, () => {
|
|
56
|
-
return HttpResponse.json(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
40
|
+
return HttpResponse.json({
|
|
41
|
+
result: "success",
|
|
42
|
+
deployment: {
|
|
43
|
+
id: deploymentId,
|
|
44
|
+
status: "data_ready",
|
|
45
|
+
live: true,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
62
48
|
})
|
|
63
49
|
);
|
|
64
50
|
}
|
|
65
51
|
|
|
66
52
|
describe("deployToMain", () => {
|
|
67
|
-
it("successfully deploys resources with
|
|
68
|
-
|
|
53
|
+
it("successfully deploys resources with auto-promote flow", async () => {
|
|
54
|
+
setupAutoPromoteSuccessFlow("deploy-abc");
|
|
69
55
|
|
|
70
|
-
const
|
|
56
|
+
const onDeploymentLive = vi.fn();
|
|
57
|
+
const result = await deployToMain(config, resources, {
|
|
58
|
+
pollIntervalMs: 1,
|
|
59
|
+
callbacks: { onDeploymentLive },
|
|
60
|
+
});
|
|
71
61
|
|
|
72
62
|
expect(result.success).toBe(true);
|
|
73
63
|
expect(result.result).toBe("success");
|
|
74
64
|
expect(result.buildId).toBe("deploy-abc");
|
|
75
65
|
expect(result.datasourceCount).toBe(1);
|
|
76
66
|
expect(result.pipeCount).toBe(1);
|
|
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);
|
|
67
|
+
expect(onDeploymentLive).toHaveBeenCalledWith("deploy-abc");
|
|
105
68
|
});
|
|
106
69
|
|
|
107
70
|
it("handles deploy failure with single error", async () => {
|
|
108
71
|
server.use(
|
|
109
72
|
http.post(`${BASE_URL}/v1/deploy`, () => {
|
|
110
|
-
return HttpResponse.json(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
73
|
+
return HttpResponse.json(createBuildFailureResponse("Permission denied"), {
|
|
74
|
+
status: 200,
|
|
75
|
+
});
|
|
114
76
|
})
|
|
115
77
|
);
|
|
116
78
|
|
|
@@ -144,10 +106,7 @@ describe("Deploy API", () => {
|
|
|
144
106
|
it("handles HTTP error responses", async () => {
|
|
145
107
|
server.use(
|
|
146
108
|
http.post(`${BASE_URL}/v1/deploy`, () => {
|
|
147
|
-
return HttpResponse.json(
|
|
148
|
-
{ result: "failed", error: "Forbidden" },
|
|
149
|
-
{ status: 403 }
|
|
150
|
-
);
|
|
109
|
+
return HttpResponse.json({ result: "failed", error: "Forbidden" }, { status: 403 });
|
|
151
110
|
})
|
|
152
111
|
);
|
|
153
112
|
|
|
@@ -167,28 +126,24 @@ describe("Deploy API", () => {
|
|
|
167
126
|
})
|
|
168
127
|
);
|
|
169
128
|
|
|
170
|
-
await expect(deployToMain(config, resources)).rejects.toThrow(
|
|
171
|
-
"Failed to parse response"
|
|
172
|
-
);
|
|
129
|
+
await expect(deployToMain(config, resources)).rejects.toThrow("Failed to parse response");
|
|
173
130
|
});
|
|
174
131
|
|
|
175
|
-
it("uses /v1/deploy endpoint
|
|
132
|
+
it("uses /v1/deploy endpoint and sends auto_promote by default", async () => {
|
|
176
133
|
let capturedUrl: string | null = null;
|
|
177
134
|
|
|
178
135
|
server.use(
|
|
179
136
|
http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
|
|
180
137
|
capturedUrl = request.url;
|
|
181
138
|
return HttpResponse.json(
|
|
182
|
-
createDeploySuccessResponse({ deploymentId: "deploy-url-test" })
|
|
139
|
+
createDeploySuccessResponse({ deploymentId: "deploy-url-test", status: "pending" })
|
|
183
140
|
);
|
|
184
141
|
}),
|
|
185
142
|
http.get(`${BASE_URL}/v1/deployments/deploy-url-test`, () => {
|
|
186
|
-
return HttpResponse.json(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
http.post(`${BASE_URL}/v1/deployments/deploy-url-test/set-live`, () => {
|
|
191
|
-
return HttpResponse.json(createSetLiveSuccessResponse());
|
|
143
|
+
return HttpResponse.json({
|
|
144
|
+
result: "success",
|
|
145
|
+
deployment: { id: "deploy-url-test", status: "data_ready", live: true },
|
|
146
|
+
});
|
|
192
147
|
})
|
|
193
148
|
);
|
|
194
149
|
|
|
@@ -197,49 +152,76 @@ describe("Deploy API", () => {
|
|
|
197
152
|
const parsed = new URL(capturedUrl ?? "");
|
|
198
153
|
expect(parsed.pathname).toBe("/v1/deploy");
|
|
199
154
|
expect(parsed.searchParams.get("from")).toBe("ts-sdk");
|
|
155
|
+
expect(parsed.searchParams.get("auto_promote")).toBe("true");
|
|
200
156
|
});
|
|
201
157
|
|
|
202
|
-
it("
|
|
158
|
+
it("passes allow_destructive_operations when explicitly enabled", async () => {
|
|
159
|
+
let capturedUrl: string | null = null;
|
|
160
|
+
|
|
203
161
|
server.use(
|
|
204
|
-
http.post(`${BASE_URL}/v1/deploy`, () => {
|
|
162
|
+
http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
|
|
163
|
+
capturedUrl = request.url;
|
|
205
164
|
return HttpResponse.json(
|
|
206
|
-
createDeploySuccessResponse({ deploymentId: "deploy-
|
|
165
|
+
createDeploySuccessResponse({ deploymentId: "deploy-destructive", status: "pending" })
|
|
207
166
|
);
|
|
208
167
|
}),
|
|
209
|
-
http.get(`${BASE_URL}/v1/deployments/deploy-
|
|
210
|
-
return HttpResponse.json(
|
|
211
|
-
|
|
212
|
-
|
|
168
|
+
http.get(`${BASE_URL}/v1/deployments/deploy-destructive`, () => {
|
|
169
|
+
return HttpResponse.json({
|
|
170
|
+
result: "success",
|
|
171
|
+
deployment: { id: "deploy-destructive", status: "data_ready", live: true },
|
|
172
|
+
});
|
|
213
173
|
})
|
|
214
174
|
);
|
|
215
175
|
|
|
216
|
-
|
|
176
|
+
await deployToMain(config, resources, {
|
|
177
|
+
pollIntervalMs: 1,
|
|
178
|
+
allowDestructiveOperations: true,
|
|
179
|
+
});
|
|
217
180
|
|
|
218
|
-
|
|
219
|
-
expect(
|
|
181
|
+
const parsed = new URL(capturedUrl ?? "");
|
|
182
|
+
expect(parsed.searchParams.get("allow_destructive_operations")).toBe("true");
|
|
183
|
+
expect(parsed.searchParams.get("auto_promote")).toBe("true");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("does not send auto_promote in check mode", async () => {
|
|
187
|
+
let capturedUrl: string | null = null;
|
|
188
|
+
|
|
189
|
+
server.use(
|
|
190
|
+
http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
|
|
191
|
+
capturedUrl = request.url;
|
|
192
|
+
return HttpResponse.json({ result: "success" });
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const result = await deployToMain(config, resources, { check: true });
|
|
197
|
+
|
|
198
|
+
expect(result.success).toBe(true);
|
|
199
|
+
const parsed = new URL(capturedUrl ?? "");
|
|
200
|
+
expect(parsed.searchParams.get("check")).toBe("true");
|
|
201
|
+
expect(parsed.searchParams.get("auto_promote")).toBeNull();
|
|
220
202
|
});
|
|
221
203
|
|
|
222
|
-
it("
|
|
204
|
+
it("adds actionable guidance to Forward/Classic workspace errors", async () => {
|
|
223
205
|
server.use(
|
|
224
206
|
http.post(`${BASE_URL}/v1/deploy`, () => {
|
|
225
207
|
return HttpResponse.json(
|
|
226
|
-
|
|
208
|
+
{
|
|
209
|
+
result: "failed",
|
|
210
|
+
error:
|
|
211
|
+
"This is a Tinybird Forward workspace, and this operation is only available for Tinybird Classic workspaces.",
|
|
212
|
+
},
|
|
213
|
+
{ status: 400 }
|
|
227
214
|
);
|
|
228
|
-
}),
|
|
229
|
-
http.get(`${BASE_URL}/v1/deployments/deploy-setlive-fail`, () => {
|
|
230
|
-
return HttpResponse.json(
|
|
231
|
-
createDeploymentStatusResponse({ deploymentId: "deploy-setlive-fail", status: "data_ready" })
|
|
232
|
-
);
|
|
233
|
-
}),
|
|
234
|
-
http.post(`${BASE_URL}/v1/deployments/deploy-setlive-fail/set-live`, () => {
|
|
235
|
-
return HttpResponse.json({ error: "Set live failed" }, { status: 500 });
|
|
236
215
|
})
|
|
237
216
|
);
|
|
238
217
|
|
|
239
|
-
const result = await deployToMain(config, resources
|
|
218
|
+
const result = await deployToMain(config, resources);
|
|
240
219
|
|
|
241
220
|
expect(result.success).toBe(false);
|
|
242
|
-
expect(result.error).toContain("
|
|
221
|
+
expect(result.error).toContain("Tinybird Forward workspace");
|
|
222
|
+
expect(result.error).toContain(
|
|
223
|
+
"Use the Tinybird Classic CLI (`tb`) from a Tinybird Classic workspace for this operation."
|
|
224
|
+
);
|
|
243
225
|
});
|
|
244
226
|
|
|
245
227
|
it("normalizes baseUrl with trailing slash", async () => {
|
|
@@ -249,16 +231,14 @@ describe("Deploy API", () => {
|
|
|
249
231
|
http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
|
|
250
232
|
capturedUrl = request.url;
|
|
251
233
|
return HttpResponse.json(
|
|
252
|
-
createDeploySuccessResponse({ deploymentId: "deploy-slash" })
|
|
234
|
+
createDeploySuccessResponse({ deploymentId: "deploy-slash", status: "pending" })
|
|
253
235
|
);
|
|
254
236
|
}),
|
|
255
237
|
http.get(`${BASE_URL}/v1/deployments/deploy-slash`, () => {
|
|
256
|
-
return HttpResponse.json(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
http.post(`${BASE_URL}/v1/deployments/deploy-slash/set-live`, () => {
|
|
261
|
-
return HttpResponse.json(createSetLiveSuccessResponse());
|
|
238
|
+
return HttpResponse.json({
|
|
239
|
+
result: "success",
|
|
240
|
+
deployment: { id: "deploy-slash", status: "data_ready", live: true },
|
|
241
|
+
});
|
|
262
242
|
})
|
|
263
243
|
);
|
|
264
244
|
|
|
@@ -271,29 +251,7 @@ describe("Deploy API", () => {
|
|
|
271
251
|
const parsed = new URL(capturedUrl ?? "");
|
|
272
252
|
expect(parsed.pathname).toBe("/v1/deploy");
|
|
273
253
|
expect(parsed.searchParams.get("from")).toBe("ts-sdk");
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
it("times out when deployment never becomes ready", async () => {
|
|
277
|
-
server.use(
|
|
278
|
-
http.post(`${BASE_URL}/v1/deploy`, () => {
|
|
279
|
-
return HttpResponse.json(
|
|
280
|
-
createDeploySuccessResponse({ deploymentId: "deploy-timeout", status: "pending" })
|
|
281
|
-
);
|
|
282
|
-
}),
|
|
283
|
-
http.get(`${BASE_URL}/v1/deployments/deploy-timeout`, () => {
|
|
284
|
-
return HttpResponse.json(
|
|
285
|
-
createDeploymentStatusResponse({ deploymentId: "deploy-timeout", status: "pending" })
|
|
286
|
-
);
|
|
287
|
-
})
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
const result = await deployToMain(config, resources, {
|
|
291
|
-
pollIntervalMs: 1,
|
|
292
|
-
maxPollAttempts: 3,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
expect(result.success).toBe(false);
|
|
296
|
-
expect(result.error).toContain("Deployment timed out");
|
|
254
|
+
expect(parsed.searchParams.get("auto_promote")).toBe("true");
|
|
297
255
|
});
|
|
298
256
|
});
|
|
299
257
|
});
|
package/src/api/deploy.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deploy resources to Tinybird main workspace
|
|
3
|
-
* Uses the /v1/deploy endpoint
|
|
3
|
+
* Uses the /v1/deploy endpoint with auto-promotion enabled
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { GeneratedResources } from "../generator/index.js";
|
|
7
7
|
import type { BuildConfig, BuildApiResult } from "./build.js";
|
|
8
8
|
import { tinybirdFetch } from "./fetcher.js";
|
|
9
9
|
|
|
10
|
+
const FORWARD_CLASSIC_GUIDANCE =
|
|
11
|
+
"Use the Tinybird Classic CLI (`tb`) from a Tinybird Classic workspace for this operation.";
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
14
|
* Feedback item from deployment response
|
|
12
15
|
*/
|
|
@@ -28,13 +31,6 @@ export interface Deployment {
|
|
|
28
31
|
feedback?: DeploymentFeedback[];
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
/**
|
|
32
|
-
* Response from /v1/deployments list endpoint
|
|
33
|
-
*/
|
|
34
|
-
export interface DeploymentsListResponse {
|
|
35
|
-
deployments: Deployment[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
34
|
/**
|
|
39
35
|
* Response from /v1/deploy endpoint
|
|
40
36
|
*/
|
|
@@ -45,6 +41,14 @@ export interface DeployResponse {
|
|
|
45
41
|
errors?: Array<{ filename?: string; error: string }>;
|
|
46
42
|
}
|
|
47
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Response from /v1/deployments/{id} endpoint
|
|
46
|
+
*/
|
|
47
|
+
export interface DeploymentStatusResponse {
|
|
48
|
+
result: string;
|
|
49
|
+
deployment: Deployment;
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
/**
|
|
49
53
|
* Detailed deployment information with resource changes
|
|
50
54
|
*/
|
|
@@ -71,21 +75,11 @@ export interface DeploymentDetails extends Deployment {
|
|
|
71
75
|
errors?: Array<{ filename?: string; error: string }>;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
/**
|
|
75
|
-
* Response from /v1/deployments/{id} endpoint
|
|
76
|
-
*/
|
|
77
|
-
export interface DeploymentStatusResponse {
|
|
78
|
-
result: string;
|
|
79
|
-
deployment: Deployment;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
78
|
/**
|
|
83
79
|
* Deploy generated resources to Tinybird main workspace
|
|
84
80
|
*
|
|
85
81
|
* Uses the /v1/deploy endpoint which accepts all resources in a single
|
|
86
|
-
* multipart form request.
|
|
87
|
-
* 1. Polls until the deployment is ready (status === 'data_ready')
|
|
88
|
-
* 2. Sets the deployment as live via /v1/deployments/{id}/set-live
|
|
82
|
+
* multipart form request.
|
|
89
83
|
*
|
|
90
84
|
* @param config - Build configuration with API URL and token
|
|
91
85
|
* @param resources - Generated resources to deploy
|
|
@@ -158,13 +152,14 @@ export async function deployToMain(
|
|
|
158
152
|
pollIntervalMs?: number;
|
|
159
153
|
maxPollAttempts?: number;
|
|
160
154
|
check?: boolean;
|
|
155
|
+
autoPromote?: boolean;
|
|
161
156
|
allowDestructiveOperations?: boolean;
|
|
162
157
|
callbacks?: DeployCallbacks;
|
|
163
158
|
}
|
|
164
159
|
): Promise<BuildApiResult> {
|
|
165
160
|
const debug = options?.debug ?? !!process.env.TINYBIRD_DEBUG;
|
|
166
161
|
const pollIntervalMs = options?.pollIntervalMs ?? 1000;
|
|
167
|
-
const maxPollAttempts = options?.maxPollAttempts ?? 120;
|
|
162
|
+
const maxPollAttempts = options?.maxPollAttempts ?? 120;
|
|
168
163
|
const baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
169
164
|
|
|
170
165
|
const formData = new FormData();
|
|
@@ -199,46 +194,17 @@ export async function deployToMain(
|
|
|
199
194
|
);
|
|
200
195
|
}
|
|
201
196
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
const deploymentsUrl = `${baseUrl}/v1/deployments`;
|
|
205
|
-
const deploymentsResponse = await tinybirdFetch(deploymentsUrl, {
|
|
206
|
-
headers: {
|
|
207
|
-
Authorization: `Bearer ${config.token}`,
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
if (deploymentsResponse.ok) {
|
|
212
|
-
const deploymentsBody = (await deploymentsResponse.json()) as DeploymentsListResponse;
|
|
213
|
-
const staleDeployments = deploymentsBody.deployments.filter(
|
|
214
|
-
(d) => !d.live && d.status !== "live"
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
for (const stale of staleDeployments) {
|
|
218
|
-
if (debug) {
|
|
219
|
-
console.log(`[debug] Cleaning up stale deployment: ${stale.id} (status: ${stale.status})`);
|
|
220
|
-
}
|
|
221
|
-
await tinybirdFetch(`${baseUrl}/v1/deployments/${stale.id}`, {
|
|
222
|
-
method: "DELETE",
|
|
223
|
-
headers: {
|
|
224
|
-
Authorization: `Bearer ${config.token}`,
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
} catch (e) {
|
|
230
|
-
// Ignore errors during cleanup - we'll try to deploy anyway
|
|
231
|
-
if (debug) {
|
|
232
|
-
console.log(`[debug] Failed to clean up stale deployments: ${e}`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Step 1: Create deployment via /v1/deploy
|
|
197
|
+
// Create deployment via /v1/deploy.
|
|
198
|
+
// `auto_promote=true` makes the API promote the deployment automatically.
|
|
237
199
|
const deployUrlBase = `${baseUrl}/v1/deploy`;
|
|
238
200
|
const urlParams = new URLSearchParams();
|
|
239
201
|
if (options?.check) {
|
|
240
202
|
urlParams.set("check", "true");
|
|
241
203
|
}
|
|
204
|
+
const autoPromote = options?.autoPromote ?? !options?.check;
|
|
205
|
+
if (autoPromote) {
|
|
206
|
+
urlParams.set("auto_promote", "true");
|
|
207
|
+
}
|
|
242
208
|
if (options?.allowDestructiveOperations) {
|
|
243
209
|
urlParams.set("allow_destructive_operations", "true");
|
|
244
210
|
}
|
|
@@ -280,7 +246,7 @@ export async function deployToMain(
|
|
|
280
246
|
.map((f) => {
|
|
281
247
|
// Extract just the filename from "Datasource events.datasource" format
|
|
282
248
|
const resourceName = f.resource.split(" ").pop() ?? f.resource;
|
|
283
|
-
return `${resourceName}: ${f.message}`;
|
|
249
|
+
return `${resourceName}: ${normalizeDeployErrorMessage(f.message)}`;
|
|
284
250
|
})
|
|
285
251
|
.join("\n");
|
|
286
252
|
};
|
|
@@ -298,12 +264,12 @@ export async function deployToMain(
|
|
|
298
264
|
return body.errors
|
|
299
265
|
.map((e) => {
|
|
300
266
|
const prefix = e.filename ? `[${e.filename}] ` : "";
|
|
301
|
-
return `${prefix}${e.error}`;
|
|
267
|
+
return `${prefix}${normalizeDeployErrorMessage(e.error)}`;
|
|
302
268
|
})
|
|
303
269
|
.join("\n");
|
|
304
270
|
}
|
|
305
271
|
if (body.error) {
|
|
306
|
-
return body.error;
|
|
272
|
+
return normalizeDeployErrorMessage(body.error);
|
|
307
273
|
}
|
|
308
274
|
// Include raw response body for debugging when no structured error is available
|
|
309
275
|
return `HTTP ${response.status}: ${response.statusText}\nResponse: ${rawBody}`;
|
|
@@ -405,18 +371,22 @@ export async function deployToMain(
|
|
|
405
371
|
});
|
|
406
372
|
}
|
|
407
373
|
|
|
408
|
-
|
|
409
|
-
let
|
|
410
|
-
let attempts = 0;
|
|
374
|
+
let deployment = deploymentDetails;
|
|
375
|
+
let statusAttempts = 0;
|
|
411
376
|
|
|
412
377
|
options?.callbacks?.onWaitingForReady?.();
|
|
413
378
|
|
|
414
|
-
while (
|
|
379
|
+
while (
|
|
380
|
+
deployment.status !== "data_ready" &&
|
|
381
|
+
deployment.status !== "failed" &&
|
|
382
|
+
deployment.status !== "error" &&
|
|
383
|
+
statusAttempts < maxPollAttempts
|
|
384
|
+
) {
|
|
415
385
|
await sleep(pollIntervalMs);
|
|
416
|
-
|
|
386
|
+
statusAttempts++;
|
|
417
387
|
|
|
418
388
|
if (debug) {
|
|
419
|
-
console.log(`[debug] Polling deployment status (attempt ${
|
|
389
|
+
console.log(`[debug] Polling deployment status (attempt ${statusAttempts})...`);
|
|
420
390
|
}
|
|
421
391
|
|
|
422
392
|
const statusUrl = `${baseUrl}/v1/deployments/${deploymentId}`;
|
|
@@ -440,30 +410,13 @@ export async function deployToMain(
|
|
|
440
410
|
|
|
441
411
|
const statusBody = (await statusResponse.json()) as DeploymentStatusResponse;
|
|
442
412
|
deployment = statusBody.deployment;
|
|
443
|
-
|
|
444
|
-
if (debug) {
|
|
445
|
-
console.log(`[debug] Deployment status: ${deployment.status}`);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Check for failed status
|
|
449
|
-
if (deployment.status === "failed" || deployment.status === "error") {
|
|
450
|
-
return {
|
|
451
|
-
success: false,
|
|
452
|
-
result: "failed",
|
|
453
|
-
error: `Deployment failed with status: ${deployment.status}`,
|
|
454
|
-
datasourceCount: resources.datasources.length,
|
|
455
|
-
pipeCount: resources.pipes.length,
|
|
456
|
-
connectionCount: resources.connections?.length ?? 0,
|
|
457
|
-
buildId: deploymentId,
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
413
|
}
|
|
461
414
|
|
|
462
|
-
if (deployment.status
|
|
415
|
+
if (deployment.status === "failed" || deployment.status === "error") {
|
|
463
416
|
return {
|
|
464
417
|
success: false,
|
|
465
418
|
result: "failed",
|
|
466
|
-
error: `Deployment
|
|
419
|
+
error: `Deployment failed with status: ${deployment.status}`,
|
|
467
420
|
datasourceCount: resources.datasources.length,
|
|
468
421
|
pipeCount: resources.pipes.length,
|
|
469
422
|
connectionCount: resources.connections?.length ?? 0,
|
|
@@ -471,28 +424,11 @@ export async function deployToMain(
|
|
|
471
424
|
};
|
|
472
425
|
}
|
|
473
426
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
// Step 3: Set the deployment as live
|
|
477
|
-
const setLiveUrl = `${baseUrl}/v1/deployments/${deploymentId}/set-live`;
|
|
478
|
-
|
|
479
|
-
if (debug) {
|
|
480
|
-
console.log(`[debug] POST ${setLiveUrl}`);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const setLiveResponse = await tinybirdFetch(setLiveUrl, {
|
|
484
|
-
method: "POST",
|
|
485
|
-
headers: {
|
|
486
|
-
Authorization: `Bearer ${config.token}`,
|
|
487
|
-
},
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
if (!setLiveResponse.ok) {
|
|
491
|
-
const setLiveBody = await setLiveResponse.text();
|
|
427
|
+
if (deployment.status !== "data_ready") {
|
|
492
428
|
return {
|
|
493
429
|
success: false,
|
|
494
430
|
result: "failed",
|
|
495
|
-
error: `
|
|
431
|
+
error: `Deployment timed out after ${maxPollAttempts} attempts. Last status: ${deployment.status}`,
|
|
496
432
|
datasourceCount: resources.datasources.length,
|
|
497
433
|
pipeCount: resources.pipes.length,
|
|
498
434
|
connectionCount: resources.connections?.length ?? 0,
|
|
@@ -500,11 +436,66 @@ export async function deployToMain(
|
|
|
500
436
|
};
|
|
501
437
|
}
|
|
502
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
|
+
|
|
503
491
|
if (debug) {
|
|
504
|
-
|
|
492
|
+
const stateLabel = deployment.live ? "live" : "ready";
|
|
493
|
+
console.log(`[debug] Deployment ${deploymentId} is now ${stateLabel}`);
|
|
505
494
|
}
|
|
506
495
|
|
|
507
|
-
|
|
496
|
+
if (deployment.live) {
|
|
497
|
+
options?.callbacks?.onDeploymentLive?.(deploymentId);
|
|
498
|
+
}
|
|
508
499
|
|
|
509
500
|
return {
|
|
510
501
|
success: true,
|
|
@@ -526,9 +517,19 @@ export async function deployToMain(
|
|
|
526
517
|
};
|
|
527
518
|
}
|
|
528
519
|
|
|
529
|
-
/**
|
|
530
|
-
* Helper function to sleep for a given number of milliseconds
|
|
531
|
-
*/
|
|
532
520
|
function sleep(ms: number): Promise<void> {
|
|
533
521
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
534
522
|
}
|
|
523
|
+
|
|
524
|
+
function normalizeDeployErrorMessage(message: string): string {
|
|
525
|
+
const trimmedMessage = message.trim();
|
|
526
|
+
const isForwardClassicError =
|
|
527
|
+
trimmedMessage.includes("Tinybird Forward workspace") &&
|
|
528
|
+
trimmedMessage.includes("Tinybird Classic workspaces");
|
|
529
|
+
|
|
530
|
+
if (!isForwardClassicError || trimmedMessage.includes(FORWARD_CLASSIC_GUIDANCE)) {
|
|
531
|
+
return message;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return `${trimmedMessage}\n${FORWARD_CLASSIC_GUIDANCE}`;
|
|
535
|
+
}
|