linear

package
v0.62.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 13, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package linear provides client and data types for the Linear GraphQL API.

This package handles all interactions with Linear's issue tracking system, including fetching, creating, and updating issues. It provides bidirectional mapping between Linear's data model and Beads' internal types.

Index

Constants

View Source
const (
	// DefaultAPIEndpoint is the Linear GraphQL API endpoint.
	DefaultAPIEndpoint = "https://api.linear.app/graphql"

	// DefaultTimeout is the default HTTP request timeout.
	DefaultTimeout = 30 * time.Second

	// MaxRetries is the maximum number of retries for rate-limited requests.
	MaxRetries = 3

	// RetryDelay is the base delay between retries (exponential backoff).
	RetryDelay = time.Second

	// MaxPageSize is the maximum number of issues to fetch per page.
	MaxPageSize = 100
)

API configuration constants.

Variables

This section is empty.

Functions

func BuildLinearDescription

func BuildLinearDescription(issue *types.Issue) string

BuildLinearDescription formats a Beads issue for Linear's description field. This mirrors the payload used during push to keep hash comparisons consistent.

func BuildLinearToLocalUpdates

func BuildLinearToLocalUpdates(li *Issue, config *MappingConfig) map[string]interface{}

BuildLinearToLocalUpdates creates an updates map from a Linear issue to apply to a local Beads issue. This is used when Linear wins a conflict.

func CanonicalizeLinearExternalRef

func CanonicalizeLinearExternalRef(externalRef string) (canonical string, ok bool)

CanonicalizeLinearExternalRef returns a stable Linear issue URL without the slug. Example: https://linear.app/team/issue/TEAM-123/title -> https://linear.app/team/issue/TEAM-123 Returns ok=false if the URL isn't a recognizable Linear issue URL.

func ExtractLinearIdentifier

func ExtractLinearIdentifier(url string) string

ExtractLinearIdentifier extracts the Linear issue identifier (e.g., "TEAM-123") from a Linear URL.

func GenerateIssueIDs

func GenerateIssueIDs(issues []*types.Issue, prefix, creator string, opts IDGenerationOptions) error

GenerateIssueIDs generates unique hash-based IDs for issues that don't have one. Tracks used IDs to prevent collisions within the batch (and optionally against existing IDs). The creator parameter is used as part of the hash input (e.g., "linear-import").

func IsLinearExternalRef

func IsLinearExternalRef(externalRef string) bool

IsLinearExternalRef checks if an external_ref URL is a Linear issue URL.

func LabelToIssueType

func LabelToIssueType(labels *Labels, config *MappingConfig) types.IssueType

LabelToIssueType infers issue type from label names. Uses configurable mapping from linear.label_type_map.* config.

func MapEpicToProjectState

func MapEpicToProjectState(status types.Status) string

MapEpicToProjectState maps a Beads status to Linear project state.

func NormalizeIssueForLinearHash

func NormalizeIssueForLinearHash(issue *types.Issue) *types.Issue

NormalizeIssueForLinearHash returns a copy of the issue using Linear's description formatting and clears fields not present in Linear's model to avoid false conflicts.

func ParseBeadsStatus

func ParseBeadsStatus(s string) types.Status

ParseBeadsStatus converts a status string to types.Status.

func ParseIssueType

func ParseIssueType(s string) types.IssueType

ParseIssueType converts an issue type string to types.IssueType.

func PriorityToBeads

func PriorityToBeads(linearPriority int, config *MappingConfig) int

PriorityToBeads maps Linear priority (0-4) to Beads priority (0-4). Linear: 0=no priority, 1=urgent, 2=high, 3=medium, 4=low Beads: 0=critical, 1=high, 2=medium, 3=low, 4=backlog Uses configurable mapping from linear.priority_map.* config.

func PriorityToLinear

func PriorityToLinear(beadsPriority int, config *MappingConfig) int

PriorityToLinear maps Beads priority (0-4) to Linear priority (0-4). Uses configurable mapping by inverting linear.priority_map.* config.

func ProjectToEpic

func ProjectToEpic(lp *Project) *types.Issue

ProjectToEpic converts a Linear Project to a Beads Epic issue.

func RelationToBeadsDep

func RelationToBeadsDep(relationType string, config *MappingConfig) string

RelationToBeadsDep converts a Linear relation to a Beads dependency type. Uses configurable mapping from linear.relation_map.* config.

func StateToBeadsStatus

func StateToBeadsStatus(state *State, config *MappingConfig) types.Status

StateToBeadsStatus maps Linear state type to Beads status. Checks both state type (backlog, unstarted, etc.) and state name for custom workflows. Uses configurable mapping from linear.state_map.* config.

func StatusToLinearStateType

func StatusToLinearStateType(status types.Status) string

StatusToLinearStateType converts Beads status to Linear state type for filtering. This is used when pushing issues to Linear to find the appropriate state.

Types

type Client

type Client struct {
	APIKey     string
	TeamID     string
	ProjectID  string // Optional: filter issues to a specific project
	Endpoint   string // GraphQL endpoint URL (defaults to DefaultAPIEndpoint)
	HTTPClient *http.Client
}

Client provides methods to interact with the Linear GraphQL API.

func NewClient

func NewClient(apiKey, teamID string) *Client

NewClient creates a new Linear client with the given API key and team ID.

func (*Client) CreateIssue

func (c *Client) CreateIssue(ctx context.Context, title, description string, priority int, stateID string, labelIDs []string) (*Issue, error)

CreateIssue creates a new issue in Linear.

func (*Client) CreateProject

func (c *Client) CreateProject(ctx context.Context, name, description, state string) (*Project, error)

CreateProject creates a new project in Linear.

func (*Client) Execute

func (c *Client) Execute(ctx context.Context, req *GraphQLRequest) (json.RawMessage, error)

Execute sends a GraphQL request to the Linear API. Handles rate limiting with exponential backoff.

func (*Client) FetchIssueByIdentifier

func (c *Client) FetchIssueByIdentifier(ctx context.Context, identifier string) (*Issue, error)

FetchIssueByIdentifier retrieves a single issue from Linear by its identifier (e.g., "TEAM-123"). Returns nil if the issue is not found.

func (*Client) FetchIssues

func (c *Client) FetchIssues(ctx context.Context, state string) ([]Issue, error)

FetchIssues retrieves issues from Linear with optional filtering by state. 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 (*Client) FetchIssuesSince

func (c *Client) FetchIssuesSince(ctx context.Context, state string, since time.Time) ([]Issue, error)

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. The state parameter can be: "open", "closed", or "all". If ProjectID is set on the client, only issues from that project are returned.

func (*Client) FetchProjects

func (c *Client) FetchProjects(ctx context.Context, state string) ([]Project, error)

FetchProjects retrieves projects from Linear with optional filtering by state. state can be: "planned", "started", "paused", "completed", "canceled", or "all"/"".

func (*Client) FetchTeams

func (c *Client) FetchTeams(ctx context.Context) ([]Team, error)

FetchTeams retrieves all teams accessible with the current API key. This is useful for discovering the team ID needed for configuration.

func (*Client) GetTeamStates

func (c *Client) GetTeamStates(ctx context.Context) ([]State, error)

GetTeamStates fetches the workflow states for the configured team.

func (*Client) UpdateIssue

func (c *Client) UpdateIssue(ctx context.Context, issueID string, updates map[string]interface{}) (*Issue, error)

UpdateIssue updates an existing issue in Linear.

func (*Client) UpdateProject

func (c *Client) UpdateProject(ctx context.Context, projectID string, updates map[string]interface{}) (*Project, error)

UpdateProject updates an existing project in Linear.

func (*Client) WithEndpoint

func (c *Client) WithEndpoint(endpoint string) *Client

WithEndpoint returns a new client configured to use the specified endpoint. This is useful for testing with mock servers or connecting to self-hosted instances.

func (*Client) WithHTTPClient

func (c *Client) WithHTTPClient(httpClient *http.Client) *Client

WithHTTPClient returns a new client configured to use the specified HTTP client. This is useful for testing or customizing timeouts and transport settings.

func (*Client) WithProjectID

func (c *Client) WithProjectID(projectID string) *Client

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.

type ConfigLoader

type ConfigLoader interface {
	GetAllConfig() (map[string]string, error)
}

ConfigLoader is an interface for loading configuration values. This allows the mapping package to be decoupled from the storage layer.

type Conflict

type Conflict struct {
	IssueID           string    // Beads issue ID
	LocalUpdated      time.Time // When the local version was last modified
	LinearUpdated     time.Time // When the Linear version was last modified
	LinearExternalRef string    // URL to the Linear issue
	LinearIdentifier  string    // Linear issue identifier (e.g., "TEAM-123")
	LinearInternalID  string    // Linear's internal UUID (for API updates)
}

Conflict represents a conflict between local and Linear versions. A conflict occurs when both the local and Linear versions have been modified since the last sync.

type DependencyInfo

type DependencyInfo struct {
	FromLinearID string // Linear identifier of the dependent issue (e.g., "TEAM-123")
	ToLinearID   string // Linear identifier of the dependency target
	Type         string // Beads dependency type (blocks, related, duplicates, parent-child)
}

DependencyInfo represents a dependency to be created after issue import. Stored separately since we need all issues imported before linking dependencies.

type GraphQLError

type GraphQLError struct {
	Message    string   `json:"message"`
	Path       []string `json:"path,omitempty"`
	Extensions struct {
		Code string `json:"code,omitempty"`
	} `json:"extensions,omitempty"`
}

GraphQLError represents a GraphQL error.

type GraphQLRequest

type GraphQLRequest struct {
	Query     string                 `json:"query"`
	Variables map[string]interface{} `json:"variables,omitempty"`
}

GraphQLRequest represents a GraphQL request payload.

type GraphQLResponse

type GraphQLResponse struct {
	Data   []byte         `json:"data"`
	Errors []GraphQLError `json:"errors,omitempty"`
}

GraphQLResponse represents a generic GraphQL response.

type IDGenerationOptions

type IDGenerationOptions struct {
	BaseLength int             // Starting hash length (3-8)
	MaxLength  int             // Maximum hash length (3-8)
	UsedIDs    map[string]bool // Pre-populated set to avoid collisions (e.g., DB IDs)
}

IDGenerationOptions configures Linear hash ID generation.

type Issue

type Issue struct {
	ID          string     `json:"id"`
	Identifier  string     `json:"identifier"` // e.g., "TEAM-123"
	Title       string     `json:"title"`
	Description string     `json:"description"`
	URL         string     `json:"url"`
	Priority    int        `json:"priority"` // 0=no priority, 1=urgent, 2=high, 3=medium, 4=low
	State       *State     `json:"state"`
	Assignee    *User      `json:"assignee"`
	Labels      *Labels    `json:"labels"`
	Project     *Project   `json:"project,omitempty"`
	Parent      *Parent    `json:"parent,omitempty"`
	Relations   *Relations `json:"relations,omitempty"`
	CreatedAt   string     `json:"createdAt"`
	UpdatedAt   string     `json:"updatedAt"`
	CompletedAt string     `json:"completedAt,omitempty"`
}

Issue represents an issue from the Linear API.

type IssueConversion

type IssueConversion struct {
	Issue        interface{} // *types.Issue - avoiding circular import
	Dependencies []DependencyInfo
}

IssueConversion holds the result of converting a Linear issue to Beads. It includes the issue and any dependencies that should be created.

func IssueToBeads

func IssueToBeads(li *Issue, config *MappingConfig) *IssueConversion

IssueToBeads converts a Linear issue to a Beads issue.

type IssueCreateResponse

type IssueCreateResponse struct {
	IssueCreate struct {
		Success bool  `json:"success"`
		Issue   Issue `json:"issue"`
	} `json:"issueCreate"`
}

IssueCreateResponse represents the response from issueCreate mutation.

type IssueUpdateResponse

type IssueUpdateResponse struct {
	IssueUpdate struct {
		Success bool  `json:"success"`
		Issue   Issue `json:"issue"`
	} `json:"issueUpdate"`
}

IssueUpdateResponse represents the response from issueUpdate mutation.

type IssuesResponse

type IssuesResponse struct {
	Issues struct {
		Nodes    []Issue `json:"nodes"`
		PageInfo struct {
			HasNextPage bool   `json:"hasNextPage"`
			EndCursor   string `json:"endCursor"`
		} `json:"pageInfo"`
	} `json:"issues"`
}

IssuesResponse represents the response from issues query.

type Label

type Label struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

Label represents a label in Linear.

type Labels

type Labels struct {
	Nodes []Label `json:"nodes"`
}

Labels represents paginated labels on an issue.

type MappingConfig

type MappingConfig struct {
	// PriorityMap maps Linear priority (0-4) to Beads priority (0-4).
	// Key is Linear priority as string, value is Beads priority.
	PriorityMap map[string]int

	// StateMap maps Linear state types/names to Beads statuses.
	// Key is lowercase state type or name, value is Beads status string.
	StateMap map[string]string

	// LabelTypeMap maps Linear label names to Beads issue types.
	// Key is lowercase label name, value is Beads issue type.
	LabelTypeMap map[string]string

	// RelationMap maps Linear relation types to Beads dependency types.
	// Key is Linear relation type, value is Beads dependency type.
	RelationMap map[string]string
}

MappingConfig holds configurable mappings between Linear and Beads. All maps use lowercase keys for case-insensitive matching.

func DefaultMappingConfig

func DefaultMappingConfig() *MappingConfig

DefaultMappingConfig returns sensible default mappings.

func LoadMappingConfig

func LoadMappingConfig(loader ConfigLoader) *MappingConfig

LoadMappingConfig loads mapping configuration from a config loader. Config keys follow the pattern: linear.<category>_map.<key> = <value> Examples:

linear.priority_map.0 = 4       (Linear "no priority" -> Beads backlog)
linear.state_map.started = in_progress
linear.label_type_map.bug = bug
linear.relation_map.blocks = blocks

type Parent

type Parent struct {
	ID         string `json:"id"`
	Identifier string `json:"identifier"`
}

Parent represents a parent issue reference.

type Project

type Project struct {
	ID          string  `json:"id"`
	Name        string  `json:"name"`
	Description string  `json:"description"`
	SlugId      string  `json:"slugId"`
	URL         string  `json:"url"`
	State       string  `json:"state"` // "planned", "started", "paused", "completed", "canceled"
	Progress    float64 `json:"progress"`
	CreatedAt   string  `json:"createdAt"`
	UpdatedAt   string  `json:"updatedAt"`
	CompletedAt string  `json:"completedAt,omitempty"`
}

Project represents a project in Linear.

type ProjectCreateResponse

type ProjectCreateResponse struct {
	ProjectCreate struct {
		Success bool    `json:"success"`
		Project Project `json:"project"`
	} `json:"projectCreate"`
}

ProjectCreateResponse represents the response from projectCreate mutation.

type ProjectUpdateResponse

type ProjectUpdateResponse struct {
	ProjectUpdate struct {
		Success bool    `json:"success"`
		Project Project `json:"project"`
	} `json:"projectUpdate"`
}

ProjectUpdateResponse represents the response from projectUpdate mutation.

type ProjectsResponse

type ProjectsResponse struct {
	Projects struct {
		Nodes    []Project `json:"nodes"`
		PageInfo struct {
			HasNextPage bool   `json:"hasNextPage"`
			EndCursor   string `json:"endCursor"`
		} `json:"pageInfo"`
	} `json:"projects"`
}

ProjectsResponse represents the response from projects query.

type PullStats

type PullStats struct {
	Created     int
	Updated     int
	Skipped     int
	Incremental bool   // Whether this was an incremental sync
	SyncedSince string // Timestamp we synced since (if incremental)
}

PullStats tracks pull operation statistics.

type PushStats

type PushStats struct {
	Created int
	Updated int
	Skipped int
	Errors  int
}

PushStats tracks push operation statistics.

type Relation

type Relation struct {
	ID           string `json:"id"`
	Type         string `json:"type"` // "blocks", "blockedBy", "duplicate", "related"
	RelatedIssue struct {
		ID         string `json:"id"`
		Identifier string `json:"identifier"`
	} `json:"relatedIssue"`
}

Relation represents a relation between issues in Linear.

type Relations

type Relations struct {
	Nodes []Relation `json:"nodes"`
}

Relations wraps the nodes array for relations.

type State

type State struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	Type string `json:"type"` // "backlog", "unstarted", "started", "completed", "canceled"
}

State represents a workflow state in Linear.

type StateCache

type StateCache struct {
	States      []State
	StatesByID  map[string]State
	OpenStateID string // First "unstarted" or "backlog" state
}

StateCache caches workflow states for the team to avoid repeated API calls.

func BuildStateCache

func BuildStateCache(ctx context.Context, client *Client) (*StateCache, error)

BuildStateCache fetches and caches team states.

func BuildStateCacheFromTracker

func BuildStateCacheFromTracker(ctx context.Context, t *Tracker) (*StateCache, error)

BuildStateCacheFromTracker builds a StateCache using the tracker's internal client. This allows CLI code to set up PushHooks.BuildStateCache without accessing the client directly.

func (*StateCache) FindStateForBeadsStatus

func (sc *StateCache) FindStateForBeadsStatus(status types.Status) string

FindStateForBeadsStatus returns the best Linear state ID for a Beads status.

type StatesWrapper

type StatesWrapper struct {
	Nodes []State `json:"nodes"`
}

StatesWrapper wraps the nodes array for states.

type SyncResult

type SyncResult struct {
	Success  bool      `json:"success"`
	Stats    SyncStats `json:"stats"`
	LastSync string    `json:"last_sync,omitempty"`
	Error    string    `json:"error,omitempty"`
	Warnings []string  `json:"warnings,omitempty"`
}

SyncResult represents the result of a Linear sync operation.

type SyncStats

type SyncStats struct {
	Pulled    int `json:"pulled"`
	Pushed    int `json:"pushed"`
	Created   int `json:"created"`
	Updated   int `json:"updated"`
	Skipped   int `json:"skipped"`
	Errors    int `json:"errors"`
	Conflicts int `json:"conflicts"`
}

SyncStats tracks statistics for a Linear sync operation.

type Team

type Team struct {
	ID   string `json:"id"`   // UUID
	Name string `json:"name"` // Display name
	Key  string `json:"key"`  // Short key used in issue identifiers (e.g., "ENG")
}

Team represents a team in Linear.

type TeamResponse

type TeamResponse struct {
	Team TeamStates `json:"team"`
}

TeamResponse represents the response from team query.

type TeamStates

type TeamStates struct {
	ID     string         `json:"id"`
	States *StatesWrapper `json:"states"`
}

TeamStates represents workflow states for a team.

type TeamsResponse

type TeamsResponse struct {
	Teams struct {
		Nodes []Team `json:"nodes"`
	} `json:"teams"`
}

TeamsResponse represents the response from teams query.

type Tracker

type Tracker struct {
	// contains filtered or unexported fields
}

Tracker implements tracker.IssueTracker for Linear.

func (*Tracker) BuildExternalRef

func (t *Tracker) BuildExternalRef(issue *tracker.TrackerIssue) string

func (*Tracker) Close

func (t *Tracker) Close() error

func (*Tracker) ConfigPrefix

func (t *Tracker) ConfigPrefix() string

func (*Tracker) CreateIssue

func (t *Tracker) CreateIssue(ctx context.Context, issue *types.Issue) (*tracker.TrackerIssue, error)

func (*Tracker) DisplayName

func (t *Tracker) DisplayName() string

func (*Tracker) ExtractIdentifier

func (t *Tracker) ExtractIdentifier(ref string) string

func (*Tracker) FetchIssue

func (t *Tracker) FetchIssue(ctx context.Context, identifier string) (*tracker.TrackerIssue, error)

func (*Tracker) FetchIssues

func (t *Tracker) FetchIssues(ctx context.Context, opts tracker.FetchOptions) ([]tracker.TrackerIssue, error)

func (*Tracker) FieldMapper

func (t *Tracker) FieldMapper() tracker.FieldMapper

func (*Tracker) Init

func (t *Tracker) Init(ctx context.Context, store storage.Storage) error

func (*Tracker) IsExternalRef

func (t *Tracker) IsExternalRef(ref string) bool

func (*Tracker) Name

func (t *Tracker) Name() string

func (*Tracker) UpdateIssue

func (t *Tracker) UpdateIssue(ctx context.Context, externalID string, issue *types.Issue) (*tracker.TrackerIssue, error)

func (*Tracker) Validate

func (t *Tracker) Validate() error

type User

type User struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Email       string `json:"email"`
	DisplayName string `json:"displayName"`
}

User represents a user in Linear.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL