From d8100f85a5cb1ff0dfa380d3b9fa7f2aab42f117 Mon Sep 17 00:00:00 2001 From: Manuel Martin Morante Date: Thu, 23 Apr 2026 20:27:56 +0200 Subject: [PATCH 1/5] add tinybird.config.json support --- src/api/branches.test.ts | 32 ++++++ src/cli/commands/build.test.ts | 177 ++++++++++++++++++++++++++++++ src/cli/commands/build.ts | 6 +- src/cli/commands/deploy.test.ts | 1 + src/cli/commands/dev.ts | 6 +- src/cli/commands/generate.test.ts | 3 + src/cli/commands/preview.test.ts | 136 ++++++++++++++++++++++- src/cli/commands/preview.ts | 7 +- src/cli/config-types.ts | 6 + src/cli/config.test.ts | 92 ++++++++++++++++ src/cli/config.ts | 30 ++++- src/cli/index.ts | 10 +- src/client/base.test.ts | 1 + 13 files changed, 498 insertions(+), 9 deletions(-) diff --git a/src/api/branches.test.ts b/src/api/branches.test.ts index 7027101..4ae0a9a 100644 --- a/src/api/branches.test.ts +++ b/src/api/branches.test.ts @@ -189,6 +189,38 @@ describe("Branch API client", () => { ); }); + it("uses last_partition=1 wire format when option is enabled", 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", { lastPartition: true }); + + const [createUrl] = mockFetch.mock.calls[0]; + const createParsed = expectFromParam(createUrl); + expect(createParsed.searchParams.get("name")).toBe("my-feature"); + expect(createParsed.searchParams.get("last_partition")).toBe("1"); + }); + it("uses custom fetch when provided", async () => { const customFetch = vi .fn() diff --git a/src/cli/commands/build.test.ts b/src/cli/commands/build.test.ts index 5e1e957..1852faa 100644 --- a/src/cli/commands/build.test.ts +++ b/src/cli/commands/build.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { runBuild } from "./build.js"; +import { BranchDataOnCreate } from "../config-types.js"; // Mock all dependencies vi.mock("../config.js", () => ({ @@ -64,6 +65,7 @@ describe("Build Command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, + branchDataOnCreate: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ @@ -156,6 +158,7 @@ describe("Build Command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, + branchDataOnCreate: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ @@ -242,6 +245,7 @@ describe("Build Command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, + branchDataOnCreate: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ @@ -307,4 +311,177 @@ describe("Build Command", () => { expect(getLocalTokens).toHaveBeenCalled(); }); }); + + describe("branch_data_on_create wiring", () => { + it("uses config-only last_partition when flag is absent", async () => { + const { loadConfigAsync } = await import("../config.js"); + const { buildFromInclude } = await import("../../generator/index.js"); + const { getOrCreateBranch } = await import("../../api/branches.js"); + const { buildToTinybird } = await import("../../api/build.js"); + const { getWorkspace } = await import("../../api/workspaces.js"); + const { getBranchDashboardUrl } = await import("../../api/dashboard.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, + branchDataOnCreate: BranchDataOnCreate.LAST_PARTITION, + }); + vi.mocked(buildFromInclude).mockResolvedValue({ + resources: { datasources: [], pipes: [], connections: [] }, + entities: { datasources: {}, pipes: {}, connections: {}, rawDatasources: [], rawPipes: [], sourceFiles: [] }, + stats: { datasourceCount: 0, pipeCount: 0, connectionCount: 0 }, + }); + vi.mocked(getOrCreateBranch).mockResolvedValue({ + id: "branch-id", + name: "feature_test", + token: "branch-token", + wasCreated: false, + created_at: "2024-01-01", + }); + vi.mocked(getWorkspace).mockResolvedValue({ + id: "ws-id", + name: "test-workspace", + user_id: "user-id", + user_email: "user@test.com", + scope: "USER", + main: null, + }); + vi.mocked(getBranchDashboardUrl).mockReturnValue("https://app.tinybird.co/dashboard"); + vi.mocked(buildToTinybird).mockResolvedValue({ + success: true, + result: "success", + datasourceCount: 0, + pipeCount: 0, + connectionCount: 0, + }); + + await runBuild(); + expect(getOrCreateBranch).toHaveBeenCalledWith( + expect.any(Object), + "feature_test", + { lastPartition: true } + ); + }); + + it("keeps CLI --last-partition precedence over config", async () => { + const { loadConfigAsync } = await import("../config.js"); + const { buildFromInclude } = await import("../../generator/index.js"); + const { getOrCreateBranch } = await import("../../api/branches.js"); + const { buildToTinybird } = await import("../../api/build.js"); + const { getWorkspace } = await import("../../api/workspaces.js"); + const { getBranchDashboardUrl } = await import("../../api/dashboard.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, + branchDataOnCreate: 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(getOrCreateBranch).mockResolvedValue({ + id: "branch-id", + name: "feature_test", + token: "branch-token", + wasCreated: false, + created_at: "2024-01-01", + }); + vi.mocked(getWorkspace).mockResolvedValue({ + id: "ws-id", + name: "test-workspace", + user_id: "user-id", + user_email: "user@test.com", + scope: "USER", + main: null, + }); + vi.mocked(getBranchDashboardUrl).mockReturnValue("https://app.tinybird.co/dashboard"); + vi.mocked(buildToTinybird).mockResolvedValue({ + success: true, + result: "success", + datasourceCount: 0, + pipeCount: 0, + connectionCount: 0, + }); + + await runBuild({ lastPartition: true }); + expect(getOrCreateBranch).toHaveBeenCalledWith( + expect.any(Object), + "feature_test", + { lastPartition: true } + ); + }); + + it("ignores config branch_data_on_create in local mode", async () => { + const { loadConfigAsync } = await import("../config.js"); + const { buildFromInclude } = await import("../../generator/index.js"); + const { getOrCreateBranch } = await import("../../api/branches.js"); + const { getLocalTokens, getOrCreateLocalWorkspace, getLocalWorkspaceName } = await import("../../api/local.js"); + const { buildToTinybird } = await import("../../api/build.js"); + const { getWorkspace } = await import("../../api/workspaces.js"); + const { getLocalDashboardUrl } = await import("../../api/dashboard.js"); + + vi.mocked(loadConfigAsync).mockResolvedValue({ + include: ["test.ts"], + token: "p.test-token", + baseUrl: "https://api.tinybird.co", + configPath: "/test/tinybird.config.json", + devMode: "local", + cwd: "/test", + gitBranch: "feature-test", + tinybirdBranch: "feature_test", + isMainBranch: false, + branchDataOnCreate: BranchDataOnCreate.LAST_PARTITION, + }); + vi.mocked(buildFromInclude).mockResolvedValue({ + resources: { datasources: [], pipes: [], connections: [] }, + entities: { datasources: {}, pipes: {}, connections: {}, rawDatasources: [], rawPipes: [], sourceFiles: [] }, + stats: { datasourceCount: 0, pipeCount: 0, connectionCount: 0 }, + }); + vi.mocked(getLocalTokens).mockResolvedValue({ + admin_token: "admin-token", + user_token: "user-token", + workspace_admin_token: "workspace-admin-token", + }); + vi.mocked(getWorkspace).mockResolvedValue({ + id: "ws-id", + name: "test-workspace", + user_id: "user-id", + user_email: "user@test.com", + scope: "USER", + main: null, + }); + vi.mocked(getLocalWorkspaceName).mockReturnValue("feature_test_workspace"); + vi.mocked(getOrCreateLocalWorkspace).mockResolvedValue({ + workspace: { id: "local-ws-id", name: "feature_test_workspace", token: "local-token" }, + wasCreated: false, + }); + vi.mocked(getLocalDashboardUrl).mockReturnValue("http://localhost:7181/dashboard"); + vi.mocked(buildToTinybird).mockResolvedValue({ + success: true, + result: "success", + datasourceCount: 0, + pipeCount: 0, + connectionCount: 0, + }); + + await runBuild(); + expect(getOrCreateBranch).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/cli/commands/build.ts b/src/cli/commands/build.ts index 842123f..7231fb2 100644 --- a/src/cli/commands/build.ts +++ b/src/cli/commands/build.ts @@ -3,6 +3,7 @@ */ import { loadConfigAsync, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js"; +import { BranchDataOnCreate } from "../config-types.js"; import { buildFromInclude, type BuildFromIncludeResult } from "../../generator/index.js"; import { buildToTinybird, type BuildApiResult } from "../../api/build.js"; import { getOrCreateBranch } from "../../api/branches.js"; @@ -225,13 +226,16 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise { tinybirdBranch: "feature_pro_610", isMainBranch: false, devMode: "branch", + branchDataOnCreate: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index b07d82e..429821d 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -14,6 +14,7 @@ import { type ResolvedConfig, type DevMode, } from "../config.js"; +import { BranchDataOnCreate } from "../config-types.js"; import { runBuild, type BuildCommandResult } from "./build.js"; import { getOrCreateBranch, type TinybirdBranch } from "../../api/branches.js"; import { browserLogin } from "../auth.js"; @@ -239,6 +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 lastPartitionFromConfig = + config.branchDataOnCreate === BranchDataOnCreate.LAST_PARTITION; + const lastPartitionFromFlag = Boolean(options.lastPartition); // Always fetch fresh from API to avoid stale cache issues const tinybirdBranch = await getOrCreateBranch( @@ -247,7 +251,7 @@ export async function runDev( token: config.token, }, branchName, - { lastPartition: options.lastPartition } + { lastPartition: lastPartitionFromFlag || lastPartitionFromConfig } ); if (!tinybirdBranch.token) { diff --git a/src/cli/commands/generate.test.ts b/src/cli/commands/generate.test.ts index c0e0918..092bcf3 100644 --- a/src/cli/commands/generate.test.ts +++ b/src/cli/commands/generate.test.ts @@ -35,6 +35,7 @@ describe("Generate command", () => { tinybirdBranch: "feature_x", isMainBranch: false, devMode: "branch", + branchDataOnCreate: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ @@ -108,6 +109,7 @@ describe("Generate command", () => { tinybirdBranch: "feature_x", isMainBranch: false, devMode: "branch", + branchDataOnCreate: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ @@ -167,6 +169,7 @@ describe("Generate command", () => { tinybirdBranch: "feature_x", isMainBranch: false, devMode: "branch", + branchDataOnCreate: null, }); vi.mocked(buildFromInclude).mockRejectedValue( new Error("generator failed") diff --git a/src/cli/commands/preview.test.ts b/src/cli/commands/preview.test.ts index e2f072e..262ab73 100644 --- a/src/cli/commands/preview.test.ts +++ b/src/cli/commands/preview.test.ts @@ -1,7 +1,46 @@ -import { describe, it, expect } from "vitest"; -import { generatePreviewBranchName } from "./preview.js"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { generatePreviewBranchName, runPreview } from "./preview.js"; +import { BranchDataOnCreate } from "../config-types.js"; + +vi.mock("../config.js", () => ({ + loadConfigAsync: vi.fn(), + LOCAL_BASE_URL: "http://localhost:7181", +})); + +vi.mock("../../generator/index.js", () => ({ + buildFromInclude: vi.fn(), +})); + +vi.mock("../../api/branches.js", () => ({ + createBranch: vi.fn(), + deleteBranch: vi.fn(), + getBranch: vi.fn(), +})); + +vi.mock("../../api/deploy.js", () => ({ + deployToMain: vi.fn(), +})); + +vi.mock("../../api/build.js", () => ({ + buildToTinybird: vi.fn(), +})); + +vi.mock("../../api/local.js", () => ({ + getLocalTokens: vi.fn(), + getOrCreateLocalWorkspace: vi.fn(), + LocalNotRunningError: class LocalNotRunningError extends Error {}, +})); + +vi.mock("../git.js", () => ({ + sanitizeBranchName: (value: string) => value.replace(/[^a-zA-Z0-9]/g, "_"), + getCurrentGitBranch: vi.fn(() => "feature/test"), +})); describe("Preview command", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + describe("generatePreviewBranchName", () => { it("generates name with tmp_ci prefix", () => { const result = generatePreviewBranchName("feature-branch"); @@ -34,4 +73,97 @@ describe("Preview command", () => { expect(result1).toBe(result2); }); }); + + describe("branch_data_on_create wiring", () => { + it("uses config-only last_partition when creating cloud preview branch", 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, + branchDataOnCreate: BranchDataOnCreate.LAST_PARTITION, + }); + 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", + { lastPartition: true } + ); + }); + + it("ignores config branch_data_on_create in local mode", async () => { + const { loadConfigAsync } = await import("../config.js"); + const { buildFromInclude } = await import("../../generator/index.js"); + const { createBranch } = await import("../../api/branches.js"); + const { getLocalTokens, getOrCreateLocalWorkspace } = await import("../../api/local.js"); + const { buildToTinybird } = await import("../../api/build.js"); + + vi.mocked(loadConfigAsync).mockResolvedValue({ + include: ["test.ts"], + token: "p.test-token", + baseUrl: "https://api.tinybird.co", + configPath: "/test/tinybird.config.json", + devMode: "local", + cwd: "/test", + gitBranch: "feature-test", + tinybirdBranch: "feature_test", + isMainBranch: false, + branchDataOnCreate: BranchDataOnCreate.LAST_PARTITION, + }); + vi.mocked(buildFromInclude).mockResolvedValue({ + resources: { datasources: [], pipes: [], connections: [] }, + entities: { datasources: {}, pipes: {}, connections: {}, rawDatasources: [], rawPipes: [], sourceFiles: [] }, + stats: { datasourceCount: 0, pipeCount: 0, connectionCount: 0 }, + }); + vi.mocked(getLocalTokens).mockResolvedValue({ + admin_token: "admin-token", + user_token: "user-token", + workspace_admin_token: "workspace-admin-token", + }); + vi.mocked(getOrCreateLocalWorkspace).mockResolvedValue({ + workspace: { id: "lw1", name: "tmp_ci_feature_test", token: "local-token" }, + wasCreated: true, + }); + vi.mocked(buildToTinybird).mockResolvedValue({ + success: true, + result: "success", + datasourceCount: 0, + pipeCount: 0, + connectionCount: 0, + }); + + await runPreview(); + expect(createBranch).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/cli/commands/preview.ts b/src/cli/commands/preview.ts index 5eda94a..7dbc59b 100644 --- a/src/cli/commands/preview.ts +++ b/src/cli/commands/preview.ts @@ -3,6 +3,7 @@ */ import { loadConfigAsync, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js"; +import { BranchDataOnCreate } from "../config-types.js"; import { buildFromInclude, type BuildFromIncludeResult } from "../../generator/index.js"; import { createBranch, deleteBranch, getBranch, type TinybirdBranch } from "../../api/branches.js"; import { deployToMain } from "../../api/deploy.js"; @@ -226,6 +227,8 @@ export async function runPreview(options: PreviewCommandOptions = {}): Promise

{ expect(result.devMode).toBe("local"); }); + + it("resolves branch_data_on_create as last_partition", () => { + const config = { + include: ["lib/datasources.ts"], + token: "test-token", + branch_data_on_create: "last_partition", + }; + fs.writeFileSync( + path.join(tempDir, "tinybird.json"), + JSON.stringify(config) + ); + + const result = loadConfig(tempDir); + expect(result.branchDataOnCreate).toBe("last_partition"); + }); + + it("defaults branch_data_on_create to last_partition 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.branchDataOnCreate).toBe("last_partition"); + }); + + it("defaults empty branch_data_on_create to last_partition", () => { + const config = { + include: ["lib/datasources.ts"], + token: "test-token", + branch_data_on_create: " ", + }; + fs.writeFileSync( + path.join(tempDir, "tinybird.json"), + JSON.stringify(config) + ); + + const result = loadConfig(tempDir); + expect(result.branchDataOnCreate).toBe("last_partition"); + }); + + it("throws when branch_data_on_create is all_partitions", () => { + const config = { + include: ["lib/datasources.ts"], + token: "test-token", + branch_data_on_create: "all_partitions", + }; + fs.writeFileSync( + path.join(tempDir, "tinybird.json"), + JSON.stringify(config) + ); + + expect(() => loadConfig(tempDir)).toThrow("disabled"); + }); + + it("throws when branch_data_on_create is invalid", () => { + const config = { + include: ["lib/datasources.ts"], + token: "test-token", + branch_data_on_create: "invalid", + }; + fs.writeFileSync( + path.join(tempDir, "tinybird.json"), + JSON.stringify(config) + ); + + expect(() => loadConfig(tempDir)).toThrow("Invalid branch_data_on_create"); + }); + + it("warns when branch_data_on_create is set with devMode local", () => { + const config = { + include: ["lib/datasources.ts"], + token: "test-token", + devMode: "local", + branch_data_on_create: "last_partition", + }; + fs.writeFileSync( + path.join(tempDir, "tinybird.json"), + JSON.stringify(config) + ); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + loadConfig(tempDir); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("branch_data_on_create is set") + ); + }); }); describe("loadConfigAsync", () => { diff --git a/src/cli/config.ts b/src/cli/config.ts index 569a08c..0ea2270 100644 --- a/src/cli/config.ts +++ b/src/cli/config.ts @@ -7,8 +7,9 @@ import * as path from "path"; import { config as loadDotenv } from "dotenv"; import { getCurrentGitBranch, isMainBranch, getTinybirdBranchName } from "./git.js"; -// Re-export types from config-types.ts (separate file to avoid bundling esbuild) -export type { DevMode, TinybirdConfig } from "./config-types.js"; +// Re-export config types/constants from config-types.ts (separate file to avoid bundling esbuild) +export { BranchDataOnCreate, type DevMode, type TinybirdConfig } from "./config-types.js"; +import { BranchDataOnCreate } from "./config-types.js"; import type { DevMode, TinybirdConfig } from "./config-types.js"; /** @@ -33,6 +34,8 @@ export interface ResolvedConfig { isMainBranch: boolean; /** Development mode: "branch" or "local" */ devMode: DevMode; + /** Branch data mode configured in tinybird.config.json */ + branchDataOnCreate?: BranchDataOnCreate | null; } /** @@ -196,6 +199,21 @@ export function findConfigFile(startDir: string): ConfigFileResult | null { // Import the universal config loader import { loadConfigFile } from "./config-loader.js"; +function resolveBranchDataOnCreate(raw: Record): BranchDataOnCreate | null { + const value = raw["branch_data_on_create"]; + if (value === undefined || value === null) return BranchDataOnCreate.LAST_PARTITION; + if (typeof value !== "string") throw new Error("branch_data_on_create must be a string."); + const mode = value.trim().toLowerCase(); + if (!mode) return BranchDataOnCreate.LAST_PARTITION; + if (mode !== BranchDataOnCreate.LAST_PARTITION && mode !== BranchDataOnCreate.ALL_PARTITIONS) { + throw new Error(`Invalid branch_data_on_create '${value}'. Allowed values are: last_partition, all_partitions.`); + } + if (mode === BranchDataOnCreate.ALL_PARTITIONS) { + throw new Error("branch_data_on_create 'all_partitions' is currently disabled."); + } + return mode as BranchDataOnCreate; +} + /** * Resolve a TinybirdConfig to a ResolvedConfig */ @@ -255,6 +273,13 @@ function resolveConfig(config: TinybirdConfig, configPath: string): ResolvedConf // Resolve devMode (default to "branch") const devMode: DevMode = config.devMode ?? "branch"; + const branchDataOnCreate = resolveBranchDataOnCreate(config as unknown as Record); + if (branchDataOnCreate && devMode === "local") { + console.warn( + "branch_data_on_create is set in tinybird.config.json but dev_mode='local'. " + + "Branch data settings only apply to cloud branches." + ); + } return { include, @@ -266,6 +291,7 @@ function resolveConfig(config: TinybirdConfig, configPath: string): ResolvedConf tinybirdBranch, isMainBranch: isMainBranch(), devMode, + branchDataOnCreate, }; } diff --git a/src/cli/index.ts b/src/cli/index.ts index fa5a24a..d2e1f4a 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -270,7 +270,10 @@ function createCli(): Command { .option("--debug", "Show debug output including API requests/responses") .option("--local", "Use local Tinybird container") .option("--branch", "Use Tinybird cloud with branches") - .option("--last-partition", "Copy the last partition of production data when creating a branch") + .option( + "--last-partition", + '[DEPRECATED] Use `branch_data_on_create: "last_partition"` in tinybird.config.json. Copy the last partition of production data when creating a cloud branch.' + ) .action(async (options) => { if (options.debug) { process.env.TINYBIRD_DEBUG = "1"; @@ -679,7 +682,10 @@ function createCli(): Command { .description("Watch for changes and sync with Tinybird") .option("--local", "Use local Tinybird container") .option("--branch", "Use Tinybird cloud with branches") - .option("--last-partition", "Copy the last partition of production data when creating a branch") + .option( + "--last-partition", + '[DEPRECATED] Use `branch_data_on_create: "last_partition"` in tinybird.config.json. Copy the last partition of production data when creating a cloud branch.' + ) .action(async (options) => { // Determine devMode override let devModeOverride: DevMode | undefined; diff --git a/src/client/base.test.ts b/src/client/base.test.ts index 00cc1b2..6c013e1 100644 --- a/src/client/base.test.ts +++ b/src/client/base.test.ts @@ -167,6 +167,7 @@ describe("TinybirdClient", () => { tinybirdBranch: "feature_add_fetch", isMainBranch: false, devMode: "branch", + branchDataOnCreate: null, }); mockedGetOrCreateBranch.mockResolvedValue({ id: "branch-123", From 7b09c03c98afe393e207c60ab99d44158647a4c4 Mon Sep 17 00:00:00 2001 From: Manuel Martin Morante Date: Tue, 9 Jun 2026 12:15:46 +0200 Subject: [PATCH 2/5] improve the workflow --- src/api/branches.test.ts | 3 +- src/api/branches.ts | 5 ++- src/cli/commands/build.test.ts | 18 ++++----- src/cli/commands/build.ts | 4 +- src/cli/commands/clear.ts | 10 ++++- src/cli/commands/deploy.test.ts | 2 +- src/cli/commands/dev.ts | 4 +- src/cli/commands/generate.test.ts | 6 +-- src/cli/commands/preview.test.ts | 10 ++--- src/cli/commands/preview.ts | 4 +- src/cli/config-types.ts | 5 +-- src/cli/config.test.ts | 65 +++++++++++++++++++++++-------- src/cli/config.ts | 39 ++++++++++--------- src/cli/index.ts | 4 +- src/client/base.test.ts | 5 ++- src/client/base.ts | 9 ++++- 16 files changed, 122 insertions(+), 71 deletions(-) diff --git a/src/api/branches.test.ts b/src/api/branches.test.ts index 4ae0a9a..6f2bf76 100644 --- a/src/api/branches.test.ts +++ b/src/api/branches.test.ts @@ -526,7 +526,7 @@ describe("Branch API client", () => { json: () => Promise.resolve(newBranch), }); - const result = await clearBranch(config, "my-feature"); + const result = await clearBranch(config, "my-feature", { lastPartition: true }); expect(mockFetch).toHaveBeenCalledTimes(5); @@ -547,6 +547,7 @@ describe("Branch API client", () => { const createParsed = expectFromParam(createUrl); expect(createParsed.pathname).toBe("/v1/environments"); expect(createParsed.searchParams.get("name")).toBe("my-feature"); + expect(createParsed.searchParams.get("last_partition")).toBe("1"); expect(createInit.method).toBe("POST"); expect(result).toEqual(newBranch); diff --git a/src/api/branches.ts b/src/api/branches.ts index 439e382..a5e43a9 100644 --- a/src/api/branches.ts +++ b/src/api/branches.ts @@ -377,13 +377,14 @@ export async function getOrCreateBranch( */ export async function clearBranch( config: BranchApiConfig, - name: string + name: string, + options?: CreateBranchOptions ): Promise { // Delete the branch await deleteBranch(config, name); // Recreate the branch - const branch = await createBranch(config, name); + const branch = await createBranch(config, name, options); return branch; } diff --git a/src/cli/commands/build.test.ts b/src/cli/commands/build.test.ts index 1852faa..309cb44 100644 --- a/src/cli/commands/build.test.ts +++ b/src/cli/commands/build.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { runBuild } from "./build.js"; -import { BranchDataOnCreate } from "../config-types.js"; +import { BranchDataMode } from "../config-types.js"; // Mock all dependencies vi.mock("../config.js", () => ({ @@ -65,7 +65,7 @@ describe("Build Command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataOnCreate: null, + branchDataMode: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ @@ -158,7 +158,7 @@ describe("Build Command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataOnCreate: null, + branchDataMode: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ @@ -245,7 +245,7 @@ describe("Build Command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataOnCreate: null, + branchDataMode: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ @@ -312,7 +312,7 @@ describe("Build Command", () => { }); }); - describe("branch_data_on_create wiring", () => { + describe("branch_data_mode wiring", () => { it("uses config-only last_partition when flag is absent", async () => { const { loadConfigAsync } = await import("../config.js"); const { buildFromInclude } = await import("../../generator/index.js"); @@ -331,7 +331,7 @@ describe("Build Command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataOnCreate: BranchDataOnCreate.LAST_PARTITION, + branchDataMode: BranchDataMode.LAST_PARTITION, }); vi.mocked(buildFromInclude).mockResolvedValue({ resources: { datasources: [], pipes: [], connections: [] }, @@ -388,7 +388,7 @@ describe("Build Command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataOnCreate: null, + branchDataMode: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ resources: { datasources: [], pipes: [], connections: [] }, @@ -427,7 +427,7 @@ describe("Build Command", () => { ); }); - it("ignores config branch_data_on_create in local mode", async () => { + it("ignores config branch_data_mode in local mode", async () => { const { loadConfigAsync } = await import("../config.js"); const { buildFromInclude } = await import("../../generator/index.js"); const { getOrCreateBranch } = await import("../../api/branches.js"); @@ -446,7 +446,7 @@ describe("Build Command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataOnCreate: BranchDataOnCreate.LAST_PARTITION, + branchDataMode: BranchDataMode.LAST_PARTITION, }); vi.mocked(buildFromInclude).mockResolvedValue({ resources: { datasources: [], pipes: [], connections: [] }, diff --git a/src/cli/commands/build.ts b/src/cli/commands/build.ts index 7231fb2..8a4c438 100644 --- a/src/cli/commands/build.ts +++ b/src/cli/commands/build.ts @@ -3,7 +3,7 @@ */ import { loadConfigAsync, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js"; -import { BranchDataOnCreate } from "../config-types.js"; +import { BranchDataMode } from "../config-types.js"; import { buildFromInclude, type BuildFromIncludeResult } from "../../generator/index.js"; import { buildToTinybird, type BuildApiResult } from "../../api/build.js"; import { getOrCreateBranch } from "../../api/branches.js"; @@ -227,7 +227,7 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise { }); // Clear the branch (delete and recreate) + const branchOptions: CreateBranchOptions | undefined = + config.devMode !== "local" && config.branchDataMode === BranchDataMode.LAST_PARTITION + ? { lastPartition: true } + : undefined; + const newBranch = await clearBranch( { baseUrl: config.baseUrl, token: config.token, }, - branchName + branchName, + branchOptions ); // Update the cached token with the new branch token diff --git a/src/cli/commands/deploy.test.ts b/src/cli/commands/deploy.test.ts index 64389fd..9f9f390 100644 --- a/src/cli/commands/deploy.test.ts +++ b/src/cli/commands/deploy.test.ts @@ -37,7 +37,7 @@ describe("Deploy command", () => { tinybirdBranch: "feature_pro_610", isMainBranch: false, devMode: "branch", - branchDataOnCreate: null, + branchDataMode: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 429821d..18c687e 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -14,7 +14,7 @@ import { type ResolvedConfig, type DevMode, } from "../config.js"; -import { BranchDataOnCreate } from "../config-types.js"; +import { BranchDataMode } from "../config-types.js"; import { runBuild, type BuildCommandResult } from "./build.js"; import { getOrCreateBranch, type TinybirdBranch } from "../../api/branches.js"; import { browserLogin } from "../auth.js"; @@ -241,7 +241,7 @@ export async function runDev( if (config.tinybirdBranch) { const branchName = config.tinybirdBranch; // Sanitized name for Tinybird const lastPartitionFromConfig = - config.branchDataOnCreate === BranchDataOnCreate.LAST_PARTITION; + config.branchDataMode === BranchDataMode.LAST_PARTITION; const lastPartitionFromFlag = Boolean(options.lastPartition); // Always fetch fresh from API to avoid stale cache issues diff --git a/src/cli/commands/generate.test.ts b/src/cli/commands/generate.test.ts index 092bcf3..ece239c 100644 --- a/src/cli/commands/generate.test.ts +++ b/src/cli/commands/generate.test.ts @@ -35,7 +35,7 @@ describe("Generate command", () => { tinybirdBranch: "feature_x", isMainBranch: false, devMode: "branch", - branchDataOnCreate: null, + branchDataMode: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ @@ -109,7 +109,7 @@ describe("Generate command", () => { tinybirdBranch: "feature_x", isMainBranch: false, devMode: "branch", - branchDataOnCreate: null, + branchDataMode: null, }); vi.mocked(buildFromInclude).mockResolvedValue({ @@ -169,7 +169,7 @@ describe("Generate command", () => { tinybirdBranch: "feature_x", isMainBranch: false, devMode: "branch", - branchDataOnCreate: null, + branchDataMode: null, }); vi.mocked(buildFromInclude).mockRejectedValue( new Error("generator failed") diff --git a/src/cli/commands/preview.test.ts b/src/cli/commands/preview.test.ts index 262ab73..a983e02 100644 --- a/src/cli/commands/preview.test.ts +++ b/src/cli/commands/preview.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { generatePreviewBranchName, runPreview } from "./preview.js"; -import { BranchDataOnCreate } from "../config-types.js"; +import { BranchDataMode } from "../config-types.js"; vi.mock("../config.js", () => ({ loadConfigAsync: vi.fn(), @@ -74,7 +74,7 @@ describe("Preview command", () => { }); }); - describe("branch_data_on_create wiring", () => { + describe("branch_data_mode wiring", () => { it("uses config-only last_partition when creating cloud preview branch", async () => { const { loadConfigAsync } = await import("../config.js"); const { buildFromInclude } = await import("../../generator/index.js"); @@ -91,7 +91,7 @@ describe("Preview command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataOnCreate: BranchDataOnCreate.LAST_PARTITION, + branchDataMode: BranchDataMode.LAST_PARTITION, }); vi.mocked(buildFromInclude).mockResolvedValue({ resources: { datasources: [], pipes: [], connections: [] }, @@ -121,7 +121,7 @@ describe("Preview command", () => { ); }); - it("ignores config branch_data_on_create in local mode", async () => { + it("ignores config branch_data_mode in local mode", async () => { const { loadConfigAsync } = await import("../config.js"); const { buildFromInclude } = await import("../../generator/index.js"); const { createBranch } = await import("../../api/branches.js"); @@ -138,7 +138,7 @@ describe("Preview command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataOnCreate: BranchDataOnCreate.LAST_PARTITION, + branchDataMode: BranchDataMode.LAST_PARTITION, }); vi.mocked(buildFromInclude).mockResolvedValue({ resources: { datasources: [], pipes: [], connections: [] }, diff --git a/src/cli/commands/preview.ts b/src/cli/commands/preview.ts index 7dbc59b..febe0d7 100644 --- a/src/cli/commands/preview.ts +++ b/src/cli/commands/preview.ts @@ -3,7 +3,7 @@ */ import { loadConfigAsync, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js"; -import { BranchDataOnCreate } from "../config-types.js"; +import { BranchDataMode } from "../config-types.js"; import { buildFromInclude, type BuildFromIncludeResult } from "../../generator/index.js"; import { createBranch, deleteBranch, getBranch, type TinybirdBranch } from "../../api/branches.js"; import { deployToMain } from "../../api/deploy.js"; @@ -228,7 +228,7 @@ export async function runPreview(options: PreviewCommandOptions = {}): Promise

{ expect(result.devMode).toBe("local"); }); - it("resolves branch_data_on_create as last_partition", () => { + it("resolves branch_data_mode as last_partition", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", - branch_data_on_create: "last_partition", + branch_data_mode: "last_partition", }; fs.writeFileSync( path.join(tempDir, "tinybird.json"), @@ -340,10 +340,10 @@ describe("Config", () => { ); const result = loadConfig(tempDir); - expect(result.branchDataOnCreate).toBe("last_partition"); + expect(result.branchDataMode).toBe("last_partition"); }); - it("defaults branch_data_on_create to last_partition when missing", () => { + it("defaults branch_data_mode to last_partition when missing", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", @@ -354,14 +354,14 @@ describe("Config", () => { ); const result = loadConfig(tempDir); - expect(result.branchDataOnCreate).toBe("last_partition"); + expect(result.branchDataMode).toBe("last_partition"); }); - it("defaults empty branch_data_on_create to last_partition", () => { + it("defaults empty branch_data_mode to last_partition", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", - branch_data_on_create: " ", + branch_data_mode: " ", }; fs.writeFileSync( path.join(tempDir, "tinybird.json"), @@ -369,43 +369,43 @@ describe("Config", () => { ); const result = loadConfig(tempDir); - expect(result.branchDataOnCreate).toBe("last_partition"); + expect(result.branchDataMode).toBe("last_partition"); }); - it("throws when branch_data_on_create is all_partitions", () => { + it("throws when branch_data_mode is all_partitions", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", - branch_data_on_create: "all_partitions", + branch_data_mode: "all_partitions", }; fs.writeFileSync( path.join(tempDir, "tinybird.json"), JSON.stringify(config) ); - expect(() => loadConfig(tempDir)).toThrow("disabled"); + expect(() => loadConfig(tempDir)).toThrow("Invalid branch_data_mode"); }); - it("throws when branch_data_on_create is invalid", () => { + it("throws when branch_data_mode is invalid", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", - branch_data_on_create: "invalid", + branch_data_mode: "invalid", }; fs.writeFileSync( path.join(tempDir, "tinybird.json"), JSON.stringify(config) ); - expect(() => loadConfig(tempDir)).toThrow("Invalid branch_data_on_create"); + expect(() => loadConfig(tempDir)).toThrow("Invalid branch_data_mode"); }); - it("warns when branch_data_on_create is set with devMode local", () => { + it("warns when branch_data_mode is set with devMode local", () => { const config = { include: ["lib/datasources.ts"], token: "test-token", devMode: "local", - branch_data_on_create: "last_partition", + branch_data_mode: "last_partition", }; fs.writeFileSync( path.join(tempDir, "tinybird.json"), @@ -416,9 +416,40 @@ describe("Config", () => { loadConfig(tempDir); expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining("branch_data_on_create is set") + expect.stringContaining("branch_data_mode is set") ); }); + + it("does not warn when branch_data_mode is implicit in local mode", () => { + const config = { + include: ["lib/datasources.ts"], + token: "test-token", + devMode: "local", + }; + fs.writeFileSync( + path.join(tempDir, "tinybird.json"), + JSON.stringify(config) + ); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + loadConfig(tempDir); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it("throws when legacy branch_data_on_create key is used", () => { + const config = { + include: ["lib/datasources.ts"], + token: "test-token", + branch_data_on_create: "last_partition", + }; + fs.writeFileSync( + path.join(tempDir, "tinybird.json"), + JSON.stringify(config) + ); + + expect(() => loadConfig(tempDir)).toThrow("renamed to `branch_data_mode`"); + }); }); describe("loadConfigAsync", () => { diff --git a/src/cli/config.ts b/src/cli/config.ts index 0ea2270..e8036ba 100644 --- a/src/cli/config.ts +++ b/src/cli/config.ts @@ -8,8 +8,8 @@ import { config as loadDotenv } from "dotenv"; import { getCurrentGitBranch, isMainBranch, getTinybirdBranchName } from "./git.js"; // Re-export config types/constants from config-types.ts (separate file to avoid bundling esbuild) -export { BranchDataOnCreate, type DevMode, type TinybirdConfig } from "./config-types.js"; -import { BranchDataOnCreate } from "./config-types.js"; +export { BranchDataMode, type DevMode, type TinybirdConfig } from "./config-types.js"; +import { BranchDataMode } from "./config-types.js"; import type { DevMode, TinybirdConfig } from "./config-types.js"; /** @@ -35,7 +35,7 @@ export interface ResolvedConfig { /** Development mode: "branch" or "local" */ devMode: DevMode; /** Branch data mode configured in tinybird.config.json */ - branchDataOnCreate?: BranchDataOnCreate | null; + branchDataMode?: BranchDataMode | null; } /** @@ -199,19 +199,20 @@ export function findConfigFile(startDir: string): ConfigFileResult | null { // Import the universal config loader import { loadConfigFile } from "./config-loader.js"; -function resolveBranchDataOnCreate(raw: Record): BranchDataOnCreate | null { - const value = raw["branch_data_on_create"]; - if (value === undefined || value === null) return BranchDataOnCreate.LAST_PARTITION; - if (typeof value !== "string") throw new Error("branch_data_on_create must be a string."); - const mode = value.trim().toLowerCase(); - if (!mode) return BranchDataOnCreate.LAST_PARTITION; - if (mode !== BranchDataOnCreate.LAST_PARTITION && mode !== BranchDataOnCreate.ALL_PARTITIONS) { - throw new Error(`Invalid branch_data_on_create '${value}'. Allowed values are: last_partition, all_partitions.`); +function resolveBranchDataMode(raw: Record): { mode: BranchDataMode | null; explicit: boolean } { + if (raw["branch_data_on_create"] !== undefined) { + throw new Error("`branch_data_on_create` has been renamed to `branch_data_mode`."); } - if (mode === BranchDataOnCreate.ALL_PARTITIONS) { - throw new Error("branch_data_on_create 'all_partitions' is currently disabled."); + + const value = raw["branch_data_mode"]; + if (value === undefined || value === null) return { mode: BranchDataMode.LAST_PARTITION, 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: BranchDataMode.LAST_PARTITION, explicit: false }; + if (mode !== BranchDataMode.LAST_PARTITION) { + throw new Error(`Invalid branch_data_mode '${value}'. Allowed values are: last_partition.`); } - return mode as BranchDataOnCreate; + return { mode: mode as BranchDataMode, explicit: true }; } /** @@ -273,10 +274,12 @@ function resolveConfig(config: TinybirdConfig, configPath: string): ResolvedConf // Resolve devMode (default to "branch") const devMode: DevMode = config.devMode ?? "branch"; - const branchDataOnCreate = resolveBranchDataOnCreate(config as unknown as Record); - if (branchDataOnCreate && devMode === "local") { + const { mode: branchDataMode, explicit: branchDataModeExplicit } = resolveBranchDataMode( + config as unknown as Record + ); + if (branchDataModeExplicit && branchDataMode && devMode === "local") { console.warn( - "branch_data_on_create is set in tinybird.config.json but dev_mode='local'. " + + "branch_data_mode is set in tinybird.config.json but dev_mode='local'. " + "Branch data settings only apply to cloud branches." ); } @@ -291,7 +294,7 @@ function resolveConfig(config: TinybirdConfig, configPath: string): ResolvedConf tinybirdBranch, isMainBranch: isMainBranch(), devMode, - branchDataOnCreate, + branchDataMode, }; } diff --git a/src/cli/index.ts b/src/cli/index.ts index d2e1f4a..6d0f6f3 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -272,7 +272,7 @@ function createCli(): Command { .option("--branch", "Use Tinybird cloud with branches") .option( "--last-partition", - '[DEPRECATED] Use `branch_data_on_create: "last_partition"` in tinybird.config.json. Copy the last partition of production data when creating a cloud branch.' + '[DEPRECATED] Use `branch_data_mode: "last_partition"` in tinybird.config.json. Copy the last partition of production data when creating a cloud branch.' ) .action(async (options) => { if (options.debug) { @@ -684,7 +684,7 @@ function createCli(): Command { .option("--branch", "Use Tinybird cloud with branches") .option( "--last-partition", - '[DEPRECATED] Use `branch_data_on_create: "last_partition"` in tinybird.config.json. Copy the last partition of production data when creating a cloud branch.' + '[DEPRECATED] Use `branch_data_mode: "last_partition"` in tinybird.config.json. Copy the last partition of production data when creating a cloud branch.' ) .action(async (options) => { // Determine devMode override diff --git a/src/client/base.test.ts b/src/client/base.test.ts index 6c013e1..697aca9 100644 --- a/src/client/base.test.ts +++ b/src/client/base.test.ts @@ -167,7 +167,7 @@ describe("TinybirdClient", () => { tinybirdBranch: "feature_add_fetch", isMainBranch: false, devMode: "branch", - branchDataOnCreate: null, + branchDataMode: null, }); mockedGetOrCreateBranch.mockResolvedValue({ id: "branch-123", @@ -193,7 +193,8 @@ describe("TinybirdClient", () => { token: "workspace-token", fetch: customFetch, }, - "feature_add_fetch" + "feature_add_fetch", + undefined ); expect(context.token).toBe("branch-token"); expect(context.isBranchToken).toBe(true); diff --git a/src/client/base.ts b/src/client/base.ts index 5bb8e76..08217d7 100644 --- a/src/client/base.ts +++ b/src/client/base.ts @@ -299,6 +299,7 @@ export class TinybirdClient { // out of the client bundle when not using dev mode const { loadConfigAsync } = await import("../cli/config.js"); const { getOrCreateBranch } = await import("../api/branches.js"); + const { BranchDataMode } = await import("../cli/config-types.js"); const { isPreviewEnvironment, getPreviewBranchName } = await import( "./preview.js" ); @@ -340,6 +341,11 @@ export class TinybirdClient { } const branchName = config.tinybirdBranch; + const branchOptions = + config.devMode !== "local" && + config.branchDataMode === BranchDataMode.LAST_PARTITION + ? { lastPartition: true } + : undefined; // Get or create branch (always fetch fresh to avoid stale cache issues) const branch = await getOrCreateBranch( @@ -348,7 +354,8 @@ export class TinybirdClient { token: this.config.token, fetch: this.config.fetch, }, - branchName + branchName, + branchOptions ); if (!branch.token) { From 7eba794f173ff53d831215c0df523da542449627 Mon Sep 17 00:00:00 2001 From: Manuel Martin Morante Date: Mon, 15 Jun 2026 11:04:07 +0200 Subject: [PATCH 3/5] add changlog and bump version --- CHANGELOG.md | 18 ++++++++++++++++++ package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a99e28f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.0.78] - 2026-06-15 + +### Changed + +- Updated Tinybird CLI behavior to match the `4.6.1` branch-management changes. +- Branch data config handling now uses `branch_data_mode`; legacy `branch_data_on_create` now triggers an explicit migration error. +- `branch_data_mode` now only accepts `last_partition` as a user-facing value. +- In local development mode, branch data mode warnings are now shown only when `branch_data_mode` is explicitly set in `tinybird.config.json`. +- `tinybird branch create` and `tinybird branch clear` now show a deprecation warning (instead of failing) when `--ignore-datasource` is passed, then continue by ignoring that flag. diff --git a/package.json b/package.json index 57a499f..775cc38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tinybirdco/sdk", - "version": "0.0.77", + "version": "0.0.78", "description": "TypeScript SDK for Tinybird Forward - define datasources and pipes as TypeScript", "type": "module", "main": "./dist/index.js", From 3a5a98c50be42f7049ea277afdedb80fd3ae8955 Mon Sep 17 00:00:00 2001 From: Manuel Martin Morante Date: Fri, 19 Jun 2026 12:17:31 -0700 Subject: [PATCH 4/5] fix error in the API params to create a branch --- src/api/branches.test.ts | 15 ++++++++++----- src/api/branches.ts | 9 +++++---- src/cli/commands/build.test.ts | 4 ++-- src/cli/commands/build.ts | 12 ++++++++---- src/cli/commands/clear.ts | 2 +- src/cli/commands/dev.ts | 12 ++++++++---- src/cli/commands/preview.test.ts | 2 +- src/cli/commands/preview.ts | 10 +++++----- src/client/base.ts | 2 +- 9 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/api/branches.test.ts b/src/api/branches.test.ts index 6f2bf76..8687378 100644 --- a/src/api/branches.test.ts +++ b/src/api/branches.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { BranchDataMode } from "../cli/config-types.js"; import { BranchApiError, createBranch, @@ -189,7 +190,7 @@ describe("Branch API client", () => { ); }); - it("uses last_partition=1 wire format when option is enabled", async () => { + it("uses data=last_partition wire format when branch_data_mode is set", async () => { const mockBranch = { id: "branch-123", name: "my-feature", @@ -213,12 +214,14 @@ describe("Branch API client", () => { json: () => Promise.resolve(mockBranch), }); - await createBranch(config, "my-feature", { lastPartition: true }); + await createBranch(config, "my-feature", { + branch_data_mode: BranchDataMode.LAST_PARTITION, + }); const [createUrl] = mockFetch.mock.calls[0]; const createParsed = expectFromParam(createUrl); expect(createParsed.searchParams.get("name")).toBe("my-feature"); - expect(createParsed.searchParams.get("last_partition")).toBe("1"); + expect(createParsed.searchParams.get("data")).toBe("last_partition"); }); it("uses custom fetch when provided", async () => { @@ -526,7 +529,9 @@ describe("Branch API client", () => { json: () => Promise.resolve(newBranch), }); - const result = await clearBranch(config, "my-feature", { lastPartition: true }); + const result = await clearBranch(config, "my-feature", { + branch_data_mode: BranchDataMode.LAST_PARTITION, + }); expect(mockFetch).toHaveBeenCalledTimes(5); @@ -547,7 +552,7 @@ describe("Branch API client", () => { const createParsed = expectFromParam(createUrl); expect(createParsed.pathname).toBe("/v1/environments"); expect(createParsed.searchParams.get("name")).toBe("my-feature"); - expect(createParsed.searchParams.get("last_partition")).toBe("1"); + expect(createParsed.searchParams.get("data")).toBe("last_partition"); expect(createInit.method).toBe("POST"); expect(result).toEqual(newBranch); diff --git a/src/api/branches.ts b/src/api/branches.ts index a5e43a9..db71c97 100644 --- a/src/api/branches.ts +++ b/src/api/branches.ts @@ -3,6 +3,7 @@ * Uses the /v1/environments endpoints (Forward API) */ +import { BranchDataMode } from "../cli/config-types.js"; import { createTinybirdFetcher } from "./fetcher.js"; /** @@ -152,8 +153,8 @@ async function pollJob( * @returns The created branch with token */ export interface CreateBranchOptions { - /** Copy the last partition of production data into the branch */ - lastPartition?: boolean; + /** Data mode applied when creating the branch */ + branch_data_mode?: BranchDataMode; } export async function createBranch( @@ -164,8 +165,8 @@ export async function createBranch( const fetchFn = getFetch(config); const url = new URL("/v1/environments", config.baseUrl); url.searchParams.set("name", name); - if (options?.lastPartition) { - url.searchParams.set("last_partition", "1"); + if (options?.branch_data_mode) { + url.searchParams.set("data", options.branch_data_mode); } const debug = !!process.env.TINYBIRD_DEBUG; diff --git a/src/cli/commands/build.test.ts b/src/cli/commands/build.test.ts index 309cb44..906b2a3 100644 --- a/src/cli/commands/build.test.ts +++ b/src/cli/commands/build.test.ts @@ -366,7 +366,7 @@ describe("Build Command", () => { expect(getOrCreateBranch).toHaveBeenCalledWith( expect.any(Object), "feature_test", - { lastPartition: true } + { branch_data_mode: BranchDataMode.LAST_PARTITION } ); }); @@ -423,7 +423,7 @@ describe("Build Command", () => { expect(getOrCreateBranch).toHaveBeenCalledWith( expect.any(Object), "feature_test", - { lastPartition: true } + { branch_data_mode: BranchDataMode.LAST_PARTITION } ); }); diff --git a/src/cli/commands/build.ts b/src/cli/commands/build.ts index 8a4c438..0e272ec 100644 --- a/src/cli/commands/build.ts +++ b/src/cli/commands/build.ts @@ -226,16 +226,20 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise { // Clear the branch (delete and recreate) const branchOptions: CreateBranchOptions | undefined = config.devMode !== "local" && config.branchDataMode === BranchDataMode.LAST_PARTITION - ? { lastPartition: true } + ? { branch_data_mode: BranchDataMode.LAST_PARTITION } : undefined; const newBranch = await clearBranch( diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 18c687e..2cf4e33 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -240,9 +240,13 @@ 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 lastPartitionFromConfig = - config.branchDataMode === BranchDataMode.LAST_PARTITION; - const lastPartitionFromFlag = Boolean(options.lastPartition); + const branchDataMode = + options.lastPartition || config.branchDataMode === BranchDataMode.LAST_PARTITION + ? BranchDataMode.LAST_PARTITION + : undefined; + const branchOptions = branchDataMode + ? { branch_data_mode: branchDataMode } + : undefined; // Always fetch fresh from API to avoid stale cache issues const tinybirdBranch = await getOrCreateBranch( @@ -251,7 +255,7 @@ export async function runDev( token: config.token, }, branchName, - { lastPartition: lastPartitionFromFlag || lastPartitionFromConfig } + branchOptions ); if (!tinybirdBranch.token) { diff --git a/src/cli/commands/preview.test.ts b/src/cli/commands/preview.test.ts index a983e02..69b533d 100644 --- a/src/cli/commands/preview.test.ts +++ b/src/cli/commands/preview.test.ts @@ -117,7 +117,7 @@ describe("Preview command", () => { expect(createBranch).toHaveBeenCalledWith( expect.any(Object), "tmp_ci_feature_test", - { lastPartition: true } + { branch_data_mode: BranchDataMode.LAST_PARTITION } ); }); diff --git a/src/cli/commands/preview.ts b/src/cli/commands/preview.ts index febe0d7..9dff589 100644 --- a/src/cli/commands/preview.ts +++ b/src/cli/commands/preview.ts @@ -227,8 +227,10 @@ export async function runPreview(options: PreviewCommandOptions = {}): Promise

Date: Mon, 22 Jun 2026 23:14:16 -0700 Subject: [PATCH 5/5] use type union instead of enum --- src/api/branches.test.ts | 5 ++--- src/api/branches.ts | 2 +- src/cli/commands/build.test.ts | 9 ++++----- src/cli/commands/build.ts | 9 ++++----- src/cli/commands/clear.ts | 5 ++--- src/cli/commands/dev.ts | 8 ++++---- src/cli/commands/preview.test.ts | 7 +++---- src/cli/commands/preview.ts | 9 ++++----- src/cli/config-types.ts | 5 ++--- src/cli/config.ts | 23 ++++++++++++++++------- src/client/base.ts | 9 ++++----- src/index.ts | 2 +- 12 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/api/branches.test.ts b/src/api/branches.test.ts index 8687378..fa239d4 100644 --- a/src/api/branches.test.ts +++ b/src/api/branches.test.ts @@ -1,5 +1,4 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { BranchDataMode } from "../cli/config-types.js"; import { BranchApiError, createBranch, @@ -215,7 +214,7 @@ describe("Branch API client", () => { }); await createBranch(config, "my-feature", { - branch_data_mode: BranchDataMode.LAST_PARTITION, + branch_data_mode: "last_partition", }); const [createUrl] = mockFetch.mock.calls[0]; @@ -530,7 +529,7 @@ describe("Branch API client", () => { }); const result = await clearBranch(config, "my-feature", { - branch_data_mode: BranchDataMode.LAST_PARTITION, + branch_data_mode: "last_partition", }); expect(mockFetch).toHaveBeenCalledTimes(5); diff --git a/src/api/branches.ts b/src/api/branches.ts index db71c97..ecfff37 100644 --- a/src/api/branches.ts +++ b/src/api/branches.ts @@ -3,7 +3,7 @@ * Uses the /v1/environments endpoints (Forward API) */ -import { BranchDataMode } from "../cli/config-types.js"; +import type { BranchDataMode } from "../cli/config-types.js"; import { createTinybirdFetcher } from "./fetcher.js"; /** diff --git a/src/cli/commands/build.test.ts b/src/cli/commands/build.test.ts index 906b2a3..48e95f4 100644 --- a/src/cli/commands/build.test.ts +++ b/src/cli/commands/build.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { runBuild } from "./build.js"; -import { BranchDataMode } from "../config-types.js"; // Mock all dependencies vi.mock("../config.js", () => ({ @@ -331,7 +330,7 @@ describe("Build Command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataMode: BranchDataMode.LAST_PARTITION, + branchDataMode: "last_partition", }); vi.mocked(buildFromInclude).mockResolvedValue({ resources: { datasources: [], pipes: [], connections: [] }, @@ -366,7 +365,7 @@ describe("Build Command", () => { expect(getOrCreateBranch).toHaveBeenCalledWith( expect.any(Object), "feature_test", - { branch_data_mode: BranchDataMode.LAST_PARTITION } + { branch_data_mode: "last_partition" } ); }); @@ -423,7 +422,7 @@ describe("Build Command", () => { expect(getOrCreateBranch).toHaveBeenCalledWith( expect.any(Object), "feature_test", - { branch_data_mode: BranchDataMode.LAST_PARTITION } + { branch_data_mode: "last_partition" } ); }); @@ -446,7 +445,7 @@ describe("Build Command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataMode: BranchDataMode.LAST_PARTITION, + branchDataMode: "last_partition", }); vi.mocked(buildFromInclude).mockResolvedValue({ resources: { datasources: [], pipes: [], connections: [] }, diff --git a/src/cli/commands/build.ts b/src/cli/commands/build.ts index 0e272ec..a23414b 100644 --- a/src/cli/commands/build.ts +++ b/src/cli/commands/build.ts @@ -2,8 +2,7 @@ * Build command - generates and pushes resources to Tinybird branches */ -import { loadConfigAsync, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js"; -import { BranchDataMode } from "../config-types.js"; +import { loadConfigAsync, LOCAL_BASE_URL, type ResolvedConfig, type DevMode, type BranchDataMode } from "../config.js"; import { buildFromInclude, type BuildFromIncludeResult } from "../../generator/index.js"; import { buildToTinybird, type BuildApiResult } from "../../api/build.js"; import { getOrCreateBranch } from "../../api/branches.js"; @@ -226,9 +225,9 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise { // Clear the branch (delete and recreate) const branchOptions: CreateBranchOptions | undefined = - config.devMode !== "local" && config.branchDataMode === BranchDataMode.LAST_PARTITION - ? { branch_data_mode: BranchDataMode.LAST_PARTITION } + config.devMode !== "local" && config.branchDataMode === "last_partition" + ? { branch_data_mode: "last_partition" } : undefined; const newBranch = await clearBranch( diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 2cf4e33..493a0d8 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -13,8 +13,8 @@ import { LOCAL_BASE_URL, type ResolvedConfig, type DevMode, + type BranchDataMode, } from "../config.js"; -import { BranchDataMode } from "../config-types.js"; import { runBuild, type BuildCommandResult } from "./build.js"; import { getOrCreateBranch, type TinybirdBranch } from "../../api/branches.js"; import { browserLogin } from "../auth.js"; @@ -240,9 +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 = - options.lastPartition || config.branchDataMode === BranchDataMode.LAST_PARTITION - ? BranchDataMode.LAST_PARTITION + const branchDataMode: BranchDataMode | undefined = + options.lastPartition || config.branchDataMode === "last_partition" + ? "last_partition" : undefined; const branchOptions = branchDataMode ? { branch_data_mode: branchDataMode } diff --git a/src/cli/commands/preview.test.ts b/src/cli/commands/preview.test.ts index 69b533d..12deaae 100644 --- a/src/cli/commands/preview.test.ts +++ b/src/cli/commands/preview.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { generatePreviewBranchName, runPreview } from "./preview.js"; -import { BranchDataMode } from "../config-types.js"; vi.mock("../config.js", () => ({ loadConfigAsync: vi.fn(), @@ -91,7 +90,7 @@ describe("Preview command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataMode: BranchDataMode.LAST_PARTITION, + branchDataMode: "last_partition", }); vi.mocked(buildFromInclude).mockResolvedValue({ resources: { datasources: [], pipes: [], connections: [] }, @@ -117,7 +116,7 @@ describe("Preview command", () => { expect(createBranch).toHaveBeenCalledWith( expect.any(Object), "tmp_ci_feature_test", - { branch_data_mode: BranchDataMode.LAST_PARTITION } + { branch_data_mode: "last_partition" } ); }); @@ -138,7 +137,7 @@ describe("Preview command", () => { gitBranch: "feature-test", tinybirdBranch: "feature_test", isMainBranch: false, - branchDataMode: BranchDataMode.LAST_PARTITION, + branchDataMode: "last_partition", }); vi.mocked(buildFromInclude).mockResolvedValue({ resources: { datasources: [], pipes: [], connections: [] }, diff --git a/src/cli/commands/preview.ts b/src/cli/commands/preview.ts index 9dff589..42c0f6f 100644 --- a/src/cli/commands/preview.ts +++ b/src/cli/commands/preview.ts @@ -3,9 +3,8 @@ */ import { loadConfigAsync, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js"; -import { BranchDataMode } from "../config-types.js"; import { buildFromInclude, type BuildFromIncludeResult } from "../../generator/index.js"; -import { createBranch, deleteBranch, getBranch, type TinybirdBranch } from "../../api/branches.js"; +import { createBranch, deleteBranch, getBranch, type CreateBranchOptions, type TinybirdBranch } from "../../api/branches.js"; import { deployToMain } from "../../api/deploy.js"; import { buildToTinybird } from "../../api/build.js"; import { @@ -227,9 +226,9 @@ export async function runPreview(options: PreviewCommandOptions = {}): Promise

): { mode: BranchData } const value = raw["branch_data_mode"]; - if (value === undefined || value === null) return { mode: BranchDataMode.LAST_PARTITION, explicit: false }; + if (value === undefined || value === null) return { mode: DEFAULT_BRANCH_DATA_MODE, 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: BranchDataMode.LAST_PARTITION, explicit: false }; - if (mode !== BranchDataMode.LAST_PARTITION) { - throw new Error(`Invalid branch_data_mode '${value}'. Allowed values are: last_partition.`); + if (!mode) return { mode: DEFAULT_BRANCH_DATA_MODE, 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(", ")}.` + ); } return { mode: mode as BranchDataMode, explicit: true }; } diff --git a/src/client/base.ts b/src/client/base.ts index e8bc18f..e453338 100644 --- a/src/client/base.ts +++ b/src/client/base.ts @@ -19,6 +19,7 @@ import type { } from "./types.js"; import { TinybirdError } from "./types.js"; import { TinybirdApi, TinybirdApiError } from "../api/api.js"; +import type { CreateBranchOptions } from "../api/branches.js"; import { TokensNamespace } from "./tokens.js"; /** @@ -299,7 +300,6 @@ export class TinybirdClient { // out of the client bundle when not using dev mode const { loadConfigAsync } = await import("../cli/config.js"); const { getOrCreateBranch } = await import("../api/branches.js"); - const { BranchDataMode } = await import("../cli/config-types.js"); const { isPreviewEnvironment, getPreviewBranchName } = await import( "./preview.js" ); @@ -341,10 +341,9 @@ export class TinybirdClient { } const branchName = config.tinybirdBranch; - const branchOptions = - config.devMode !== "local" && - config.branchDataMode === BranchDataMode.LAST_PARTITION - ? { branch_data_mode: BranchDataMode.LAST_PARTITION } + const branchOptions: CreateBranchOptions | undefined = + config.devMode !== "local" && config.branchDataMode === "last_partition" + ? { branch_data_mode: "last_partition" } : undefined; // Get or create branch (always fetch fresh to avoid stale cache issues) diff --git a/src/index.ts b/src/index.ts index 0e9ce9a..8c7e200 100644 --- a/src/index.ts +++ b/src/index.ts @@ -312,4 +312,4 @@ export type { // ============ Config Types ============ // Import from config-types.ts to avoid bundling esbuild in client code -export type { TinybirdConfig, DevMode } from "./cli/config-types.js"; +export type { TinybirdConfig, DevMode, BranchDataMode } from "./cli/config-types.js";