Monday, November 7, 2016

Git simple merge conflict resolution

I was trying to merge something from feature/sonar_fixes to dev, but dev had changes from someone else. This is what I had to do to resolve conflicts:

Step 1: Fetch the changes (saving the target branch as FETCH_HEAD).


git fetch origin dev



Step 2: Checkout the source branch and merge in the changes from the target branch. Resolve conflicts.


git checkout feature/sonar_fixes
git merge FETCH_HEAD


Thar be merge conflicts here: Open the files and look for FETCH_HEAD

Step 3: After the merge conflicts are resolved, stage the changes accordingly, commit the changes and push.


git add <file name with conflicts>
git commit
git push origin HEAD

Sunday, July 31, 2016

Datatables events not working on second page

I've revisited the datatables library for jquery. The new 1.10 version has a lot of improvements.

But I immediately ran into some trouble. After the easy zero config initialization I added a select box to one of the cells, and then added a a jquery "on change" event handler to that select box.

The problem is this works on the first page, but fails on subsequent pages, or if the page was ordered or filtered etc. This is specified in the "My events don't work on the second page" section of the Datatables FAQ and basically happens because Datatables manipulates the DOM.

My solution was to re-assign the event handler on each draw event, like this:





var table;

$(document).ready(function () {

    table = $('#example')
        .on('draw.dt', function () {
            handleSelectEvent();
        })

        .DataTable();
});


var handleSelectEvent = function () {
    console.log('in handleselect fn');

    $('#example select').off('change')
        .on('change', function () {

        // do whatever

    });

}


If I didn't specifically unbind the event handler (with the off), the event handler would get loaded loaded on each draw of the table. So I would start with 1 call, but then end up have 2, 3,4 calls each time I did a reorder, pagination search or so on.

Minutes after I fixed this issue, I found another nice slick way to handle this issue here: http://www.gyrocode.com/articles/jquery-datatables-why-click-event-handler-does-not-work/
which basically say you specify a selector as the second argument for the event handler, in this case a CSS class around the select box.

Wednesday, July 6, 2016

Change file permissions on git

If you push an executable file to git, its likely you wont have the right permissions.


D:\git\app>git ls-tree HEAD
100644 blob 390ce9106bc0e91a631cf345621b35b96bf74d20    flow.sh


As you can see its 644 (Ignore the 100)

Change it to 755:


D:\git\app>git update-index --chmod=+x flow.sh
D:\git\app>git status
On branch feature/app_move
Your branch is up-to-date with 'origin/feature/app_move'.

Changes to be committed:
  (use "git reset HEAD ..." to unstage)

        modified:   flow.sh

D:\git\app>git commit -m "SLUG-11698 : Changing flow.sh file perms"
D:\git\app>git push
D:\git\app>git ls-tree HEAD
100755 blob 390ce9106bc0e91a631cf345621b35b96bf74d20    flow.sh


flow.sh is now executable :)

Thursday, June 23, 2016

Squash several commits in git into one

I had several commits on my branch, all related to the same change. Instead of merging all the commits into master, I wanted to squash all of them into one, and merge the single commit.  Less junk on the master branch that way.

This https://gist.github.com/nicholashagen/2855167 has a shows a very nice way to merge and squash together. But since I use bitbucket(aka stash) and pull requests, the git merge --squash command doesn't work.

I had been looking for a slick way of squashing commits before generating a pull request, and recently found it.

I had 4 commits I wanted to squash (all the ones with the comment "Add sonar jobs for all apps") on the branch feature/add-sonar:

git log shows me the commit IDs


/git/docker-jenkins-master  on  feature/add-sonar * $ git log
commit 02041bddd50de5d5bd40bd3146ae44323fabb6a7
Author: Somaiah Kumbera <somaiah.kumbera@somewhere.com>
Date:   Thu Jun 23 08:50:42 2016 +0200

    Add sonar jobs for all apps

commit 17f11e416982bf486da2db732b3e1de580c59877
Author: Somaiah Kumbera <somaiah.kumbera@somewhere.com>
Date:   Wed Jun 22 14:15:35 2016 +0200

    Add sonar jobs for all apps

commit cc396d605bfe0db279d2beb11833880b3365edc7
Author: Somaiah Kumbera <somaiah.kumbera@somewhere.com>
Date:   Wed Jun 22 13:07:43 2016 +0200

    Add sonar jobs for all apps

commit affcae8432efef916d353dc4f204d4507422eebd
Author: Somaiah Kumbera <somaiah.kumbera@somewhere.com>
Date:   Wed Jun 22 13:04:52 2016 +0200

    Add sonar jobs for all apps

commit 29fa6593a0bc619d269c4b84b8f857b2dbe466e8
Merge: 78f0f8a 9751714
Author: Someone Else <someone.else@somewhere.com>
Date:   Wed Jun 1 10:29:49 2016 +0200

   Something unrelated


git rebase -i allows me to squash the ones from the current point (HEAD) back to the last n commits (in my case n = 4)



/git/docker-jenkins-master  on  feature/add-sonar * $ git rebase -i HEAD~4


At this point I got an editor open up with the list of the last 4 commits, so I could do something with them:



pick affcae8 Add sonar jobs for all apps
pick cc396d6 Add sonar jobs for all apps
pick 17f11e4 Add sonar jobs for all apps
pick 02041bd Add sonar jobs for all apps



Notice that the commits are in reverse chronological order - so the oldest one shows up first. I wanted to pick the oldest one, and squash the others. So I changed the text to look like this:



pick affcae8 Add sonar jobs for all apps
f cc396d6 Add sonar jobs for all apps
f 17f11e4 Add sonar jobs for all apps
f 02041bd Add sonar jobs for all apps


Ctrl-X (to save)
y (to confirm)

The rebase is successful:



[detached HEAD 4d51329] Add sonar jobs for all apps
 2 files changed, 89 insertions(+), 1 deletion(-)
 create mode 100644 path/dir/filename.xml
Successfully rebased and updated refs/heads/feature/add-sonar.



Now all you have to do is push. But you need to force push.



/git/docker-jenkins-master  on  feature/add-sonar * $ git push --force


Now git log shows only 1 commit:



/git/docker-jenkins-master  on  feature/add-sonar  $ git log
commit 4d513295b564ad253c6d738496163c7d392af3d6
Author: Somaiah Kumbera <somaiah.kumbera@somewhere.com>
Date:   Wed Jun 22 13:04:52 2016 +0200

    Add sonar jobs for all apps

commit 29fa6593a0bc619d269c4b84b8f857b2dbe466e8
Merge: 78f0f8a 9751714
Author: Someone Else <someone.else@somewhere.com>
Date:   Wed Jun 1 10:29:49 2016 +0200

   Something unrelated


Now you can merge that single commit in any way you choose :)

Thursday, June 9, 2016

Sonar maven-surefire-plugin quirks

I was integrating my maven multi module project with sonar, and ran into some quirky behavior:

1. After some config changes I suddenly started seeing


Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (SONAR.PROJECTS_KEE) violated

The error may involve org.sonar.db.component.ComponentMapper.insert-Inline

The error occurred while setting parameters

Cause: java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (SONAR.PROJECTS_KEE) violated

Turns out I was calling mvn sonar:sonar with -Dsonar.exclusions and this exclusion was also set on the sonar server, under Administration > General Settings > Analysis Scope > Files as a source file exclusion.

Apparently excluding the same regex on the server and with the maven argument makes Sonar choke.

You can read the full explanation on my stackoverflow question


2. Some packages did not get code coverage with maven-surefire-plugin. Try as I might I always had 0% coverage on these packages. You can view my set up here. The maven output showed that the tests were run, and surefire did not produce any warnings.

It turned out that maven-surefire-plugin was included as a  plugin in the build section in both the parent pom and the child pom. Hence the surefire plugin ran in the child module, but the resultant coverage, which was taken from the file specified by the sonar.jacoco.reportPath property did have these tests. Makes sense now of course, but it was a tricky bug to find :)

Sunday, June 5, 2016

REST API calls with RestTemplate and Basic authorizarion

I was playing around with RestTemplate, and it seems like a nice way to make REST calls. It also allows for easy handling of basic HTTP authorization.

Just add the username and password to the header:

String plainCreds = username + ":" + password;
byte[] base64CredsBytes = Base64.getEncoder().encode(plainCreds.getBytes());
String base64Creds = new String(base64CredsBytes);

HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + base64Creds);

For GET requests thats about all you need. A simple call can be made like this:

HttpEntity<?> requestEntity = new HttpEntity(httpHeaders);
return restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);

POST request are not very much more complicated:

httpHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> requestEntity = new HttpEntity<String>(createIssueJSON, httpHeaders);
return restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);

The entire code looks like this:

import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;

import java.util.Base64;

public class Application {

    private static final String username = "user@domain.com";
    private static final String password = "password";
    private static final String jiraBaseURL = "https://jira.domain.com/rest/api/2/";
    private RestTemplate restTemplate;
    private HttpHeaders httpHeaders;

    public Application() {
        restTemplate = new RestTemplate();
        httpHeaders = createHeadersWithAuthentication();
    }

    private HttpHeaders createHeadersWithAuthentication() {
        String plainCreds = username + ":" + password;
        byte[] base64CredsBytes = Base64.getEncoder().encode(plainCreds.getBytes());
        String base64Creds = new String(base64CredsBytes);

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Basic " + base64Creds);

        return headers;
    }

    public ResponseEntity getIssue(String issueId) {
        String url = jiraBaseURL + "issue/" + issueId;

        HttpEntity<?> requestEntity = new HttpEntity(httpHeaders);
        return restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
    }

    public ResponseEntity createIssue(String key, String summary, String description, String issueType) {
        String createIssueJSON = createCreateIssueJSON(key, summary, description, issueType);

        String url = jiraBaseURL + "issue";

        httpHeaders.setContentType(MediaType.APPLICATION_JSON);

        HttpEntity<String> requestEntity = new HttpEntity<String>(createIssueJSON, httpHeaders);

        return restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);

    }

    private String createCreateIssueJSON(String key, String summary, String description, String issueType) {
        String createIssueJSON = "{\"fields\":{\"project\":{\"key\":\"$KEY\"},\"summary\":\"$SUMMARY\",\"description\":\"$DESCRIPTION\",\"issuetype\": {\"name\": \"$ISSUETYPE\"}}}";

        createIssueJSON = createIssueJSON.replace("$KEY", key);
        createIssueJSON = createIssueJSON.replace("$SUMMARY", summary);
        createIssueJSON = createIssueJSON.replace("$DESCRIPTION", description);
        return createIssueJSON.replace("$ISSUETYPE", issueType);
    }

}


Download the code and tests from github: https://github.com/somaiah/restTemplate

Sunday, May 15, 2016

Jira + Confluence = < 3

I'm a big fan of Atlassian products, in particular Jira and Confluence, and I've already mentioned how helpful they have been in my previous projects

If you're not already using them, you should. And you should be using them together.

Why?

First the obvious :

You can manage sprints with the Meeting notes Blueprint and Retrospectives Blueprint
You can turn your Confluence into a knowledgebase. Everything is searchable and editing docs is easy. If you keep all your docs on Confluence, everyone always knows where info is. Of course confluence allows you to protect sensitive areas with password protection, so your knowledgebase can only reveal stuff you want to.

You can display Confluence info in JIRA - Links requirements in confluence in jira tasks using the Product Requirements Blueprint
But the real beauty of integrating Jira and Confluence is when you can display Jira data in Confluence. 
For example:


  • Display JIRA data like status reports and change logs in Confluence with JIRA reports Blueprint
  • Automatic linking in confluence to Jira tasks
  • Embed Jira Queries into Confluence to show task lists - for example to show which tasks are included in a release
  • Use the Jira Chart Macro to show the results of a Jira query (JQL) as a chart in Confluence
  • Use the Jira REST API to get detailed info about tasks. Search queries are returned as JSON objects which can be parsed and used to display charts on confluence with the confluence REST API


The last point is really my favorite.

Assume you are managing a team of developers in a large organization, and upper management would like periodic status updates of the team's performance. You could show them burn down charts, or really any of the plethora of charts that Jira offers out of the box, but what if they wanted more info? What if they wanted to see, for example,

  • How long a release took to implement (time to market)? 
  • Or how many bugs were found in each delivery (delivery quality)? 
  • Or what percent of each delivery was defects vs enhancements (productivity)?
Assuming the developers logged their hours faithfully against their Jira tasks, you can display this info quite easily.

I'll explain how to get the info for time to market. The other two are just a matter of getting different info from the JSON retrieved by the Jira REST API, and some bash magic


Assumptions:

1. For this example, I used the free Jira trial, and created 2 releases. My Jira looked like this:




Release 1 looked like this:



Release 2 looked like this:



Release 3 was unreleased, so we won't bother about it.

2. I also used the Confluence free trial. I created a space called "Moon Rocket Launch Info", and a page to display graphs in, called "Time to market graph"

3. I had the beautiful JQ JSON parser for bash installed (https://stedolan.github.io/jq/)

The code

Lets say I am interested in Bug, Story and Task issues. My JQL would look like this:

project = "MRL" AND issuetype in (Bug, Story, Task) AND fixVersion in releasedVersions()

This search returns a bunch of results that looks like this:




We can use curl to get these results into our script:


JIRA_REST_API_URL="https://somaiah.atlassian.net/rest/api/2"
JIRA_USER="admin"
JIRA_PASSWORD="secret"

JIRA_SEARCH_URL="${JIRA_REST_API_URL}/search?jql=project=%22MRL%22%20AND%20issuetype%20in%20(Bug,%20Story,%20Task)%20AND%20fixVersion%20in%20releasedVersions()"

JIRA_FILTER_INFO=`curl --globoff --insecure --silent -u ${JIRA_USER}:${JIRA_PASSWORD} -X GET -H 'Content-Type: application/json' "${JIRA_SEARCH_URL}"`


This returns a LOT of info about each jira tasks. In fact, each Jira issue returned looks like this, with a whole lot of info:



    {
        "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
        "id": "10021",
        "self": "https://somaiah.atlassian.net/rest/api/2/issue/10021",
        "key": "MRL-22",
        "fields": {
            "issuetype": {
                "self": "https://somaiah.atlassian.net/rest/api/2/issuetype/10001",
                "id": "10001",
                "description": "gh.issue.story.desc",
                "iconUrl": "https://somaiah.atlassian.net/images/icons/issuetypes/story.svg",
                "name": "Story",
                "subtask": false
            },
            "timespent": 21600,
            "project": {
                "self": "https://somaiah.atlassian.net/rest/api/2/project/10000",
                "id": "10000",
                "key": "MRL",
                "name": "Moon Rocket Launch",
                "avatarUrls": {
                    "48x48": "https://somaiah.atlassian.net/secure/projectavatar?avatarId=10324",
                    "24x24": "https://somaiah.atlassian.net/secure/projectavatar?size=small&avatarId=10324",
                    "16x16": "https://somaiah.atlassian.net/secure/projectavatar?size=xsmall&avatarId=10324",
                    "32x32": "https://somaiah.atlassian.net/secure/projectavatar?size=medium&avatarId=10324"
                }
            },
            "fixVersions": [
                {
                    "self": "https://somaiah.atlassian.net/rest/api/2/version/10000",
                    "id": "10000",
                    "name": "Version 1.0",
                    "archived": false,
                    "released": true,
                    "releaseDate": "2016-05-07"
                }
            ],
            "aggregatetimespent": 21600,
            "resolution": {
                "self": "https://somaiah.atlassian.net/rest/api/2/resolution/10000",
                "id": "10000",
                "description": "Work has been completed on this issue.",
                "name": "Done"
            },
            "resolutiondate": "2016-05-03T16:52:16.000+0200",
            "workratio": -1,
            "lastViewed": "2016-05-15T21:35:37.060+0200",
            "watches": {
                "self": "https://somaiah.atlassian.net/rest/api/2/issue/MRL-22/watchers",
                "watchCount": 0,
                "isWatching": false
            },
            "created": "2016-04-23T09:55:16.000+0200",
            "customfield_10022": 2.0,
            "priority": {
                "self": "https://somaiah.atlassian.net/rest/api/2/priority/3",
                "iconUrl": "https://somaiah.atlassian.net/images/icons/priorities/medium.svg",
                "name": "Medium",
                "id": "3"
            },
            "labels": [],
            "customfield_10016": "0|i0004n:",
            "customfield_10017": ["com.atlassian.greenhopper.service.sprint.Sprint@14c71ff[id=2,rapidViewId=1,state=CLOSED,name=Sample Sprint 1,startDate=2016-04-23T09:55:19.342+02:00,endDate=2016-05-07T09:55:19.342+02:00,completeDate=2016-05-07T08:35:19.342+02:00,sequence=2]"],
            "customfield_10018": null,
            "timeestimate": 0,
            "aggregatetimeoriginalestimate": null,
            "versions": [],
            "issuelinks": [],
            "assignee": {
                "self": "https://somaiah.atlassian.net/rest/api/2/user?username=admin",
                "name": "admin",
                "key": "admin",
                "emailAddress": "somaiah@gmail.com",
                "avatarUrls": {
                    "48x48": "https://somaiah.atlassian.net/secure/useravatar?avatarId=10351",
                    "24x24": "https://somaiah.atlassian.net/secure/useravatar?size=small&avatarId=10351",
                    "16x16": "https://somaiah.atlassian.net/secure/useravatar?size=xsmall&avatarId=10351",
                    "32x32": "https://somaiah.atlassian.net/secure/useravatar?size=medium&avatarId=10351"
                },
                "displayName": "Somaiah  [Administrator]",
                "active": true,
                "timeZone": "Europe/Berlin"
            },
            "updated": "2016-05-14T20:37:11.000+0200",
            "status": {
                "self": "https://somaiah.atlassian.net/rest/api/2/status/10001",
                "description": "",
                "iconUrl": "https://somaiah.atlassian.net/",
                "name": "Done",
                "id": "10001",
                "statusCategory": {
                    "self": "https://somaiah.atlassian.net/rest/api/2/statuscategory/3",
                    "id": 3,
                    "key": "done",
                    "colorName": "green",
                    "name": "Done"
                }
            },
            "components": [],
            "timeoriginalestimate": null,
            "description": null,
            "customfield_10010": null,
            "customfield_10011": null,
            "customfield_10012": null,
            "customfield_10013": null,
            "customfield_10014": "Not started",
            "customfield_10015": null,
            "customfield_10005": null,
            "customfield_10006": null,
            "customfield_10007": null,
            "customfield_10008": null,
            "customfield_10009": null,
            "aggregatetimeestimate": 0,
            "summary": "As a user, I'd like a historical story to show in reports",
            "creator": {
                "self": "https://somaiah.atlassian.net/rest/api/2/user?username=admin",
                "name": "admin",
                "key": "admin",
                "emailAddress": "somaiah@gmail.com",
                "avatarUrls": {
                    "48x48": "https://somaiah.atlassian.net/secure/useravatar?avatarId=10351",
                    "24x24": "https://somaiah.atlassian.net/secure/useravatar?size=small&avatarId=10351",
                    "16x16": "https://somaiah.atlassian.net/secure/useravatar?size=xsmall&avatarId=10351",
                    "32x32": "https://somaiah.atlassian.net/secure/useravatar?size=medium&avatarId=10351"
                },
                "displayName": "Somaiah  [Administrator]",
                "active": true,
                "timeZone": "Europe/Berlin"
            },
            "subtasks": [],
            "reporter": {
                "self": "https://somaiah.atlassian.net/rest/api/2/user?username=admin",
                "name": "admin",
                "key": "admin",
                "emailAddress": "somaiah@gmail.com",
                "avatarUrls": {
                    "48x48": "https://somaiah.atlassian.net/secure/useravatar?avatarId=10351",
                    "24x24": "https://somaiah.atlassian.net/secure/useravatar?size=small&avatarId=10351",
                    "16x16": "https://somaiah.atlassian.net/secure/useravatar?size=xsmall&avatarId=10351",
                    "32x32": "https://somaiah.atlassian.net/secure/useravatar?size=medium&avatarId=10351"
                },
                "displayName": "Somaiah  [Administrator]",
                "active": true,
                "timeZone": "Europe/Berlin"
            },
            "customfield_10000": null,
            "aggregateprogress": {
                "progress": 21600,
                "total": 21600,
                "percent": 100
            },
            "customfield_10001": "10000_*:*_1_*:*_889020000_*|*_10001_*:*_1_*:*_0",
            "customfield_10002": "com.atlassian.servicedesk.plugins.approvals.internal.customfield.ApprovalsCFValue@37e4eb",
            "customfield_10003": null,
            "customfield_10004": null,
            "environment": null,
            "duedate": null,
            "progress": {
                "progress": 21600,
                "total": 21600,
                "percent": 100
            },
            "votes": {
                "self": "https://somaiah.atlassian.net/rest/api/2/issue/MRL-22/votes",
                "votes": 0,
                "hasVoted": false
            }
        }
    }




What we are interested in is only the version, resolutionDate and createdDate. Running the jira response via JQ to filter these results:


echo ${JIRA_FILTER_INFO} | jq -r '.issues | map(.fields | (.fixVersions[] | { version: .name }) + { resolutionDate: .resolutiondate} + {createdDate: .created})'`

Returns an array of elements that look like this:

  {
    "version": "Version 1.0",
    "resolutionDate": "2016-05-05T18:30:16.000+0200",
    "createdDate": "2016-04-23T09:55:16.000+0200"
  },
  {
    "version": "Version 1.0",
    "resolutionDate": "2016-05-03T16:52:16.000+0200",
    "createdDate": "2016-04-23T09:55:16.000+0200"
  }


Now its just a matter of some bash magic to get the info from this array and present it to confluence. The full is available on github here: https://github.com/somaiah/jira-confluence-graphs/blob/master/src/timeToMarket.sh

But the final HTML to be sent to confluence looks like this:


<h2>Average time to market per release </h2>
<table>
    <tbody>
    <tr>
        <td>
            <h2>Project: Moon Rocket Launch</h2>

            <div style='line-height:50px;'><br/></div>
            <ac:macro ac:name='chart'>
                <ac:parameter ac:name='type'>line</ac:parameter>
                <ac:parameter ac:name='width'>400</ac:parameter>
                <ac:parameter ac:name='height'>600</ac:parameter>
                <ac:parameter ac:name='forgive'>true</ac:parameter>
                <ac:parameter ac:name='xLabel'>Release</ac:parameter>
                <ac:parameter ac:name='yLabel'>Days</ac:parameter>
                <ac:parameter ac:name='categoryLabelPosition'>down90</ac:parameter>
                <ac:rich-text-body>
                    <table>
                        <tbody>
                        <tr>
                            <th><p>&nbsp;</p></th>
                            <th><p> Version 1.0</p></th>
                            <th><p> Version 2.0</p></th>
                        </tr>
                        <tr>
                            <td><p>Average days used per release</p></td>
                            <td><p>8</p></td>
                            <td><p>2</p></td>
                        </tr>
                        </tbody>
                    </table>
                </ac:rich-text-body>
            </ac:macro>
            <div style='line-height:50px;'><br/></div>
        </td>
    </tr>
    </tbody>
</table>


Now you just need to update the page in confluence with a REST PUT:


echo '{"id":"'${CONFLUENCE_PAGE_ID}'","type":"page","title":"'${PAGE_NAME}'","space":{"key":"'${CONFLUENCE_SPACE}'"},"body":{"storage":{"value":"'${CONTENT}'","representation":"storage"}},"version":{"number":'${NEXT_PAGE_VERSION}'}}' > body.json

RESPONSE=`curl --globoff --insecure --silent -u ${CONFLUENCE_USER}:${CONFLUENCE_PASSWORD} -X PUT -H 'Content-Type: application/json' --data @body.json ${CONFLUENCE_REST_API_PAGE_URL}/${CONFLUENCE_PAGE_ID}`


The confluence page ID is the ID of the page you created (see assumptions)
The page name is the page title
The space key is the confluence space your page is in
The content is a code generated.
The page version should be updated by 1

The resulting graph on confluence looks like this:



If you are using Jenkins for CI, you can simply stick this script in Jenkins, and run it periodically (say every 2 weeks or so on)
Now you have a nice visual display of your TTM, thats automatically updated without any manual interevention. Neat?

As you can guess from the Jira info that's returned in the REST call, you can do a whole bunch of stuff - as I said before, delivery quality and productivity are only 2 of them. Simply tailor your JQL and you're set.

You can find the full code on github: https://github.com/somaiah/jira-confluence-graphs
Since I used the trial versions of jira and confluence, the responses and requests are saved in https://github.com/somaiah/jira-confluence-graphs/tree/master/resources

Thursday, February 11, 2016

Sonar + Jacoco + Maven multi module projects + Jenkins

I recently had to come up with a sonar set up for a multi maven project. I found several examples for stuff that came close to this, but it wasn't EXACTLY what I was looking for.

My set up:

sonarQube 5.3 set up on a server, running with an Oracle backend
Jenkins job running a build
Junit tests
Jenkins > Configure > Sonar Runner added



0. Set up the Jenkins job to build your code:

These details are out of scope here. This is just a normal Jenkins maven project that builds stuff into a workspace.

1. Set up the Jenkins job for sonar:

As recommended by sonarqube.org I created a separate job and in the Build section, chose to Invoke Standalone Solar Analysis

Note that under Advanced Project Options, I had to select Use custom workspace, and fill the workspace of the job that built my code in step 0.


My Build section looked like this:



For readability, my sonar properties were:



# Metadata
sonar.host.url=http://sonar.domain.com
sonar.projectKey=somekey-sonar-runner
sonar.projectName=SomeNameSonarQube Runner
sonar.projectVersion=dev

# Source info
sonar.forceAnalysis=true
sonar.sourceEncoding=ISO-8859-15
maven.test.failure.ignore=true
sonar.sources=.
sonar.exclusions=**/SomeJavaFile.java,**/target/**/*,**/resources/min/**/*.js,**/node_modules/**/*,**/generated-sources/**/*,**/generated_sources/**/*,**/resources/lib/**/*

# Tests
sonar.junit.reportsPath=**/target/surefire-reports
sonar.surefire.reportsPath=**/target/surefire-reports
sonar.jacoco.reportPath=${WORKSPACE}/target/jacoco.exec
sonar.jacoco.itReportPath=${WORKSPACE}/target/jacoco-it.exec
sonar.java.binaries=**/target/classes
sonar.java.coveragePlugin=jacoco

# Debug
sonar.verbose=true

Step 2: Maven pom.xml

Add the following properties:

<sonar.core.codeCoveragePlugin>jacoco</sonar.core.codeCoveragePlugin>
<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
<sonar.jacoco.reportPath>${projectRoot}/target/jacoco.exec</sonar.jacoco.reportPath>
<sonar.jacoco.itReportPath>${projectRoot}/target/jacoco-it.exec</sonar.jacoco.itReportPath>
<sonar.language>java</sonar.language>

Add the jacoco-maven-plugin to your build
<build>
    <plugins>
        <plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.5.201505241946</version>
<executions>
<execution>
<id>agent-for-ut</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<destFile>${sonar.jacoco.reportPath}</destFile>
<append>true</append>
</configuration>
</execution>
<execution>
<id>agent-for-it</id>
<goals>
<goal>prepare-agent-integration</goal>
</goals>
<configuration>
<destFile>${sonar.jacoco.itReportPath}</destFile>
<append>true</append>
</configuration>
</execution>
</executions>
   </plugin>
</plugins>
</build>

Thats it! Build your jenkins job (from step 0) to build your code, and the jacoco-maven-plugin will automatically create one large jacoco.exec and jacoco-it.exec file in the jenkins workspace of the sonar job under
   workspace/target

Now when you run the sonar jenkins job (from step 1), sonar will grab the jacoco exec files and figure out the code coverage and create those beautiful graphs which you can see from your sonar server.