5 min read

How to use ScriptRunner to automate your code review best practices

Teamwork for Peer Review best practice

I’m Josh, Software Engineer on the ScriptRunner for Confluence team here at Adaptavist. If you’re using Jira and Confluence for your development workflow, this post is for you. 

After a little encouragement from the creator of ScriptRunner himself, Jamie Echlin, I’m going to share with you how we built out some simple automations (using ScriptRunner) to simplify a team-defined process for code-review best practice. Plus, I’ve included some free code snippets so you can build it for yourself today!

Here's an xkcd comic that basically lays out the situation... 

Image source: XKCD, The General Problem, https://xkcd.com/974/
Image source: XKCD, The General Problem, https://xkcd.com/974/

What was the problem?

On the ScriptRunner for Confluence team, we have a peer code review system where each developer has an assigned person who reviews their pull requests. If you're not familiar with the pull request process, it's basically just a phase in the development cycle where new code is reviewed in order to provide feedback and suggest improvements.

At the beginning of each sprint, every developer is assigned a review "buddy", meaning that twice a month, you get someone new to review your code. It may sound cutesy or juvenile to assign a buddy, but it’s actually code review best practice and we've learned that, without this system, confusion and a lack of ownership can occur. 

Before we adopted our rotating peer reviewer system, we’d sometimes get confusion around who should review each of our pull requests. Also, it  wasn't always clear to the reviewer that they were supposed to provide feedback in a timely manner. So, that’s why we came up with a buddy system. How did we do it? We simply made a table on a Confluence page detailing who the buddy was for the current sprint.

But, the problem with using a simple table on your Confluence page is that it needs to be manually edited every two weeks to ensure that each person gets a new buddy. You may be thinking: ‘Josh, that really doesn't sound hard...!’ And you would be right, but I don't want to have to manually edit a table and think about which name goes where. I studied Computer Science, not Table Editing Science.

What was the solution?

To remove this manual editing process, we set up some simple automations using ScriptRunner, to do the work for us — no manual intervention required at all. In a perfect world, we also wanted to receive a notification when a rotation took place, so we didn’t have to even check the page!

How we automated our peer code review system with ScriptRunner

Here's an overview of our technical solution:

Here's an overview of our technical solution

Let's break down the four distinct parts of the workflow.

Part 1: A new sprint is started

As explained before, the rotation needs to occur at the beginning of every sprint. If you're lucky enough not to be a Product Manager, you may have never actually started a sprint yourself. That being said, whenever a sprint is started for a project in Jira, an event gets fired off within the fiery depths of Jira's internal code. That event is aptly named ‘SprintStartedEvent’. If only we could have some way to listen for that event, we could start the whole automation off...

Part 2: Script Listener

Lucky for us, a core feature of ScriptRunner is the ability to listen for specific events in specific projects. So, all we had to do was set up a Custom Listener on our Jira instance that was configured to listen for ‘SprintStartedEvent’.

Our custom listener
Our ScriptRunner custom listener triggers when a new sprint starts

Now that we could catch SprintStartedEvent, next we needed a way to communicate with our Confluence instance in order to actually change the table on the page we want to update automatically. We achieved this communication by making a simple request to our Confluence instance within a script. Because Adaptavist's Confluence and Jira instances are linked using ApplicationLinks, there’s no need to put any credentials within the script itself. (For more detail on this, see the ScriptRunner documentation.)

Here's the script we used to contact Confluence:
import com.atlassian.applinks.api.ApplicationLink import com.atlassian.applinks.api.ApplicationLinkResponseHandler import com.atlassian.applinks.api.ApplicationLinkService import com.atlassian.applinks.api.application.confluence.ConfluenceApplicationType import com.atlassian.greenhopper.service.sprint.Sprint import com.atlassian.jira.component.ComponentAccessor import com.atlassian.sal.api.net.Response import com.atlassian.sal.api.net.ResponseException import com.onresolve.scriptrunner.canned.jira.utils.plugins.RapidBoardUtils import groovy.json.JsonSlurper import static com.atlassian.sal.api.net.Request.MethodType.GET def CONFLUENCE_URL = 'https://confluence.instance.com/' // Change this to your Confluence instance URL def CONFLUENCE_REST_PATH = 'path/to/your/rest-endpoint' // Change this to the path of your endpoint def sprint = event.sprint as Sprint def rapidBoardUtils = new RapidBoardUtils() def teamBoardId = rapidBoardUtils.getBoardId("SR4C Squad", ComponentAccessor.jiraAuthenticationContext.loggedInUser) if (!(sprint.rapidViewId == teamBoardId)) { return } def appLinkService = ComponentAccessor.getComponent(ApplicationLinkService) def appLink = appLinkService.getApplicationLinks(ConfluenceApplicationType).find { ApplicationLink link -> link.displayUrl.toString().contains(CONFLUENCE_URL) } def applicationLinkRequestFactory = appLink.createAuthenticatedRequestFactory() def request = applicationLinkRequestFactory.createRequest(GET, "${CONFLUENCE_URL}${CONFLUENCE_REST_PATH}") def handler = new ApplicationLinkResponseHandler<Map>() { @Override Map credentialsRequired(Response response) throws ResponseException { return null } @Override Map handle(Response response) throws ResponseException { assert response.statusCode == 200 new JsonSlurper().parseText(response.getResponseBodyAsString()) as Map } } try { request.execute(handler) log.debug('SRCONF buddy table updated') } catch (Exception e) { log.error("Failed to update SRCONF buddy table", e) }

Part 3: The REST Endpoint on Confluence

Once our listener sees a SprintStartedEvent in Jira, it sends a request over to Confluence via a custom Script Endpoint. This endpoint actually contains the script that does the automatic editing of the table. Basically, all our script does is move down each person in the left-hand column of the table on our Confluence page. If the person on the left-hand column equals the person on the right-hand column, we shift again. (We don't want people reviewing their own code, what is this; 2015?!)

To achieve this, my colleague Aidan Derossett came up with a simple script:

import com.atlassian.confluence.pages.PageManager import com.atlassian.sal.api.component.ComponentLocator import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate import com.onresolve.scriptrunner.slack.SlackUtil import groovy.json.JsonBuilder import groovy.transform.BaseScript import org.jsoup.Jsoup import org.jsoup.nodes.Element import javax.ws.rs.core.MultivaluedMap import javax.ws.rs.core.Response @BaseScript CustomEndpointDelegate delegate updateBuddyTable(httpMethod: "GET", groups: ["adaptavist"]) { MultivaluedMap queryParams, String body -> def pageManager = ComponentLocator.getComponent(PageManager) def CONFLUENCE_PAGE_LINK = 'https://confluence.instance.com/path-to-your-page' // Change this to your Confluence instance page URL def BUDDY_PAGE_NAME = 'ScriptRunner for Confluence Pull Request Process' def USER_ATTRIBUTE_KEY = 'ri:userkey' def page = pageManager.getPage('SR', BUDDY_PAGE_NAME) def parsedBody = Jsoup.parse(page.bodyContent.body) def tableCells = parsedBody.select('td') List<Element> rightColumnCells = [] List<Element> leftColumnCells = [] tableCells.eachWithIndex { item, index -> if (index % 2 == 0) { leftColumnCells.add(item) } else { rightColumnCells.add(item) } } def leftColumnUserKeys = leftColumnCells.collect { it.getElementsByAttribute(USER_ATTRIBUTE_KEY)*.attr(USER_ATTRIBUTE_KEY) }.flatten() as List<String> def rightColumnUserKeys = rightColumnCells.collect { it.getElementsByAttribute(USER_ATTRIBUTE_KEY)*.attr(USER_ATTRIBUTE_KEY) }.flatten() as List<String> rightColumnUserKeys.remove(rightColumnUserKeys.size() - 1) Collections.rotate(leftColumnUserKeys, 1) if (leftColumnUserKeys == rightColumnUserKeys) { Collections.rotate(leftColumnUserKeys, 1) } leftColumnUserKeys.eachWithIndex { newUserKey, index -> leftColumnCells[index].getElementsByAttribute(USER_ATTRIBUTE_KEY).first().attr(USER_ATTRIBUTE_KEY, newUserKey) } pageManager.saveNewVersion(page) { it.setBodyAsString(parsedBody.toString()) } SlackUtil.message( "sr4cbot", "sr4c-squad", ":cooldog: NEW SPRINT TIME :cooldog:\n <$CONFLUENCE_PAGE_LINK|The pull request buddy table has been updated!>" ) return Response.ok(new JsonBuilder("$BUDDY_PAGE_NAME has been rotated.").toString()).build() }

Part 4: Table updated and notifying the team

So now all of this is awesome and automated: when the above script within the REST endpoint is called, it will automatically update the table and give everyone a new buddy. Job done, right?


Remember I said we wanted to let everybody know? Communication is a huge part of DevOps best practice and we have a new nifty Jira-to-Slack ScriptRunner integration to use.

At the end of the script, there's some code to automatically post in our team Slack channel whenever our table is updated. Here's an example of the message that our new SR4C bot posts in the chat:

Super cool Slackbot messages

You can see our message has the cooldoge emoji. This is completely required if you want to be cool. Speaking of cool, it was another of my awesome colleagues, Jonathan Scalise, who kindly helped me set up my Slack bot. The guy is a wizard.

Part 5: Next steps

We could probably eliminate the table all together and just programmatically set reviewers for each person within Bitbucket. This could be accomplished with ScriptRunner for Bitbucket, but our table provides a nice visual reference and it works for us... so good enough for now.


While this may not save our team too much time in the grand scheme of things, it’s a great example to show automations don't have to be incredibly complex to provide immediate value.

For me, this was a great opportunity as a ScriptRunner developer to use our own product to solve a headache. It was an awesome chance to get an overview of our products and how they can connect to create efficiency. 

If you create products, digital or physical, I'm sure you know the following to be true; it's one thing to develop something and it's an entirely different thing to actually use it. It honestly gives you a totally different perspective.

This project has also been a great chance to work with some awesome colleagues on a small project. Sometimes, it's about the journey and not the destination.

Looking for some other quick wins when it comes to ScriptRunner customisations, automations and integrations? Check out the script library where we’ve got bundles of free scripts ready for you to make your own.

Go to Script Library
with the Adaptanews monthly email