Salesforce Jira Integration - callback url

Alec Jasanovsky
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 8, 2018

Howdy Guys and Gals,

 

I'm tasked with integrating our Salesforce and Jira cloud instances. I'm following this tutorial: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/quickstart_oauth.htm

For example, I want to start this by accomplishing a simple trigger for emails sent to salesforce.support@mycompany.com to send to jira as tickets.

 

Right now i'm setting up the Connected App in salesforce but I'm not sure what my callback url should be - I would assume something in jira with an api token but I'm not sure how i'm suppose to generate this URL.

 

Any help would be greatly appreciated! 

 

3 answers

1 accepted

0 votes
Answer accepted
Alec Jasanovsky
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 9, 2018

Figured it out. 

 

I don't even need to set this up on the salesforce side since I'll only be using callouts to jira. 

 

Would have appreciated someone simply specifying callback urls as the different ways to preform certain tasks w/ examples. 

Alec Jasanovsky
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 9, 2018

Actually - (for anyone else using this for reference) you do need to do this on Salesforce side for the purpose of authentication on jira's side. When setting up the application links in jira, you need consumer keys and whatnot - all of which are generated from the salesforce "connected app". 

Vaishali Kondhare April 18, 2019

Hey Alec,

I am trying to do something similar.

Sync cases from SFDC to JIRA and vice-versa.

Could you please let me know what value goes in the callback url of the connected app?

Also, how do you configure the push from JIRA to SFDC?

 

Any help is appreciated!

0 votes
Prabu Sivagnanam May 23, 2019

Hi Alec,

 

I trying to do the code in JIRA also,  for example i am trying to implement a workflow post function to push a JIRA issue to salesforce.If you have any other mechanism to push JIRA issue to salesforce also let me know.

 

can you give me the code for pushing the JIRA issue to salesforce ?

 

Regards,

Prabu

email: prabu.mxian@gmail.com

Alec Jasanovsky
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
May 23, 2019

Hi Pradu!

 

Sorry for the terrible delay - I just switched jobs! (=D)

 

Here's my code, I'll explain under it: 

 

global class caseEmailClass {
public Opportunity m_opt { get; private set; }
public List<Opportunity_Buy__c> selllineList { get; private set; }
Public Boolean PBFlag{get;set;}
public String recordId; 
public String accountName;
Public User u;
public String link = 'https://test.com';
public caseEmailClass(ApexPages.StandardController stdController)
{
   
     final ApexPages.StandardController m_stdController; 

    
    //public Slack_ChannelUserSelectController(ApexPages.StandardController stdController)

     recordId = System.currentPageReference().getParameters().get('ID'); 
    
     m_stdController = stdController;
        try
          {
              m_opt = (Opportunity)m_stdController.getRecord();
              m_opt = [select Id,Brand__c,Jira_Key__c,CurrencyIsoCode,Biz_Analyst__c,Opportunity_Number__c,Gross_Budget__c,Account.Name, Name from Opportunity where Id =: recordId];         
              selllineList = [select Id, Planned_Payable_Impressions__c from Opportunity_Buy__c where opportunity__c =: recordId];
              
              
                        
          }
          
          
        
          catch( exception e )
          {
            ApexPages.addMessage( new ApexPages.Message(ApexPages.Severity.ERROR, 'Error of geting parent ' + e) );
              return;
          }
}

    public PageReference makeCallout (){
        
    
    
    //@future(callout=true)
        u = [select Id, Name from User where User.Id=:m_opt.Biz_Analyst__c];
      
        PBFlag = true; // make ticket submitted visable
        system.debug('test during first callout---------------' + m_opt.Biz_Analyst__c);
        system.debug('checking what sell lines object looks like at place 1---------------' + u.Name);
        
        HttpRequest request = new HttpRequest();
        request.setMethod('POST');
        
        String userpassStr = 'username@domain.com:XG1SkjcR7fYR1eYNlo7D72C7';//jira email + jira api token
        Blob userpassBlob = Blob.valueOf(userpassStr);
        
        string encodedStr = EncodingUtil.base64Encode(userpassBlob);
 
        String authorizationHeader = 'Basic ' + 'YWxlYy5qYXNhbm92c2t5QHhheGlzLmNvbTpHb2Rpc25vdGdyZWF0MSEhMQ==';
        
        request.setHeader('Authorization', authorizationHeader);
        request.setHeader('Content-Type','application/json');
        
        
        String endpoint = 'https://dev-envir.atlassian.net/rest/api/2/issue';       
        
        request.setBody('{"fields": {"project":{"key": "SAL"},"summary":" ' + m_opt.Name + ' ","customfield_10051":" ' + m_opt.Opportunity_Number__c + ' ","customfield_10055":" ' + u.Name + ' ","customfield_10049":" ' + m_opt.Brand__c + ' ","customfield_10063":" ' + selllineList + ' ","customfield_10048":" ' + m_opt.Account.Name + ' ","customfield_10052":" ' + m_opt.Gross_Budget__c + ' ","description": "test","issuetype": {"name": "Bug"}}}');
        
        
        request.setEndPoint(endpoint);
       
       // corresponding jira key 
       String jiraKey = '';
       
       
       if (m_opt.Jira_Key__c == null){
        // Send the HTTP request and get the response.
        HttpResponse response = new HTTP().send(request);
        // Parse the JSON response
        if (response.getStatusCode() != 201) {
            System.debug('The status code returned was not expected: ' + response.getStatusCode() + ' ' + response.getStatus());
        } else {
            System.debug('------response is ' + response.getBody());
            JSONParser parser = JSON.createParser(response.getBody());
            
          
             
                while (parser.nextToken() != null) {
                    if (parser.getCurrentName() == 'key'){
                        jiraKey = parser.getText();
                        m_opt.Jira_Key__c = jiraKey;
                        update m_opt;
                
                }
            }
            
        }

       } else{// end of checking for already existing ticket
       // runs if jira key already exists, updates the ticket
           jiraKey = m_opt.Jira_Key__c;   
           
           request.setMethod('PUT');
             
        
           
           endpoint = 'https://dev-envir.atlassian.net/rest/api/2/issue/' + jiraKey;       
        
           
           request.setBody('{"fields": { "summary":" ' + m_opt.Name + ' ","customfield_10064":{"value": "Yes"},"customfield_10070":{"name": "admin"},"customfield_10061":" ' + link + ' ","customfield_10060":" ' + link + ' ","customfield_10059":" ' + link + ' ","customfield_10058":" ' + link + ' ","customfield_10055":" ' + u.Name + ' ","customfield_10051":" ' + m_opt.Opportunity_Number__c + ' ","customfield_10063":" ' + selllineList + ' ","customfield_10052":" ' + m_opt.CurrencyIsoCode + m_opt.Gross_Budget__c + ' ","customfield_10049":" ' + m_opt.Brand__c + ' ","customfield_10048":" ' + m_opt.Account.Name + ' "}}');
           request.setEndPoint(endpoint);
           HttpResponse response = new HTTP().send(request);
           System.debug('------------------- update ticket response-------------' + response.getBody());
       }     
         return null;   
        } // end of method
        
        
            }

 

In order to tell whether the ticket was created already, I just created a new ticket on campaigns called Jira Key - if it was null, then it's yet to be ported to jira so it'll use the POST request call to create a new ticket.

 

If the Jira Key has a value, then it appends to the PUT call and updates the fields based on whatever was updated in SF. 

 

I also used a visual force button to check and submit on click:

 

<apex:page standardcontroller="Opportunity" extensions="caseEmailClass">

<apex:form >
  
<apex:pageBlock > 

Select the Button to submit this Opportunity into Jira at xaxistech.atlassian.net <br></br>

<apex:commandButton action="{!makeCallout}" value="Submit To jira" id="btnUserSearch"/>
<apex:commandButton action="null" value="Pull from Jira" id="pullJira"/>
 


<apex:pageBlockSection id="pageBlock" rendered="{!PBFlag}">

<br> Ticket Submitted </br>

</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>
Prabu Sivagnanam May 23, 2019

Thank you so much Alec! by the way i started learning JIRA code also since my business wants to push the issue from JIRA to SFDC  from JIRA side(or by JIRA Code)...so i'm trying to get JIRA code...

FYI, I'm communicating with another JIRA expert in this link :https://community.atlassian.com/t5/Jira-Core-questions/how-to-start-development-with-JIRA/qaq-p/1086992#M43191

 

Regards,

Prabu

Prabu Sivagnanam June 19, 2019

Hi @Alec Jasanovsky ,

 

I would like to say thank you

Done with my Salesforce -- JIRA integration POC

 

Demo Video of MY POC: https://drive.google.com/file/d/10MsHbK9kHH-0YmI7aaEJv66YkzqdRavp/view?usp=sharing

Salesforce & JIRA Code of my POC : https://drive.google.com/file/d/1sUepNqdIEoo3n9P6yfdI-wDOYxXtb_RE/view?usp=sharing

 

Thank you so much for your guidance!

 

Regards,

Prabu

Chris Dunne
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
September 25, 2019

Hi Prabu,

I am working on something very similar. Would it be possible for you to share the google drive links again - the ones posted are not working.

 

Thanks

Chris

0 votes
Deleted user August 8, 2018

Hi @Alec Jasanovsky,

Perhaps you can consider using Salesforce and Jira Cloud Connector app for the integration between Jira Cloud and Salesforce.

We have documented the full steps on how to configure the Connector in the following documentation.

Feel free to send us an email to support@servicerocket.com if you need further assistance for the installation for the Salesforce and Jira Cloud Connector.

Alec Jasanovsky
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 9, 2018

Hi Victor,

 

Thanks but I'm not really interested in using a third party addon. I'd like to create this on my own. 

Like Prabu Sivagnanam likes this
Prabu Sivagnanam May 8, 2019

Hi Alec,

 

Did you complete your  integration with JIRA & SFDC ? i need help.

 

Regards,

Prabu

whatsapp:+91 9080487629

Alec Jasanovsky
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
May 9, 2019

Hi Prabu,

 

I did - where are you stuck? 

Like Prabu Sivagnanam likes this
Prabu Sivagnanam May 10, 2019

Hi Alec,

Thanks for your reply! i'm doing  POC on Salesforce-JIRA integration using REST-API since business dont want to use third-party connectors since they are charging.

For this POC i'm using my developer edition sfdc sandbox & Jira cloud my own account which i created for this POC (most probably the actual implementation will be inbetween SFDC - Jira Server)

Today instead of embedding username & password in the Url, i generated the token in Jira and used that token in apex code, encoded it and assigned it to the Authorization parameter in request header..

now in the response i can see the record is returned from Jira

below is the code i used for connection (i'm not attaching entire class to avoid confusion..Will come back to you if i get any doubt going forward

/************************************************************************/

username = 'prabu.s@mookambikainfo.com';
//username = 'Prabu Sivagnanam';
//password = 'Pass@123word';
password = 'f9rHPdeHiFgDCq8RPgr0591A'; // f9rHPdeHiFgDCq8RPgr0591A is the security token from Atlassian account profile --> Manage your Account-->Security-->API token-->Create and manage API tokens--> Manage your account-->API tokens
String jira_host = 'https://prabusmi.atlassian.net';
String issue = 'GHXTES-2';
String auth_header = 'Basic ' + EncodingUtil.base64Encode(Blob.valueOf(username + ':' + password));


String url = jira_host + '/rest/api/2/issue/' + issue;
url = 'https://prabusmi.atlassian.net/rest/api/2/search?jql='+ '&fields=' + fieldsQuery + '&startAt='+ startAt +'&maxResults='+maxResults;

Http http = new Http();
HttpRequest req = new HttpRequest();
req.setHeader('Content-Type', 'application/json');
req.setHeader('Accept','application/json');
req.setHeader('Authorization', auth_header);
req.setHeader('X-Atlassian-Token', 'nocheck');
req.setMethod('GET');
req.setEndpoint(url);

HttpResponse res = http.send(req); // commented by Prabu

// Log the JSON content
System.debug('*************************JSON Response Start******************************************');
System.debug('JSON Response: ' + res.getBody()); // commented by Prabu
System.debug('*************************JSON Response End******************************************');
String JSONContent = res.getBody(); // commented by Prabu

supportIssues = new List<JiraIssue__c>();

JSONParser parserJira = JSON.createParser(JSONContent);
System.debug('*************************Outside While*****************************************');
while (parserJira.nextToken() != null)
{
System.debug('*************************Inside While*****************************************');
// Start at the array of issues.
if (parserJira.getCurrentToken() == JSONToken.START_ARRAY) {
System.debug('*************************Inside if*****************************************');
while (parserJira.nextToken() != null) {
System.debug('*************************Inside inner While*****************************************');
if ((parserJira .getCurrentToken() == JSONToken.START_OBJECT)){
System.debug('*************************Inside inner if*****************************************');
issue jiraIssue = (issue)parserJira.readValueAs(issue.class);
System.debug('Inside inner if jiraIssue : ' + jiraIssue);
JiraIssue__c supportIssue = new JiraIssue__c();
supportIssue.Issue_Key__c = jiraIssue.key;
System.debug('Inside inner if jiraIssue.key : ' + jiraIssue.key);
System.debug('Inside inner if jiraIssue.key : ' + jiraIssue.fields);
supportIssue.Summary__c = jiraIssue.fields.summary;
System.debug('Inside inner if jiraIssue.fields.summary : ' + jiraIssue.fields.summary);
supportIssues.add(supportIssue);
System.debug('supportIssues : ' + supportIssues);
}


}
}
} //insert supportIssues; //added by Prabu

}

/****************************************************************************/

 

output in debug log:

13:16:37.42 (1051671363)|USER_DEBUG|[328]|DEBUG|supportIssues  : (JiraIssue__c:{Issue_Key__c=GHXTES-2, Summary__c=test bug}, JiraIssue__c:{Issue_Key__c=GHXTES-1, Summary__c=Test issue})
Prabu Sivagnanam May 10, 2019

below is my complete code (this is not complete product just started my poc & getting the reponse from JIRA thats it)

 

public class JiraIntegrationController {

public List<JiraIssue__c> supportIssues {get;set;}
public String issueKey{get;set;}
string searchKey = '';
public string username;
public string password;

public List<JiraIssue__c> getSupportIssues(){
return supportIssues;
}

public integer startAt = 0;
public integer maxResults= 10;

public string fieldsQuery = 'key,'+
'assignee,'+
'project,'+
'summary,'+
'created,'+
'updated,'+
'priority,'+
'status';
/******************Issue Class*****************/
//ISSUE CLASS GOES HERE. REMOVED FOR BREVITY
public class issue{
public string expand;
public fields fields;
public string id;
public string key;
public string self;
}
public class Status {
public String self;
public String description;
public String iconUrl;
public String name;
public String id;
public StatusCategory statusCategory;
}

public class Assignee {
public String self;
public String name;
public String key;
public String emailAddress;
public AvatarUrls avatarUrls;
public String displayName;
public Boolean active;
public String timeZone;
}

public class Comment {
public Integer startAt;
public Integer maxResults;
public Integer total;
public List<Comments> comments;
}

public String expand;
public String id;
public String self;
public String key;
public Fields fields;

public class links {
public String self;
}

public class Priority {
public String self;
public String iconUrl;
public String name;
public String id;
}

public class Comments {
public String self;
public String id;
public Assignee author;
public String body;
public Assignee updateAuthor;
public String created;
public String updated;
}

public class Aggregateprogress {
public Integer progress;
public Integer total;
}

public class Watches {
public String self;
public Integer watchCount;
public Boolean isWatching;
}

public class StartTime {
public String iso8601;
public String friendly;
public Long epochMillis;
}

public class Customfield_10025 {
public Integer id;
public String name;
public links links;
public List<FixVersions> completedCycles;
public OngoingCycle ongoingCycle;
}

public class Customfield_10026 {
public String errorMessage;
}

public class Customfield_10015 {
public String errorMessage;
public I18nErrorMessage i18nErrorMessage;
}

public class OngoingCycle {
public StartTime startTime;
public StartTime breachTime;
public Boolean breached;
public Boolean paused;
public Boolean withinCalendarHours;
public GoalDuration goalDuration;
public GoalDuration elapsedTime;
public GoalDuration remainingTime;
}

public class Project {
public String self;
public String id;
public String key;
public String name;
public AvatarUrls avatarUrls;
}

public class StatusCategory {
public String self;
public Integer id;
public String key;
public String colorName;
public String name;
}

public class Worklog {
public Integer startAt;
public Integer maxResults;
public Integer total;
public List<FixVersions> worklogs;
}

public class Fields {
public Issuetype issuetype;
public Object timespent;
public Project project;
public List<FixVersions> fixVersions;
public Object aggregatetimespent;
public Object resolution;
public Object resolutiondate;
public Integer workratio;
public String lastViewed;
public Watches watches;
public String created;
public Object customfield_10020;
public Object customfield_10021;
public Priority priority;
public Customfield_10025 customfield_10025;
public List<FixVersions> labels;
public Customfield_10026 customfield_10026;
public List<FixVersions> customfield_10016;
public Object customfield_10017;
public Object customfield_10018;
public String customfield_10019;
public Object timeestimate;
public Object aggregatetimeoriginalestimate;
public List<FixVersions> versions;
public List<FixVersions> issuelinks;
public Assignee assignee;
public String updated;
public Status status;
public List<FixVersions> components;
public Object timeoriginalestimate;
public String description;
public Object customfield_10010;
public Object customfield_10011;
public Object customfield_10012;
public String customfield_10013;
public Object customfield_10014;
public FixVersions timetracking;
public Customfield_10015 customfield_10015;
public Object customfield_10005;
public Object customfield_10006;
public Object customfield_10007;
public Object customfield_10008;
public List<FixVersions> attachment;
public Object customfield_10009;
public Object aggregatetimeestimate;
public String summary;
public Assignee creator;
public List<FixVersions> subtasks;
public Assignee reporter;
public Object customfield_10000;
public Aggregateprogress aggregateprogress;
public Object customfield_10001;
public Object customfield_10002;
public Object customfield_10003;
public Object customfield_10004;
public Object environment;
public String duedate;
public Aggregateprogress progress;
public Comment comment;
public Votes votes;
public Worklog worklog;
}

public class FixVersions {
}

public class I18nErrorMessage {
public String i18nKey;
public List<FixVersions> parameters;
}

public class Issuetype {
public String self;
public String id;
public String description;
public String iconUrl;
public String name;
public Boolean subtask;
}

public class AvatarUrls {
public String fortyEight;
public String twentyFour;
public String sixteen;
public String thirtyTwo;
}

public class GoalDuration {
public Integer millis;
public String friendly;
}

public class Votes {
public String self;
public Integer votes;
public Boolean hasVoted;
}


/**********************************************/

public void sameAsJIC(){
username = 'prabu.s@mookambikainfo.com';
//username = 'Prabu Sivagnanam';
//password = 'Pass@123word';
password = 'f9rHPdeHiFgDCq8RPgr0591A'; // f9rHPdeHiFgDCq8RPgr0591A is the security token from Atlassian account profile --> Manage your Account-->Security-->API token-->Create and manage API tokens--> Manage your account-->API tokens
String jira_host = 'https://prabusmi.atlassian.net';
String issue = 'GHXTES-2';
String auth_header = 'Basic ' + EncodingUtil.base64Encode(Blob.valueOf(username + ':' + password));


String url = jira_host + '/rest/api/2/issue/' + issue;
url = 'https://prabusmi.atlassian.net/rest/api/2/search?jql='+ '&fields=' + fieldsQuery + '&startAt='+ startAt +'&maxResults='+maxResults;

Http http = new Http();
HttpRequest req = new HttpRequest();
req.setHeader('Content-Type', 'application/json');
req.setHeader('Accept','application/json');
req.setHeader('Authorization', auth_header);
req.setHeader('X-Atlassian-Token', 'nocheck');
req.setMethod('GET');
req.setEndpoint(url);

HttpResponse res = http.send(req); // commented by Prabu

// Log the JSON content
System.debug('*************************JSON Response Start******************************************');
System.debug('JSON Response: ' + res.getBody()); // commented by Prabu
System.debug('*************************JSON Response End******************************************');
String JSONContent = res.getBody(); // commented by Prabu

supportIssues = new List<JiraIssue__c>();

JSONParser parserJira = JSON.createParser(JSONContent);
System.debug('*************************Outside While*****************************************');
while (parserJira.nextToken() != null)
{
System.debug('*************************Inside While*****************************************');
// Start at the array of issues.
if (parserJira.getCurrentToken() == JSONToken.START_ARRAY) {
System.debug('*************************Inside if*****************************************');
while (parserJira.nextToken() != null) {
System.debug('*************************Inside inner While*****************************************');
if ((parserJira .getCurrentToken() == JSONToken.START_OBJECT)){
System.debug('*************************Inside inner if*****************************************');
issue jiraIssue = (issue)parserJira.readValueAs(issue.class);
System.debug('Inside inner if jiraIssue : ' + jiraIssue);
JiraIssue__c supportIssue = new JiraIssue__c();
supportIssue.Issue_Key__c = jiraIssue.key;
System.debug('Inside inner if jiraIssue.key : ' + jiraIssue.key);
System.debug('Inside inner if jiraIssue.key : ' + jiraIssue.fields);
supportIssue.Summary__c = jiraIssue.fields.summary;
System.debug('Inside inner if jiraIssue.fields.summary : ' + jiraIssue.fields.summary);
supportIssues.add(supportIssue);
System.debug('supportIssues : ' + supportIssues);
}


}
}
} //insert supportIssues; //added by Prabu

}

public JiraIntegrationController() {
Jira_Adapter__c[] jiraUsers = [SELECT Username__c, Password__c FROM Jira_Adapter__c];
if(jiraUsers.size() > 0){
System.debug('jiraUsers: ' + jiraUsers);
username = jiraUsers[0].Username__c;
password = jiraUsers[0].Password__c;
}
sameAsJIC();
}
}

Prabu Sivagnanam May 10, 2019

Can you give the mechanisms and the functionalities you added for SFDC-JIRA integration ?  i mean the  flow because here i am only going to design & Develop..i mean the blueprint of your integration which you developed

 

Since you already developed this your inputs will helpful for me . if possible give me the code also

Prabu Sivagnanam May 19, 2019

Hi Alec,

 

as of now i am doing POC on Salesforce & JIRA Integration (in between my Salesforce & my own Personal Atlassian Jira Cloud)

Note- I made a integration between 'issue' object in JIRA & the object 'JiraIssue' which i created in SFDC.

i just did 2 things in salesfoce

****1.Pushing issue to the JIRA , From Salesforce****

Trigger on salesforce object 'JiraIssue' - whenever this record created in saleforce , i am creating the same issue in Jira by using REST API Call (url = jira_host + '/rest/api/2/issue/')

****2.Pulling issue from JIRA, From Salesforce****

Running a batch job in Salesforce periodically and pulling the issue from JIRA and inserting or updating in salesforce , by using the REST API Call (url = 'https://prabusmi.atlassian.net/rest/api/2/search?jql='+ '&fields=' + fieldsQuery + '&startAt='+ startAt +'&maxResults='+maxResults;)


But business fine with the step 1, But in the step 2 they feeling it better to pushing the issue from JIRA whenever update happens in issue record instead of pulling from salesforce by batch,

 

so in your project you coded in JIRA side also ?

 

Regards,

Prabu

Lex Bolden November 7, 2019

Hi Prabu

Suggest an answer

Log in or Sign up to answer
TAGS
AUG Leaders

Atlassian Community Events