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
- Variables
- func LooksLikeID(s string) bool
- func StatusCategory(stateType string) (cat string, ok bool)
- type Attachment
- type AttachmentConn
- type Client
- func (c *Client) CompleteAttachments(ctx context.Context, iss *Issue) error
- func (c *Client) CompleteComments(ctx context.Context, iss *Issue) error
- func (c *Client) CompleteLabels(ctx context.Context, iss *Issue) error
- func (c *Client) CreateAttachment(ctx context.Context, issueID, url, title string) (Attachment, error)
- func (c *Client) CreateComment(ctx context.Context, issueID string, body string) (Comment, error)
- func (c *Client) CreateIssue(ctx context.Context, in IssueCreate) (Issue, error)
- func (c *Client) Issue(ctx context.Context, idOrIdentifier string) (Issue, error)
- func (c *Client) Issues(ctx context.Context, opts IssueOpts, fn func([]Issue) error) error
- func (c *Client) LastRateLimit() RateLimit
- func (c *Client) TakeUsage() Usage
- func (c *Client) Teams(ctx context.Context) ([]Team, error)
- func (c *Client) UpdateIssue(ctx context.Context, id string, in IssueUpdate) (Issue, error)
- func (c *Client) UploadFile(ctx context.Context, filename, contentType string, size int) (UploadTarget, error)
- func (c *Client) Usage() Usage
- func (c *Client) Users(ctx context.Context, query string) ([]User, error)
- func (c *Client) Viewer(ctx context.Context) (User, error)
- func (c *Client) WorkflowStates(ctx context.Context, teamID string) ([]WorkflowState, error)
- type Comment
- type CommentConn
- type Issue
- type IssueAttachment
- type IssueConnection
- type IssueCreate
- type IssueOpts
- type IssueUpdate
- type Label
- type LabelConn
- type PageInfo
- type ParentRef
- type RateLimit
- type Team
- type TeamConnection
- type UploadTarget
- type Usage
- type User
- type WorkflowState
- type WorkflowStateConnection
Constants ¶
const AttachmentsPageSize = 50
AttachmentsPageSize is the inline attachment page on every issue row.
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).
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.
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 침묵 절단").
Variables ¶
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 LooksLikeID ¶ added in v0.17.0
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 StatusCategory ¶ added in v0.17.0
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 ¶
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
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
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
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
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) CreateIssue ¶ added in v0.16.1
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) Issue ¶ added in v0.16.1
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 ¶
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 ¶
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 ¶
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) UpdateIssue ¶ added in v0.16.1
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) Users ¶ added in v0.16.1
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 ¶
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 ¶
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 ¶
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. 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"`
}
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 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 ¶
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
}
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 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
}
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 ¶
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 ¶
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 ParentRef ¶
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 UploadTarget ¶ added in v0.16.1
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"`
}