From ee44498659aedf4ffc5884867edcd68ab7d2ffee Mon Sep 17 00:00:00 2001 From: Zachary Piazza <42187340+zjpiazza@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:05:48 -0600 Subject: [PATCH] feat(linear): add --type and --exclude-type flags for sync filtering (#1205) * feat(linear): add --type and --exclude-type flags for sync filtering Add type filtering support to `bd linear sync --push` to allow users to control which issue types are synced to Linear. New flags: - --type: Only sync issues matching these types (e.g., --type=task,feature) - --exclude-type: Exclude issues of these types (e.g., --exclude-type=wisp) Use cases: - Sync only work items (tasks, features, bugs) while excluding internal telemetry (wisps, messages) - Push only specific issue types to Linear Fixes #1204 Co-Authored-By: Claude Opus 4.5 * fix(linear): update test to match new doPushToLinear signature --------- Co-authored-by: Claude Opus 4.5 --- cmd/bd/linear.go | 20 +++++++++++++++----- cmd/bd/linear_sync.go | 32 +++++++++++++++++++++++++++++++- cmd/bd/linear_test.go | 2 +- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/cmd/bd/linear.go b/cmd/bd/linear.go index d7b102d8..25eeb904 100644 --- a/cmd/bd/linear.go +++ b/cmd/bd/linear.go @@ -82,16 +82,22 @@ Modes: --push Export issues from beads to Linear (no flags) Bidirectional sync: pull then push, with conflict resolution +Type Filtering (--push only): + --type task,feature Only sync issues of these types + --exclude-type wisp Exclude issues of these types + Conflict Resolution: By default, newer timestamp wins. Override with: --prefer-local Always prefer local beads version --prefer-linear Always prefer Linear version Examples: - bd linear sync --pull # Import from Linear - bd linear sync --push --create-only # Push new issues only - bd linear sync --dry-run # Preview without changes - bd linear sync --prefer-local # Bidirectional, local wins`, + bd linear sync --pull # Import from Linear + bd linear sync --push --create-only # Push new issues only + bd linear sync --push --type=task,feature # Push only tasks and features + bd linear sync --push --exclude-type=wisp # Push all except wisps + bd linear sync --dry-run # Preview without changes + bd linear sync --prefer-local # Bidirectional, local wins`, Run: runLinearSync, } @@ -130,6 +136,8 @@ func init() { linearSyncCmd.Flags().Bool("create-only", false, "Only create new issues, don't update existing") linearSyncCmd.Flags().Bool("update-refs", true, "Update external_ref after creating Linear issues") linearSyncCmd.Flags().String("state", "all", "Issue state to sync: open, closed, all") + linearSyncCmd.Flags().StringSlice("type", nil, "Only sync issues of these types (can be repeated)") + linearSyncCmd.Flags().StringSlice("exclude-type", nil, "Exclude issues of these types (can be repeated)") linearCmd.AddCommand(linearSyncCmd) linearCmd.AddCommand(linearStatusCmd) @@ -146,6 +154,8 @@ func runLinearSync(cmd *cobra.Command, args []string) { createOnly, _ := cmd.Flags().GetBool("create-only") updateRefs, _ := cmd.Flags().GetBool("update-refs") state, _ := cmd.Flags().GetString("state") + typeFilters, _ := cmd.Flags().GetStringSlice("type") + excludeTypes, _ := cmd.Flags().GetStringSlice("exclude-type") if !dryRun { CheckReadonly("linear sync") @@ -314,7 +324,7 @@ func runLinearSync(cmd *cobra.Command, args []string) { fmt.Println("→ Pushing issues to Linear...") } - pushStats, err := doPushToLinear(ctx, dryRun, createOnly, updateRefs, forceUpdateIDs, skipUpdateIDs) + pushStats, err := doPushToLinear(ctx, dryRun, createOnly, updateRefs, forceUpdateIDs, skipUpdateIDs, typeFilters, excludeTypes) if err != nil { result.Success = false result.Error = err.Error() diff --git a/cmd/bd/linear_sync.go b/cmd/bd/linear_sync.go index 4f743e36..dc7d2f26 100644 --- a/cmd/bd/linear_sync.go +++ b/cmd/bd/linear_sync.go @@ -209,7 +209,9 @@ func doPullFromLinear(ctx context.Context, dryRun bool, state string, skipLinear } // doPushToLinear exports issues to Linear using the GraphQL API. -func doPushToLinear(ctx context.Context, dryRun bool, createOnly bool, updateRefs bool, forceUpdateIDs map[string]bool, skipUpdateIDs map[string]bool) (*linear.PushStats, error) { +// typeFilters includes only issues matching these types (empty means all). +// excludeTypes excludes issues matching these types. +func doPushToLinear(ctx context.Context, dryRun bool, createOnly bool, updateRefs bool, forceUpdateIDs map[string]bool, skipUpdateIDs map[string]bool, typeFilters []string, excludeTypes []string) (*linear.PushStats, error) { stats := &linear.PushStats{} client, err := getLinearClient(ctx) @@ -222,6 +224,34 @@ func doPushToLinear(ctx context.Context, dryRun bool, createOnly bool, updateRef return stats, fmt.Errorf("failed to get local issues: %w", err) } + // Apply type filters + if len(typeFilters) > 0 || len(excludeTypes) > 0 { + typeSet := make(map[string]bool, len(typeFilters)) + for _, t := range typeFilters { + typeSet[strings.ToLower(t)] = true + } + excludeSet := make(map[string]bool, len(excludeTypes)) + for _, t := range excludeTypes { + excludeSet[strings.ToLower(t)] = true + } + + var filtered []*types.Issue + for _, issue := range allIssues { + issueType := strings.ToLower(string(issue.IssueType)) + + // If type filters specified, issue must match one + if len(typeFilters) > 0 && !typeSet[issueType] { + continue + } + // If exclude types specified, issue must not match any + if excludeSet[issueType] { + continue + } + filtered = append(filtered, issue) + } + allIssues = filtered + } + var toCreate []*types.Issue var toUpdate []*types.Issue diff --git a/cmd/bd/linear_test.go b/cmd/bd/linear_test.go index 10021453..a259a658 100644 --- a/cmd/bd/linear_test.go +++ b/cmd/bd/linear_test.go @@ -1220,7 +1220,7 @@ func TestDoPushToLinearPreferLocalForcesUpdate(t *testing.T) { }) forceUpdateIDs := map[string]bool{issue.ID: true} - stats, err := doPushToLinear(ctx, false, false, true, forceUpdateIDs, nil) + stats, err := doPushToLinear(ctx, false, false, true, forceUpdateIDs, nil, nil, nil) if err != nil { t.Fatalf("doPushToLinear failed: %v", err) }