Behaviours - Conflict Checker
When you book an Environment, you need ensure that you are not booking it on the same timeslot than a colleague. With ScriptRunner Behaviour, you can display a warning banner on your booking creation screen, in case a conflict is detected. This guide explains how to configure a Conflict Checker banner on your booking creation screen in Jira Data Center.
Requirements
You must be using Jira Data Center (not compatible with Jira Cloud).
Golive Scheduling must be configured as per the Scheduling Environments documentation.
ScriptRunner Jira App must be installed on your Jira Data Center.
Configuration Steps
Step 1: Add a New Script
Add our script in the ScriptRunner Script Editor (code reference below).
Important
Ensure the folder hierarchy (com/apwide/behaviour
) matches the import
line in the script.
Info
Ignore any warnings in the script editor related to unknown classes (environment
, ctx
).
package com.apwide.behaviour
import com.apwide.env.api.Environment
import com.apwide.env.api.Golive
import com.apwide.env.api.ScheduledEvent
import com.apwide.env.api.ScheduledEvents
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.entity.WithKey
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.context.IssueContext
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.query.Query
import com.onresolve.jira.groovy.user.FormField
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import com.opensymphony.workflow.InvalidInputException
import groovy.util.logging.Log4j
import org.apache.log4j.Level
import org.codehaus.groovy.runtime.typehandling.GroovyCastException
import java.util.stream.Collectors
import static com.atlassian.jira.config.properties.APKeys.JIRA_DATE_TIME_PICKER_JAVA_FORMAT
import static com.atlassian.jira.web.bean.PagerFilter.getUnlimitedFilter
@WithPlugin("com.holydev.env.plugin.jira-holydev-env-plugin")
@Log4j
class ConflictChecker {
static final String JIRA_JQL_DATE_FORMAT = 'yyyy-MM-dd HH:mm'
def ctx
Golive golive
String dateFormat
String issueTypeName
String startDateFieldName
String endDateFieldName
String environmentFieldName
String timelineUrl
String helpFieldName
boolean blocking = false
boolean showDependencies = true
Integer calendarId
String validationErrorMessage
Level logLevel
void validate() {
String message = evaluate()
if (!message.isEmpty()) {
log.debug("Validator rejects")
throw new InvalidInputException(validationErrorMessage)
}
}
void check() {
writeHelpText(evaluate())
}
String evaluate() {
log.setLevel(logLevel ?: Level.INFO)
Date startFieldValue = getDateValue(startDateFieldName)
Date endFieldValue = getDateValue(endDateFieldName)
List bookedEnvironmentIds = getEnvironmentsValue(environmentFieldName)
if (!startFieldValue || !endFieldValue || !bookedEnvironmentIds) {
log.debug("No values found")
return ""
}
List<Environment> dependencies = showDependencies ? getDependencies(bookedEnvironmentIds) : []
List<Issue> conflicts = searchForConflicts(startFieldValue, endFieldValue, bookedEnvironmentIds)
List<ScheduledEvent> events = searchForEvents(startFieldValue, endFieldValue, bookedEnvironmentIds, calendarId)
if (conflicts.isEmpty() && events.isEmpty() && dependencies.isEmpty()) {
log.debug("Nothing to display")
return ""
}
log.debug("Found ${dependencies.size()} depencencies")
log.debug("Found ${conflicts.size()} conflicts")
log.debug("Found ${events.size()} events")
def dependenciesInformation = ""
def conflictsWarning = ""
def eventsWarning = ""
if (!dependencies.isEmpty()) {
dependenciesInformation =
"""
<div class="aui-message aui-message-info">
<p style="padding: 0">Unbooked Dependencies</p>
<table style="margin: 15px 0px; width:100%">
<tr>
<th>Name</th>
<th>Category</th>
<th>Application</th>
</tr>
${renderEnvironmentList(dependencies)}
</table>
</div>
"""
}
if (!conflicts.isEmpty()) {
conflictsWarning = """
<p style="padding: 0">There are conflicting Booking Requests:</p>
<table style="margin: 15px 0px; width:100%">
<tr>
<th>Key</th>
<th>Summary</th>
<th>Period</th>
<th>Environment(s)</th>
</tr>
${renderConflictList(conflicts, bookedEnvironmentIds)}
</table>
"""
}
if (!events.isEmpty()) {
eventsWarning = """
<p style="padding: 0">There are conflicting Events:</p>
<table style="margin: 15px 0px; width:100%">
<tr>
<th>Id</th>
<th>Summary</th>
<th>Period</th>
<th>Environment(s)</th>
</tr>
${renderEventConflictList(events, bookedEnvironmentIds)}
</table>
"""
}
def conflictsInformation = ""
if (!conflicts.isEmpty() || !events.isEmpty()) {
conflictsInformation = """
<div class="aui-message aui-message-${blocking ? 'error' : 'warning'}">
${conflictsWarning}
${eventsWarning}
<a target="_blank" href="${timelineUrl}" class="aui-button">
<span class="aui-icon aui-icon-small aui-iconfont-search"></span> Open the Timeline
</a>
</div>
"""
}
return """
${dependenciesInformation}
${conflictsInformation}
"""
}
private String renderEnvironmentList(List environments) {
return environments.collect { environment ->
return """
<tr>
<td><a href="/secure/ApwideGolive.jspa#/home/environment/${environment.id}" target="_blank">${environment.name}</a></td>
<td>${environment.category.name ?: 'None'}</td>
<td>${environment.application.name ?: 'None'}</td>
</tr>
"""
}.join("\n")
}
private String renderEventConflictList(List<ScheduledEvent> conflicts, Collection<String> environmentFieldValue) {
return conflicts.collect { event ->
Date startDate = event.getStart()
Date endDate = event.getEnd()
List environments = (List) event.getPlannedEnvironments().findAll { environment -> (environmentFieldValue?.find { (it + "").equals(environment.id + "") }) }
return """
<tr>
<td>${event.id}</td>
<td>${event.title}</td>
<td>${displayedDate(startDate)} - ${displayedDate(endDate)}</td>
<td>${displayedEnvironments(environments)}</td>
</tr>
"""
}.join("\n")
}
private String renderConflictList(List<Issue> conflicts, Collection<String> environmentFieldValue) {
CustomField startCustomField = getCustomFieldByName(startDateFieldName)
CustomField endCustomField = getCustomFieldByName(endDateFieldName)
CustomField environmentCustomField = getCustomFieldByName(environmentFieldName)
return conflicts.collect { issue ->
Date startDate = (Date) issue.getCustomFieldValue(startCustomField)
Date endDate = (Date) issue.getCustomFieldValue(endCustomField)
Collection environments = issue.getCustomFieldValue(environmentCustomField)?.findAll { environment -> (environmentFieldValue?.find { (it + "").equals(environment.id + "") }) }
return """
<tr>
<td><a target="_blank" href="${baseUrl()}/browse/${issue.key}">${issue.key}</a></td>
<td>${issue.summary}</td>
<td>${displayedDate(startDate)} - ${displayedDate(endDate)}</td>
<td>${displayedEnvironments(environments)}</td>
</tr>
"""
}.join("\n")
}
private void writeHelpText(String helpText) {
FormField endField = ctx.getFieldByName(helpFieldName ?: environmentFieldName)
if (blocking) {
endField.setError(helpText)
} else {
endField.setHelpText(helpText)
}
}
private List<Environment> getDependencies(List<String> envIds) {
if (!envIds || envIds.isEmpty()) {
return []
}
Collection<Environment> dependencies = envIds.stream().flatMap {
Environment env = golive.environments.getById(Integer.valueOf(it))
return env.outgoingDependencies.stream()
}
.filter { !envIds.contains(it.id.toString()) }
.collect(Collectors.toList())
return dependencies.unique { a, b -> a.id <=> b.id }
}
private List<Issue> searchForConflicts(Date startFieldValue, Date endFieldValue, List environmentFieldValue) {
String jql = toQuery(startFieldValue, endFieldValue, environmentFieldValue)
Query query = searchService().parseQuery(loggedInUser(), jql).query
return searchService().searchOverrideSecurity(loggedInUser(), query, getUnlimitedFilter()).results
}
private String toQuery(Date startFieldValue, Date endFieldValue, List environmentFieldValue) {
String stringQuery = """
type = "${issueTypeName}"
AND "${startDateFieldName}" < "${jqlDate(endFieldValue)}"
AND "${endDateFieldName}" > "${jqlDate(startFieldValue)}"
AND "${environmentFieldName}" in (${environmentFieldValue.join(",")})
${notCurrentIssue()}
"""
log.debug("query is: ${stringQuery}")
return stringQuery
}
private String notCurrentIssue() {
IssueContext issueContext = ctx.getIssueContext()
if (issueContext instanceof WithKey) {
return "AND key != \"${((WithKey) issueContext).getKey()}\""
} else {
return ""
}
}
private String jqlDate(Date date) {
return date.format(JIRA_JQL_DATE_FORMAT)
}
private List<ScheduledEvent> searchForEvents(Date startFieldValue, Date endFieldValue, List environmentFieldValue, Integer calendarId) {
if (calendarId == null || environmentFieldValue == null) {
return Collections.emptyList()
}
return golive.scheduledEvents.find(ScheduledEvents.SearchCriteria.builder()
.calendarId(calendarId)
.startBefore(endFieldValue)
.endAfter(startFieldValue)
.plannedEnvironmentIds(environmentFieldValue.collect { Integer.valueOf(it) })
.build()
)
}
private Date getDateValue(String fieldName) {
try {
FormField dateField = ctx.getFieldByName(fieldName)
return dateField ? (Date) dateField.getValue() : null
} catch (GroovyCastException ex) {
return null
}
}
private List getEnvironmentsValue(String fieldName) {
try {
FormField environmentField = ctx.getFieldByName(fieldName)
return environmentField ? (List) environmentField.getValue() : null
} catch (GroovyCastException ex) {
return null
}
}
private String displayedDate(Date date) {
return date.format(displayDateFormat())
}
private String displayedEnvironments(Collection environments) {
return environments ? environments.collect { it.name }.join("<br />") : "-"
}
private String displayedPlannedEnvironments(Collection plannedEnvironments) {
return displayedEnvironments(plannedEnvironments.collect { golive.environments.getById(it.getEnvironmentId()) })
}
private String displayDateFormat() {
def format = dateFormat != null ? dateFormat : ComponentAccessor.getApplicationProperties().getString(JIRA_DATE_TIME_PICKER_JAVA_FORMAT)
log.debug("date format to be used will '${format}'")
return format
}
private String baseUrl() {
return ComponentAccessor.getApplicationProperties().getString("jira.baseurl")
}
private CustomField getCustomFieldByName(String name) {
return ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName(name)
}
private SearchService searchService() {
return ComponentAccessor.getComponent(SearchService.class)
}
private ApplicationUser loggedInUser() {
return ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
}
}
Step 2: Add a New Behaviour
Create a new Behaviour and map it with your Booking Request project and issue type.
3. Call the Script from our Behaviour
In your Behaviour, create an Initialiser Script.
2. Copy-paste the following code in the opened text area to call our Conflict Checker script:
package com.apwide.behaviour
import com.apwide.behaviour.ConflictChecker
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import com.apwide.env.api.GoliveAccessor
import org.apache.log4j.Level
@WithPlugin("com.holydev.env.plugin.jira-holydev-env-plugin")
@PluginModule
GoliveAccessor goliveAccessor
new ConflictChecker(
ctx: this,
golive: goliveAccessor.golive(),
issueTypeName: "Booking Request",
startDateFieldName: "Start time",
endDateFieldName: "End time",
environmentFieldName: "Environment(s) to book",
helpFieldName: "Environment(s) to book",
timelineUrl: "/secure/ApwideGolive.jspa#/home/timeline?view=timeline318",
dateFormat: "MMM d",
blocking: false,
showDependencies: true,
logLevel: Level.DEBUG
).check()
Script Parameters:
issueTypeName
: Name of your Booking Request issue type.startDateFieldName
: Name of your booking start date/time field.endDateFieldName
: Name of your booking end date/time field.environmentFieldName
: Name of your Environment custom field.helpFieldName
: Field name for displaying warning messages. Defaults toendDateFieldName
if undefined.timelineUrl
: URL for the “Open the Timeline” button in warning messages.dateFormat
: Format for the displayed period. Defaults to Jira's date format if undefined.blocking
: Set totrue
to block request creation when conflicts are detected (red warning). Set tofalse
for non-blocking behavior (yellow warning):
showDependencies
: Set totrue
to display a list of booked dependencies (blue background). Set tofalse
to hide them.calendarId
(optional): ID of a Scheduled Event Calendar to include in the conflict detection. Retrieve this ID from the “Shared Calendars” section in the HTML source code.
Checking Conflicts in your Booking Request
To check for conflicts each time the fields are updated in your Booking Request:,
1. Add the following fields to your Behavior:
Environment field
Start time field
End time field
Mark these fields as Required using the toggle.
Add the same Server-side script for these fields as you use for the Initialiser.
Info
Ensure all scripts within the same Behavior are identical. We recommend using a shared script file for the Initialiser and Fields to maintain consistency and avoid copying code.
Need Assistance?
For support with integrating Golive and Jira Automation, reach out to our help center.