jira

package
v0.14.2 Latest Latest
Warning

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

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

Documentation

Overview

Package jira is the Atlassian Cloud REST client: read paths plus user-initiated writes.

The token lives only in the Authorization header. It is never put in an error, a log line or a URL (constitution article 8), which is why transport reports the method and path but never the request itself.

Index

Constants

View Source
const Layout = "2006-01-02T15:04:05.000-0700"

Layout is how Jira stamps timestamps: ISO-8601 with a numeric offset and no colon in it, which is why time.RFC3339 does not parse them.

Variables

View Source
var ErrAuth = atlhttp.Auth("jira")

ErrAuth is the Jira-named rejected credential. It unwraps to atlhttp.ErrAuth so Watch detects it without a per-source branch. Error() keeps the "jira:" prefix so last_error names the source. Callers keep using errors.Is(err, jira.ErrAuth).

Functions

func Category

func Category(key string) string

Category maps Jira's statusCategory key onto the three values data-model.md documents. An unknown key becomes "new", which can only ever miss a reopen, never invent one.

func Doc

func Doc(text string, mentions map[string]string) json.RawMessage

Doc builds the ADF document a comment body has to be. The composer sends plain text with `@Display Name` typed into it plus the account ids it resolved, so this is where those become real mention nodes — a mention that stays plain text notifies nobody, which is the whole point of typing it.

mentions maps display name to account id.

func DocWithMedia

func DocWithMedia(text string, mentions map[string]string, media []Media) json.RawMessage

DocWithMedia is Doc plus inline images, appended after the text — where a screenshot belongs in a comment that describes it.

func ISOTime

func ISOTime(s string) string

ISOTime normalizes a Jira timestamp to the ISO-8601 UTC form every stored column uses (data-model.md, "Conventions"), so string comparison sorts chronologically. An unparseable value passes through untouched.

func PlainText

func PlainText(raw json.RawMessage) string

PlainText flattens an ADF document to plain text: it is what FTS indexes and what makes a repro-steps custom field searchable. A field that holds a bare string (the older wiki-markup shape) passes through unchanged.

Types

type APIError

type APIError struct {
	Status   int
	Messages []string
	Errors   map[string]string
	Body     string
}

APIError is a non-2xx answer with its body parsed. Errors is Jira's per-field rejection map, which the server passes to the client as `jira_errors` so the message lands on the input that caused it. Neither the request nor the credential is ever recorded here (constitution article 8).

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Message

func (e *APIError) Message() string

Message is the first thing worth showing a person: Jira's own message when it sent one, the field errors when it only rejected fields, the raw body last.

type Attachment

type Attachment struct {
	ID       string `json:"id"`
	Filename string `json:"filename"`
	MimeType string `json:"mimeType"`
	Size     int64  `json:"size"`
	Author   User   `json:"author"`
	Created  string `json:"created"`
}

type Changelog

type Changelog struct {
	Total      int       `json:"total"`
	MaxResults int       `json:"maxResults"`
	Histories  []History `json:"histories"`
}

Changelog as returned inline by expand=changelog. Total above len(Histories) means it was truncated and the dedicated endpoint has to be paged.

type Client

type Client struct {
	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 Atlassian Cloud site over REST. The credential is held only as an Authorization header value; it is never copied into an error, a log line, or a URL. Retries and Backoff apply to reads; writes use a narrower policy (see write).

func New

func New(site, email, token string) *Client

New builds a Client for site using Basic auth (email:token). The HTTP client times out at 60s; Retries is 5; the first Backoff is 1s.

func (*Client) AddComment

func (c *Client) AddComment(ctx context.Context, key string, adf json.RawMessage) (Comment, error)

AddComment posts an ADF body (not plain text). Mentions must already be mention nodes — a leftover "@Name" string notifies nobody.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL is the site origin, used to build deep links.

func (*Client) Changelog

func (c *Client) Changelog(ctx context.Context, key string) ([]History, error)

Changelog pages the full history of one issue, for the issues whose inline expand=changelog came back truncated.

func (*Client) Comments

func (c *Client) Comments(ctx context.Context, key string) ([]Comment, error)

Comments pages every comment on one issue, for the issues with more than the inline limit.

func (*Client) Count

func (c *Client) Count(ctx context.Context, jql string) (int, error)

Count returns Jira's approximate issue count for a JQL. It exists only to give progress output a denominator, so a failure is not the caller's problem: callers treat any error as "unknown" and keep going.

func (*Client) CreateIssue

func (c *Client) CreateIssue(ctx context.Context, fields map[string]any) (string, error)

CreateIssue returns the new issue's key.

func (*Client) CreateMeta

func (c *Client) CreateMeta(ctx context.Context, projects []string) ([]CreateMetaProject, error)

CreateMeta lists what can be created. Restricted to the configured projects: the site-wide answer is large and most of it is unreachable from this UI.

func (*Client) EditIssue added in v0.14.1

func (c *Client) EditIssue(ctx context.Context, key string, fields, update map[string]any) error

EditIssue PUTs /issue/{key} with fields and/or update. Either map may be empty; empty maps are omitted so a labels-only edit is {"update":…} only.

func (*Client) EditMeta

func (c *Client) EditMeta(ctx context.Context, key string) (map[string]FieldMeta, error)

EditMeta returns the fields this user may edit on this issue, keyed by field id.

func (*Client) Fields

func (c *Client) Fields(ctx context.Context) ([]FieldInfo, error)

Fields returns every system and custom field the site exposes to this user.

func (*Client) MediaRef

func (c *Client) MediaRef(ctx context.Context, attachmentID string) (mediaID, filename string, err error)

MediaRef resolves an attachment id to both the media UUID Jira needs in an ADF node and the filename our own renderer matches on (`alt`), which is what makes an inline image resolve without persisting the UUID anywhere.

There is no documented endpoint for this. Requesting the attachment's content answers 3xx to a pre-signed media URL that carries the UUID, so the redirect is read rather than followed — following it would download the whole file for a string, and the credential must not travel to the media host.

func (*Client) MyFilters added in v0.13.0

func (c *Client) MyFilters(ctx context.Context) ([]SavedFilter, error)

MyFilters returns filters the user owns, plus visible favourites when includeFavourites is honoured (Cloud's GET /filter/my).

func (*Client) Myself

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

Myself verifies a credential and identifies its owner. It is the only call `PUT credential/` makes before storing a token.

func (*Client) Priorities

func (c *Client) Priorities(ctx context.Context) ([]string, error)

Priorities returns the site's priority names, most urgent first, which is the order priority_rank counts from.

func (*Client) PriorityCatalog

func (c *Client) PriorityCatalog(ctx context.Context) ([]NamedID, error)

PriorityCatalog is the site's priority list, most urgent first. Names are in the account language; writes should send the id.

func (*Client) Projects

func (c *Client) Projects(ctx context.Context, limit int) (list []Project, truncated bool, err error)

Projects lists the projects this credential can browse, newest API first (`/project/search` is the only paged, permission-filtered listing).

It stops after limit projects and reports truncated=true, because a very large site would otherwise turn a first-run picker into hundreds of requests.

func (*Client) Raw

func (c *Client) Raw(ctx context.Context, method, path string, body []byte, mutating bool) (status int, out []byte, err error)

Raw sends a request and returns the HTTP status and response body without JSON decoding. Path must be site-relative (leading "/"); absolute URLs and scheme-relative paths are rejected so the Authorization header never leaves the configured site. mutating selects the write retry policy (429/503 only).

A completed HTTP response always returns err == nil with the status and body (including non-2xx). err is reserved for transport failures and bad paths.

func (*Client) Search

func (c *Client) Search(ctx context.Context, jql string, fields []string, withChangelog bool, fn func([]Issue) error) error

Search pages a JQL query and calls fn once per page, which is what lets sync commit page by page. Pagination is by nextPageToken: the legacy startAt search is deprecated and drifts under concurrent writes.

func (*Client) SearchUsers

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

SearchUsers backs the assignee picker. Jira's own endpoint decides what matches; there is no local user table to search.

func (*Client) SetAssignee

func (c *Client) SetAssignee(ctx context.Context, key, accountID string) error

SetAssignee assigns or, with an empty id, unassigns. Jira distinguishes "no assignee" (null) from "default assignee" (-1); the UI only ever asks for the former.

func (*Client) Statuses

func (c *Client) Statuses(ctx context.Context) (map[string]string, error)

Statuses maps every status id on the site to its category. This is the input the derived-field rules need, because a changelog entry carries ids only.

func (*Client) TakeUsage

func (c *Client) TakeUsage() Usage

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

LastThrottledAt is a timestamp, not a counter: it is included in the snapshot but is NOT cleared. The in-process "last 429" stays visible until the process exits or a later 429 overwrites it.

func (*Client) Transition

func (c *Client) Transition(ctx context.Context, key, transitionID string) error

Transition performs the transition id on key. It uses the write retry policy: only 429 and 503 are retried, because a 500 may mean Jira already acted.

func (*Client) Transitions

func (c *Client) Transitions(ctx context.Context, key string) ([]Transition, error)

Transitions lists the status changes Jira will currently accept on key. Each entry's To carries StatusCategory so callers can key on the stable category; names are localized per account.

func (*Client) UpdateFields

func (c *Client) UpdateFields(ctx context.Context, key string, fields map[string]any) error

UpdateFields sets raw field values. The caller is responsible for the shape each field id expects, which EditMeta describes.

func (*Client) Upload

func (c *Client) Upload(ctx context.Context, key, filename string, file io.Reader) ([]Attachment, error)

Upload attaches one file. Jira requires the nosniff header on this endpoint and answers with the created attachments.

ponytail: buffers the whole file in memory. Fine for the screenshots this is for; stream with io.Pipe if someone starts attaching video.

func (*Client) Usage

func (c *Client) Usage() Usage

Usage returns the current counters without resetting them.

type Comment

type Comment struct {
	ID      string          `json:"id"`
	Author  User            `json:"author"`
	Body    json.RawMessage `json:"body"`
	Created string          `json:"created"`
	Updated string          `json:"updated"`
}

type CommentPage

type CommentPage struct {
	Comments   []Comment `json:"comments"`
	Total      int       `json:"total"`
	MaxResults int       `json:"maxResults"`
	StartAt    int       `json:"startAt"`
}

type CreateMetaProject

type CreateMetaProject struct {
	Key        string    `json:"key"`
	Name       string    `json:"name"`
	IssueTypes []NamedID `json:"issuetypes"`
}

CreateMetaProject is one project a person may file into, with its issue types.

type FieldInfo

type FieldInfo struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Custom bool   `json:"custom"`
	Schema struct {
		Type   string `json:"type"`
		Custom string `json:"custom"`
		Items  string `json:"items"`
	} `json:"schema"`
}

FieldInfo is one row from GET /rest/api/3/field — the site-wide field catalog. Distinct from FieldMeta (editmeta for one issue); do not reuse that type here.

type FieldMeta

type FieldMeta struct {
	Required   bool     `json:"required"`
	Operations []string `json:"operations"`
	Schema     struct {
		Type   string `json:"type"`
		Items  string `json:"items"`
		Custom string `json:"custom"`
	} `json:"schema"`
	AllowedValues []struct {
		ID    string `json:"id"`
		Value string `json:"value"`
		Name  string `json:"name"`
	} `json:"allowedValues"`
}

FieldMeta is one editable field as Jira describes it: what it accepts and, for a closed set, every value it accepts.

type Fields

type Fields struct {
	Summary     string          `json:"summary"`
	Description json.RawMessage `json:"description"`
	Environment json.RawMessage `json:"environment"`
	IssueType   NamedID         `json:"issuetype"`
	Status      Status          `json:"status"`
	Priority    *NamedID        `json:"priority"`
	Assignee    *User           `json:"assignee"`
	Reporter    *User           `json:"reporter"`
	Creator     *User           `json:"creator"`
	Project     struct {
		Key string `json:"key"`
	} `json:"project"`
	Parent *struct {
		Key string `json:"key"`
	} `json:"parent"`
	Labels      []string     `json:"labels"`
	Components  []NamedID    `json:"components"`
	FixVersions []NamedID    `json:"fixVersions"`
	Versions    []NamedID    `json:"versions"` // affects versions
	Duedate     string       `json:"duedate"`
	Resolution  *NamedID     `json:"resolution"`
	Created     string       `json:"created"`
	Updated     string       `json:"updated"`
	Comment     CommentPage  `json:"comment"`
	Attachment  []Attachment `json:"attachment"`
	IssueLinks  []IssueLink  `json:"issuelinks"`
}

type History

type History struct {
	ID      string        `json:"id"`
	Created string        `json:"created"`
	Author  User          `json:"author"`
	Items   []HistoryItem `json:"items"`
}

type HistoryItem

type HistoryItem struct {
	Field      string `json:"field"`
	FieldID    string `json:"fieldId"`
	From       string `json:"from"`
	FromString string `json:"fromString"`
	To         string `json:"to"`
	ToString   string `json:"toString"`
}

HistoryItem's FieldID is the stable identifier ("status", "assignee"); Field is the display name and is localized.

type Issue

type Issue struct {
	ID        string
	Key       string
	Fields    Fields
	Extra     map[string]json.RawMessage
	Raw       json.RawMessage
	Changelog *Changelog
}

Issue keeps the fields object three ways: typed for the mapping, verbatim per field id so configured custom fields need no code, and whole as Raw.

func (*Issue) UnmarshalJSON

func (i *Issue) UnmarshalJSON(b []byte) error
type IssueLink struct {
	Type struct {
		Name string `json:"name"`
	} `json:"type"`
	InwardIssue *struct {
		Key string `json:"key"`
	} `json:"inwardIssue"`
	OutwardIssue *struct {
		Key string `json:"key"`
	} `json:"outwardIssue"`
}

type Media

type Media struct {
	ID       string
	Filename string
}

Media is one inline image in a comment: the Jira media UUID (not the attachment id — see Client.MediaRef) plus the filename, which is carried as `alt` so our own renderer can match the node to the attachment without persisting the UUID anywhere (web/src/lib/adf.ts, findAttachment).

type NamedID

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

type Project

type Project struct {
	Key     string `json:"key"`
	Name    string `json:"name"`
	TypeKey string `json:"projectTypeKey"`
}

Project is one row of the site's project list, as the onboarding picker needs it: the key sync will use, a name to recognise it by, and Jira's own type slug.

type SavedFilter added in v0.13.0

type SavedFilter struct {
	ID        string
	Name      string
	JQL       string
	Favourite bool
	Owner     string
}

SavedFilter is a Jira filter the account owns or has starred.

type Status

type Status struct {
	ID             string `json:"id"`
	Name           string `json:"name"`
	StatusCategory struct {
		Key string `json:"key"`
	} `json:"statusCategory"`
}

Status carries the category because every piece of logic keys on it: names come back in the account's display language (contracts/sync.md, "Localization hazard").

type Transition

type Transition struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	To   Status `json:"to"`
}

Transition is one available status change, with the target's category so the UI can colour it without knowing the site's status names.

type Usage

type Usage = atlhttp.Usage

Usage is a point-in-time snapshot of this client's outbound Jira traffic. Counters are process-local until a caller persists them (see store.api_usage).

Requests counts every HTTP attempt, including retries: that is the unit that draws from Jira's rate budget. This is our own call volume, not Jira's remaining point pool — the site does not expose that.

type User

type User struct {
	AccountID   string `json:"accountId"`
	DisplayName string `json:"displayName"`
	Email       string `json:"emailAddress"`
	// The two below are only ever filled by the user search the assignee picker
	// calls; the mirror stores neither.
	AvatarURLs map[string]string `json:"avatarUrls"`
	Active     bool              `json:"active"`
}

func (User) Avatar

func (u User) Avatar() string

Avatar is the 48px avatar, or empty when Jira sent none.

Jump to

Keyboard shortcuts

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