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 <noreply@anthropic.com>

* fix(linear): update test to match new doPushToLinear signature

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Zachary Piazza
2026-01-20 16:05:48 -06:00
committed by GitHub
parent e36e2f6679
commit ee44498659
3 changed files with 47 additions and 7 deletions

View File

@@ -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()

View File

@@ -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

View File

@@ -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)
}