Hi Community, I am having some difficulty getting the code to work for auto creating tempo plans to users based upon getting assigned.
I referred following link - https://library.adaptavist.com/entity/create-tempo-planning-information-when-issue-is-assigned
for my use case.
Use Case: Auto create tempo plans with Time Tracking and assigned user
However when i am assigning issue to a user i am getting following error:
-----------------------------------------------------------------------------------------------------
*************************************************************************************************
ERROR:
2023-05-24 11:49:29,991 ERROR [runner.AbstractScriptListener]: ************************************************************************************* 2023-05-24 11:49:29,999 ERROR [runner.AbstractScriptListener]: Script function failed on event: com.atlassian.jira.event.issue.IssueEvent, file: null com.atlassian.sal.api.net.ResponseStatusException: Unexpected response received. Status code: 400 at com.atlassian.sal.core.net.HttpClientRequest.lambda$execute$0(HttpClientRequest.java:77) at com.atlassian.sal.core.net.HttpClientRequest.executeAndReturn(HttpClientRequest.java:102) at com.atlassian.sal.core.net.HttpClientTrustedRequest.executeAndReturn(HttpClientTrustedRequest.java:53) at com.atlassian.sal.core.net.HttpClientRequest.execute(HttpClientRequest.java:75) at com.atlassian.sal.api.net.Request$execute$0.call(Unknown Source) at Script1.run(Script1.groovy:59)
-----------------------------------------------------------------------------------------------------
*************************************************************************************************
Script used:
import com.atlassian.sal.api.ApplicationProperties
import com.atlassian.sal.api.UrlMode
import com.atlassian.sal.api.net.Request
import com.atlassian.sal.api.net.TrustedRequestFactory
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import groovy.json.JsonOutput
import groovyx.net.http.URIBuilder
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@WithPlugin('com.tempoplugin.tempo-plan-core')
@PluginModule
ApplicationProperties applicationProperties
@PluginModule
TrustedRequestFactory trustedRequestFactory
// Default start time
final startTime = '09:00'
// Weekends and holidays are not included by default. If a plan needs to be created on weekends and holidays, set this to "true"
final includeNonWorkingDays = false
final today = LocalDate.now()
def issue = event.issue
def endDate = issue.dueDate?.toLocalDateTime()?.toLocalDate()
// Do nothing if the issue has been unassigned
if (!issue.assignee) {
return
}
if (!(issue.estimate && endDate)) {
log.error('Issue requires both an estimate and a due date. Plan not created')
return
}
def url = applicationProperties.getBaseUrl(UrlMode.CANONICAL) + '/rest/tempo-planning/1/plan'
def request = trustedRequestFactory.createTrustedRequest(Request.MethodType.POST, url)
def host = new URIBuilder(url).host
request.addTrustedTokenAuthentication(host)
request.setRequestBody(JsonOutput.toJson([
planItemType : 'ISSUE',
planItemId : issue.id,
assigneeKey : 'admin',
start : DateTimeFormatter.ISO_LOCAL_DATE.format(today),
startTime : startTime,
day : DateTimeFormatter.ISO_LOCAL_DATE.format(today),
end : DateTimeFormatter.ISO_LOCAL_DATE.format(endDate),
includeNonWorkingDays: includeNonWorkingDays,
secondsPerDay : issue.estimate
]), 'application/json')
request.execute()
-----------------------------------------------------------------------------------------------------
*************************************************************************************************
Thanks,
BAZ
Hi Baz!
I've just answered the ticket you raised to Adaptavist Support, but the answer may be beneficial on this Community Post too for other to see :)
Essentially, since the creation of our Library Script, the Tempo API has been updated to require the assigneeKey.
We can modify the Library Script to include this data:
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.sal.api.ApplicationProperties
import com.atlassian.sal.api.UrlMode
import com.atlassian.sal.api.net.Request
import com.atlassian.sal.api.net.TrustedRequestFactory
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import groovy.json.JsonOutput
import groovyx.net.http.URIBuilder
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@WithPlugin('com.tempoplugin.tempo-plan-core')
@PluginModule
ApplicationProperties applicationProperties
@PluginModule
TrustedRequestFactory trustedRequestFactory
// Default start time
final startTime = '09:00'
// Weekends and holidays are not included by default. If a plan needs to be created on weekends and holidays, set this to "true"
final includeNonWorkingDays = false
final today = LocalDate.now()
def issue = event.issue
def endDate = issue.dueDate?.toLocalDateTime()?.toLocalDate()
// Do nothing if the issue has been unassigned
if (!issue.assignee) {
return
}
if (!(issue.estimate && endDate)) {
log.error('Issue requires both an estimate and a due date. Plan not created')
return
}
def url = applicationProperties.getBaseUrl(UrlMode.CANONICAL) + '/rest/tempo-planning/1/plan'
def request = trustedRequestFactory.createTrustedRequest(Request.MethodType.POST, url)
def host = new URIBuilder(url).host
request.addTrustedTokenAuthentication(host)
request.setRequestBody(JsonOutput.toJson([
assigneeKey : 'JIRAUSER10000',
assigneeType : 'USER',
day : DateTimeFormatter.ISO_LOCAL_DATE.format(today),
end : DateTimeFormatter.ISO_LOCAL_DATE.format(endDate),
includeNonWorkingDays: includeNonWorkingDays,
planItemId : issue.id,
planItemType : 'ISSUE',
secondsPerDay : issue.estimate,
start : DateTimeFormatter.ISO_LOCAL_DATE.format(today),
startTime : startTime
]), 'application/json')
request.execute()
I'm working on updating the Library Script with this additional variable, as well as make it clearer to use the User's Key for assigneeKey.
If anyone's wondering, you can retrieve the User's Key with the following in the Script Console:
import com.atlassian.jira.component.ComponentAccessor log.warn ComponentAccessor.userManager.getUser('exampleUsername').getKey()
Hope this helps! :D
Kind regards,
Olly
Hi @baz89103
The listener appears to be complaining about the event. Could you please confirm which event you have configured for it?
It would be helpful if you could share a screenshot of the Listener configuration.
I am looking forward to your clarification.
Thank you and Kind regards,
Ram
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Thanks for the reply @Ram Kumar Aravindakshan _Adaptavist_
I have configured Issue assigned event, adding snapshots
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
So basically you need to debug why the endpoint gives you a 400
com.atlassian.sal.api.net.ResponseStatusException: Unexpected response received. Status code: 400
at com.atlassian.sal.core.net.HttpClientRequest.lambda$execute
...
at Script1.run(Script1.groovy:59)
Where groovy:59 refers to your line, the last command in this stack is an execute closure. Putting 2 and 2 together this refers to the method 'requet.execute()' in your code.
So, you're sending data to the endpoint, and it returns 400 as a reply, and for whatever reason the http client underneath the "request" throws an exception because of it.
I can't tell you why that is, but probably something in that json is no likey. You should otherwise get a 403 or something if it was not authenticated, but if it gives 400 then the request went through, got to the endpoint, but the endpoint decided something is invalid, so again this makes me think it's something with the data.
So personally I would just take that code apart and only debug the http post, then put it back after finding the flaw.
You also might want to compare the json data with the actual documentation - https://www.tempo.io/server-api-documentation/planner#tag/Plan/operation/postPlan
The script in library was written at some point in time, possibly for older versions, so it is quite likely you need to update the template for that endpoint to work.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Thanks for your reply, I am new to writing scripts, it will be really helpful if you could point or share some link or docs so that i i can debug this.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
I have changed assigneekey to one of our admin account is this something i am doing correct or it should be the 'admin' itself. After the change error is different.
2023-05-24 14:30:36,538 ERROR [runner.AbstractScriptListener]: ************************************************************************************* 2023-05-24 14:30:36,554 ERROR [runner.AbstractScriptListener]: Script function failed on event: com.atlassian.jira.event.issue.IssueEvent, file: null com.atlassian.sal.api.net.ResponseStatusException: Unexpected response received. Status code: 400 at com.atlassian.sal.core.net.HttpClientRequest.lambda$execute$0(HttpClientRequest.java:77) at com.atlassian.sal.core.net.HttpClientRequest.executeAndReturn(HttpClientRequest.java:102) at com.atlassian.sal.core.net.HttpClientTrustedRequest.executeAndReturn(HttpClientTrustedRequest.java:53) at com.atlassian.sal.core.net.HttpClientRequest.execute(HttpClientRequest.java:75) at com.atlassian.sal.api.net.Request$execute$0.call(Unknown Source) at Script2.run(Script2.groovy:59)
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Well it's this part:
def url = applicationProperties.getBaseUrl(UrlMode.CANONICAL) + '/rest/tempo-planning/1/plan'
def request = trustedRequestFactory.createTrustedRequest(Request.MethodType.POST, url)
def host = new URIBuilder(url).host
request.addTrustedTokenAuthentication(host)
request.setRequestBody(JsonOutput.toJson([
planItemType : 'ISSUE',
planItemId : issue.id,
assigneeKey : 'admin',
start : DateTimeFormatter.ISO_LOCAL_DATE.format(today),
startTime : startTime,
day : DateTimeFormatter.ISO_LOCAL_DATE.format(today),
end : DateTimeFormatter.ISO_LOCAL_DATE.format(endDate),
includeNonWorkingDays: includeNonWorkingDays,
secondsPerDay : issue.estimate
]), 'application/json')
request.execute() // <---- this gives 400 Bad Request
So I made a few adjustments to fit this into script console so that I can test it:
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.sal.api.ApplicationProperties
import com.atlassian.sal.api.UrlMode
import com.atlassian.sal.api.net.Request
import com.atlassian.sal.api.net.TrustedRequestFactory
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import groovy.json.JsonOutput
import groovyx.net.http.URIBuilder
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@PluginModule
ApplicationProperties applicationProperties
@PluginModule
TrustedRequestFactory trustedRequestFactory
// Default start time
final startTime = '09:00'
// Weekends and holidays are not included by default. If a plan needs to be created on weekends and holidays, set this to "true"
final includeNonWorkingDays = false
final today = LocalDate.now()
//def issue = event.issue ; use a specific test issue in console
def issue = ComponentAccessor.getIssueManager().getIssueObject("SENDHELP-1")
def endDate = issue.dueDate?.toLocalDateTime()?.toLocalDate()
// Do nothing if the issue has been unassigned
if (!issue.assignee) {
return
}
if (!(issue.estimate && endDate)) {
log.error('Issue requires both an estimate and a due date. Plan not created')
return
}
def url = applicationProperties.getBaseUrl(UrlMode.CANONICAL) + '/rest/tempo-planning/1/plan'
def request = trustedRequestFactory.createTrustedRequest(Request.MethodType.POST, url)
def host = new URIBuilder(url).host
request.addTrustedTokenAuthentication(host)
/*
BODY log to console so we can see what's in it
*/
def json = JsonOutput.toJson([
planItemType : 'ISSUE',
planItemId : issue.id,
assigneeKey : 'admin',
start : DateTimeFormatter.ISO_LOCAL_DATE.format(today),
startTime : startTime,
day : DateTimeFormatter.ISO_LOCAL_DATE.format(today),
end : DateTimeFormatter.ISO_LOCAL_DATE.format(endDate),
includeNonWorkingDays: includeNonWorkingDays,
secondsPerDay : issue.estimate
])
log.warn(json.toString())
request.setRequestBody(json, 'application/json')
request.execute()
You can just change the issue key, everything else is fine, put it into console, run it, and then switch to Logs to see what's in the json before it's sent to the endpoint.
In my test case, I get:
{"planItemType":"ISSUE","planItemId":3086710,"assigneeKey":"admin","start":"2023-05-24","startTime":"09:00","day":"2023-05-24","end":"2023-05-25","includeNonWorkingDays":false,"secondsPerDay":28800}
Now, compare this with the documentation - https://www.tempo.io/server-api-documentation/planner#tag/Plan/operation/postPlan
What I see from this is:
Otherwise all other required fields are there, in right format.
Thus, I reckon you just need to add this to the json:
JsonOutput.toJson([
...
assigneeType: 'string',
...
])
Which seems a little weird.. but that's according to what I see in the documentation. Probably this required parameter was added at some point, but the library contains old code.
Also, note I can't test it if it really does work - I don't have tempo, this is just coding on paper.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
As for the assigneeKey, if it really is key, remember not to confuse it with the username. Jira has this beautiful age old conflict of what identifier to use..
You can get the user key from:
<baseurl>/rest/api/2/user?username=<username>
Old users will have same key as username, but new users will have something like "JIRAUSER12345" as key!
And if you're working with the issue assignee, like this:
def userkey = issue.getAssignee()?.getKey()
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
added the code to console, still getting the same error:
***********************************************************************************
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hm, then perhaps it's with the TrustedRequestFactory.. Frankly, I don't have a good idea anymore.. If the data fits the documentation, the request is authenticated, sounds like a bug in that endpoint.
Have you checked if tempo logs anything to the logs when you make the request? Something that could hint to what it doesn't like.
Alternatively I would try to create a new HttpClient, provide base64 auth to it, and then read the response (maybe it says what it doesn't like), since we cannot see the response because it throws an exception with the request factory client.
The documentation also states:
HTTP: basicAuth
HTTP Authorization Scheme: basic
So it's entirely possible the vendor has implemented some dog$%&!ery which is unable to properly handle requests from trusted apps. Maybe it really does only work with basic auth.. Well we can test that:
import org.apache.http.HttpEntity
import org.apache.http.HttpHeaders
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClientBuilder
import groovy.json.JsonOutput
import org.apache.http.util.EntityUtils
final String name = "admin"
final String pwd = "12345"
String base64auth = new String(Base64.getEncoder().encode((name + ":" + pwd).getBytes()))
CloseableHttpClient closeableHttpClient = HttpClientBuilder.create().build()
try {
HttpPost httpPost = new HttpPost("https://myurl.com/rest/tempo-planning/1/plan")
httpPost.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + base64auth)
def json = JsonOutput.toJson([
planItemType : 'ISSUE',
planItemId : /* set me */,
assigneeKey : 'admin',
assigneeType: 'string',
start : /* set me */,
startTime : /* set me */,
day : /* set me */,
end : /* set me */,
includeNonWorkingDays: /* set me */,
secondsPerDay : /* set me */
])
// P.S.: mind the value types, quotes for Strings, no quotes for integers
httpPost.setEntity(new StringEntity(json))
httpPost.setHeader(HttpHeaders.CONTENT_TYPE, "application/json")
CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(httpPost)
try {
int statusCode = closeableHttpResponse.getStatusLine().getStatusCode()
String responseBody = null
HttpEntity entity = closeableHttpResponse.getEntity()
if (entity != null)
responseBody = EntityUtils.toString(entity)
return statusCode + "<br>" + responseBody
}
finally {
closeableHttpResponse.close()
}
}
finally {
closeableHttpClient.close()
}
Again, untested since.. no tempo, but this should theoretically work. Just set the basic auth credentials and fill out the json template with some specific test data (which you can copy from previous test since we logged the contents).
NOTE - scriptrunner does store audit logs, so if you want to NOT have those credentials stored anywhere, you need to disable this logging: https://docs.adaptavist.com/sr4js/latest/best-practices/logging/audit-logging
And re-enable it back when you're done since it's generally pretty useful.
Now, the idea with this is to test whether the problem is with TrustedRequestFactory (and the endpoint being dumb and not knowing how to handle it), and whether we can get it to work if we adhere to that "basicAuth" requirement they mention.
Either, this works, or it doesn't and we may see a message in response, rather than the exception which hides it.
Or, do also verify that tempo doesn't log anything in {jira_home}/log/atlassian-jira.log - maybe it contains a hint when the endpoint returns 400.
If this still doesn't work, I would probably open a ticket with Tempo so they can clarify / reproduce / explain what the issue is.
Edit: fixed httpclient constructor
(this thread is becoming a bit of a code camp lol)
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Thank You so much for the support, it was really a code camp for me :)
The script is working, Plans are getting created, However, looks like one issue with the logic. Plans are getting assigned each day for the total estimated hours ex: if i am setting original estimate as 2week it will create plan for 80hrs each day till the due date, if original estimate is 1week, it will create plans for 40hrs each day to the user.
Following script:
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
This really got me to learn many things, Thanks for all the support.
The script is creating the plans now, however, looks like there is one small logical error due to which Plans are getting assigned each day for the total estimated hours ex: if i am setting original estimate as 2week it will create plan for 80hrs each day till the due date, if original estimate is 1week, it will create plans for 40hrs each day to the user.
Script :
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
It sounds like this in json
secondsPerDay : issue.estimate
but I'm not familiar with tempo so I don't know how it works. Not sure what you want to configure there but I guess secondsPerDay param sounds the closest to what you describe?
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
@Radek Dostál Yeah, I tried setting seconds per the day, however still it is assigning total estimations each day.
Thank You
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.