diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4b791..dcf4b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ## [Unreleased] +### Changed + +- 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 ### Security diff --git a/README.md b/README.md index c289670..8eabf39 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` | `"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 fa239d4..1222406 100644 --- a/src/api/branches.test.ts +++ b/src/api/branches.test.ts @@ -223,6 +223,38 @@ describe("Branch API client", () => { expect(createParsed.searchParams.get("data")).toBe("last_partition"); }); + it("omits the data param when no branch_data_mode is given", 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"); + + 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/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..718596e 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 not set", 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: null, + }); + 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", + undefined + ); + }); + 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("throws when branch_data_mode is none", () => { + const config = { + include: ["lib/datasources.ts"], + token: "test-token", + branch_data_mode: "none", + }; + fs.writeFileSync( + path.join(tempDir, "tinybird.json"), + JSON.stringify(config) + ); + + expect(() => loadConfig(tempDir)).toThrow( + "Invalid branch_data_mode 'none'. Allowed values are: last_partition." + ); + }); + + it("defaults branch_data_mode to no data when missing", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", @@ -354,10 +370,10 @@ describe("Config", () => { ); const result = loadConfig(tempDir); - expect(result.branchDataMode).toBe("last_partition"); + expect(result.branchDataMode).toBeNull(); }); - it("defaults empty branch_data_mode to last_partition", () => { + it("defaults empty branch_data_mode to no data", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", @@ -369,7 +385,7 @@ describe("Config", () => { ); const result = loadConfig(tempDir); - expect(result.branchDataMode).toBe("last_partition"); + 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 0f1e2ad..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 = "last_partition"; /** * 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(", ")}.` 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)