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:
@@ -82,16 +82,22 @@ Modes:
|
|||||||
--push Export issues from beads to Linear
|
--push Export issues from beads to Linear
|
||||||
(no flags) Bidirectional sync: pull then push, with conflict resolution
|
(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:
|
Conflict Resolution:
|
||||||
By default, newer timestamp wins. Override with:
|
By default, newer timestamp wins. Override with:
|
||||||
--prefer-local Always prefer local beads version
|
--prefer-local Always prefer local beads version
|
||||||
--prefer-linear Always prefer Linear version
|
--prefer-linear Always prefer Linear version
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
bd linear sync --pull # Import from Linear
|
bd linear sync --pull # Import from Linear
|
||||||
bd linear sync --push --create-only # Push new issues only
|
bd linear sync --push --create-only # Push new issues only
|
||||||
bd linear sync --dry-run # Preview without changes
|
bd linear sync --push --type=task,feature # Push only tasks and features
|
||||||
bd linear sync --prefer-local # Bidirectional, local wins`,
|
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,
|
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("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().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().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(linearSyncCmd)
|
||||||
linearCmd.AddCommand(linearStatusCmd)
|
linearCmd.AddCommand(linearStatusCmd)
|
||||||
@@ -146,6 +154,8 @@ func runLinearSync(cmd *cobra.Command, args []string) {
|
|||||||
createOnly, _ := cmd.Flags().GetBool("create-only")
|
createOnly, _ := cmd.Flags().GetBool("create-only")
|
||||||
updateRefs, _ := cmd.Flags().GetBool("update-refs")
|
updateRefs, _ := cmd.Flags().GetBool("update-refs")
|
||||||
state, _ := cmd.Flags().GetString("state")
|
state, _ := cmd.Flags().GetString("state")
|
||||||
|
typeFilters, _ := cmd.Flags().GetStringSlice("type")
|
||||||
|
excludeTypes, _ := cmd.Flags().GetStringSlice("exclude-type")
|
||||||
|
|
||||||
if !dryRun {
|
if !dryRun {
|
||||||
CheckReadonly("linear sync")
|
CheckReadonly("linear sync")
|
||||||
@@ -314,7 +324,7 @@ func runLinearSync(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Println("→ Pushing issues to Linear...")
|
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 {
|
if err != nil {
|
||||||
result.Success = false
|
result.Success = false
|
||||||
result.Error = err.Error()
|
result.Error = err.Error()
|
||||||
|
|||||||
@@ -209,7 +209,9 @@ func doPullFromLinear(ctx context.Context, dryRun bool, state string, skipLinear
|
|||||||
}
|
}
|
||||||
|
|
||||||
// doPushToLinear exports issues to Linear using the GraphQL API.
|
// 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{}
|
stats := &linear.PushStats{}
|
||||||
|
|
||||||
client, err := getLinearClient(ctx)
|
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)
|
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 toCreate []*types.Issue
|
||||||
var toUpdate []*types.Issue
|
var toUpdate []*types.Issue
|
||||||
|
|
||||||
|
|||||||
@@ -1220,7 +1220,7 @@ func TestDoPushToLinearPreferLocalForcesUpdate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
forceUpdateIDs := map[string]bool{issue.ID: true}
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("doPushToLinear failed: %v", err)
|
t.Fatalf("doPushToLinear failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user