# go-jira: a Go client for Atlassian Jira that is mid-way through a v2 rewrite

> go-jira is a Go client library for Atlassian Jira that covers issue retrieval and creation, transitions, several authentication schemes, and a fallback path for any endpoint it does not implement directly. The important thing to know before you depend on it is that the main branch holds an unstable v2 with breaking changes, while the last stable release is v1.16.0 from 2022.

**andygrunwald/go-jira** — Go client library for Atlassian Jira

- Repository: https://github.com/andygrunwald/go-jira
- Website: https://pkg.go.dev/github.com/andygrunwald/go-jira?tab=doc
- Stars: 1,621 · Forks: 502
- Language: Go
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/andygrunwald-go-jira

## The main branch is not the version you probably want

The most important paragraph in the README is the warning at the top, and it is easy to skim past. v2 of the library is in development, v2 will contain breaking changes, and the current main branch contains the development version of v2. The project treats that version as unstable and says breaking changes are expected. If you want to stay more stable, use v1.x, and the latest stable release named in the documentation is v1.16.0. The stated goals for v2 are idiomatic Go usage, proper documentation, compliance with different kinds of Atlassian Jira products, on-premise against cloud, and removing flaws introduced in the early days of the library. There is a milestone for the road to v2 and an issue number 489 for feedback on it. The practical consequence for an adopter is that the module path in the repository's go.mod ends in /v2 while the stable line is the unversioned path, and the two are not interchangeable in an import statement. The repository also carries a MIGRATE.md file at the root, which is where the differences between the lines are written down.

## Getting it and building it

Installation is the standard go get:

```sh
go get github.com/andygrunwald/go-jira
```

The requirements are Go 1.14 or newer and Jira v6.3.4 and v7.1.2, with a note that the tests are also run against 1.13 even though only the last two versions are officially supported. The module file itself is more demanding than that, declaring Go 1.21 with a toolchain directive of go1.23, which is what you would expect from a v2 line that has been modernised. The Makefile is the usual quality gate set, with a test target that runs everything with the race detector and the verbose flag, and coverage targets that write a coverage profile and then open it in a browser through the tool cover command. There is a vet target, an fmt target that runs gofmt in diff and simplify mode, and a staticcheck target that installs the linter at its latest version before running it. An all target runs the four together, and a docs-serve target starts the mkdocs development server, which matters because the documentation site is built from the doc directory with a mkdocs.yml at the root. The module file itself is more demanding than the README requirements, declaring a Go version and a toolchain directive:

```go
module github.com/andygrunwald/go-jira/v2

go 1.21

toolchain go1.23
```

## Retrieving an issue is three lines of setup

The canonical example fetches a single issue from a public Jira instance, and it shows the shape of the API: a client is created with a base URL, the Get method takes an issue key and an optional query, and the result is a value plus two error returns that the example discards.

```go
package main

import (
	"fmt"
	jira "github.com/andygrunwald/go-jira"
)

func main() {
	jiraClient, _ := jira.NewClient(nil, "https://issues.apache.org/jira/")
	issue, _, _ := jiraClient.Issue.Get("MESOS-3325", nil)

	fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
	fmt.Printf("Type: %s\n", issue.Fields.Type.Name)
	fmt.Printf("Priority: %s\n", issue.Fields.Priority.Name)

	// MESOS-3325: Running mesos-slave@0.23 in a container causes slave to be lost after a restart
	// Type: Bug
	// Priority: Critical
}
```

The NewClient call takes an http.Client as its first argument, and passing nil is the anonymous unauthenticated case, which is what the public Apache Mesos instance needs. The response is structured rather than a generic map, with the summary, type and priority reached through the Fields member, and the type and priority themselves being nested objects with a Name field, so a type change in Jira shows up as a field you have to read rather than a string you compare.

## Authentication is mostly somebody else's job

The design decision here is worth stating plainly, because it is unusual for a client library to hand this much to the caller. The library does not handle most authentication directly. Instead, authentication should be handled within an http.Client, and that client is then passed into NewClient. Basic and cookie based authentication are included in the main library for convenience, and the rest are composed by the caller. For Atlassian Cloud, token based authentication uses the basic authentication scheme with a user generated API token in place of the password, and the library provides a transport type for it:

```go
func main() {
	tp := jira.BasicAuthTransport{
		Username: "<username>",
		Password: "<api-token>",
	}

	client, err := jira.NewClient(tp.Client(), "https://my.jira.com")

	u, _, err = client.User.GetCurrentUser(context.Background())

	fmt.Printf("Email: %v\n", u.EmailAddress)
	fmt.Println("Success!")
}
```

The transport produces the http.Client you pass in, so the shape is the same whichever scheme you use, which is what makes swapping between a Cloud token and a self-hosted credential a local change.

## Cloud tokens and self-hosted PATs are different schemes

The two token mechanisms are not interchangeable, and confusing them is the most common authentication mistake. A personal access token is for self-hosted Jira, v8.14 and later, and it uses the Bearer authentication scheme, with an example kept in the onpremise directory of the repository. An Atlassian Cloud API token uses basic authentication with the token in place of the password, and a runnable example for that lives in the cloud directory. Password based basic authentication works for self-hosted Jira only, and it has been deprecated for users of Atlassian Cloud. Depending on the Jira version, either token example can be used with a user's password substituted for the generated token. OAuth is not built in at all, and the project points at a third party example written by a contributor for connecting to a Jira Cloud instance, and at issue number 56 for discussion. The tree makes the split visible, with cloud and onpremise directories at the root, each holding its own examples.

## Creating an issue means building a struct of pointers

Creation works the other way round, with the issue assembled as a value and then posted. The structure is nested and mostly pointer typed, which is a direct reflection of how Jira's payload nests, and it means a field you do not set stays nil rather than being sent as an empty value:

```go
package main

import (
	"fmt"
	"github.com/andygrunwald/go-jira"
)

func main() {
	base := "https://my.jira.com"
	tp := jira.BasicAuthTransport{
		Username: "username",
		Password: "token",
	}

	jiraClient, err := jira.NewClient(tp.Client(), base)
	if err != nil {
		panic(err)
	}

	i := jira.Issue{
		Fields: &jira.IssueFields{
			Assignee: &jira.User{
				Name: "myuser",
			},
			Reporter: &jira.User{
				Name: "youruser",
			},
			Description: "Test Issue",
			Type: jira.IssueType{
				Name: "Bug",
			},
			Project: jira.Project{
				Key: "PROJ1",
			},
			Summary: "Just a demo issue",
		},
	}
```

The assignee and reporter are jira.User values carrying a name, the type is a jira.IssueType naming an issue type, and the project is a jira.Project with a key. The fields struct is a pointer, so a partially filled issue is still explicit about which half you are populating.

## Not API complete, but every endpoint is reachable

The feature list makes a claim that is easy to miss and is the reason to keep the library even when it lacks a typed method. The package is not Jira API complete yet, but you can call every API endpoint you want. The mechanism is a generic request path for endpoints the library does not implement directly, and the README links to the section explaining how to use it and to the latest Jira REST API documentation for the list of what exists. That is a deliberate division of labour. The endpoints a program actually uses, issues and transitions, are modelled with real structs and real methods, so the common case is type safe and readable. The long tail, which for Jira is very long, goes through a generic call that returns the raw response, and you write the decoding for that endpoint yourself. The API reference points to the GoDoc page for the detailed description, and it says the latest Jira REST API documentation was the base document for the package, which tells you the library is modelled on the documented surface rather than on an observed one.

## What the repository tells you about the project itself

The tree is a good map of the project. Besides the cloud and onpremise example directories there is a docs directory with an mkdocs.yml at the root for the documentation site, a testing directory, a CHANGELOG, a Makefile, a renovate.json for dependency updates and a LICENSE. The module dependencies are few and reveal what the library is actually doing. github.com/google/go-querystring encodes struct fields into query parameters, which is how the optional query arguments on the typed methods are passed, and github.com/fatih/structs reads and writes struct fields generically, which is what makes the generic call path able to send a struct as a request body. github.com/trivago/tgo is a Telegram client, which is unusual in a Jira library and points at a command notification feature somewhere in the tree, github.com/golang-jwt/jwt/v4 handles tokens, and github.com/google/go-cmp is the comparison library the tests use. The licence field on the repository says MIT. The last push was on 2026-10-16, with v1.17.0 released in September 2025 and v1.16.1 a few days earlier, while v1.16.0 itself dates from 2022.

## Conclusion

go-jira suits a Go service that needs to read and write Jira issues, especially one that needs a token scheme specific to its Jira deployment, either the Cloud API token or the self-hosted personal access token. It does not suit a team that needs the whole Jira REST surface typed out, because the library is not API complete and leans on a generic call path for anything it does not model. Before you depend on it, decide which line you are on, since the main branch is the v2 development version with expected breaking changes while the latest stable is v1.16.0, and check the MIGRATE.md file in the repository root if you are moving between them.

## FAQ

### Is the main branch of go-jira stable?

No. The README states that the current main branch contains the development version of v2, that it is treated as unstable and that breaking changes are expected. If you want stability, use v1.x, and the latest stable release named in the documentation is v1.16.0.

### How do I authenticate to Jira Cloud from Go?

Token based authentication uses the basic authentication scheme with a user generated API token in place of the password. You build a jira.BasicAuthTransport with the username and token, pass its Client method result to jira.NewClient, and the library includes a runnable example in the cloud directory.

### What happens if go-jira does not implement the endpoint I need?

You can still call it. The package is not Jira API complete, but it offers a way to call any API endpoint even when it is not directly implemented, and the README points to the section explaining how, along with the latest Jira REST API documentation for the full list of endpoints.

### How are the tests run in the go-jira repository?

The Makefile test target runs go test -v -race over everything, and there are coverage targets that produce a coverage profile and open it in a browser, plus vet, fmt and staticcheck targets and an all target that runs them together. A docs-serve target starts the mkdocs development server.

## Sources

- [andygrunwald/go-jira on GitHub](https://github.com/andygrunwald/go-jira)
- [License: MIT](https://github.com/andygrunwald/go-jira/blob/main/LICENSE)
- [Project website](https://pkg.go.dev/github.com/andygrunwald/go-jira?tab=doc)
- [README](https://github.com/andygrunwald/go-jira/blob/main/README.md)
- [Releases](https://github.com/andygrunwald/go-jira/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/andygrunwald-go-jira
