feat(linear): add project_id filter for sync (#938)

Add project_id filter for Linear sync

When linear.project_id is configured, bd linear sync will only fetch issues
belonging to that project instead of all team issues.

Closes #937
This commit is contained in:
Paddo
2026-01-08 14:45:00 +10:00
committed by GitHub
parent 3c9ceaa74d
commit 5254ade346
3 changed files with 41 additions and 0 deletions
+5
View File
@@ -26,6 +26,7 @@ var linearCmd = &cobra.Command{
Configuration: Configuration:
bd config set linear.api_key "YOUR_API_KEY" bd config set linear.api_key "YOUR_API_KEY"
bd config set linear.team_id "TEAM_ID" bd config set linear.team_id "TEAM_ID"
bd config set linear.project_id "PROJECT_ID" # Optional: sync only this project
Environment variables (alternative to config): Environment variables (alternative to config):
LINEAR_API_KEY - Linear API key LINEAR_API_KEY - Linear API key
@@ -586,6 +587,10 @@ func getLinearClient(ctx context.Context) (*linear.Client, error) {
if endpoint, _ := store.GetConfig(ctx, "linear.api_endpoint"); endpoint != "" { if endpoint, _ := store.GetConfig(ctx, "linear.api_endpoint"); endpoint != "" {
client = client.WithEndpoint(endpoint) client = client.WithEndpoint(endpoint)
} }
// Filter to specific project if configured
if projectID, _ := store.GetConfig(ctx, "linear.project_id"); projectID != "" {
client = client.WithProjectID(projectID)
}
} }
return client, nil return client, nil
+35
View File
@@ -92,6 +92,7 @@ func (c *Client) WithEndpoint(endpoint string) *Client {
return &Client{ return &Client{
APIKey: c.APIKey, APIKey: c.APIKey,
TeamID: c.TeamID, TeamID: c.TeamID,
ProjectID: c.ProjectID,
Endpoint: endpoint, Endpoint: endpoint,
HTTPClient: c.HTTPClient, HTTPClient: c.HTTPClient,
} }
@@ -103,11 +104,24 @@ func (c *Client) WithHTTPClient(httpClient *http.Client) *Client {
return &Client{ return &Client{
APIKey: c.APIKey, APIKey: c.APIKey,
TeamID: c.TeamID, TeamID: c.TeamID,
ProjectID: c.ProjectID,
Endpoint: c.Endpoint, Endpoint: c.Endpoint,
HTTPClient: httpClient, HTTPClient: httpClient,
} }
} }
// WithProjectID returns a new client configured to filter issues by the specified project.
// When set, FetchIssues and FetchIssuesSince will only return issues belonging to this project.
func (c *Client) WithProjectID(projectID string) *Client {
return &Client{
APIKey: c.APIKey,
TeamID: c.TeamID,
ProjectID: projectID,
Endpoint: c.Endpoint,
HTTPClient: c.HTTPClient,
}
}
// Execute sends a GraphQL request to the Linear API. // Execute sends a GraphQL request to the Linear API.
// Handles rate limiting with exponential backoff. // Handles rate limiting with exponential backoff.
func (c *Client) Execute(ctx context.Context, req *GraphQLRequest) (json.RawMessage, error) { func (c *Client) Execute(ctx context.Context, req *GraphQLRequest) (json.RawMessage, error) {
@@ -178,6 +192,7 @@ func (c *Client) Execute(ctx context.Context, req *GraphQLRequest) (json.RawMess
// FetchIssues retrieves issues from Linear with optional filtering by state. // FetchIssues retrieves issues from Linear with optional filtering by state.
// state can be: "open" (unstarted/started), "closed" (completed/canceled), or "all". // state can be: "open" (unstarted/started), "closed" (completed/canceled), or "all".
// If ProjectID is set on the client, only issues from that project are returned.
func (c *Client) FetchIssues(ctx context.Context, state string) ([]Issue, error) { func (c *Client) FetchIssues(ctx context.Context, state string) ([]Issue, error) {
var allIssues []Issue var allIssues []Issue
var cursor string var cursor string
@@ -189,6 +204,16 @@ func (c *Client) FetchIssues(ctx context.Context, state string) ([]Issue, error)
}, },
}, },
} }
// Add project filter if configured
if c.ProjectID != "" {
filter["project"] = map[string]interface{}{
"id": map[string]interface{}{
"eq": c.ProjectID,
},
}
}
switch state { switch state {
case "open": case "open":
filter["state"] = map[string]interface{}{ filter["state"] = map[string]interface{}{
@@ -242,6 +267,7 @@ func (c *Client) FetchIssues(ctx context.Context, state string) ([]Issue, error)
// FetchIssuesSince retrieves issues from Linear that have been updated since the given time. // FetchIssuesSince retrieves issues from Linear that have been updated since the given time.
// This enables incremental sync by only fetching issues modified after the last sync. // This enables incremental sync by only fetching issues modified after the last sync.
// The state parameter can be: "open", "closed", or "all". // The state parameter can be: "open", "closed", or "all".
// If ProjectID is set on the client, only issues from that project are returned.
func (c *Client) FetchIssuesSince(ctx context.Context, state string, since time.Time) ([]Issue, error) { func (c *Client) FetchIssuesSince(ctx context.Context, state string, since time.Time) ([]Issue, error) {
var allIssues []Issue var allIssues []Issue
var cursor string var cursor string
@@ -260,6 +286,15 @@ func (c *Client) FetchIssuesSince(ctx context.Context, state string, since time.
}, },
} }
// Add project filter if configured
if c.ProjectID != "" {
filter["project"] = map[string]interface{}{
"id": map[string]interface{}{
"eq": c.ProjectID,
},
}
}
// Add state filter if specified // Add state filter if specified
switch state { switch state {
case "open": case "open":
+1
View File
@@ -32,6 +32,7 @@ const (
type Client struct { type Client struct {
APIKey string APIKey string
TeamID string TeamID string
ProjectID string // Optional: filter issues to a specific project
Endpoint string // GraphQL endpoint URL (defaults to DefaultAPIEndpoint) Endpoint string // GraphQL endpoint URL (defaults to DefaultAPIEndpoint)
HTTPClient *http.Client HTTPClient *http.Client
} }