Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
32 changes: 32 additions & 0 deletions src/api/branches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 3 additions & 4 deletions src/cli/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,9 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise<Build
console.log(`[debug] Getting/creating Tinybird branch: ${config.tinybirdBranch}`);
}
try {
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;
Expand Down
4 changes: 2 additions & 2 deletions src/cli/commands/clear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ async function clearCloudBranch(config: ResolvedConfig): Promise<ClearResult> {

// 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(
Expand Down
7 changes: 3 additions & 4 deletions src/cli/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
46 changes: 46 additions & 0 deletions src/cli/commands/preview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions src/cli/commands/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ export async function runPreview(options: PreviewCommandOptions = {}): Promise<P
try {
const apiConfig = { baseUrl: config.baseUrl, token: config.token };
const branchOptions: CreateBranchOptions | undefined =
config.branchDataMode === "last_partition"
? { branch_data_mode: "last_partition" }
config.branchDataMode
? { branch_data_mode: config.branchDataMode }
: undefined;

// Check if branch already exists and delete it for a fresh start
Expand Down
5 changes: 4 additions & 1 deletion src/cli/config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
24 changes: 20 additions & 4 deletions src/cli/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,23 @@ describe("Config", () => {
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",
Expand All @@ -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",
Expand All @@ -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", () => {
Expand Down
7 changes: 3 additions & 4 deletions src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

Expand Down Expand Up @@ -212,10 +211,10 @@ function resolveBranchDataMode(raw: Record<string, unknown>): { 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(", ")}.`
Expand Down
4 changes: 2 additions & 2 deletions src/client/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading