Advanced Reporting Plugin

13 minute read

This tutorial provides instructions to create a Reporting Plugin called SampleJIRAReporting that has a single procedure called CollectReportingData, which sends user story data from a Jira instance to Devops Insight to enable metrics to be shown in RCC.

Prerequisites

These are assumptions for this tutorial.

  • /pdfgroovy/tutorialbasicreporting tutorial has been completed.

  • pdk is installed and setup.

  • An active CloudBees CD instance.

  • An active CloudBees CD DevOps Insight Center that is configured and connected to CloudBees CD instance.

  • Internet connection.

  • An accessible Jira Instance.

  • Write Access to a Git Repository.

Step 1 : Generate plugin from scratch

Create a plugin workspace with Plugin name as SampleJIRAReporting and Plugin type as reporting.

`pdk generate workspace`

Once done. the response from the command prompt and the workspace layout should look as follows:

image

image

Step 2 : Configuring plugin specifications and generating a plugin

Open the config/pluginspec.yaml file. Generated file will have the following content:

pluginInfo: # This is default sample specification # Feel free to change it # Call flowpdk showdoc pluginspec to see the list of available fields and their description pluginName: 'SampleJIRAPlugin' version: '1.0.0' description: 'Advanced - Reporting Tutorial Plugin' author: 'Sample Author' supportUrl: 'none' category: 'Utilities' shell: 'ec-groovy' # The reporting configuration, will generate a procedure and a bunch of configuration scripts required for DOIS to work devOpsInsight: supportedReports: - reportObjectType: 'build' parameters: - name: param1 documentation: null required: true type: entry label: Sample Reporting Parameter - name: param2 documentation: null required: false type: entry label: Sample Reporting Parameter 2 # The name of the source as it will appear in the dashboards datasourceName: 'My Source Name' language: 'groovy' # Plugin configuration description configuration: # A set of fields will be added to process debug level in the configuration hasDebugLevel: true parameters: - name: config documentation: The name for the created configuration required: true type: entry label: Configuration Name - name: desc documentation: Description for the configuration required: false type: entry label: Description - name: endpoint documentation: Third-party endpoint to connect to. required: false type: entry label: Endpoint - name: credential documentation: A sample credential required: true type: credential label: Credential

Make the following changes to config/pluginspec.yaml:

  • Add jiraProjectName parameter to the procedure.

  • Add the configuration section restConfigInfo to enable check connection feature.

You can replace the content or check for the difference and implement them one by one:

pluginInfo: # This is default sample specification # Feel free to change it # Call flowpdk showdoc pluginspec to see the list of available fields and their description pluginName: 'SampleJIRAReporting' version: '1.0.0' description: 'Sample plugin for RCC <->Jira feature reporting' author: 'Sample Author' supportUrl: '<>' category: 'Utilities' shell: 'ec-groovy' # The reporting configuration, will generate a procedure and a bunch of configuration scripts required for DOIS to work devOpsInsight: supportedReports: - reportObjectType: 'feature' parameters: - name: jiraProjectName documentation: AJira project that will be used required: true type: entry label: Jira project name # The name of the source as it will appear in the dashboards datasourceName: 'JIRA Datasource from tutorial' language: 'groovy' # Plugin configuration description configuration: # A set of fields will be added to process debug level in the configuration hasDebugLevel: true restConfigInfo: endpointDescription: sample endpointLabel: Jira Server Endpoint checkConnectionUri: /rest/api/2/configuration headers: {Accept: "application/json"} # Auth schemes for the plugin authSchemes: basic: userNameLabel: Jira User Name passwordLabel: Jira User Password description: A username and password to connect to the Jira instance. checkConnectionUri: /rest/api/2/configuration credentialLabel: Jira Credentials

Run pdk generate plugin. The output will look something like this:

image

Step 3 : Writing reporting logic using pdk reporting component

Here’s the small overview of how the CollectReportingData works:

  • Read the metadata.

  • If no metadata was found, the reporting procedure should get initial amount of issues.

  • If metadata is found, get the last updated issue and check if we’ve already reported it.

  • If metadata is old, get issues updated after the last reported issue.

Each of the operation in bold has a boilerplate code that should be extended by the plugin developer.

Investigating autogenerated code

Let’s see what pdk has generated for the reporting plugin.

image

ReportingSampleJIRAReporting.groovy contains the following boilerplate code:

import com.cloudbees.flowpdf.* import sun.reflect.generics.reflectiveObjects.NotImplementedException import com.cloudbees.flowpdf.components.reporting.Dataset import com.cloudbees.flowpdf.components.reporting.Metadata import com.cloudbees.flowpdf.components.reporting.Reporting /** * User implementation of the reporting classes */ class ReportingSampleJIRAPlugin extends Reporting { @Override int compareMetadata(Metadata param1, Metadata param2) { def value1 = param1.getValue() def value2 = param2.getValue() def pluginObject = this.getPluginObject() // Return 1 if there are newer records than record to which metadata is pointing. throw new NotImplementedException() } @Override List<Map<String, Object>> initialGetRecords(FlowPlugin flowPlugin, int i = 10) { def params = flowPlugin.getContext().getRuntimeParameters().getAsMap() throw new NotImplementedException() return records } @Override List<Map<String, Object>> getRecordsAfter(FlowPlugin flowPlugin, Metadata metadata) { def params = flowPlugin.getContext().getRuntimeParameters().getAsMap() def metadataValues = metadata.getValue() def log = flowPlugin.getLog() log.info("Got metadata value in getRecordsAfter: ${metadataValues.toString()}") throw NotImplementedException() log.info("Records after GetRecordsAfter ${records.toString()}") return records } @Override Map<String, Object> getLastRecord(FlowPlugin flowPlugin) { def params = flowPlugin.getContext().getRuntimeParameters().getAsMap() def log = flowPlugin.getLog() log.info("Last record runtime params: ${params.toString()}") throw new NotImplementedException() } @Override Dataset buildDataset(FlowPlugin plugin, List<Map> records) { def dataset = this.newDataset(['your report object type'], []) def context = plugin.getContext() def params = context.getRuntimeParameters().getAsMap() def log = plugin.getLog() log.info("Start procedure buildDataset") log.info("buildDataset received params: ${params}") throw new NotImplementedException() return dataset } }

And the content of generated SampleJIRAPlugin.groovy contains:

import sun.reflect.generics.reflectiveObjects.NotImplementedException import com.cloudbees.flowpdf.components.ComponentManager import com.cloudbees.flowpdf.* /** * SampleJIRAPlugin */ class SampleJIRAPlugin extends FlowPlugin { @Override Map<String, Object> pluginInfo() { return [ pluginName : '@PLUGIN_KEY@', pluginVersion : '@PLUGIN_VERSION@', configFields : ['config'], configLocations: ['ec_plugin_cfgs'], defaultConfigValues: [:] ] } /** * Procedure parameters: * @param config * @param jiraProjectName * @param previewMode * @param transformScript * @param debug * @param releaseName * @param releaseProjectName */ def collectReportingData(StepParameters paramsStep, StepResult sr) { def params = paramsStep.getAsMap() throw new NotImplementedException() if (params['debug']) { log.setLogLevel(log.LOG_DEBUG) } Reporting reporting = (Reporting) ComponentManager.loadComponent(ReportingSampleJIRAPlugin.class, [ reportObjectTypes : ['feature'], metadataUniqueKey : 'fill me in', payloadKeys : ['fill me in'], ], this) reporting.collectReportingData() } // === step ends === }

Notice that at line #43, the reporting component gets loaded with three special parameters, and the reportObjectTypes key is already set to feature. We need to fill these two:

  • metadataUniqueKey

  • payloadKeys

Configuration of the reporting component

Let’s check into meaning of each one closely:

  • metadataUniqueKey

    Metadata facilitates Change Data Capture where in the plugin is aware if there is any new change that would entail sending reporting data to the DevOps Insight Data Server. The fields that participate in Metadata essentially provide the mechanism to capture if that data has changed. Note that this mechanism is required as the plugin is using the PULL model, where in it is periodically looking for what has changed. Since the key has to be unique for the project and query, and the only variable in the query is project, we choose it to be the Jira project name from the data source configuration.

    metadataUniqueKey: params['jiraProjectName']
  • payloadKeys

    To perform check of the local and remote states for the synchronization we should be able to compare them. Usually, we would like to know if the remote system has new updates. For this we will use "Updated" field of The Jira issue, which is reported as a 'modifiedOn' attribute of the DevOps Insight report object (Note: we will check into report object attributes later). The payloadKeys array of strings contains the values we want to store in the metadata. To have an additional information, we will also include a key of the last reported issue, which is reported as a key.

    payloadKeys : ['key', 'modifiedOn']

Replace the collectReportingData() step method in SampleJIRAReporting.groovy with the following:

def collectReportingData(StepParameters paramsStep, StepResult sr) { def params = paramsStep.getAsMap() if (params['debug']) { log.setLogLevel(log.LOG_DEBUG) } Reporting reporting = (Reporting) ComponentManager.loadComponent(ReportingSampleJIRAReporting.class, [ reportObjectTypes: ['feature'], metadataUniqueKey: params['jiraProjectName'], payloadKeys : ['key', 'modifiedOn'] ] as Map<String, Object>, this) reporting.collectReportingData() }

Receiving issues from the Jira server

As we will use JQL to get information from Jira, ensure that the following JQL returns a list of issues.

project = "TEST"

To connect our plugin with the reporting system (Jira) we have to define a process of pulling the freshest issues in a correct order with a limit. To get issues we will use GET search API endpoint with jql and maxResults parameters.

Let’s implement function to perform the GET request. You can copy the following code to your SampleJIRAReporting.groovy.

def get(String path, Map<String, String> queryParams) { Context context = getContext() REST rest = context.newRESTClient() return rest.request('GET', '/rest/api/2/' + path, queryParams) }

REST instance will get the values from the configuration, apply the authorization and encode the result.

Let’s implement the next method: searching for issues with limiting count of the results:

def getIssues(String projectName, Map<String, String> opts) { def requestParams = [jql: "project=$projectName AND issuetype=Story ORDER BY updatedDate ASC"] if (opts['limit']) { requestParams['maxResults'] = opts['limit'] } def issues = get('search', requestParams) return issues['issues'] }

And the method that will return the issue that was updated last:

def getLastUpdatedIssue(String projectName) { String lastJql = "project=$projectName AND issuetype=Story ORDER BY updatedDate DESC" def result = get('search', [jql: lastJql, maxResults: '1']) if (result['total'] > 0 && result.issues.size()) { return result['issues'][0] } throw new UnexpectedMissingValue("JIRA does not return the last updated issue") }

If our reporting component discovers fresh issues, we search for issues that were updated after last synchronization. Before we proceed, note that DevOps Insight server has date format in ISO8601 with a UTC timezone. For example:

2019-01-01T14:38:01.000Z

The Jira server uses different format for records and another for JQL. Here’re two simple methods that will convert Jira datetime string to ISO8601 and the ISO8601 to a format that JQL accepts. Add the following code to your SampleJIRAReporting.groovy:

String jiraDatetimeToISODatetime(String rawDate) { if (rawDate == null || !rawDate) return '' SimpleDateFormat jiraDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") jiraDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")) Date parsedDate = jiraDateFormat.parse(rawDate) SimpleDateFormat devopsDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") devopsDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")) String formatted = devopsDateFormat.format(parsedDate) return formatted } String isoDatetimeToJqlDatetime(String isoDate) { SimpleDateFormat devopsDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") devopsDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")) Date parsedDate = devopsDateFormat.parse(isoDate) SimpleDateFormat jqlDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm") jqlDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")) // The timezone here should be adjusted to Jira value String jiraFormattedDate = jqlDateFormat.format(parsedDate) return jiraFormattedDate }

Now we can implement method that perform JQL search for issues updated after the last reported issue. Add the following code to your SampleJIRAReporting.groovy:

def getIssuesAfterDate(String projectName, String lastUpdateDateISO) { String jiraFormattedDate = isoDatetimeToJqlDatetime(lastUpdateDateISO) String storyJql = "project='$projectName' AND issuetype=Story AND updatedDate >= \"${jiraFormattedDate}\" ORDER BY updatedDate DESC" def result = get('search', [jql: storyJql]) if (result['total'] > 0 && result.issues.size()) { return result['issues'] } throw new UnexpectedMissingValue("JIRA did not return issue updated after ${jiraFormattedDate}. Check the timezone.") }

Implementing the reporting component

Now we will edit ReportingSampleJIRAReporting.groovy file. Make sure that it has the following imports:

import com.cloudbees.flowpdf.FlowPlugin import com.cloudbees.flowpdf.components.reporting.Dataset import com.cloudbees.flowpdf.components.reporting.Metadata import com.cloudbees.flowpdf.components.reporting.Reporting import net.sf.json.JSONObject import java.text.SimpleDateFormat

Let’s implement the reporting methods one by one:

compareMetadata

The purpose of the compareMetadata method is to tell the reporting component if one issue has been updated later than another.

This method is used in two cases:

  • Reporting compares stored metadata with a metadata built from last modified issue.

  • Reporting sorts the payloads before sending it to the DevOps Insight Server.

The method receives two metadata instances. The Metadata.getValue() returns object that represents stored value. As we have supplied key and modifiedOn as the payloadKeys, metadata value will have a following structure:

{"key":"TEST-26","modifiedOn":"2020-02-05T13:43:08.000Z"}
The value of 'modifiedOn' contains date in DevOps Insight format (ISO8601 in the UTC timezone).

If the last updated Jira has modifiedOn time in the future, Reporting should send new changes to the DevOps Insight Server. Replace the method compareMetadata() in ReportingSampleJIRAReporting.groovy by the following code:

@Override int compareMetadata(Metadata param1, Metadata param2) { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") format.setTimeZone(TimeZone.getTimeZone("UTC")) Date date1 = format.parse(param1.getValue()['modifiedOn']) Date date2 = format.parse(param2.getValue()['modifiedOn']) // Return 1 if there are newer records than record to which metadata is pointing. return date2.compareTo(date1) }

initialGetRecords

This method is called when there is no metadata stored for the context. We will use our SampleJIRAReporting.getIssues method to retrieve issues. Replace the method initialGetRecords() in ReportingSampleJIRAReporting.groovy by the following code:

@Override List<Map<String, Object>> initialGetRecords(FlowPlugin flowPlugin, int i = 10) { def params = flowPlugin.getContext().getRuntimeParameters().getAsMap() return flowPlugin.getIssues(params['jiraProjectName'], [limit: i]) }

To prevent loading too much information, limit should be supplied. It can be changed in the schedule parameters or in the Reporting component initialization values.

If you want to change the limit to a huge amount, you should be aware that Jira returns search results in chunks if the number of the results is greater than the count specified in the Jira configuration. Look to the source code of SampleJIRAOAuth for implementation of the chunked response retrieval.

getLastRecord

This method will be called when we want to check if the state of the reporting system has changed and should return the last updated issue. We have implemented it in the plugin code (SampleJIRAReporting.groovy) so here method will look very simple:

Replace the method getLastRecord() in ReportingSampleJIRAReporting.groovy by the following code:

@Override Map<String, Object> getLastRecord(FlowPlugin flowPlugin) { def params = flowPlugin.getContext().getRuntimeParameters().getAsMap() return flowPlugin.getLastUpdatedIssue(params['jiraProjectName']) }

getRecordsAfter

When we know that there are updated issues in the reporting system, we want to grab only new ones. Modified date will be given in Metadata instance.

Replace the method getRecordsAfter() in ReportingSampleJIRAReporting.groovy by the following code:

@Override List<Map<String, Object>> getRecordsAfter(FlowPlugin flowPlugin, Metadata metadata) { def params = flowPlugin.getContext().getRuntimeParameters().getAsMap() def metadataValue = metadata.getValue() return flowPlugin.getIssuesAfterDate(params['jiraProjectName'], metadataValue['modifiedOn']) }

buildDataset

Now, the last piece. We should tell the Reporting component how our Jira issues should be transformed into the DevOps Insight Server payload.

First, we should know how the payload should look like. Go to the DevOps Insight user guide to see the list of fields for the feature report. Additionally, you can use the following call to get the report attributes that are defined on CloudBees CD server.

ectool --format json getReportObjectAttributes feature
* There are specific requirements for fields featuretype, resolution and status. They should contain one of the predefined values. As Jira can be tuned by the user to contain additional statuses and resolutions or they can differ between The Jira releases, we should map the original value to one that is expected by the DevOps Insight Server.

+ *Some of the fields, like pluginConfiguration, pluginName, releaseName and releaseProjectName are well-known and will be supplied by the Reporting component if omitted.

Replace the method buildDataset() in ReportingSampleJIRAReporting.groovy by the following code:

@Override Dataset buildDataset(FlowPlugin plugin, List<Map> records) { SampleJIRAReporting jiraPlugin = plugin as SampleJIRAReporting Dataset dataset = this.newDataset(['feature'], []) Context context = plugin.getContext() Map params = context.getRuntimeParameters().getAsMap() for (Map<String, Object> issue : records) { plugin.log.debug("Issue:", JSONObject.fromObject(issue).toString()) Map<String, String> statusMappings = [ 'Done' : 'Closed', 'Closed' : 'Closed', 'In Progress': 'In Progress', 'Open' : 'Open', 'To Do' : 'Open', 'Reopened' : 'Reopened', 'Resolved' : 'Resolved', ] Map<String, String> resolutionMappings = [ 'Cannot Reproduce': 'Cannot Reproduce', 'Duplicate' : 'Duplicate', 'Done' : 'Fixed', 'Won\'t Do' : 'Incomplete', 'Won\'t Do' : 'Won\'t Fix' ] String rawStatus = issue.fields.status?.statusCategory?.name ?: '' String rawResolution = issue.fields.resolution?.name ?: '' String jiraModifiedOn = issue.fields.updated ?: '' String jiraCreatedOn = issue.fields.created ?: '' String jiraResolvedOn = issue.fields.resolutionDate ?: '' String modifiedOn = jiraPlugin.jiraDatetimeToISODatetime(jiraModifiedOn) String createdOn = jiraPlugin.jiraDatetimeToISODatetime(jiraCreatedOn) String resolvedOn = jiraPlugin.jiraDatetimeToISODatetime(jiraResolvedOn) dataset.newData([ reportObjectType: 'feature', values: [ source : 'JIRA', sourceUrl : params['endpoint'], type : issue.fields.issuetype?.name, featureName: issue.fields.summary, storyPoints: issue.fields.storyPoints ?: '', key : issue.key, resolution : resolutionMappings[rawResolution] ?: '', status : statusMappings[rawStatus] ?: 'Open', modifiedOn : modifiedOn, createdOn : createdOn, resolvedOn : resolvedOn, ]]) } return dataset }

Step 4 : Review the resulting code

After all the modifications, this is what SampleJIRAReporting.groovy should contain:

import com.cloudbees.flowpdf.* import com.cloudbees.flowpdf.client.REST import com.cloudbees.flowpdf.components.ComponentManager import com.cloudbees.flowpdf.components.reporting.Reporting import com.cloudbees.flowpdf.exceptions.UnexpectedMissingValue import java.text.SimpleDateFormat /** * SampleJIRAReporting */ class SampleJIRAReporting extends FlowPlugin { @Override Map<String, Object> pluginInfo() { return [ pluginName : '@PLUGIN_KEY@', pluginVersion : '@PLUGIN_VERSION@', configFields : ['config'], configLocations : ['ec_plugin_cfgs'], defaultConfigValues: [authScheme: 'basic'] ] } /** * Procedure parameters: * @param config * @param jiraProjectName * @param previewMode * @param transformScript * @param debug * @param releaseName * @param releaseProjectName */ def collectReportingData(StepParameters paramsStep, StepResult sr) { def params = paramsStep.getAsMap() if (params['debug']) { log.setLogLevel(log.LOG_DEBUG) } Reporting reporting = (Reporting) ComponentManager.loadComponent(ReportingSampleJIRAReporting.class, [ reportObjectTypes: ['feature'], metadataUniqueKey: params['jiraProjectName'], payloadKeys : ['key', 'modifiedOn'] ] as Map<String, Object>, this) reporting.collectReportingData() } // === step ends === def get(String path, Map<String, String> queryParams) { Context context = getContext() REST rest = context.newRESTClient() return rest.request('GET', '/rest/api/2/' + path, queryParams) } def getIssues(String projectName, Map<String, String> opts) { def requestParams = [jql: "project=$projectName AND issuetype=Story ORDER BY updatedDate ASC"] if (opts['limit']) { requestParams['maxResults'] = opts['limit'] } def issues = get('search', requestParams) return issues['issues'] } def getLastUpdatedIssue(String projectName) { String lastJql = "project=$projectName AND issuetype=Story ORDER BY updatedDate DESC" def result = get('search', [jql: lastJql, maxResults: '1']) if (result['total'] > 0 && result.issues.size()) { return result['issues'][0] } throw new UnexpectedMissingValue("JIRA does not returned the last updated issue") } String jiraDatetimeToISODatetime(String rawDate) { if (rawDate == null || !rawDate) return '' SimpleDateFormat jiraDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") jiraDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")) Date parsedDate = jiraDateFormat.parse(rawDate) SimpleDateFormat devopsDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") devopsDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")) String formatted = devopsDateFormat.format(parsedDate) return formatted } String isoDatetimeToJqlDatetime(String isoDate) { SimpleDateFormat devopsDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") devopsDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")) Date parsedDate = devopsDateFormat.parse(isoDate) SimpleDateFormat jqlDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm") jqlDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")) // The timezone here should be adjusted to Jira value String jiraFormattedDate = jqlDateFormat.format(parsedDate) return jiraFormattedDate } def getIssuesAfterDate(String projectName, String lastUpdateDateISO) { String jiraFormattedDate = isoDatetimeToJqlDatetime(lastUpdateDateISO) String storyJql = "project='$projectName' AND issuetype=Story AND updatedDate >= \"${jiraFormattedDate}\" ORDER BY updatedDate DESC" def result = get('search', [jql: storyJql]) if (result['total'] > 0 && result.issues.size()) { return result['issues'] } throw new UnexpectedMissingValue("JIRA did not return issue updated after ${jiraFormattedDate}. Check the timezone.") } }

And this is content of ReportingSampleJIRAReporting.groovy:

import com.cloudbees.flowpdf.FlowPlugin import com.cloudbees.flowpdf.components.reporting.Dataset import com.cloudbees.flowpdf.components.reporting.Metadata import com.cloudbees.flowpdf.components.reporting.Reporting import net.sf.json.JSONObject import java.text.SimpleDateFormat /** * User implementation of the reporting classes */ class ReportingSampleJIRAReporting extends Reporting { @Override int compareMetadata(Metadata param1, Metadata param2) { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") format.setTimeZone(TimeZone.getTimeZone("UTC")) Date date1 = format.parse(param1.getValue()['modifiedOn']) Date date2 = format.parse(param2.getValue()['modifiedOn']) // Return 1 if there are newer records than record to which metadata is pointing. return date2.compareTo(date1) } @Override List<Map<String, Object>> initialGetRecords(FlowPlugin flowPlugin, int i = 10) { def params = flowPlugin.getContext().getRuntimeParameters().getAsMap() return flowPlugin.getIssues(params['jiraProjectName'], [limit: i]) } @Override Map<String, Object> getLastRecord(FlowPlugin flowPlugin) { def params = flowPlugin.getContext().getRuntimeParameters().getAsMap() return flowPlugin.getLastUpdatedIssue(params['jiraProjectName']) } @Override List<Map<String, Object>> getRecordsAfter(FlowPlugin flowPlugin, Metadata metadata) { def params = flowPlugin.getContext().getRuntimeParameters().getAsMap() def metadataValue = metadata.getValue() return flowPlugin.getIssuesAfterDate(params['jiraProjectName'], metadataValue['modifiedOn']) } @Override Dataset buildDataset(FlowPlugin plugin, List<Map> records) { SampleJIRAReporting jiraPlugin = plugin as SampleJIRAReporting Dataset dataset = this.newDataset(['feature'], []) Context context = plugin.getContext() Map params = context.getRuntimeParameters().getAsMap() for (Map<String, Object> issue : records) { plugin.log.debug("Issue:", JSONObject.fromObject(issue).toString()) Map<String, String> statusMappings = [ 'Done' : 'Closed', 'Closed' : 'Closed', 'In Progress': 'In Progress', 'Open' : 'Open', 'To Do' : 'Open', 'Reopened' : 'Reopened', 'Resolved' : 'Resolved', ] Map<String, String> resolutionMappings = [ 'Cannot Reproduce': 'Cannot Reproduce', 'Duplicate' : 'Duplicate', 'Done' : 'Fixed', 'Won\'t Do' : 'Incomplete', 'Won\'t Do' : 'Won\'t Fix' ] String rawStatus = issue.fields.status?.statusCategory?.name ?: '' String rawResolution = issue.fields.resolution?.name ?: '' String jiraModifiedOn = issue.fields.updated ?: '' String jiraCreatedOn = issue.fields.created ?: '' String jiraResolvedOn = issue.fields.resolutionDate ?: '' String modifiedOn = jiraPlugin.jiraDatetimeToISODatetime(jiraModifiedOn) String createdOn = jiraPlugin.jiraDatetimeToISODatetime(jiraCreatedOn) String resolvedOn = jiraPlugin.jiraDatetimeToISODatetime(jiraResolvedOn) dataset.newData([ reportObjectType: 'feature', values: [ source : 'JIRA', sourceUrl : params['endpoint'], type : issue.fields.issuetype?.name, featureName: issue.fields.summary, storyPoints: issue.fields.storyPoints ?: '', key : issue.key, resolution : resolutionMappings[rawResolution] ?: '', status : statusMappings[rawStatus] ?: 'Open', modifiedOn : modifiedOn, createdOn : createdOn, resolvedOn : resolvedOn, ]]) } return dataset } }

Step 5 : DOIS setup and reporting check

Install and promote the plugin. Then navigate to releases and create Jira Test Project release as follows:

image

image

Navigate to DevOps Insight Center:

image

Navigate to Release Command Center:

image

Select the Dashboard editor:

image

Select Setup:

image

Select Add button for User stories section:

image

Select our tutorial plugin, fill in the Jira project name parameter and click for new configuration creation:

image

Enter valid config values:

Your Jira endpoint should contain an URL of the Jira instance you have access to.

image

Go to Platform Home Page  Projects, choose your project and select Schedules.

image

Run the schedule to immediately collect the issues.

image

Navigate to Release Command Center again to see your Release data being populated.

Your reporting numbers may be different.

The Jira instance used for this tutorial has a Story Points field as a custom field. Custom fields are not covered for the simplicity.

image