linear

package
v0.22.1 Latest Latest
Warning

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

Go to latest
Published: Sep 14, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package linear is the GraphQL client for a Linear workspace: viewer, teams, workflow states, cursor-paged issues with an updatedAt watermark filter, and — since GDK-360 — the three write verbs (create issue, update issue, comment) the origin.Writer adapter routes through. The constitution article is unchanged: writes pass through the origin, and these methods are that origin path for Linear; the mirror itself is still never written directly.

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 AttachmentsPageSize = 50

AttachmentsPageSize is the inline attachment page on every issue row.

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.

View Source
const LabelsPageSize = 50

LabelsPageSize is the inline label page on every issue row. Without an explicit first + pageInfo the connection was silently truncated at the server default; HasNextPage makes the cut observable, same contract as comments (GDK-263 audit, "labels 침묵 절단").

View Source
const RelationsPageSize = 50

RelationsPageSize is the inline page on Issue.relations and Issue.inverseRelations (GDK-1299). Same contract as the three above: HasNextPage marks the cut and the sync pass counts it. No follow-up fetch yet — an issue related to more than fifty others is a shape none of the measured workspaces have, and the count will say when one does.

View Source
const UploadsHost = "uploads.linear.app"

UploadsHost is the only Linear storage hostname allowed to receive the API key on a download GET. Subdomains and port variants are not this host; http is not this scheme.

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

func CycleState added in v0.22.0

func CycleState(startsAt, endsAt, completedAt string, now time.Time) string

CycleState is the sprint_state of a cycle at instant now — the single owner of the date rule, used by the issue projection and the sprints listing alike so the two agree by construction. A cycle has no state field on the wire: completedAt set, or endsAt at or before now, means closed; else startsAt at or before now means active; else future. Unparseable stamps read as absent — the same honest-absence rule MAPPING.md applies everywhere, never a guess that flips a state.

func Download added in v0.22.0

func Download(ctx context.Context, target, apiKey string) (io.ReadCloser, string, error)

Download opens an attachment's bytes from Linear's storage host. The caller closes the body. A target that is not an uploads.linear.app https URL is refused before any request leaves the process.

func IsUploadsURL added in v0.22.0

func IsUploadsURL(target string) bool

IsUploadsURL reports whether target is an https URL whose host is exactly uploads.linear.app. Linear documents that downloads from this host accept the API key in Authorization. That is a different path from the upload PUT to a signed URL, which must not carry the key (write.go UploadFile).

A stored URL is data the mirror copied from the origin, so it is never fetched unless it passes this (GDK-560: SSRF).

func LooksLikeID added in v0.17.0

func LooksLikeID(s string) bool

LooksLikeID reports whether s is a Linear UUID (the shape issues.assignee_id stores). Assign of a mirror id must reach Users() as id.eq, not only as displayName/email contains.

func SprintID added in v0.22.0

func SprintID(uuid string) int64

SprintID derives the integer the mirror stores for a Linear cycle (and for the one-board-per-team boards row, from the team UUID): FNV-1a 64 of the UUID string, top bit cleared so it fits SQLite's signed INTEGER, 0 mapped to 1 so the id is never zero (0 would read as "no cycle"). One owner by design — sync builds the sprints/boards rows and the issue's sprint_id from it, and the write adapter resolves a sprint id back to a cycle by the same derive, so the two directions cannot drift. The UUID itself stays in sprints.external_id; the mirror never stores origin data it cannot rebuild, and the derived integer is presentation of the id, not a second identity. A collision with another UUID's derive is a 63-bit hash clash and is not defended against — a workspace would need ~3×10⁹ cycles.

func StatusCategory added in v0.17.0

func StatusCategory(stateType string) (cat string, ok bool)

StatusCategory maps a WorkflowState.type onto gadak's status_category contract (new | inprogress | done). The enum is open — Linear added "duplicate" after the original six — so ok reports whether the type was known; unknown collapses to new (an issue can only be misread as open, never as silently done). Never key on state names: they are display text ("진행 중").

This is the single owner of the Linear type→category collapse (GDK-665). The read path (internal/sync) and the write path (origin.linearWriter) both call it. The write path then maps the gadak token through statuscat.CategoryKey onto the Jira REST statusCategory key that origin.Transition.To carries, because that is the shape jira.Client unmarshals and the HTTP handler already runs through statuscat.Category.

Types

type Attachment added in v0.16.1

type Attachment struct {
	ID    string `json:"id"`
	Title string `json:"title"`
	URL   string `json:"url"`
}

Attachment is the record attachmentCreate returns. Linear attachments are URL-first: a file becomes an attachment only after it is uploaded to the workspace's storage and its assetUrl is attached.

type AttachmentConn added in v0.16.1

type AttachmentConn struct {
	PageInfo PageInfo          `json:"pageInfo"`
	Nodes    []IssueAttachment `json:"nodes"`
}

AttachmentConn is the nested attachments connection on an issue. PageInfo.HasNextPage means more than AttachmentsPageSize — truncation is observable. CompleteAttachments follows the cursor.

type Client

type Client struct {

	// 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.

func New

func New(apiKey string) *Client

New builds a Client for a personal API key. HTTP timeout, Retries, and Backoff come from httppolicy (same owner as jira and confluence New).

func (*Client) CompleteAttachments added in v0.17.0

func (c *Client) CompleteAttachments(ctx context.Context, iss *Issue) error

CompleteAttachments follows Issue.Attachments.PageInfo the same way CompleteComments follows comments. Truncation stays observable if the cap is hit.

func (*Client) CompleteComments added in v0.16.1

func (c *Client) CompleteComments(ctx context.Context, iss *Issue) error

CompleteComments follows Issue.Comments.PageInfo until HasNextPage is false (or the follow-up cap). The inline page is kept; later nodes are appended. A no-op when there is no next page. Callers replace the child list from the completed Nodes slice.

func (*Client) CompleteLabels added in v0.17.0

func (c *Client) CompleteLabels(ctx context.Context, iss *Issue) error

CompleteLabels follows Issue.Labels.PageInfo the same way CompleteComments follows comments. Truncation stays observable if the cap is hit.

func (*Client) CreateAttachment added in v0.16.1

func (c *Client) CreateAttachment(ctx context.Context, issueID, url, title string) (Attachment, error)

CreateAttachment attaches an already-uploaded (or external) URL to an issue. For files, the url is the assetUrl UploadFile returned after the caller's PUT succeeded.

func (*Client) CreateComment added in v0.16.1

func (c *Client) CreateComment(ctx context.Context, issueID string, body string) (Comment, error)

CreateComment posts one comment; body is markdown, the same format Comment.body carries on the read path (never ADF — see MAPPING.md "comments"). Whether an empty body is acceptable is Linear's call; the client does not invent a rule for it.

func (*Client) CreateCommentAt added in v0.20.0

func (c *Client) CreateCommentAt(ctx context.Context, issueID, body, createdAt string) (Comment, error)

CreateCommentAt is CreateComment with a backdated createdAt (ISO-8601 UTC) — the import case. Empty createdAt omits the field.

func (*Client) CreateCycle added in v0.22.0

func (c *Client) CreateCycle(ctx context.Context, in CycleCreate) (Cycle, error)

CreateCycle files one cycle on a team and returns it with the full listing field set, so the caller can commit the sprints row without a refetch.

func (*Client) CreateIssue added in v0.16.1

func (c *Client) CreateIssue(ctx context.Context, in IssueCreate) (Issue, error)

CreateIssue files one issue and returns it with the full read-path field set, so the mirror can commit the row without a refetch.

func (*Client) CreateLabel added in v0.20.0

func (c *Client) CreateLabel(ctx context.Context, teamID, name string) (Label, error)

CreateLabel creates a team-scoped label.

func (*Client) CreateRelation added in v0.20.0

func (c *Client) CreateRelation(ctx context.Context, issueID, relatedID, typ string) error

CreateRelation links two issues: issueID <typ> relatedID (for "blocks", issueID blocks relatedID). Linear materializes the inverse itself.

func (*Client) DeleteComment added in v0.22.0

func (c *Client) DeleteComment(ctx context.Context, id string) error

DeleteComment removes one comment by id (GDK-1647).

func (*Client) Issue added in v0.16.1

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

Issue fetches one issue by UUID or human identifier ("MID-5") — issue(id:) accepts both. The write adapter resolves user-typed keys through this.

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) Labels added in v0.20.0

func (c *Client) Labels(ctx context.Context) ([]Label, error)

Labels lists every label the credential can see — workspace-level ones (team null) and team ones alike, since both apply to an issue and a team label cannot reuse a workspace label's name.

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) TeamCycles added in v0.22.0

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

TeamCycles lists one team's cycles — the sprint listing of the Linear sprint mapping (GDK-1667). An unknown or invisible team answers null and reads as zero cycles: a scoped workspace naming a team the credential cannot see mirrors no boards for it, the same honest absence the issue pass produces.

func (*Client) Teams

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

Teams lists every team the credential can see.

func (*Client) UpdateComment added in v0.22.0

func (c *Client) UpdateComment(ctx context.Context, id, body string) (Comment, error)

UpdateComment edits one comment by id and returns it with the full read-path field set; body is markdown, the same format Comment.body carries (never ADF — the CreateComment rule, and an edit sends what a post sends). id is the comment UUID, not an issue key (GDK-1647).

func (*Client) UpdateCycle added in v0.22.0

func (c *Client) UpdateCycle(ctx context.Context, id string, in CycleUpdate) (Cycle, error)

UpdateCycle patches one cycle by id and returns it with the full listing field set. Dates arrive as the ISO-8601 stamps the listing carries.

func (*Client) UpdateIssue added in v0.16.1

func (c *Client) UpdateIssue(ctx context.Context, id string, in IssueUpdate) (Issue, error)

UpdateIssue patches one issue by id and returns the updated issue with the full read-path field set. An all-nil IssueUpdate is a legal wire no-op — the input travels as {} and Linear answers success with the unchanged issue, the same tolerance jira's EditIssue gives empty maps.

func (*Client) UploadFile added in v0.16.1

func (c *Client) UploadFile(ctx context.Context, filename, contentType string, size int) (UploadTarget, error)

UploadFile reserves storage for one file and returns where to PUT it. The PUT itself is the caller's (it goes to storage, not the GraphQL endpoint, and must not carry the API key).

func (*Client) Usage

func (c *Client) Usage() Usage

Usage returns the current counters without resetting them.

func (*Client) Users added in v0.16.1

func (c *Client) Users(ctx context.Context, query string) ([]User, error)

Users searches workspace members by name for assignee pickers. An empty query lists the first page unfiltered. A UUID query also matches id.eq so `gadak assign KEY <issues.assignee_id>` reaches the same user the hint names.

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 Cycle added in v0.22.0

type Cycle struct {
	ID          string `json:"id"`
	Number      int    `json:"number"`
	Name        string `json:"name"`
	Description string `json:"description"`
	StartsAt    string `json:"startsAt"`
	EndsAt      string `json:"endsAt"`
	CompletedAt string `json:"completedAt"`
}

Cycle is a team's date-bounded iteration — the Linear object gadak maps onto sprints (GDK-1667). A cycle has no state field of its own: it is future until startsAt, active between startsAt and endsAt, and closed once completedAt is set or endsAt has passed (CycleState is that rule's single owner). Timestamps are Linear's verbatim ISO-8601 UTC strings, the same format Issue's stamps carry.

type CycleConn added in v0.22.0

type CycleConn struct {
	PageInfo PageInfo `json:"pageInfo"`
	Nodes    []Cycle  `json:"nodes"`
}

CycleConn is the cycles connection on a team.

type CycleCreate added in v0.22.0

type CycleCreate struct {
	TeamID      string
	Name        string
	Description string
	StartsAt    string
	EndsAt      string
}

CycleCreate is the input to CreateCycle — the sprint-create verb's Linear shape (GDK-1667). TeamID, Name, StartsAt and EndsAt are always sent: a Linear cycle is its date window, and the origin's own UI never files one without both ends.

type CycleRef added in v0.22.0

type CycleRef struct {
	ID          string `json:"id"`
	Number      int    `json:"number"`
	Name        string `json:"name"`
	StartsAt    string `json:"startsAt"`
	EndsAt      string `json:"endsAt"`
	CompletedAt string `json:"completedAt"`
}

CycleRef is the cycle embedded on an issue — the fields a membership needs. The listing and mutation payloads answer the fuller Cycle.

type CycleUpdate added in v0.22.0

type CycleUpdate struct {
	Name        *string
	Description *string
	StartsAt    *string
	EndsAt      *string
}

CycleUpdate is the patch for UpdateCycle. Nil fields are omitted — unchanged; the three-way distinction is the same one IssueUpdate encodes, minus the fields a cycle does not have (no state: see mutCycleUpdate).

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"`
	StartedAt     string `json:"startedAt"`
	CompletedAt   string `json:"completedAt"`
	CanceledAt    string `json:"canceledAt"`
	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"`
	// Cycle is the cycle the issue sits in; nil is the backlog (GDK-1667,
	// MAPPING.md "cycle → sprint"). CycleRef, not Cycle: membership does not
	// need the description.
	Cycle *CycleRef `json:"cycle"`

	// 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. CompleteComments follows the cursor.
	Comments CommentConn `json:"comments"`

	// Attachments is the first inline page (AttachmentsPageSize).
	// CompleteAttachments follows HasNextPage the same way comments do.
	Attachments AttachmentConn `json:"attachments"`

	// Relations are the relations this issue owns — it is `issue`, the
	// other side is `relatedIssue` — and InverseRelations the ones another
	// issue owns that point here. The mirror stores the first as outward
	// links and the second as inward, the way a Jira issuelinks payload
	// splits into outwardIssue / inwardIssue (GDK-1299). First inline page
	// each (RelationsPageSize); HasNextPage is counted by the sync pass so
	// truncation is observable, never silent.
	Relations        RelationConn `json:"relations"`
	InverseRelations RelationConn `json:"inverseRelations"`
}

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. StartedAt / CompletedAt / CanceledAt are empty when Linear has them null — they feed the flow-column hints (started_at, resolved_at), not stored columns of their own.

type IssueAttachment added in v0.16.1

type IssueAttachment struct {
	ID        string         `json:"id"`
	Title     string         `json:"title"`
	URL       string         `json:"url"`
	CreatedAt string         `json:"createdAt"`
	Metadata  map[string]any `json:"metadata"`
}

IssueAttachment is one node of Issue.attachments. Title is the filename; URL is the origin content URL (store it verbatim — the proxy must not reconstruct a Jira path). Metadata may carry size/mimeType; those fields are not first-class on the Linear attachment type (MAPPING.md).

type IssueConnection

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

IssueConnection and its siblings are the standard Linear connection shape.

type IssueCreate added in v0.16.1

type IssueCreate struct {
	// TeamID and Title are the required fields of IssueCreateInput; the
	// client checks them first so the error names the field instead of the
	// server's generic rejection.
	TeamID string
	Title  string
	// Description is markdown, the same format Issue.description carries.
	Description string
	// StateID is a workflow-state UUID from WorkflowStates.
	StateID string
	// Priority is Linear's 0-4 scale (0 = No priority … 4 = Low; MAPPING.md
	// "priority"). Nil omits the field.
	Priority *int
	// AssigneeID is a user UUID; empty omits (a create has nothing to clear).
	AssigneeID string
	// LabelIDs becomes the issue's whole label set; empty omits.
	LabelIDs []string
	// DueDate is "YYYY-MM-DD" exactly; empty omits.
	DueDate string
	// ParentID nests the new issue under an existing one (sub-issue);
	// empty omits.
	ParentID string
	// CreatedAt backdates the issue (ISO-8601 UTC) — the import case, where
	// the source's timestamp is the truth. Empty omits and Linear stamps
	// now. Whether the server honors it is the caller's to verify against
	// the returned Issue.CreatedAt.
	CreatedAt string
}

IssueCreate is the input to CreateIssue. Zero-value strings and slices are omitted from the wire input so Linear applies its own defaults.

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 IssueRelation added in v0.20.2

type IssueRelation struct {
	ID           string    `json:"id"`
	Type         string    `json:"type"`
	Issue        ParentRef `json:"issue"`
	RelatedIssue ParentRef `json:"relatedIssue"`
}

IssueRelation is one IssueRelation node. Type is the IssueRelationType enum — blocks, duplicate, related, similar (introspected live 2026-09-06). `issue` owns the relation; for `blocks` it is the blocker, for `duplicate` the issue Linear moved into its Duplicate state.

type IssueUpdate added in v0.16.1

type IssueUpdate struct {
	Title       *string
	Description *string
	// StateID is a workflow-state UUID from WorkflowStates.
	StateID *string
	// Priority is Linear's 0-4 scale (MAPPING.md "priority").
	Priority *int
	// AssigneeID: a pointer to a user UUID assigns; a pointer to the empty
	// string sends assigneeId: null, which the documented schema defines as
	// clearing (unassigning); nil leaves the assignee unchanged.
	AssigneeID *string
	// LabelIDs: a pointer to a slice replaces the whole label set (an empty
	// slice clears labels); nil leaves them unchanged.
	LabelIDs *[]string
	// DueDate: a pointer to "YYYY-MM-DD" sets; nil is unchanged. Clearing a
	// due date (explicit null) is not expressible in this shape — extend it
	// with the AssigneeID convention if the adapter ever needs unsetting.
	DueDate *string
	// ParentID: a pointer to an issue UUID nests under it; nil is unchanged.
	ParentID *string
	// CycleID: a pointer to a cycle UUID moves the issue into that cycle
	// (the sprint add verb); nil leaves the cycle unchanged. A pointer to
	// the empty string is an error, not a clear — clearing is ClearCycle, so
	// an accidental "" cannot silently send cycleId: null and drop the issue
	// to the backlog.
	CycleID *string
	// ClearCycle sends cycleId: null — the explicit un-membership the sprint
	// remove verb wants (backlog = no cycle). Mutually exclusive with
	// CycleID; the client rejects the combination before the wire.
	ClearCycle bool
}

IssueUpdate is the patch for UpdateIssue. A nil field is omitted from the wire input — unchanged. Set fields are sent as values; the one special case is AssigneeID, where the empty string means the explicit null that unassigns.

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 {
	PageInfo PageInfo `json:"pageInfo"`
	Nodes    []Label  `json:"nodes"`
}

LabelConn is the nested labels connection on an issue. PageInfo.HasNextPage set means the issue has more labels than LabelsPageSize — truncation is observable, never silent (same contract as CommentConn). CompleteLabels follows the cursor.

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 RelationConn added in v0.20.2

type RelationConn struct {
	PageInfo PageInfo        `json:"pageInfo"`
	Nodes    []IssueRelation `json:"nodes"`
}

RelationConn is the connection shape of Issue.relations and Issue.inverseRelations.

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 UploadTarget added in v0.16.1

type UploadTarget struct {
	UploadURL string
	AssetURL  string
	Headers   map[string]string
}

UploadTarget is what fileUpload hands back: a signed PUT destination and the stable assetUrl the uploaded bytes will live at. Headers must be sent verbatim on the PUT (measured 2026-08-20: the signed URL rejects the write without them).

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