Back

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

By Percy Bolmer
By Percy Bolmer

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.

JIRA API Token creation
JIRA API Token creation

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"
.env — The environment variables to use in the test program

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)
}
JIRA — Connecting to JIRA cloud and grabbing user profile

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)
	}
}
JIRA — Listing all projects that are available for the user

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)
	}
}
JIRA — Searching for issues based on a input JQL string

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'")
}
JIRA — List all issues based on your JQL string

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
}
JIRA — Get all issues even though there are more than max results

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)
	}
JIRA — Creating a new issue

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
}
JIRA — Get transitions, right now only returning an empty transition

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)
			}

		}

	}
}
JIRA — Moving an issue from Hold to Progress

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
}
JIRA — Transition an issue from one status to the next

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

Sign up for my Awesome newsletter