Skip to content
No description, website, or topics provided.
Kotlin HTML JavaScript
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
gradle/wrapper
src
.gitignore
LICENSE.txt
README.md
build.gradle.kts
gradle.properties
gradlew
gradlew.bat
settings.gradle.kts

README.md

Synchronizing issues between multiple tracking platforms

This tool synchronizes issues between Atlassian JIRA and IBM RTC. Synchronisation is configured field-per-field in one JSON file. The tool runs as a small web server to allow for processing of (JIRA) webhook calls.

Configuration

The application will look for a file named application.properties or application.yml in

* a /config subdirectory of the current directory
* the current directory

Place a file in either of the two locations, and define the path to your settings JSON along with application-wide properties:

spring:
  security:
    user:
      password: a-better-admin-password

sync:
  settingsLocation: /opt/issue-tracking-sync/config/settings.json
  pollingCron: 0 0/15 0 * * ?
  notificationChannelProperties:
    - classname: ch.loewenfels.issuetrackingsync.notification.SlackChannel
      endpoint: https://hooks.slack.com/services/mywebhookthingy
      username: The sync tool
      subject: jira2rtc
      avatar: ":cyclone:"

Beware that setting username, subject and avatar only works on legacy Slack webhooks.

Finally, define the settings.json. An example file can be found here

settings.json

Optionally, set a earliestSyncDate in the format yyyy-MM-ddTHH:mm:ss. If provided, polling will initially seek for issues updated after earliestSyncDate. If nothing is defined, polling will start with application start.

trackingApplications

Define the basic issue tracking applications in use in the section trackingApplications of the settings.json

{
  "trackingApplications": [
    {
      "name": "JIRA",
      "className": "ch.loewenfels.issuetrackingsync.syncclient.jira.JiraClient",
      "username": "foobar",
      "password": "mysecret",
      "endpoint": "http://localhost:8080/jira",
      "polling": false
    }
  ]
}

The currently defined client implementations are:

  • ch.loewenfels.issuetrackingsync.syncclient.jira.JiraClient
  • ch.loewenfels.issuetrackingsync.syncclient.rtc.RtcClient

actionDefinitions

An action definition represents a synchronization sequence, similar to a macro. Multiple actions can be stringed together to form a synchronization flow. By defining actions separately, they can be re-used in multiple flows.

{
  "actionDefinitions": [
    {
      "name": "SimpleFieldsRtcToJira",
      "classname": "ch.loewenfels.issuetrackingsync.executor.SimpleSynchronizationAction",
      "fieldMappingDefinitions": [
        {
          "sourceName": "summary",
          "targetName": "title"
        }
      ]
    },
    {
      "name": "SynchronizeComments",
      "classname": "ch.loewenfels.issuetrackingsync.executor.actions.CommentsSynchronizationAction"
    }
  ]
}
  • ch.loewenfels.issuetrackingsync.executor.SimpleSynchronizationAction reads a list of fieldMappingDefinitions (see below).
  • ch.loewenfels.issuetrackingsync.executor.actions.CommentsSynchronizationAction adds all comments present in the source client but missing on the target (equality is based on source comment text being found in target comment text or vice versa)
  • ch.loewenfels.issuetrackingsync.executor.actions.AttachmentsSynchronizationAction adds all attachments present in the source client but missing on the target (equality is based on content hash)
fieldMappingDefinitions

The fieldMappingDefinitions allows for a quiet generic approach to synchronizing simple fields.

Fields available in RTC

Property Read as Write as
archived archived -
category category category
com.ibm.team.apt.attribute.acceptance com.ibm.team.apt.attribute.acceptance com.ibm.team.apt.attribute.acceptance
com.ibm.team.apt.attribute.complexity com.ibm.team.apt.attribute.complexity com.ibm.team.apt.attribute.complexity
com.ibm.team.apt.estimate.maximal com.ibm.team.apt.estimate.maximal com.ibm.team.apt.estimate.maximal
com.ibm.team.apt.estimate.minimal com.ibm.team.apt.estimate.minimal com.ibm.team.apt.estimate.minimal
com.ibm.team.rtc.attribute.affectedTeams com.ibm.team.rtc.attribute.affectedTeams com.ibm.team.rtc.attribute.affectedTeams
com.ibm.team.rtc.attribute.impact com.ibm.team.rtc.attribute.impact com.ibm.team.rtc.attribute.impact
contextId contextId contextId
correctedEstimate correctedEstimate correctedEstimate
creationDate creationDate -
creator creator -
description HTMLDescription.plainText
HTMLDescription using HtmlToWikiFieldMapper
description
dueDate dueDate dueDate
duration duration duration
foundIn foundIn foundIn
id id -
internalApprovalDescriptors internalApprovalDescriptors internalApprovalDescriptors
internalApprovals internalApprovals internalApprovals
internalComments internalComments internalComments
internalPriority internalPriority internalPriority
internalResolution internalResolution internalResolution
internalSequenceValue internalSequenceValue internalSequenceValue
internalSeverity internalSeverity internalSeverity
internalState internalState internalState
internalStateTransitions internalStateTransitions internalStateTransitions
internalSubscriptions internalSubscriptions internalSubscriptions
internalTags internalTags internalTags
modified modified -
modifiedBy modifiedBy -
owner owner owner
projectArea projectArea -
resolutionDate resolutionDate -
resolver resolver resolver
startDate startDate -
summary HTMLSummary.plainText
HTMLSummary using HtmlToWikiFieldMapper
summary
target target target
timeSpent timeSpent timeSpent
workItemType workItemType -

Additionally on RTC, custom fields can be read/written using the internal FQN (eg. ch.loewenfels.team.workitem.attribute.defectdescription)

Fields available in JIRA

Property Read as Write as
affectedVersionsNames affectedVersionsNames affectedVersionsNames
assignee assignee assignee
assigneeName assigneeName assigneeName
description description description
dueDate dueDate dueDate
fixVersionsNames fixVersionsNames fixVersionsNames
priority.name priority.name priority.name
priority.id priority.id priority.id
priorityId priorityId priorityId
reporter reporter reporter
reporterName reporterName reporterName
resolution.id resolution.id resolution.id
resolution.name resolution.name resolution.name
summary summary summary
status.id status.id status.id
status.name status.name status.name
creationDate creationDate creationDate
timeTracking.originalEstimateMinutes timeTracking.originalEstimateMinutes timeTracking.originalEstimateMinutes
timeTracking.remainingEstimateMinutes timeTracking.remainingEstimateMinutes timeTracking.remainingEstimateMinutes
timeTracking.timeSpentMinutes timeTracking.timeSpentMinutes timeTracking.timeSpentMinutes

Additionally on JIRA, custom field can be read/written using the internal name (like customfield_123456) or the display name (like Customer reference)

Field mappers

The default field mapper is ch.loewenfels.issuetrackingsync.executor.fields.DirectFieldMapper, which attempts to read the property sourceName and write it to targetName.

ch.loewenfels.issuetrackingsync.executor.fields.HtmlToWikiFieldMapper is useful for rich text fields which allow for markup in JIRA, and/or HTML in RTC (eg. RTC 'description').

ch.loewenfels.issuetrackingsync.executor.fields.CompoundStringFieldMapper can be used to map multiple text fields onto a single text field, and split it back. This mapper expects associations for each field definition except one (which will hold "the rest")

To merge multiple fields:

{
  "sourceName": "ch.loewenfels.team.workitem.attribute.requirement,ch.loewenfels.team.workitem.attribute.defectdescription,ch.loewenfels.team.workitem.attribute.expected.conduct",
  "targetName": "description",
  "mapperClassname": "ch.loewenfels.issuetrackingsync.executor.fields.CompoundStringFieldMapper",
  "associations": {
    "ch.loewenfels.team.workitem.attribute.defectdescription": "<h4>Error description</h4>",
    "ch.loewenfels.team.workitem.attribute.expected.conduct": "<h4>Expected behaviour</h4>"
  }
}

To split into multiple fields:

{
  "sourceName": "description",
  "targetName": "ch.loewenfels.team.workitem.attribute.requirement,ch.loewenfels.team.workitem.attribute.defectdescription,ch.loewenfels.team.workitem.attribute.expected.conduct",
  "mapperClassname": "ch.loewenfels.issuetrackingsync.executor.fields.CompoundStringFieldMapper",
  "associations": {
    "ch.loewenfels.team.workitem.attribute.defectdescription": "<h4>Error description</h4>",
    "ch.loewenfels.team.workitem.attribute.expected.conduct": "<h4>Expected behaviour</h4>"
  }
}

The ch.loewenfels.issuetrackingsync.executor.fields.LinkToIssueFieldMapper disregards the 'sourceName' and provides a link to the source issue. It can be written to any 'targetName'.

The ch.loewenfels.issuetrackingsync.executor.fields.PriorityAndSeverityFieldMapper is a slightly more complicated mapper, as JIRA typically knows 1 priority field, while RTC has a priority and severity. This mapper uses the associations
as a matrix to map between these fields:

{
  "sourceName": "priority,severity",
  "targetName": "priorityId",
  "mapperClassname": "ch.loewenfels.issuetrackingsync.executor.fields.PriorityAndSeverityFieldMapper",
  "associations": {
    "P1 - Critical,S1 - Minor": "Normal",
    "P2 - Critical,S1 - Major": "High",
    "P3 - Critical,S1 - Urgent": "Blocker"
  }
}

syncFlowDefinitions

Individual synchronization streams are defined as "flows". Apart from defining the source and target application (referring to the trackingApplication#name), the configuration can define a filter class, and must define a list of action references.

{
  "syncFlowDefinitions": [
    {
      "name": "Sync changes from RTC to JIRA",
      "source": "RTC",
      "target": "JIRA",
      "filterClassname": "ch.loewenfels.issuetrackingsync.custom.NotClosedChangeFilter",
      "keyFieldMappingDefinition": {
        "sourceName": "id",
        "targetName": "custom_field_12044"
      },
      "writeBackFieldMappingDefinition": {
        "sourceName": "key",
        "targetName": "ch.loewenfels.team.workitem.attribute.external_refid"
      },
      "defaultsForNewIssue": {
        "issueType": "change",
        "project": "TST",
        "category": ""
      },
      "actions": [
        "SimpleFieldsRtcToJira",
        "SynchronizeComments"
      ]
    }
  ]
}

The mandatory keyFieldMappingDefinition is used to load an issue, and thus has no issue or project context. As JIRA allows for multiple custom fields to have identical names (in different projects), the field names here must be defined by their internal name.

The optional writeBackFieldMappingDefinition allows to define a write-back of the target key to the source issue. In the example above, an RTC issue is synchronized to JIRA, but the JIRA key is written back to the RTC issue in field ch.loewenfels.team.workitem.attribute.external_refid.

The optional defaultsForNewIssue defines defaults for new issues. If missing, and no target issue is found using keyFieldMappingDefinition, synchronization will abort.

Finally, the list of actions refers to the name attribute of the actionDefinitions

For contributors

This project aims at allowing for simple issue synchronization using a purely configurative approach, while also embracing proprietary extensions. The latter can be defined using class FQNs in the settings.json, and making sure the application finds the class on the classpath (but possibly outside of this project's fat JAR).

Processing queue

Individual synchronization requests are processed in a queue (backed by ActiveMQ) with concurrency of one. This ensures that no two synchronization threads might affect the same items (configure in application.yml).

See the IssuePoller as an entry point, which runs based on a CRON expression. Found issues are then run through the SynchronizationFlowFactory, and for all issues with a matching flow, a SyncRequest is produced on the queue.

Processing a single issue

From a SyncRequest, an issue is derived and the matching SynchronizationFlow is retrieved from the SynchronizationFlowFactory. As described in syncFlowDefinitions, a SynchronizationFlow can define an issue filter, and must define a collection of SynchronizationActions.

  1. Load the source issue along with the key (=unique identifier) mapping. This step also verifies that the "last updated" timestamp of the synchronization request matches that of the loaded issue (if not, a SynchronizationAbortedException is thrown)
  2. Delegate the data mapping to one or several SynchronizationAction instances, which:
    1. Load all mapped source fields into the Issue object
    2. Optionally pass the Issue object to the target client, along with the (nullable) defaults for new issues. If the latter are missing and the target client fails to locate a target issue from the key field mapping, a SynchronizationAbortedException is thrown
You can’t perform that action at this time.