From c76356b17ac62ae48eb8b5d8b88903ca01227b00 Mon Sep 17 00:00:00 2001 From: Tommy Healy Date: Tue, 7 Jul 2026 17:59:23 +0200 Subject: [PATCH 1/4] default to no data for branch data mode --- CHANGELOG.md | 8 ++++++ README.md | 1 + src/api/branches.test.ts | 34 +++++++++++++++++++++++ src/api/branches.ts | 2 +- src/cli/commands/build.ts | 7 +++-- src/cli/commands/clear.ts | 4 +-- src/cli/commands/dev.ts | 7 +++-- src/cli/commands/preview.test.ts | 46 ++++++++++++++++++++++++++++++++ src/cli/commands/preview.ts | 4 +-- src/cli/config-types.ts | 4 +-- src/cli/config.test.ts | 23 +++++++++++++--- src/cli/config.ts | 2 +- src/client/base.ts | 4 +-- 13 files changed, 124 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4b791..2768bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ## [Unreleased] +### Added + +- `branch_data_mode: "none"` config option to create cloud branches without copying any production data. + +### Changed + +- `branch_data_mode` now defaults to `"none"`: cloud branches are created empty unless `"last_partition"` is set explicitly. This restores the pre-`branch_data_mode` behavior and avoids branch-creation timeouts on workspaces with large partitions. Set `branch_data_mode: "last_partition"` in your config to keep copying production data into branches. + ## [0.0.80] - 2026-06-24 ### Security diff --git a/README.md b/README.md index c289670..fa5a123 100644 --- a/README.md +++ b/README.md @@ -525,6 +525,7 @@ export default { | `token` | `string` | *required* | API token. Supports `${ENV_VAR}` interpolation for environment variables | | `baseUrl` | `string` | `"https://api.tinybird.co"` | Tinybird API URL. Use `"https://api.us-east.tinybird.co"` for US region | | `devMode` | `"branch"` \| `"local"` | `"branch"` | Development mode. `"branch"` uses Tinybird cloud with branches, `"local"` uses local Docker container | +| `branch_data_mode` | `"none"` \| `"last_partition"` | `"none"` | Data attached to cloud branches on creation. `"none"` creates empty branches; `"last_partition"` copies the last partition of production data | ### Local Development Mode diff --git a/src/api/branches.test.ts b/src/api/branches.test.ts index fa239d4..8c917cb 100644 --- a/src/api/branches.test.ts +++ b/src/api/branches.test.ts @@ -223,6 +223,40 @@ describe("Branch API client", () => { expect(createParsed.searchParams.get("data")).toBe("last_partition"); }); + it("omits the data param when branch_data_mode is none", async () => { + const mockBranch = { + id: "branch-123", + name: "my-feature", + token: "p.branch-token", + created_at: "2024-01-01T00:00:00Z", + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ + job: { id: "job-123", status: "waiting" }, + workspace: { id: "ws-123" }, + }), + }); + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ id: "job-123", status: "done" }), + }); + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockBranch), + }); + + await createBranch(config, "my-feature", { + branch_data_mode: "none", + }); + + const [createUrl] = mockFetch.mock.calls[0]; + const createParsed = expectFromParam(createUrl); + expect(createParsed.searchParams.get("name")).toBe("my-feature"); + expect(createParsed.searchParams.get("data")).toBeNull(); + }); + it("uses custom fetch when provided", async () => { const customFetch = vi .fn() diff --git a/src/api/branches.ts b/src/api/branches.ts index ecfff37..6e49088 100644 --- a/src/api/branches.ts +++ b/src/api/branches.ts @@ -165,7 +165,7 @@ export async function createBranch( const fetchFn = getFetch(config); const url = new URL("/v1/environments", config.baseUrl); url.searchParams.set("name", name); - if (options?.branch_data_mode) { + if (options?.branch_data_mode && options.branch_data_mode !== "none") { url.searchParams.set("data", options.branch_data_mode); } diff --git a/src/cli/commands/build.ts b/src/cli/commands/build.ts index a23414b..46c71d5 100644 --- a/src/cli/commands/build.ts +++ b/src/cli/commands/build.ts @@ -225,10 +225,9 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise { // Clear the branch (delete and recreate) const branchOptions: CreateBranchOptions | undefined = - config.devMode !== "local" && config.branchDataMode === "last_partition" - ? { branch_data_mode: "last_partition" } + config.devMode !== "local" && config.branchDataMode + ? { branch_data_mode: config.branchDataMode } : undefined; const newBranch = await clearBranch( diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 493a0d8..d0fd1f4 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -240,10 +240,9 @@ export async function runDev( // Use tinybirdBranch (sanitized name) for Tinybird API, gitBranch for display if (config.tinybirdBranch) { const branchName = config.tinybirdBranch; // Sanitized name for Tinybird - const branchDataMode: BranchDataMode | undefined = - options.lastPartition || config.branchDataMode === "last_partition" - ? "last_partition" - : undefined; + const branchDataMode: BranchDataMode | undefined = options.lastPartition + ? "last_partition" + : config.branchDataMode ?? undefined; const branchOptions = branchDataMode ? { branch_data_mode: branchDataMode } : undefined; diff --git a/src/cli/commands/preview.test.ts b/src/cli/commands/preview.test.ts index 12deaae..b62d838 100644 --- a/src/cli/commands/preview.test.ts +++ b/src/cli/commands/preview.test.ts @@ -120,6 +120,52 @@ describe("Preview command", () => { ); }); + it("creates cloud preview branch without data when branch_data_mode is none", async () => { + const { loadConfigAsync } = await import("../config.js"); + const { buildFromInclude } = await import("../../generator/index.js"); + const { getBranch, createBranch } = await import("../../api/branches.js"); + const { deployToMain } = await import("../../api/deploy.js"); + + vi.mocked(loadConfigAsync).mockResolvedValue({ + include: ["test.ts"], + token: "p.test-token", + baseUrl: "https://api.tinybird.co", + configPath: "/test/tinybird.config.json", + devMode: "branch", + cwd: "/test", + gitBranch: "feature-test", + tinybirdBranch: "feature_test", + isMainBranch: false, + branchDataMode: "none", + }); + vi.mocked(buildFromInclude).mockResolvedValue({ + resources: { datasources: [], pipes: [], connections: [] }, + entities: { datasources: {}, pipes: {}, connections: {}, rawDatasources: [], rawPipes: [], sourceFiles: [] }, + stats: { datasourceCount: 0, pipeCount: 0, connectionCount: 0 }, + }); + vi.mocked(getBranch).mockRejectedValue(new Error("not found")); + vi.mocked(createBranch).mockResolvedValue({ + id: "b1", + name: "tmp_ci_feature_test", + token: "p.branch", + created_at: "2024-01-01T00:00:00Z", + }); + vi.mocked(deployToMain).mockResolvedValue({ + success: true, + result: "success", + datasourceCount: 0, + pipeCount: 0, + connectionCount: 0, + }); + + await runPreview(); + expect(createBranch).toHaveBeenCalledWith( + expect.any(Object), + "tmp_ci_feature_test", + { branch_data_mode: "none" } + ); + }); + it("ignores config branch_data_mode in local mode", async () => { const { loadConfigAsync } = await import("../config.js"); const { buildFromInclude } = await import("../../generator/index.js"); diff --git a/src/cli/commands/preview.ts b/src/cli/commands/preview.ts index 42c0f6f..def7445 100644 --- a/src/cli/commands/preview.ts +++ b/src/cli/commands/preview.ts @@ -227,8 +227,8 @@ export async function runPreview(options: PreviewCommandOptions = {}): Promise

{ expect(result.branchDataMode).toBe("last_partition"); }); - it("defaults branch_data_mode to last_partition when missing", () => { + it("resolves branch_data_mode as none", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", + branch_data_mode: "none", }; fs.writeFileSync( path.join(tempDir, "tinybird.json"), @@ -354,10 +355,24 @@ describe("Config", () => { ); const result = loadConfig(tempDir); - expect(result.branchDataMode).toBe("last_partition"); + expect(result.branchDataMode).toBe("none"); + }); + + it("defaults branch_data_mode to none when missing", () => { + const config = { + include: ["lib/datasources.ts"], + token: "test-token", + }; + fs.writeFileSync( + path.join(tempDir, "tinybird.json"), + JSON.stringify(config) + ); + + const result = loadConfig(tempDir); + expect(result.branchDataMode).toBe("none"); }); - it("defaults empty branch_data_mode to last_partition", () => { + it("defaults empty branch_data_mode to none", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", @@ -369,7 +384,7 @@ describe("Config", () => { ); const result = loadConfig(tempDir); - expect(result.branchDataMode).toBe("last_partition"); + expect(result.branchDataMode).toBe("none"); }); it("throws when branch_data_mode is all_partitions", () => { diff --git a/src/cli/config.ts b/src/cli/config.ts index 0f1e2ad..c694b0b 100644 --- a/src/cli/config.ts +++ b/src/cli/config.ts @@ -17,7 +17,7 @@ export { import { BRANCH_DATA_MODE_VALUES } from "./config-types.js"; import type { BranchDataMode, DevMode, TinybirdConfig } from "./config-types.js"; -const DEFAULT_BRANCH_DATA_MODE: BranchDataMode = "last_partition"; +const DEFAULT_BRANCH_DATA_MODE: BranchDataMode = "none"; /** * Resolved configuration with all values expanded diff --git a/src/client/base.ts b/src/client/base.ts index e453338..637c0c8 100644 --- a/src/client/base.ts +++ b/src/client/base.ts @@ -342,8 +342,8 @@ export class TinybirdClient { const branchName = config.tinybirdBranch; const branchOptions: CreateBranchOptions | undefined = - config.devMode !== "local" && config.branchDataMode === "last_partition" - ? { branch_data_mode: "last_partition" } + config.devMode !== "local" && config.branchDataMode + ? { branch_data_mode: config.branchDataMode } : undefined; // Get or create branch (always fetch fresh to avoid stale cache issues) From 80b3ada660821f803d9ca96989804ee78aaacdff Mon Sep 17 00:00:00 2001 From: Tommy Healy Date: Wed, 8 Jul 2026 16:28:36 +0200 Subject: [PATCH 2/4] changelog update --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2768bd1..7a6c45a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ### Changed -- `branch_data_mode` now defaults to `"none"`: cloud branches are created empty unless `"last_partition"` is set explicitly. This restores the pre-`branch_data_mode` behavior and avoids branch-creation timeouts on workspaces with large partitions. Set `branch_data_mode: "last_partition"` in your config to keep copying production data into branches. +- `branch_data_mode` now defaults to `"none"`: cloud branches are created empty unless `"last_partition"` is set explicitly. This restores the default behavior of `0.0.78` and earlier; only versions `0.0.79` through `0.0.81` copied the last partition of production data into branches by default. Set `branch_data_mode: "last_partition"` in your config to keep copying production data into branches. ## [0.0.80] - 2026-06-24 From bdc22b8d11985323377b12bf7087e00bd773f329 Mon Sep 17 00:00:00 2001 From: Tommy Healy Date: Fri, 10 Jul 2026 16:08:09 +0200 Subject: [PATCH 3/4] Fix undefined vs none bug --- CHANGELOG.md | 6 +----- README.md | 2 +- src/api/branches.test.ts | 6 ++---- src/api/branches.ts | 2 +- src/cli/commands/preview.test.ts | 6 +++--- src/cli/config-types.ts | 9 ++++++--- src/cli/config.test.ts | 15 ++++++++------- src/cli/config.ts | 7 +++---- 8 files changed, 25 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a6c45a..dcf4b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,9 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ## [Unreleased] -### Added - -- `branch_data_mode: "none"` config option to create cloud branches without copying any production data. - ### Changed -- `branch_data_mode` now defaults to `"none"`: cloud branches are created empty unless `"last_partition"` is set explicitly. This restores the default behavior of `0.0.78` and earlier; only versions `0.0.79` through `0.0.81` copied the last partition of production data into branches by default. Set `branch_data_mode: "last_partition"` in your config to keep copying production data into branches. +- Cloud branches are now created empty by default: when `branch_data_mode` is omitted from the config, no production data is copied into new branches. This restores the default behavior of `0.0.78` and earlier; only versions `0.0.79` through `0.0.81` copied the last partition of production data into branches by default. Set `branch_data_mode: "last_partition"` in your config to keep copying production data into branches. ## [0.0.80] - 2026-06-24 diff --git a/README.md b/README.md index fa5a123..8eabf39 100644 --- a/README.md +++ b/README.md @@ -525,7 +525,7 @@ export default { | `token` | `string` | *required* | API token. Supports `${ENV_VAR}` interpolation for environment variables | | `baseUrl` | `string` | `"https://api.tinybird.co"` | Tinybird API URL. Use `"https://api.us-east.tinybird.co"` for US region | | `devMode` | `"branch"` \| `"local"` | `"branch"` | Development mode. `"branch"` uses Tinybird cloud with branches, `"local"` uses local Docker container | -| `branch_data_mode` | `"none"` \| `"last_partition"` | `"none"` | Data attached to cloud branches on creation. `"none"` creates empty branches; `"last_partition"` copies the last partition of production data | +| `branch_data_mode` | `"last_partition"` | Data attached to cloud branches on creation. Omit to create empty branches; set `"last_partition"` to copy the last partition of production data | ### Local Development Mode diff --git a/src/api/branches.test.ts b/src/api/branches.test.ts index 8c917cb..1222406 100644 --- a/src/api/branches.test.ts +++ b/src/api/branches.test.ts @@ -223,7 +223,7 @@ describe("Branch API client", () => { expect(createParsed.searchParams.get("data")).toBe("last_partition"); }); - it("omits the data param when branch_data_mode is none", async () => { + it("omits the data param when no branch_data_mode is given", async () => { const mockBranch = { id: "branch-123", name: "my-feature", @@ -247,9 +247,7 @@ describe("Branch API client", () => { json: () => Promise.resolve(mockBranch), }); - await createBranch(config, "my-feature", { - branch_data_mode: "none", - }); + await createBranch(config, "my-feature"); const [createUrl] = mockFetch.mock.calls[0]; const createParsed = expectFromParam(createUrl); diff --git a/src/api/branches.ts b/src/api/branches.ts index 6e49088..ecfff37 100644 --- a/src/api/branches.ts +++ b/src/api/branches.ts @@ -165,7 +165,7 @@ export async function createBranch( const fetchFn = getFetch(config); const url = new URL("/v1/environments", config.baseUrl); url.searchParams.set("name", name); - if (options?.branch_data_mode && options.branch_data_mode !== "none") { + if (options?.branch_data_mode) { url.searchParams.set("data", options.branch_data_mode); } diff --git a/src/cli/commands/preview.test.ts b/src/cli/commands/preview.test.ts index b62d838..718596e 100644 --- a/src/cli/commands/preview.test.ts +++ b/src/cli/commands/preview.test.ts @@ -120,7 +120,7 @@ describe("Preview command", () => { ); }); - it("creates cloud preview branch without data when branch_data_mode is none", async () => { + it("creates cloud preview branch without data when branch_data_mode is not set", async () => { const { loadConfigAsync } = await import("../config.js"); const { buildFromInclude } = await import("../../generator/index.js"); const { getBranch, createBranch } = await import("../../api/branches.js"); @@ -136,7 +136,7 @@ describe("Preview command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataMode: "none", + branchDataMode: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ resources: { datasources: [], pipes: [], connections: [] }, @@ -162,7 +162,7 @@ describe("Preview command", () => { expect(createBranch).toHaveBeenCalledWith( expect.any(Object), "tmp_ci_feature_test", - { branch_data_mode: "none" } + undefined ); }); diff --git a/src/cli/config-types.ts b/src/cli/config-types.ts index 047560e..ea6fe7a 100644 --- a/src/cli/config-types.ts +++ b/src/cli/config-types.ts @@ -11,8 +11,8 @@ * - "local": Use local Tinybird container at localhost:7181 */ export type DevMode = "branch" | "local"; -export type BranchDataMode = "last_partition" | "none"; -export const BRANCH_DATA_MODE_VALUES = ["last_partition", "none"] as const satisfies readonly BranchDataMode[]; +export type BranchDataMode = "last_partition"; +export const BRANCH_DATA_MODE_VALUES = ["last_partition"] as const satisfies readonly BranchDataMode[]; /** * Tinybird configuration file structure @@ -28,6 +28,9 @@ export interface TinybirdConfig { baseUrl?: string; /** Development mode: "branch" (default) or "local" */ devMode?: DevMode; - /** Branch data mode applied on cloud branch creation (shared snake_case key) */ + /** + * Branch data mode applied on cloud branch creation (shared snake_case key, + * also read by the tb CLI). Omit to create branches without data (default). + */ branch_data_mode?: BranchDataMode; } diff --git a/src/cli/config.test.ts b/src/cli/config.test.ts index 8208bc6..3179af5 100644 --- a/src/cli/config.test.ts +++ b/src/cli/config.test.ts @@ -343,7 +343,7 @@ describe("Config", () => { expect(result.branchDataMode).toBe("last_partition"); }); - it("resolves branch_data_mode as none", () => { + it("throws when branch_data_mode is none, pointing at omission", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", @@ -354,11 +354,12 @@ describe("Config", () => { JSON.stringify(config) ); - const result = loadConfig(tempDir); - expect(result.branchDataMode).toBe("none"); + expect(() => loadConfig(tempDir)).toThrow( + "Omit branch_data_mode to create branches without data" + ); }); - it("defaults branch_data_mode to none when missing", () => { + it("defaults branch_data_mode to no data when missing", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", @@ -369,10 +370,10 @@ describe("Config", () => { ); const result = loadConfig(tempDir); - expect(result.branchDataMode).toBe("none"); + expect(result.branchDataMode).toBeNull(); }); - it("defaults empty branch_data_mode to none", () => { + it("defaults empty branch_data_mode to no data", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", @@ -384,7 +385,7 @@ describe("Config", () => { ); const result = loadConfig(tempDir); - expect(result.branchDataMode).toBe("none"); + expect(result.branchDataMode).toBeNull(); }); it("throws when branch_data_mode is all_partitions", () => { diff --git a/src/cli/config.ts b/src/cli/config.ts index c694b0b..9a81c3b 100644 --- a/src/cli/config.ts +++ b/src/cli/config.ts @@ -17,7 +17,6 @@ export { import { BRANCH_DATA_MODE_VALUES } from "./config-types.js"; import type { BranchDataMode, DevMode, TinybirdConfig } from "./config-types.js"; -const DEFAULT_BRANCH_DATA_MODE: BranchDataMode = "none"; /** * Resolved configuration with all values expanded @@ -41,7 +40,7 @@ export interface ResolvedConfig { isMainBranch: boolean; /** Development mode: "branch" or "local" */ devMode: DevMode; - /** Branch data mode configured in tinybird.config.json */ + /** Branch data mode configured in tinybird.config.json (null = create branches without data, the default) */ branchDataMode?: BranchDataMode | null; } @@ -212,10 +211,10 @@ function resolveBranchDataMode(raw: Record): { mode: BranchData } const value = raw["branch_data_mode"]; - if (value === undefined || value === null) return { mode: DEFAULT_BRANCH_DATA_MODE, explicit: false }; + if (value === undefined || value === null) return { mode: null, explicit: false }; if (typeof value !== "string") throw new Error("branch_data_mode must be a string."); const mode = value.trim().toLowerCase(); - if (!mode) return { mode: DEFAULT_BRANCH_DATA_MODE, explicit: false }; + if (!mode) return { mode: null, explicit: false }; if (!BRANCH_DATA_MODE_VALUES.includes(mode as BranchDataMode)) { throw new Error( `Invalid branch_data_mode '${value}'. Allowed values are: ${BRANCH_DATA_MODE_VALUES.join(", ")}.` From 51856361fa1e2c20990a744173b127530b4e7472 Mon Sep 17 00:00:00 2001 From: Tommy Healy Date: Mon, 13 Jul 2026 13:05:07 +0200 Subject: [PATCH 4/4] fix tests --- src/cli/config.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/config.test.ts b/src/cli/config.test.ts index 3179af5..23292ff 100644 --- a/src/cli/config.test.ts +++ b/src/cli/config.test.ts @@ -343,7 +343,7 @@ describe("Config", () => { expect(result.branchDataMode).toBe("last_partition"); }); - it("throws when branch_data_mode is none, pointing at omission", () => { + it("throws when branch_data_mode is none", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", @@ -355,7 +355,7 @@ describe("Config", () => { ); expect(() => loadConfig(tempDir)).toThrow( - "Omit branch_data_mode to create branches without data" + "Invalid branch_data_mode 'none'. Allowed values are: last_partition." ); });