@tinybirdco/sdk 0.0.80 → 0.0.82

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.
Files changed (62) hide show
  1. package/README.md +1 -0
  2. package/dist/api/branches.test.js +28 -0
  3. package/dist/api/branches.test.js.map +1 -1
  4. package/dist/api/deploy.d.ts +14 -4
  5. package/dist/api/deploy.d.ts.map +1 -1
  6. package/dist/api/deploy.js +104 -100
  7. package/dist/api/deploy.js.map +1 -1
  8. package/dist/api/deploy.test.js +165 -76
  9. package/dist/api/deploy.test.js.map +1 -1
  10. package/dist/cli/commands/build.d.ts.map +1 -1
  11. package/dist/cli/commands/build.js +2 -2
  12. package/dist/cli/commands/build.js.map +1 -1
  13. package/dist/cli/commands/clear.js +2 -2
  14. package/dist/cli/commands/clear.js.map +1 -1
  15. package/dist/cli/commands/deploy.d.ts +8 -0
  16. package/dist/cli/commands/deploy.d.ts.map +1 -1
  17. package/dist/cli/commands/deploy.js +2 -0
  18. package/dist/cli/commands/deploy.js.map +1 -1
  19. package/dist/cli/commands/dev.d.ts.map +1 -1
  20. package/dist/cli/commands/dev.js +2 -2
  21. package/dist/cli/commands/dev.js.map +1 -1
  22. package/dist/cli/commands/preview.js +2 -2
  23. package/dist/cli/commands/preview.js.map +1 -1
  24. package/dist/cli/commands/preview.test.js +39 -0
  25. package/dist/cli/commands/preview.test.js.map +1 -1
  26. package/dist/cli/config-types.d.ts +4 -1
  27. package/dist/cli/config-types.d.ts.map +1 -1
  28. package/dist/cli/config.d.ts +1 -1
  29. package/dist/cli/config.d.ts.map +1 -1
  30. package/dist/cli/config.js +2 -3
  31. package/dist/cli/config.js.map +1 -1
  32. package/dist/cli/config.test.js +13 -4
  33. package/dist/cli/config.test.js.map +1 -1
  34. package/dist/cli/index.js +11 -0
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/output.d.ts +15 -0
  37. package/dist/cli/output.d.ts.map +1 -1
  38. package/dist/cli/output.js +24 -0
  39. package/dist/cli/output.js.map +1 -1
  40. package/dist/client/base.js +2 -2
  41. package/dist/client/base.js.map +1 -1
  42. package/dist/test/handlers.d.ts +2 -6
  43. package/dist/test/handlers.d.ts.map +1 -1
  44. package/dist/test/handlers.js +5 -9
  45. package/dist/test/handlers.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/api/branches.test.ts +32 -0
  48. package/src/api/deploy.test.ts +193 -100
  49. package/src/api/deploy.ts +129 -116
  50. package/src/cli/commands/build.ts +3 -4
  51. package/src/cli/commands/clear.ts +2 -2
  52. package/src/cli/commands/deploy.ts +10 -0
  53. package/src/cli/commands/dev.ts +3 -4
  54. package/src/cli/commands/preview.test.ts +46 -0
  55. package/src/cli/commands/preview.ts +2 -2
  56. package/src/cli/config-types.ts +4 -1
  57. package/src/cli/config.test.ts +20 -4
  58. package/src/cli/config.ts +3 -4
  59. package/src/cli/index.ts +18 -0
  60. package/src/cli/output.ts +27 -0
  61. package/src/client/base.ts +2 -2
  62. package/src/test/handlers.ts +6 -10
package/src/api/deploy.ts CHANGED
@@ -10,6 +10,19 @@ import { tinybirdFetch } from "./fetcher.js";
10
10
  const FORWARD_CLASSIC_GUIDANCE =
11
11
  "Use the Tinybird Classic CLI (`tb`) from a Tinybird Classic workspace for this operation.";
12
12
 
13
+ /**
14
+ * Poll interval used while waiting for a deployment to reach `data_ready`
15
+ * (and, when auto-promoting, `live`). Matches the Tinybird CLI (`tb deploy`).
16
+ */
17
+ const POLL_INTERVAL_MS = 5_000;
18
+
19
+ /**
20
+ * How many consecutive `failed` status polls to tolerate before giving up.
21
+ * With a 5s poll interval this is ~5 minutes, matching the CLI safety valve
22
+ * for deployments that fail but never auto-delete.
23
+ */
24
+ export const MAX_CONSECUTIVE_FAILED_POLLS = 60;
25
+
13
26
  /**
14
27
  * Feedback item from deployment response
15
28
  */
@@ -87,8 +100,10 @@ export interface DeploymentStatusResponse {
87
100
  *
88
101
  * Uses the /v1/deploy endpoint which accepts all resources in a single
89
102
  * 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
103
+ * 1. When `auto` is true (default), sends `auto_promote=true` so the
104
+ * server promotes the deployment as soon as it's ready.
105
+ * 2. When `wait` is true (default), polls until the deployment reaches
106
+ * `data_ready` (and, if `auto` is also true, until it becomes live).
92
107
  *
93
108
  * @param config - Build configuration with API URL and token
94
109
  * @param resources - Generated resources to deploy
@@ -139,6 +154,8 @@ export interface DeploymentChanges {
139
154
  export interface DeployCallbacks {
140
155
  /** Called when deployment is created and changes are available */
141
156
  onChanges?: (changes: DeploymentChanges) => void;
157
+ /** Called when deployment was submitted but wait was disabled */
158
+ onDeploymentSubmitted?: (deploymentId: string) => void;
142
159
  /** Called when waiting for deployment to be ready */
143
160
  onWaitingForReady?: () => void;
144
161
  /** Called when deployment is ready */
@@ -158,18 +175,17 @@ export async function deployToMain(
158
175
  resources: GeneratedResources,
159
176
  options?: {
160
177
  debug?: boolean;
161
- pollIntervalMs?: number;
162
- maxPollAttempts?: number;
163
178
  check?: boolean;
164
179
  allowDestructiveOperations?: boolean;
180
+ wait?: boolean;
181
+ auto?: boolean;
165
182
  callbacks?: DeployCallbacks;
166
183
  }
167
184
  ): Promise<BuildApiResult> {
168
185
  const debug = options?.debug ?? !!process.env.TINYBIRD_DEBUG;
169
- const pollIntervalMs = options?.pollIntervalMs ?? 1000;
170
- const maxPollAttempts = options?.maxPollAttempts ?? 120; // 2 minutes max
186
+ const wait = options?.wait ?? true;
187
+ const auto = options?.auto ?? true;
171
188
  const baseUrl = config.baseUrl.replace(/\/$/, "");
172
- let previousLiveDeploymentId: string | undefined;
173
189
 
174
190
  const formData = new FormData();
175
191
 
@@ -231,10 +247,6 @@ export async function deployToMain(
231
247
 
232
248
  if (deploymentsResponse.ok) {
233
249
  const deploymentsBody = (await deploymentsResponse.json()) as DeploymentsListResponse;
234
- const previousLiveDeployment = deploymentsBody.deployments.find(
235
- (d) => d.live || d.status === "live"
236
- );
237
- previousLiveDeploymentId = previousLiveDeployment?.id;
238
250
  const staleDeployments = deploymentsBody.deployments.filter(
239
251
  (d) => !d.live && d.status !== "live"
240
252
  );
@@ -264,6 +276,9 @@ export async function deployToMain(
264
276
  const urlParams = new URLSearchParams();
265
277
  if (options?.check) {
266
278
  urlParams.set("check", "true");
279
+ } else if (auto) {
280
+ // Server will auto-promote the deployment when it's ready
281
+ urlParams.set("auto_promote", "true");
267
282
  }
268
283
  if (options?.allowDestructiveOperations) {
269
284
  urlParams.set("allow_destructive_operations", "true");
@@ -431,18 +446,67 @@ export async function deployToMain(
431
446
  });
432
447
  }
433
448
 
434
- // Step 2: Poll until deployment is ready
449
+ const deploymentChanges = {
450
+ pipes: {
451
+ changed: deploymentDetails.changed_pipe_names ?? [],
452
+ created: deploymentDetails.new_pipe_names ?? [],
453
+ deleted: deploymentDetails.deleted_pipe_names ?? [],
454
+ },
455
+ datasources: {
456
+ changed: deploymentDetails.changed_datasource_names ?? [],
457
+ created: deploymentDetails.new_datasource_names ?? [],
458
+ deleted: deploymentDetails.deleted_datasource_names ?? [],
459
+ },
460
+ };
461
+
462
+ // If we're not waiting, return as soon as the server accepted the deployment.
463
+ if (!wait) {
464
+ options?.callbacks?.onDeploymentSubmitted?.(deploymentId);
465
+ return {
466
+ success: true,
467
+ result: "success",
468
+ datasourceCount: resources.datasources.length,
469
+ pipeCount: resources.pipes.length,
470
+ connectionCount: resources.connections?.length ?? 0,
471
+ buildId: deploymentId,
472
+ ...deploymentChanges,
473
+ };
474
+ }
475
+
476
+ // Step 2: Poll until the deployment reaches a terminal state.
435
477
  let deployment = body.deployment;
436
- let attempts = 0;
478
+ let timesSeenFailed = 0;
479
+ let notifiedReady = false;
480
+ let notifiedWaitingForPromote = false;
437
481
 
438
482
  options?.callbacks?.onWaitingForReady?.();
439
483
 
440
- while (deployment.status !== "data_ready" && attempts < maxPollAttempts) {
441
- await sleep(pollIntervalMs);
442
- attempts++;
484
+ const isDone = (): boolean => {
485
+ if (deployment.status !== "data_ready") {
486
+ return false;
487
+ }
488
+ if (auto) {
489
+ // When auto-promoting we must also wait for the server to flip it live.
490
+ return deployment.live === true;
491
+ }
492
+ return true;
493
+ };
494
+
495
+ const buildError = (message: string): BuildApiResult => ({
496
+ success: false,
497
+ result: "failed",
498
+ error: message,
499
+ datasourceCount: resources.datasources.length,
500
+ pipeCount: resources.pipes.length,
501
+ connectionCount: resources.connections?.length ?? 0,
502
+ buildId: deploymentId,
503
+ });
504
+
505
+ while (!isDone()) {
506
+ await sleep(POLL_INTERVAL_MS);
443
507
 
444
508
  if (debug) {
445
- console.log(`[debug] Polling deployment status (attempt ${attempts})...`);
509
+ console.log(`[debug] Polling deployment status...`);
446
510
  }
447
511
 
448
512
  const statusUrl = `${baseUrl}/v1/deployments/${deploymentId}`;
@@ -453,114 +517,72 @@ export async function deployToMain(
453
517
  });
454
518
 
455
519
  if (!statusResponse.ok) {
456
- return {
457
- success: false,
458
- result: "failed",
459
- error: `Failed to check deployment status: ${statusResponse.status} ${statusResponse.statusText}`,
460
- datasourceCount: resources.datasources.length,
461
- pipeCount: resources.pipes.length,
462
- connectionCount: resources.connections?.length ?? 0,
463
- buildId: deploymentId,
464
- };
520
+ return buildError(
521
+ `Failed to check deployment status: ${statusResponse.status} ${statusResponse.statusText}`
522
+ );
465
523
  }
466
524
 
467
525
  const statusBody = (await statusResponse.json()) as DeploymentStatusResponse;
468
526
  deployment = statusBody.deployment;
469
527
 
470
528
  if (debug) {
471
- console.log(`[debug] Deployment status: ${deployment.status}`);
529
+ console.log(
530
+ `[debug] Deployment status: ${deployment.status} (live=${deployment.live ?? false})`
531
+ );
472
532
  }
473
533
 
474
- // Check for failed status
475
- if (deployment.status === "failed" || deployment.status === "error") {
476
- return {
477
- success: false,
478
- result: "failed",
479
- error: `Deployment failed with status: ${deployment.status}`,
480
- datasourceCount: resources.datasources.length,
481
- pipeCount: resources.pipes.length,
482
- connectionCount: resources.connections?.length ?? 0,
483
- buildId: deploymentId,
484
- };
534
+ if (deployment.status === "failed") {
535
+ timesSeenFailed++;
536
+ if (timesSeenFailed > MAX_CONSECUTIVE_FAILED_POLLS) {
537
+ return buildError(
538
+ "Deployment failed to create and didn't start deleting automatically after 5 minutes. " +
539
+ "You might need to delete it manually in the UI."
540
+ );
541
+ }
542
+ continue;
485
543
  }
486
- }
487
-
488
- if (deployment.status !== "data_ready") {
489
- return {
490
- success: false,
491
- result: "failed",
492
- error: `Deployment timed out after ${maxPollAttempts} attempts. Last status: ${deployment.status}`,
493
- datasourceCount: resources.datasources.length,
494
- pipeCount: resources.pipes.length,
495
- connectionCount: resources.connections?.length ?? 0,
496
- buildId: deploymentId,
497
- };
498
- }
499
544
 
500
- options?.callbacks?.onDeploymentReady?.();
501
-
502
- // Step 3: Set the deployment as live
503
- const setLiveUrl = `${baseUrl}/v1/deployments/${deploymentId}/set-live`;
504
-
505
- if (debug) {
506
- console.log(`[debug] POST ${setLiveUrl}`);
507
- }
508
-
509
- const setLiveResponse = await tinybirdFetch(setLiveUrl, {
510
- method: "POST",
511
- headers: {
512
- Authorization: `Bearer ${config.token}`,
513
- },
514
- });
545
+ if (deployment.status === "deleting" || deployment.status === "deleted") {
546
+ const errors = deployment.feedback
547
+ ?.filter((f) => f.level === "ERROR")
548
+ .map((f) => f.message)
549
+ .join("\n");
550
+ const errorSuffix = errors ? `\n${errors}` : "";
551
+ return buildError(
552
+ `Deployment failed and ${
553
+ deployment.status === "deleting" ? "is being" : "was"
554
+ } deleted automatically.${errorSuffix}`
555
+ );
556
+ }
515
557
 
516
- if (!setLiveResponse.ok) {
517
- const setLiveBody = await setLiveResponse.text();
518
- return {
519
- success: false,
520
- result: "failed",
521
- error: `Failed to set deployment as live: ${setLiveResponse.status} ${setLiveResponse.statusText}\n${setLiveBody}`,
522
- datasourceCount: resources.datasources.length,
523
- pipeCount: resources.pipes.length,
524
- connectionCount: resources.connections?.length ?? 0,
525
- buildId: deploymentId,
526
- };
558
+ if (
559
+ auto &&
560
+ deployment.status === "data_ready" &&
561
+ !deployment.live &&
562
+ !notifiedWaitingForPromote
563
+ ) {
564
+ if (!notifiedReady) {
565
+ options?.callbacks?.onDeploymentReady?.();
566
+ notifiedReady = true;
567
+ }
568
+ options?.callbacks?.onWaitingForPromote?.();
569
+ notifiedWaitingForPromote = true;
570
+ }
527
571
  }
528
572
 
529
- if (debug) {
530
- console.log(`[debug] Deployment ${deploymentId} is now live`);
573
+ if (!notifiedReady) {
574
+ options?.callbacks?.onDeploymentReady?.();
575
+ notifiedReady = true;
531
576
  }
532
577
 
533
- if (previousLiveDeploymentId && previousLiveDeploymentId !== deploymentId) {
578
+ if (auto) {
534
579
  if (debug) {
535
- console.log(`[debug] Removing previous deployment: ${previousLiveDeploymentId}`);
536
- }
537
-
538
- const deletePreviousResponse = await tinybirdFetch(
539
- `${baseUrl}/v1/deployments/${previousLiveDeploymentId}`,
540
- {
541
- method: "DELETE",
542
- headers: {
543
- Authorization: `Bearer ${config.token}`,
544
- },
545
- }
546
- );
547
-
548
- if (!deletePreviousResponse.ok) {
549
- const deletePreviousBody = await deletePreviousResponse.text();
550
- return {
551
- success: false,
552
- result: "failed",
553
- error: `Failed to remove previous deployment: ${deletePreviousResponse.status} ${deletePreviousResponse.statusText}\n${deletePreviousBody}`,
554
- datasourceCount: resources.datasources.length,
555
- pipeCount: resources.pipes.length,
556
- connectionCount: resources.connections?.length ?? 0,
557
- buildId: deploymentId,
558
- };
580
+ console.log(`[debug] Deployment ${deploymentId} is now live`);
559
581
  }
582
+ options?.callbacks?.onDeploymentPromoted?.();
583
+ options?.callbacks?.onDeploymentLive?.(deploymentId);
560
584
  }
561
585
 
562
- options?.callbacks?.onDeploymentLive?.(deploymentId);
563
-
564
586
  return {
565
587
  success: true,
566
588
  result: "success",
@@ -568,16 +590,7 @@ export async function deployToMain(
568
590
  pipeCount: resources.pipes.length,
569
591
  connectionCount: resources.connections?.length ?? 0,
570
592
  buildId: deploymentId,
571
- pipes: {
572
- changed: deploymentDetails.changed_pipe_names ?? [],
573
- created: deploymentDetails.new_pipe_names ?? [],
574
- deleted: deploymentDetails.deleted_pipe_names ?? [],
575
- },
576
- datasources: {
577
- changed: deploymentDetails.changed_datasource_names ?? [],
578
- created: deploymentDetails.new_datasource_names ?? [],
579
- deleted: deploymentDetails.deleted_datasource_names ?? [],
580
- },
593
+ ...deploymentChanges,
581
594
  };
582
595
  }
583
596
 
@@ -225,10 +225,9 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise<Build
225
225
  console.log(`[debug] Getting/creating Tinybird branch: ${config.tinybirdBranch}`);
226
226
  }
227
227
  try {
228
- const branchDataMode: BranchDataMode | undefined =
229
- options.lastPartition || config.branchDataMode === "last_partition"
230
- ? "last_partition"
231
- : undefined;
228
+ const branchDataMode: BranchDataMode | undefined = options.lastPartition
229
+ ? "last_partition"
230
+ : config.branchDataMode ?? undefined;
232
231
  const branchOptions = branchDataMode
233
232
  ? { branch_data_mode: branchDataMode }
234
233
  : undefined;
@@ -149,8 +149,8 @@ async function clearCloudBranch(config: ResolvedConfig): Promise<ClearResult> {
149
149
 
150
150
  // Clear the branch (delete and recreate)
151
151
  const branchOptions: CreateBranchOptions | undefined =
152
- config.devMode !== "local" && config.branchDataMode === "last_partition"
153
- ? { branch_data_mode: "last_partition" }
152
+ config.devMode !== "local" && config.branchDataMode
153
+ ? { branch_data_mode: config.branchDataMode }
154
154
  : undefined;
155
155
 
156
156
  const newBranch = await clearBranch(
@@ -19,6 +19,14 @@ export interface DeployCommandOptions {
19
19
  check?: boolean;
20
20
  /** Allow deleting existing resources in main workspace deploys */
21
21
  allowDestructiveOperations?: boolean;
22
+ /**
23
+ * Wait for the deployment to finish. Defaults to true.
24
+ */
25
+ wait?: boolean;
26
+ /**
27
+ * Auto-promote the deployment when it's ready. Defaults to true.
28
+ */
29
+ auto?: boolean;
22
30
  /** Callbacks for deploy progress */
23
31
  callbacks?: DeployCallbacks;
24
32
  }
@@ -106,6 +114,8 @@ export async function runDeploy(options: DeployCommandOptions = {}): Promise<Dep
106
114
  {
107
115
  check: options.check,
108
116
  allowDestructiveOperations: options.allowDestructiveOperations,
117
+ wait: options.wait,
118
+ auto: options.auto,
109
119
  callbacks: options.callbacks,
110
120
  }
111
121
  );
@@ -240,10 +240,9 @@ export async function runDev(
240
240
  // Use tinybirdBranch (sanitized name) for Tinybird API, gitBranch for display
241
241
  if (config.tinybirdBranch) {
242
242
  const branchName = config.tinybirdBranch; // Sanitized name for Tinybird
243
- const branchDataMode: BranchDataMode | undefined =
244
- options.lastPartition || config.branchDataMode === "last_partition"
245
- ? "last_partition"
246
- : undefined;
243
+ const branchDataMode: BranchDataMode | undefined = options.lastPartition
244
+ ? "last_partition"
245
+ : config.branchDataMode ?? undefined;
247
246
  const branchOptions = branchDataMode
248
247
  ? { branch_data_mode: branchDataMode }
249
248
  : undefined;
@@ -120,6 +120,52 @@ describe("Preview command", () => {
120
120
  );
121
121
  });
122
122
 
123
+ it("creates cloud preview branch without data when branch_data_mode is not set", async () => {
124
+ const { loadConfigAsync } = await import("../config.js");
125
+ const { buildFromInclude } = await import("../../generator/index.js");
126
+ const { getBranch, createBranch } = await import("../../api/branches.js");
127
+ const { deployToMain } = await import("../../api/deploy.js");
128
+
129
+ vi.mocked(loadConfigAsync).mockResolvedValue({
130
+ include: ["test.ts"],
131
+ token: "p.test-token",
132
+ baseUrl: "https://api.tinybird.co",
133
+ configPath: "/test/tinybird.config.json",
134
+ devMode: "branch",
135
+ cwd: "/test",
136
+ gitBranch: "feature-test",
137
+ tinybirdBranch: "feature_test",
138
+ isMainBranch: false,
139
+ branchDataMode: null,
140
+ });
141
+ vi.mocked(buildFromInclude).mockResolvedValue({
142
+ resources: { datasources: [], pipes: [], connections: [] },
143
+ entities: { datasources: {}, pipes: {}, connections: {}, rawDatasources: [], rawPipes: [], sourceFiles: [] },
144
+ stats: { datasourceCount: 0, pipeCount: 0, connectionCount: 0 },
145
+ });
146
+ vi.mocked(getBranch).mockRejectedValue(new Error("not found"));
147
+ vi.mocked(createBranch).mockResolvedValue({
148
+ id: "b1",
149
+ name: "tmp_ci_feature_test",
150
+ token: "p.branch",
151
+ created_at: "2024-01-01T00:00:00Z",
152
+ });
153
+ vi.mocked(deployToMain).mockResolvedValue({
154
+ success: true,
155
+ result: "success",
156
+ datasourceCount: 0,
157
+ pipeCount: 0,
158
+ connectionCount: 0,
159
+ });
160
+
161
+ await runPreview();
162
+ expect(createBranch).toHaveBeenCalledWith(
163
+ expect.any(Object),
164
+ "tmp_ci_feature_test",
165
+ undefined
166
+ );
167
+ });
168
+
123
169
  it("ignores config branch_data_mode in local mode", async () => {
124
170
  const { loadConfigAsync } = await import("../config.js");
125
171
  const { buildFromInclude } = await import("../../generator/index.js");
@@ -227,8 +227,8 @@ export async function runPreview(options: PreviewCommandOptions = {}): Promise<P
227
227
  try {
228
228
  const apiConfig = { baseUrl: config.baseUrl, token: config.token };
229
229
  const branchOptions: CreateBranchOptions | undefined =
230
- config.branchDataMode === "last_partition"
231
- ? { branch_data_mode: "last_partition" }
230
+ config.branchDataMode
231
+ ? { branch_data_mode: config.branchDataMode }
232
232
  : undefined;
233
233
 
234
234
  // Check if branch already exists and delete it for a fresh start
@@ -28,6 +28,9 @@ export interface TinybirdConfig {
28
28
  baseUrl?: string;
29
29
  /** Development mode: "branch" (default) or "local" */
30
30
  devMode?: DevMode;
31
- /** Branch data mode applied on cloud branch creation (shared snake_case key) */
31
+ /**
32
+ * Branch data mode applied on cloud branch creation (shared snake_case key,
33
+ * also read by the tb CLI). Omit to create branches without data (default).
34
+ */
32
35
  branch_data_mode?: BranchDataMode;
33
36
  }
@@ -343,7 +343,23 @@ describe("Config", () => {
343
343
  expect(result.branchDataMode).toBe("last_partition");
344
344
  });
345
345
 
346
- it("defaults branch_data_mode to last_partition when missing", () => {
346
+ it("throws when branch_data_mode is none", () => {
347
+ const config = {
348
+ include: ["lib/datasources.ts"],
349
+ token: "test-token",
350
+ branch_data_mode: "none",
351
+ };
352
+ fs.writeFileSync(
353
+ path.join(tempDir, "tinybird.json"),
354
+ JSON.stringify(config)
355
+ );
356
+
357
+ expect(() => loadConfig(tempDir)).toThrow(
358
+ "Invalid branch_data_mode 'none'. Allowed values are: last_partition."
359
+ );
360
+ });
361
+
362
+ it("defaults branch_data_mode to no data when missing", () => {
347
363
  const config = {
348
364
  include: ["lib/datasources.ts"],
349
365
  token: "test-token",
@@ -354,10 +370,10 @@ describe("Config", () => {
354
370
  );
355
371
 
356
372
  const result = loadConfig(tempDir);
357
- expect(result.branchDataMode).toBe("last_partition");
373
+ expect(result.branchDataMode).toBeNull();
358
374
  });
359
375
 
360
- it("defaults empty branch_data_mode to last_partition", () => {
376
+ it("defaults empty branch_data_mode to no data", () => {
361
377
  const config = {
362
378
  include: ["lib/datasources.ts"],
363
379
  token: "test-token",
@@ -369,7 +385,7 @@ describe("Config", () => {
369
385
  );
370
386
 
371
387
  const result = loadConfig(tempDir);
372
- expect(result.branchDataMode).toBe("last_partition");
388
+ expect(result.branchDataMode).toBeNull();
373
389
  });
374
390
 
375
391
  it("throws when branch_data_mode is all_partitions", () => {
package/src/cli/config.ts CHANGED
@@ -17,7 +17,6 @@ export {
17
17
  import { BRANCH_DATA_MODE_VALUES } from "./config-types.js";
18
18
  import type { BranchDataMode, DevMode, TinybirdConfig } from "./config-types.js";
19
19
 
20
- const DEFAULT_BRANCH_DATA_MODE: BranchDataMode = "last_partition";
21
20
 
22
21
  /**
23
22
  * Resolved configuration with all values expanded
@@ -41,7 +40,7 @@ export interface ResolvedConfig {
41
40
  isMainBranch: boolean;
42
41
  /** Development mode: "branch" or "local" */
43
42
  devMode: DevMode;
44
- /** Branch data mode configured in tinybird.config.json */
43
+ /** Branch data mode configured in tinybird.config.json (null = create branches without data, the default) */
45
44
  branchDataMode?: BranchDataMode | null;
46
45
  }
47
46
 
@@ -212,10 +211,10 @@ function resolveBranchDataMode(raw: Record<string, unknown>): { mode: BranchData
212
211
  }
213
212
 
214
213
  const value = raw["branch_data_mode"];
215
- if (value === undefined || value === null) return { mode: DEFAULT_BRANCH_DATA_MODE, explicit: false };
214
+ if (value === undefined || value === null) return { mode: null, explicit: false };
216
215
  if (typeof value !== "string") throw new Error("branch_data_mode must be a string.");
217
216
  const mode = value.trim().toLowerCase();
218
- if (!mode) return { mode: DEFAULT_BRANCH_DATA_MODE, explicit: false };
217
+ if (!mode) return { mode: null, explicit: false };
219
218
  if (!BRANCH_DATA_MODE_VALUES.includes(mode as BranchDataMode)) {
220
219
  throw new Error(
221
220
  `Invalid branch_data_mode '${value}'. Allowed values are: ${BRANCH_DATA_MODE_VALUES.join(", ")}.`
package/src/cli/index.ts CHANGED
@@ -482,18 +482,33 @@ function createCli(): Command {
482
482
  "--allow-destructive-operations",
483
483
  "Allow deploys that delete existing datasources, pipes, or connections"
484
484
  )
485
+ .option("--wait", "Wait for the deployment to finish (default)")
486
+ .option("--no-wait", "Return as soon as the deployment is submitted")
487
+ .option(
488
+ "--auto",
489
+ "Auto-promote the deployment when it's ready (default)"
490
+ )
491
+ .option(
492
+ "--no-auto",
493
+ "Do not auto-promote the deployment; leave it staged for a manual promote"
494
+ )
485
495
  .option("--debug", "Show debug output including API requests/responses")
486
496
  .action(async (options) => {
487
497
  if (options.debug) {
488
498
  process.env.TINYBIRD_DEBUG = "1";
489
499
  }
490
500
 
501
+ const wait = options.wait !== false;
502
+ const auto = options.auto !== false;
503
+
491
504
  output.highlight("Deploying to main workspace...");
492
505
 
493
506
  const result = await runDeploy({
494
507
  dryRun: options.dryRun,
495
508
  check: options.check,
496
509
  allowDestructiveOperations: options.allowDestructiveOperations,
510
+ wait,
511
+ auto,
497
512
  callbacks: {
498
513
  onChanges: (deployChanges) => {
499
514
  // Show changes table immediately after deployment is created
@@ -531,8 +546,11 @@ function createCli(): Command {
531
546
 
532
547
  output.showChangesTable(changes);
533
548
  },
549
+ onDeploymentSubmitted: (id) => output.showDeploymentSubmitted(id, auto),
534
550
  onWaitingForReady: () => output.showWaitingForDeployment(),
535
551
  onDeploymentReady: () => output.showDeploymentReady(),
552
+ onWaitingForPromote: () => output.showWaitingForPromote(),
553
+ onDeploymentPromoted: () => output.showDeploymentPromoted(),
536
554
  onDeploymentLive: (id) => output.showDeploymentLive(id),
537
555
  onValidating: () => output.showValidatingDeployment(),
538
556
  },
package/src/cli/output.ts CHANGED
@@ -218,6 +218,30 @@ export function showDeploymentLive(deploymentId: string): void {
218
218
  success(`✓ Deployment #${deploymentId} is live!`);
219
219
  }
220
220
 
221
+ /**
222
+ * Show waiting for deployment promotion message
223
+ */
224
+ export function showWaitingForPromote(): void {
225
+ info("» Waiting for deployment to be promoted...");
226
+ }
227
+
228
+ /**
229
+ * Show deployment submitted message (used when --no-wait is set)
230
+ */
231
+ export function showDeploymentSubmitted(deploymentId: string, autoPromote: boolean): void {
232
+ const autoFrag = autoPromote
233
+ ? " It will be auto-promoted when ready."
234
+ : " It won't be auto-promoted when ready.";
235
+ success(`✓ Deployment #${deploymentId} submitted.${autoFrag}`);
236
+ }
237
+
238
+ /**
239
+ * Show deployment promoted message
240
+ */
241
+ export function showDeploymentPromoted(): void {
242
+ success("✓ Deployment promoted");
243
+ }
244
+
221
245
  /**
222
246
  * Show validating deployment message
223
247
  */
@@ -424,6 +448,9 @@ export const output = {
424
448
  showNoChanges,
425
449
  showWaitingForDeployment,
426
450
  showDeploymentReady,
451
+ showWaitingForPromote,
452
+ showDeploymentPromoted,
453
+ showDeploymentSubmitted,
427
454
  showDeploymentLive,
428
455
  showValidatingDeployment,
429
456
  showDeploySuccess,
@@ -342,8 +342,8 @@ export class TinybirdClient {
342
342
 
343
343
  const branchName = config.tinybirdBranch;
344
344
  const branchOptions: CreateBranchOptions | undefined =
345
- config.devMode !== "local" && config.branchDataMode === "last_partition"
346
- ? { branch_data_mode: "last_partition" }
345
+ config.devMode !== "local" && config.branchDataMode
346
+ ? { branch_data_mode: config.branchDataMode }
347
347
  : undefined;
348
348
 
349
349
  // Get or create branch (always fetch fresh to avoid stale cache issues)