Automate JIRA Cloud Workflow With Golang
Reduce workload for developers by improving the internal processes for JIRA by automating day to day tours
by Percy Bolmér, August 12, 2021

JIRA is a very popular framework for tracking issues and project management. It allows project leads or scrum masters to set up projects and creates issues that are then assigned to developers. It’s a great framework for working with agile development and tracking the work process of development teams.
JIRA offers a ton of features, but once in a while, the need to control JIRA by backend services can occur. This is a topic that is often forgotten, but sometimes services that help the development team develop faster are great. This can be achieved by creating software that manages JIRA from the backend.
In this article, we will rely on a Cloud-hosted JIRA.
Connecting to JIRA Cloud From a Golang Service
To begin using JIRA, we first need to authenticate and create a connected client. We will need to grab a new API key from the official Atlassian API key management console . You can visit the link and create your API key. At the top, you will see a blue button to create a token. Enter a name so you can relate to the token easily, and remember to save the token. You cannot view it again.

We will be using a Golang package called go-jira. This package will help us easily use the JIRA API but remember you can also use the REST API with regular HTTP. But why reinvent the wheel when somebody has done it for us already?
To connect we need to use three environment variables:
- JIRA_USER = The username of your JIRA user
- JIRA_TOKEN = This is the token that we previously created
- JIRA_URL = The base URL to your JIRA Cloud
I store the environment variables in a .env
file which I load on runtime. This is something I usually do so it’s easy for me to control environment variables for multiple projects. There is a super smooth Golang package called gotdotenv.
Let’s download those packages and then create a new main.go
and a .env
file using the following commands:
mkdir jira-test
cd jira-test
go mod init jira-test
go get github.com/andygrunwald/go-jira
go get github.com/joho/godotenv
touch main.go
touch .env
Update the .env
file with your credentials.
JIRA_USER="myuser@user.com"
JIRA_TOKEN="api-token"
JIRA_URL="https://your-cloud-name.atlassian.com"
Then open up main.go and make sure the connection works. We will first create a jira.BasicAuthTransport
struct that contains the authentication credentials supplied in the .env file.
The Jira package we use has all domains in the client, so to get user information we use client.User
etc. Here’s the code:
package main
import (
"log"
"os"
"github.com/joho/godotenv"
jira "gopkg.in/andygrunwald/go-jira.v1"
)
func main() {
// Load the .env file
godotenv.Load(".env")
// Create a BasicAuth Transport object
tp := jira.BasicAuthTransport{
Username: os.Getenv("JIRA_USER"),
Password: os.Getenv("JIRA_TOKEN"),
}
// Create a new Jira Client
client, err := jira.NewClient(tp.Client(), os.Getenv("JIRA_URL"))
if err != nil {
log.Fatal(err)
}
me, _, err := client.User.GetSelf()
if err != nil {
log.Fatal(err)
}
log.Println(me)
}
Try executing the program by running and you should see the provided user’s profile.
go run main.go
Listing all Projects and Issues
Simply printing the user profile does not help us. We can do more!
Let’s print all the projects that exist. As I mentioned before, the JIRA client has all domains separated, so just as we used the user domain to perform user queries we will use Project
to perform queries against projects.
// getProjects is a function that lists all projects
func getProjects(client *jira.Client) {
// use the Project domain to list all projects
projectList, _, err := client.Project.GetList()
if err != nil {
log.Fatal(err)
}
// Range over the projects and print the key and name
for _, project := range *projectList {
fmt.Printf("%s: %s\n", project.Key, project.Name)
}
}
We can also search for certain Issues using JQL, which stands for Jira query language. This is a simple query language that is string-based that allows us to narrow down the issues to collect.
Sending the request is done by using the issue domain in the client with the Search function. We begin by using a simple search which we will extend soon.
// getIssues will query Jira API using the provided JQL string
func getIssues(client *jira.Client, jql string) {
issues, _, err := client.Issue.Search(jql, nil)
if err != nil {
panic(err)
}
for _, i := range issues {
fmt.Printf("%s (%s/%s): %+v -> %s\n", i.Key, i.Fields.Type.Name, i.Fields.Priority.Name, i.Fields.Summary, i.Fields.Status.Name)
fmt.Printf("Assignee : %v\n", i.Fields.Assignee.DisplayName)
fmt.Printf("Reporter: %v\n", i.Fields.Reporter.DisplayName)
}
}
Let’s update the main.go
to try it out, you will need to change the JQL to match some projects in your own JIRA. We will write a JQL string that filters out only Issues related to a certain project.
project = 'PROJECTNAME'
You can also add more specific filters, like only a certain status or type.
project = 'PROJECTNAME' and Status = 'Done'
project = 'PROJECTNAME' and Type = 'Feature'
func main() {
// Load the .env file
godotenv.Load(".env")
// Create a BasicAuth Transport object
tp := jira.BasicAuthTransport{
Username: os.Getenv("JIRA_USER"),
Password: os.Getenv("JIRA_TOKEN"),
}
// Create a new Jira Client
client, err := jira.NewClient(tp.Client(), os.Getenv("JIRA_URL"))
if err != nil {
log.Fatal(err)
}
//getProjects(client)
getIssues(client, "project = 'YourProject' and Status = 'Done'")
}
Try running it after you’ve changed the JQL, and see if you get proper output. Note that JIRA does limit the number of results that are received to 1,000. So if you have more than 1,000 issues, you might need to apply search options. If you noticed, the Search
function accepts two parameters: one of them is a search option. You can make a simple for loop to drain all issues. The result that is returned from the Search
returns the total amount of issues located, so we can keep searching until we reach the maximum.
The examples of the go-jira
library provide a great example of this, and we will reuse this code. Let’s update the getIssues
to match the example with the following code:
// getIssues will query Jira API using the provided JQL string
func getIssues(client *jira.Client, jql string) ([]jira.Issue, error) {
// lastIssue is the index of the last issue returned
lastIssue := 0
// Make a loop through amount of issues
var result []jira.Issue
for {
// Add a Search option which accepts maximum amount (1000)
opt := &jira.SearchOptions{
MaxResults: 1000, // Max amount
StartAt: lastIssue, // Make sure we start grabbing issues from last checkpoint
}
issues, resp, err := client.Issue.Search(jql, opt)
if err != nil {
return nil, err
}
// Grab total amount from response
total := resp.Total
if issues == nil {
// init the issues array with the correct amount of length
result = make([]jira.Issue, 0, total)
}
// Append found issues to result
result = append(result, issues...)
// Update checkpoint index by using the response StartAt variable
lastIssue = resp.StartAt + len(issues)
// Check if we have reached the end of the issues
if lastIssue >= total {
break
}
}
for _, i := range result {
fmt.Printf("%s (%s/%s): %+v -> %s\n", i.Key, i.Fields.Type.Name, i.Fields.Priority.Name, i.Fields.Summary, i.Fields.Status.Name)
fmt.Printf("Assignee : %v\n", i.Fields.Assignee.DisplayName)
fmt.Printf("Reporter: %v\n", i.Fields.Reporter.DisplayName)
}
return result, nil
}
The library offers many more features, such as grabbing all issues from a sprint. Be sure you check the documentation out.
You can also create a new issue by using the client.Issue.Create
. The documentation has a superb example again, and I won’t be creating any issues here. But you can view the list below which is an example made by Andy Grunwald
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",
},
}
issue, _, err := jiraClient.Issue.Create(&i)
if err != nil {
panic(err)
}
Transition an Issue
Most of the time. I find that transitioning issues between states is the most common use case. So let’s try moving an issue between one status to another.
JIRA offers great documentation about what a transition is and how it works here.
We will make a function getIssueTransition
that takes in a wanted Status and returns the proper transition to use for that. The transition is important as we do need to ID the transition to move an issue. Here’s the code to do that:
// getIssueTransition will grab the available transitions for a issue
func getIssueTransition(client *jira.Client, issue jira.Issue, status string) (jira.Transition, error) {
transitions, _, err := client.Issue.GetTransitions(issue.Key)
if err != nil {
return jira.Transition{}, err
}
for _, t := range transitions {
if t.Name == status {
return t, nil
}
}
return jira.Transition{}, nil
}
Now that we have the transition needed, we can simply move an issue. Update the filtering so that you get the issue you need, either by updating the JQL or ranging over issues. I’ll make a hardcoded version that searches for a certain issue.
func main() {
// Load the .env file
godotenv.Load(".env")
// Create a BasicAuth Transport object
tp := jira.BasicAuthTransport{
Username: os.Getenv("JIRA_USER"),
Password: os.Getenv("JIRA_TOKEN"),
}
// Create a new Jira Client
client, err := jira.NewClient(tp.Client(), os.Getenv("JIRA_URL"))
if err != nil {
log.Fatal(err)
}
//getProjects(client)
issues, err := getIssues(client, "project = 'YourProject' and Status = 'ON HOLD'")
if err != nil {
log.Fatal(err)
}
for _, issue := range issues {
if issue.Key == "MyIssueKey" {
transition, err := getIssueTransition(client, issue, "In Progress")
if err != nil {
log.Fatal(err)
}
// Perform transition
err = transitionIssue(client, issue, transition)
if err != nil {
log.Fatal(err)
}
}
}
}
We should implement the transitionIssue
function that is used in the main before you can trigger it. This is a super simple wrapper function around the client.
// transitionIssue will move a issue into the new transition
func transitionIssue(client *jira.Client, issue jira.Issue, transition jira.Transition) error {
_, err := client.Issue.DoTransition(issue.ID, transition.ID)
return err
}
Execute the program after switching the issue key to the wanted issue and you should see it moved.
Conclusion
You now have the basics of how to integrate with JIRA from a backend service. The JIRA API offers a ton of functionality, and we can’t cover it all here, but you have the basics to get started. The third-party library (go-Jira) is great because it also offers the ability to use unimplemented API endpoints.
Some ideas for automation I’ve seen are all issues that enter a certain state gets filled with information from the Git repository, CI/CD failures get updated automatically, etc.
What you automate is really up to your imagination. There is the ability to modify/change attachments and much more. I hope you find the experience as smooth as I have. You can find the full code for this article on GitHub.
As always, feel free to reach out if there is anything you have questions about.
If you enjoyed my writing, please support future articles by buying me an Coffee