linear

package
v0.16.0 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package linear is the read-only GraphQL client for a Linear workspace: viewer, teams, workflow states, and cursor-paged issues with an updatedAt watermark filter. It issues no mutations — this connector mirrors, and a mirror never writes to the origin (gadak constitution, "writes pass through the origin"; Linear writes, if ever, are a separate decision).

The API key lives only in the Authorization header. It is never put in an error, a log line, or a URL — the same article-8 discipline internal/jira follows. This package contains no logging at all.

Why not internal/atlhttp: that package is the shared transport for *Atlassian Cloud* clients, and its headline guarantee is path safety — joining a caller-supplied path onto a configured site so the Authorization header cannot wander off-host. Linear has one fixed endpoint and no path parameter, so that machinery has nothing to do. Retry/backoff, Retry-After, the 64 MiB response cap, and the usage counters come from httppolicy — the host-neutral owner — not a copy. Rate-limit visibility stays here: Linear states its budget in response headers (x-ratelimit-*), which Atlassian never did. The API key is sent bare (no Bearer prefix).

Index

Constants

View Source
const CommentsPageSize = 50

CommentsPageSize is the inline comment page on every issue row. Issues with more comments than this set Comments.PageInfo.HasNextPage, which is the contract that makes the truncation visible (see Issue.Comments).

View Source
const Endpoint = "https://api.linear.app/graphql"

Endpoint is the single Linear GraphQL URL. Personal API keys are the only credential shape this client supports.

Variables

View Source
var ErrAuth error = authError{}

ErrAuth is the Linear-named rejected credential. Callers keep using errors.Is(err, linear.ErrAuth). It deliberately does not unwrap to atlhttp.ErrAuth: that sentinel's identity is "Atlassian credential rejected", and importing the Atlassian transport package for a Linear client would couple this package to a host family it never talks to.

Functions

This section is empty.

Types

type Client

type Client struct {
	// APIKey is sent bare in the Authorization header — no "Bearer" prefix.
	// Linear rejects the prefixed form with a 400 that says so outright
	// (measured 2026-08-18), which is why the header shape is pinned by a
	// test.
	APIKey string

	// Endpoint is overridable so tests can point at an httptest server.
	// Production callers get the constant from New.
	Endpoint string

	HTTP *http.Client
	// Retries is the total number of attempts per request; Backoff is the
	// first wait, doubling per attempt and capped at 30 s.
	Retries int
	Backoff time.Duration
	// contains filtered or unexported fields
}

Client talks to one Linear workspace over GraphQL, read-only.

func New

func New(apiKey string) *Client

New builds a Client for a personal API key. The HTTP client times out at 60 s; Retries is 5; the first Backoff is 1 s — the same defaults internal/jira ships.

func (*Client) Issues

func (c *Client) Issues(ctx context.Context, opts IssueOpts, fn func([]Issue) error) error

Issues pages issues oldest-updated-first and calls fn once per page, which is what lets a sync commit page by page — the same contract as jira.Client.Search. Pagination is cursor-based (pageInfo.endCursor); verified live that following the cursor returns exactly the next rows.

func (*Client) LastRateLimit

func (c *Client) LastRateLimit() RateLimit

LastRateLimit returns the headers of the most recent response, or the zero RateLimit before the first call. It is a snapshot for diagnostics — sync status output, `gadak` health — never a gate: a missing header must not fail a request that succeeded.

func (*Client) TakeUsage

func (c *Client) TakeUsage() Usage

TakeUsage returns the counters and zeroes the numeric fields so a flusher can accumulate without double-counting.

LastThrottledAt is a timestamp, not a counter, and is not cleared.

func (*Client) Teams

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

Teams lists every team the credential can see.

func (*Client) Usage

func (c *Client) Usage() Usage

Usage returns the current counters without resetting them.

func (*Client) Viewer

func (c *Client) Viewer(ctx context.Context) (User, error)

Viewer returns the authenticated user. It is the minimal credential check (Jira's /myself equivalent). The result carries personal data (name, email): log neither.

func (*Client) WorkflowStates

func (c *Client) WorkflowStates(ctx context.Context, teamID string) ([]WorkflowState, error)

WorkflowStates returns a team's status catalog. This is the id → type map the status_category mapping needs (MAPPING.md): state names are display text, types are the stable axis.

type Comment

type Comment struct {
	ID        string `json:"id"`
	Body      string `json:"body"`
	CreatedAt string `json:"createdAt"`
	UpdatedAt string `json:"updatedAt"`
	User      *User  `json:"user"`
}

Comment is one issue comment. Body is markdown — not ADF. The Jira path stores body_adf; stuffing markdown into that column would be a false mapping, so Linear comments must stay markdown (body_text / raw), and the rendering surface is a post-connector decision. See MAPPING.md, "comments".

type CommentConn

type CommentConn struct {
	PageInfo PageInfo  `json:"pageInfo"`
	Nodes    []Comment `json:"nodes"`
}

CommentConn is the nested comments connection on an issue.

type Issue

type Issue struct {
	ID            string `json:"id"`
	Identifier    string `json:"identifier"`
	Number        int    `json:"number"`
	Title         string `json:"title"`
	Description   string `json:"description"`
	URL           string `json:"url"`
	CreatedAt     string `json:"createdAt"`
	UpdatedAt     string `json:"updatedAt"`
	ArchivedAt    string `json:"archivedAt"`
	Priority      int    `json:"priority"`
	PriorityLabel string `json:"priorityLabel"`
	DueDate       string `json:"dueDate"`

	State WorkflowState `json:"state"`
	Team  struct {
		ID   string `json:"id"`
		Key  string `json:"key"`
		Name string `json:"name"`
	} `json:"team"`
	Assignee *User      `json:"assignee"`
	Creator  *User      `json:"creator"`
	Labels   LabelConn  `json:"labels"`
	Parent   *ParentRef `json:"parent"`

	// Comments is the first inline page (CommentsPageSize). A page with
	// PageInfo.HasNextPage means the issue has more comments than that and
	// the rest need a follow-up fetch — the flag exists so truncation is
	// observable, never silent.
	Comments CommentConn `json:"comments"`
}

Issue is one row of the issues connection. Timestamps are Linear's verbatim ISO-8601 UTC strings with milliseconds (the format the mirror stores unmodified). ArchivedAt is empty for live issues.

type IssueConnection

type IssueConnection struct {
	PageInfo PageInfo `json:"pageInfo"`
	Nodes    []Issue  `json:"nodes"`
}

IssueConnection and its siblings are the standard Linear connection shape.

type IssueOpts

type IssueOpts struct {
	// TeamID restricts the page to one team (filter.team.id.eq). IDs come
	// from Teams; keys are display-facing and never a filter key.
	TeamID string
	// UpdatedAfter is an ISO-8601 timestamp and becomes filter.updatedAt.gte
	// — the incremental-sync watermark. Verified live: the gte and gt
	// comparators both exist on IssueFilter.updatedAt.
	UpdatedAfter string
	// IncludeArchived asks for archived rows too (archivedAt set), which a
	// reconcile pass needs so an archived issue is not misread as deleted.
	IncludeArchived bool
	// PageSize clamps to [1, maxPageSize]; 0 means defaultPageSize.
	PageSize int
}

IssueOpts scopes an Issues call. All fields are optional; the zero value pages every issue the credential can see, live ones only.

type Label

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

Label is an issue label. Unlike Jira labels (plain strings), Linear labels carry an id; the mirror stores names, so a label renamed upstream changes the stored key the same way a Jira label edit does.

type LabelConn

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

LabelConn is the nested labels connection on an issue.

type PageInfo

type PageInfo struct {
	HasNextPage bool   `json:"hasNextPage"`
	EndCursor   string `json:"endCursor"`
}

PageInfo is the cursor pagination envelope on every connection.

type ParentRef

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

ParentRef is the embedded parent issue. Linear parents are generic sub-issue nesting, not epics; see MAPPING.md, "epic / parent".

type RateLimit

type RateLimit struct {
	// Complexity of the most recent query (x-complexity).
	Complexity int64 `json:"complexity"`
	// ComplexityLimit / ComplexityRemaining are the window's budget and what
	// is left of it; ComplexityResetMS is the epoch-ms reset instant.
	ComplexityLimit     int64 `json:"complexity_limit"`
	ComplexityRemaining int64 `json:"complexity_remaining"`
	ComplexityResetMS   int64 `json:"complexity_reset_ms"`
	// RequestsLimit / RequestsRemaining are the request-count window;
	// RequestsResetMS is the epoch-ms reset instant.
	RequestsLimit     int64 `json:"requests_limit"`
	RequestsRemaining int64 `json:"requests_remaining"`
	RequestsResetMS   int64 `json:"requests_reset_ms"`
	// ObservedAt is when these values were read (UTC).
	ObservedAt time.Time `json:"observed_at"`
}

RateLimit is the budget Linear states in response headers after every call (captured 2026-08-18 on a personal API key):

x-complexity: 1
x-ratelimit-complexity-limit: 3000000
x-ratelimit-complexity-remaining: 2999999
x-ratelimit-complexity-reset: 1787064041508
x-ratelimit-requests-limit: 2500
x-ratelimit-requests-remaining: 2499
x-ratelimit-requests-reset: 1787064041508

Two axes: a request count (2500 per window) and a query-complexity sum (3,000,000 per window), both resetting at the same epoch-millisecond stamp. This is server-claimed state, unlike Usage, which counts what this process sent — a connector that only watches one of them can be surprised by the other.

type Team

type Team struct {
	ID      string `json:"id"`
	Key     string `json:"key"`
	Name    string `json:"name"`
	Private bool   `json:"private"`
}

Team is the scope unit a workspace divides issues by — the closest Linear counterpart to a Jira project (see MAPPING.md, "project_key").

type TeamConnection

type TeamConnection struct {
	PageInfo PageInfo `json:"pageInfo"`
	Nodes    []Team   `json:"nodes"`
}

type Usage

type Usage = httppolicy.Usage

Usage is the host-neutral HTTP usage snapshot (httppolicy.Usage). The same type as atlhttp.Usage / jira.Usage / confluence.Usage, so TakeUsage satisfies sync.usageTaker without a conversion.

type User

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

User is a Linear account reference. Name/DisplayName/Email are personal data: this package never logs them, and callers must not either.

type WorkflowState

type WorkflowState struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Type     string `json:"type"`
	Position int    `json:"position"`
}

WorkflowState is one status in a team's workflow. Type is the stable axis: name is display-only and can be anything ("In Progress", "진행 중", a custom "In Review"); logic must key on Type or ID, never on Name — the same localization hazard Jira's status names carry.

Verified Type values: backlog, unstarted, started, completed, canceled, duplicate (and triage on teams with triage enabled). The set is open: Linear added duplicate after the original six, so code consuming Type must tolerate values it does not know (see MAPPING.md, "status_category").

type WorkflowStateConnection

type WorkflowStateConnection struct {
	PageInfo PageInfo        `json:"pageInfo"`
	Nodes    []WorkflowState `json:"nodes"`
}

Jump to

Keyboard shortcuts

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